KosisParser.cs 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. using System.Globalization;
  2. using System.Text.Json;
  3. namespace Infrastructure.StockData;
  4. /// <summary>
  5. /// KOSIS statisticsData.do JSON 응답 파서 — 순수 C# (System.Text.Json).
  6. /// 응답 골격: 성공 시 최상위 JSON 배열 [ { PRD_DE, DT, UNIT_NM, ITM_NM, C1, C1_NM, TBL_ID, ... }, ... ].
  7. /// 오류 시엔 배열이 아닌 객체 { "err":"21", "errMsg":"..." } → 예외 없이 빈 목록 반환 (koreaexim 파서와 동일한 방어).
  8. /// - PRD_DE(수록시점) → Period, DT(수치) → Value, UNIT_NM(단위) → Unit, ITM_NM/C1/C1_NM → 계열 식별용 보조.
  9. /// - KOSIS quirk: 통계표 하나에 여러 분류(C1 시도별/지수별)가 섞여 오므로, 계열 특정은 상위(수집 배치)가 C1 필터로 수행한다.
  10. /// - DT 는 문자열로 오며 미고시 시점은 ""·"-" 로 온다 → Value 파싱 실패 행은 skip (해당 시점 미적재, 다음 실행 재시도).
  11. /// </summary>
  12. public static class KosisParser
  13. {
  14. /// <summary>KOSIS 데이터 1행. Value 파싱 실패(빈값/"-") 행은 목록에서 제외되므로 여기 Value 는 항상 유효.</summary>
  15. public sealed record Row(
  16. string Period,
  17. decimal Value,
  18. string? Unit,
  19. string ItemName,
  20. string ClassCode,
  21. string ClassName
  22. );
  23. public static IReadOnlyList<Row> Parse(string json)
  24. {
  25. var rows = new List<Row>();
  26. if (string.IsNullOrWhiteSpace(json))
  27. {
  28. return rows;
  29. }
  30. using var doc = JsonDocument.Parse(json);
  31. if (doc.RootElement.ValueKind != JsonValueKind.Array)
  32. {
  33. return rows;
  34. }
  35. foreach (var item in doc.RootElement.EnumerateArray())
  36. {
  37. if (item.ValueKind != JsonValueKind.Object)
  38. {
  39. continue;
  40. }
  41. var period = GetString(item, "PRD_DE");
  42. if (period.Length == 0)
  43. {
  44. continue;
  45. }
  46. var value = ParseNullableDecimal(GetString(item, "DT"));
  47. if (value is null)
  48. {
  49. continue;
  50. }
  51. var unit = GetString(item, "UNIT_NM");
  52. rows.Add(new Row(
  53. period,
  54. value.Value,
  55. unit.Length == 0 ? null : unit,
  56. GetString(item, "ITM_NM"),
  57. GetString(item, "C1"),
  58. GetString(item, "C1_NM")));
  59. }
  60. return rows;
  61. }
  62. private static string GetString(JsonElement element, string name)
  63. {
  64. if (!element.TryGetProperty(name, out var value))
  65. {
  66. return string.Empty;
  67. }
  68. return value.ValueKind switch
  69. {
  70. JsonValueKind.String => value.GetString()?.Trim() ?? string.Empty,
  71. JsonValueKind.Number => value.GetRawText(),
  72. _ => string.Empty
  73. };
  74. }
  75. /// <summary>수치 문자열 → decimal (천단위 콤마 제거). ""·"-"·"X" 등 파싱 실패는 null.</summary>
  76. private static decimal? ParseNullableDecimal(string raw)
  77. {
  78. return decimal.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
  79. }
  80. }