build_field_association.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. """生成 模板-源数据-DMS 三方字段关联 CSV。
  2. 输出:
  3. output/模板_源数据_DMS字段关联.csv
  4. """
  5. from __future__ import annotations
  6. import ast
  7. import csv
  8. import importlib.util
  9. import json
  10. import re
  11. import sys
  12. from pathlib import Path
  13. sys.stdout.reconfigure(encoding="utf-8")
  14. ROOT = Path(r"E:\CODE\knowledge_agent")
  15. OUT = ROOT / "output" / "模板_源数据_DMS字段关联.csv"
  16. DATA_FILES = [
  17. p.name for p in (ROOT / "data").rglob("*")
  18. if p.is_file() and p.suffix.lower() in {".xlsx", ".xls", ".csv", ".docx", ".doc", ".pdf"}
  19. ]
  20. DMS_ALIASES: dict[str, str] = {}
  21. try:
  22. raw_dms = json.loads((ROOT / "output" / "dms_scoped_models_fields.json").read_text(encoding="utf-8-sig"))
  23. DMS_ALIASES = {
  24. k: (v.get("modelAlias") or v.get("path") or k)
  25. for k, v in raw_dms.items()
  26. if isinstance(v, dict)
  27. }
  28. except Exception: # noqa: BLE001
  29. pass
  30. def load_module(name: str, path: Path):
  31. spec = importlib.util.spec_from_file_location(name, path)
  32. mod = importlib.util.module_from_spec(spec)
  33. assert spec and spec.loader
  34. spec.loader.exec_module(mod)
  35. return mod
  36. dms_mod = load_module("dms_supplement", ROOT / "scripts" / "generate_dms_supplement.py")
  37. tpl_mod = load_module("template_gen", ROOT / "scripts" / "generate_templates.py")
  38. def norm_header(h: str) -> str:
  39. h = str(h or "").strip()
  40. h = re.sub(r"\*$", "", h)
  41. h = re.sub(r"([^)]*)$", "", h)
  42. return re.sub(r"\s+", "", h)
  43. def parse_field_sources() -> dict[str, list[tuple[str, str]]]:
  44. tree = ast.parse(Path(tpl_mod.__file__).read_text(encoding="utf-8"))
  45. namespace = {k: v for k, v in tpl_mod.__dict__.items() if not k.startswith("__")}
  46. result: dict[str, list[tuple[str, str]]] = {}
  47. for node in ast.walk(tree):
  48. if not isinstance(node, ast.Call):
  49. continue
  50. if not isinstance(node.func, ast.Name) or node.func.id != "_make_file":
  51. continue
  52. if len(node.args) < 3:
  53. continue
  54. filename_node = node.args[0]
  55. headers_node = node.args[2]
  56. try:
  57. filename = ast.literal_eval(filename_node)
  58. headers = ast.literal_eval(headers_node)
  59. except Exception: # noqa: BLE001
  60. continue
  61. if not isinstance(filename, str) or not isinstance(headers, list):
  62. continue
  63. field_sources = []
  64. for kw in node.keywords:
  65. if kw.arg != "field_sources":
  66. continue
  67. try:
  68. value = eval(compile(ast.Expression(body=kw.value), "<tpl>", "eval"),
  69. namespace, namespace)
  70. except Exception: # noqa: BLE001
  71. value = []
  72. if isinstance(value, list):
  73. field_sources = [
  74. (str(a), str(b))
  75. for a, b in value
  76. if isinstance(a, str) and isinstance(b, str)
  77. ]
  78. stem = Path(filename).stem
  79. result[stem] = field_sources
  80. return result
  81. def _split_source(desc: str) -> tuple[str, str]:
  82. desc = desc.strip()
  83. m = re.match(r"(.+?)\s*→\s*(.+)", desc, re.S)
  84. if not m:
  85. return desc, desc
  86. return m.group(1).strip(), m.group(2).strip()
  87. def _file_names(src: str) -> list[str]:
  88. names: list[str] = []
  89. for part in re.split(r"[;;]", src):
  90. part = part.strip()
  91. if not part:
  92. continue
  93. if "新增(融合)" in part or "新增(分类)" in part:
  94. continue
  95. if "关联「" in part or part in {"—", "-"}:
  96. continue
  97. name = re.split(r"[\\/]", part)[-1].strip()
  98. name = re.sub(r"[((][^))]*[))]$", "", name).strip()
  99. if re.search(r"\.(xlsx|xls|csv|docx|doc|pdf)$", name, re.I):
  100. names.append(name)
  101. continue
  102. # 字段来源里可能是“登记表名称”而非完整文件名,尝试在工作区数据文件中匹配
  103. hit = next((f for f in DATA_FILES if name and name in f), None)
  104. if hit:
  105. names.append(hit)
  106. return names
  107. def parse_source(desc: str) -> tuple[str, str]:
  108. src, field = _split_source(desc)
  109. names = _file_names(src)
  110. src_out = ";".join(names)
  111. if "新增(融合)" in desc or "新增(分类)" in desc:
  112. return "", field
  113. if not names:
  114. return "", field
  115. quoted = re.findall(r"「([^」]+)」", field)
  116. if quoted:
  117. field = " / ".join(quoted)
  118. return src_out, field
  119. def split_fields(label: str) -> list[str]:
  120. return [re.sub(r"\s+", "", x) for x in re.split(r"[/、]", label) if re.sub(r"\s+", "", x)]
  121. def main() -> None:
  122. sources = parse_field_sources()
  123. rows: list[dict] = []
  124. last_file: dict[str, str] = {}
  125. for tpl, fields in dms_mod.TEMPLATES.items():
  126. tpl_sources = sources.get(tpl, [])
  127. for field in fields:
  128. dms_source = dms_mod.DMS_SOURCES.get((tpl, field), "")
  129. dms_model = dms_field = ""
  130. if dms_source:
  131. m = re.match(r"^([^.]+)\.(.*)$", dms_source)
  132. if m:
  133. dms_model, dms_field = m.group(1), m.group(2)
  134. dms_model_display = dms_model
  135. if dms_model and dms_model in DMS_ALIASES:
  136. dms_model_display = f"{dms_model}({DMS_ALIASES[dms_model]})"
  137. meta_file = meta_field = ""
  138. matched_desc = ""
  139. for label, desc in tpl_sources:
  140. for lf in split_fields(label):
  141. if norm_header(field) == norm_header(lf) or (
  142. len(norm_header(lf)) >= 2 and norm_header(lf) in norm_header(field)
  143. ):
  144. matched_desc = desc
  145. break
  146. if matched_desc:
  147. break
  148. if matched_desc:
  149. raw_src, _ = _split_source(matched_desc)
  150. if raw_src.strip() in {"同一登记表", "同上", "同文件", "同一文件"}:
  151. meta_file = last_file.get(tpl, "")
  152. meta_field = parse_source(matched_desc)[1]
  153. else:
  154. meta_file, meta_field = parse_source(matched_desc)
  155. if meta_file:
  156. last_file[tpl] = meta_file
  157. if not meta_file:
  158. meta_field = ""
  159. rows.append({
  160. "模板文件名称": f"{tpl}.csv",
  161. "模板字段": field,
  162. "元数据文件": meta_file,
  163. "元数据字段名称": meta_field,
  164. "dms模型": dms_model_display,
  165. "dms字段名称": dms_field,
  166. "是否需要人工增加": "否" if dms_source else "是",
  167. })
  168. OUT.parent.mkdir(parents=True, exist_ok=True)
  169. with OUT.open("w", newline="", encoding="utf-8-sig") as f:
  170. w = csv.writer(f)
  171. w.writerow(["模板文件名称", "模板字段", "元数据文件", "元数据字段名称",
  172. "dms模型", "dms字段名称", "是否需要人工增加"])
  173. for r in rows:
  174. w.writerow([r["模板文件名称"], r["模板字段"], r["元数据文件"],
  175. r["元数据字段名称"], r["dms模型"], r["dms字段名称"],
  176. r["是否需要人工增加"]])
  177. print("输出:", OUT)
  178. print("字段行数:", len(rows))
  179. if __name__ == "__main__":
  180. main()