sources.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import csv
  2. import json
  3. import os
  4. import re
  5. import subprocess
  6. import urllib.request
  7. from .config import build_url, resolve_path
  8. from .filter import DOMAIN_RE, IPV4_RE, normalize_domain, flatten_values, get_by_json_path, get_values_by_path
  9. def fetch_api_json(cfg):
  10. api = cfg["api"]
  11. url = build_url(api["url"], api.get("params", {}))
  12. method = api.get("method", "GET").upper()
  13. headers = api.get("headers", {})
  14. timeout = int(api.get("timeout_sec", 10))
  15. body_obj = api.get("body")
  16. body = None
  17. if body_obj is not None:
  18. body = json.dumps(body_obj).encode("utf-8")
  19. headers = {**headers, "Content-Type": "application/json"}
  20. req = urllib.request.Request(url=url, data=body, headers=headers, method=method)
  21. with urllib.request.urlopen(req, timeout=timeout) as resp:
  22. raw = resp.read().decode("utf-8", errors="replace")
  23. return json.loads(raw)
  24. def parse_domains(payload, parser_cfg):
  25. domains = []
  26. for p in parser_cfg.get("field_paths", []):
  27. values = get_values_by_path(payload, p)
  28. domains.extend(flatten_values(values))
  29. for p in parser_cfg.get("json_paths", []):
  30. v = get_by_json_path(payload, p)
  31. if v is not None:
  32. domains.extend(flatten_values(v))
  33. if not domains:
  34. regex_s = parser_cfg.get("regex", r"[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
  35. text = json.dumps(payload, ensure_ascii=True)
  36. domains.extend(re.findall(regex_s, text))
  37. clean = []
  38. seen = set()
  39. for d in domains:
  40. d = str(d).strip().lower().rstrip(".")
  41. if (DOMAIN_RE.match(d) or IPV4_RE.match(d)) and d not in seen:
  42. seen.add(d)
  43. clean.append(d)
  44. return clean
  45. def extract_records(payload, record_mapping):
  46. records_path = str(record_mapping.get("records_path", "")).strip()
  47. raw = get_values_by_path(payload, records_path)
  48. return [x for x in raw if isinstance(x, dict)]
  49. def load_cfst_rows(cfg, config_path_abs):
  50. cfst_cfg = cfg.get("cfst_local", {})
  51. config_dir = os.path.dirname(config_path_abs)
  52. work_dir = resolve_path(config_dir, cfst_cfg.get("work_dir", "./cfst"))
  53. binary_path = resolve_path(work_dir, cfst_cfg.get("binary", "./cfst"))
  54. result_file = resolve_path(work_dir, cfst_cfg.get("result_file", "result.csv"))
  55. encoding = str(cfst_cfg.get("encoding", "utf-8")).strip() or "utf-8"
  56. skip_run = bool(cfst_cfg.get("skip_run", False))
  57. timeout_sec = int(cfst_cfg.get("run_timeout_sec", 600))
  58. run_args = cfst_cfg.get("run_args", ["-o", os.path.basename(result_file)])
  59. if not isinstance(run_args, list):
  60. raise ValueError("cfst_local.run_args must be an array")
  61. command = [binary_path] + [str(x) for x in run_args]
  62. if not skip_run:
  63. completed = subprocess.run(
  64. command,
  65. cwd=work_dir,
  66. check=False,
  67. capture_output=True,
  68. text=True,
  69. encoding=encoding,
  70. errors="replace",
  71. timeout=timeout_sec,
  72. )
  73. if completed.returncode != 0:
  74. stderr = (completed.stderr or "").strip()
  75. stdout = (completed.stdout or "").strip()
  76. details = stderr or stdout or f"exit code {completed.returncode}"
  77. raise RuntimeError(f"cfst run failed: {details}")
  78. if not os.path.exists(result_file):
  79. raise RuntimeError(f"cfst result file not found: {result_file}")
  80. with open(result_file, "r", encoding=encoding, errors="replace", newline="") as f:
  81. reader = csv.reader(f)
  82. rows = [row for row in reader if any(str(col).strip() for col in row)]
  83. header_rows = int(cfst_cfg.get("header_rows", 1))
  84. if len(rows) <= header_rows:
  85. raise RuntimeError("cfst result has no data rows")
  86. columns_cfg = cfst_cfg.get("columns", {})
  87. if not isinstance(columns_cfg, dict):
  88. raise ValueError("cfst_local.columns must be an object")
  89. def col_index(name, default_index):
  90. raw = columns_cfg.get(name, default_index)
  91. try:
  92. idx = int(raw)
  93. except Exception as exc:
  94. raise ValueError(f"cfst_local.columns.{name} must be an integer") from exc
  95. if idx < 0:
  96. raise ValueError(f"cfst_local.columns.{name} must be >= 0")
  97. return idx
  98. ip_idx = col_index("ip", 0)
  99. sent_idx = col_index("sent", 1)
  100. received_idx = col_index("received", 2)
  101. loss_idx = col_index("loss_rate", 3)
  102. latency_idx = col_index("avg_latency", 4)
  103. speed_idx = col_index("download_speed", 5)
  104. region_idx = col_index("region", 6)
  105. out = []
  106. for row in rows[header_rows:]:
  107. if ip_idx >= len(row):
  108. continue
  109. domain = normalize_domain(row[ip_idx])
  110. if not domain:
  111. continue
  112. out.append(
  113. {
  114. "domain": domain,
  115. "ip": domain,
  116. "sent": row[sent_idx].strip() if sent_idx < len(row) else "",
  117. "received": row[received_idx].strip() if received_idx < len(row) else "",
  118. "loss_rate": row[loss_idx].strip() if loss_idx < len(row) else "",
  119. "avg_latency": row[latency_idx].strip() if latency_idx < len(row) else "",
  120. "download_speed": row[speed_idx].strip() if speed_idx < len(row) else "",
  121. "region": row[region_idx].strip() if region_idx < len(row) else "",
  122. }
  123. )
  124. if not out:
  125. raise RuntimeError("cfst result parsed to zero valid rows")
  126. return out