_unify_version_records.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. # -*- coding: utf-8 -*-
  2. """Unify version records to V0.1 初版建立 + V0.2 增加版本号 in delivery docx."""
  3. from __future__ import annotations
  4. import re
  5. from pathlib import Path
  6. from docx import Document
  7. from docx.shared import Pt
  8. BASE = Path(
  9. r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目管理\产互联项目管理\交付文档"
  10. )
  11. V01 = "V0.1"
  12. V02 = "V0.2"
  13. NOTE_V01 = "初版建立"
  14. NOTE_V02 = "增加版本号"
  15. def parse_metadata(doc: Document) -> dict[str, str]:
  16. meta = {"author": "智造易项目组", "created": "2026-06-10", "updated": "2026-06-10"}
  17. for p in doc.paragraphs[:25]:
  18. t = p.text.strip()
  19. if t.startswith("文档作者:"):
  20. meta["author"] = t.split(":", 1)[1].strip()
  21. if t.startswith("创建日期:"):
  22. meta["created"] = t.split(":", 1)[1].strip()
  23. meta["_has_created"] = True
  24. elif t.startswith("更新日期:"):
  25. meta["updated"] = t.split(":", 1)[1].strip()
  26. elif t.startswith("版本:"):
  27. m = re.search(r"作者:\s*(.+)$", t)
  28. if m:
  29. meta["author"] = m.group(1).strip()
  30. m = re.search(r"日期:\s*([^作者]+?)(?:\s+作者:|$)", t)
  31. if m:
  32. d = normalize_date(m.group(1).strip())
  33. meta["updated"] = d
  34. if not meta.get("_has_created"):
  35. meta["created"] = d
  36. if doc.tables:
  37. for row in doc.tables[0].rows:
  38. cells = [c.text.strip() for c in row.cells]
  39. if len(cells) >= 2 and cells[0] == "编制单位" and cells[1]:
  40. meta["author"] = cells[1]
  41. if len(cells) >= 2 and cells[0] == "版本" and cells[1].startswith("V"):
  42. pass
  43. return meta
  44. def normalize_date(text: str) -> str:
  45. text = text.strip()
  46. m = re.match(r"(\d{4})年(\d{1,2})月(\d{1,2})日", text)
  47. if m:
  48. y, mo, d = m.groups()
  49. return f"{y}-{int(mo):02d}-{int(d):02d}"
  50. m = re.match(r"(\d{4})/(\d{1,2})/(\d{1,2})", text)
  51. if m:
  52. y, mo, d = m.groups()
  53. return f"{y}-{int(mo):02d}-{int(d):02d}"
  54. return text
  55. def set_cell_text(cell, text: str, *, bold: bool = False) -> None:
  56. cell.text = ""
  57. run = cell.paragraphs[0].add_run(text)
  58. run.font.size = Pt(10.5)
  59. run.bold = bold
  60. def resize_table_rows(table, data_row_count: int) -> None:
  61. target = data_row_count + 1
  62. while len(table.rows) > target:
  63. table._tbl.remove(table.rows[-1]._tr)
  64. while len(table.rows) < target:
  65. table.add_row()
  66. def find_paragraph(doc: Document, text: str) -> int | None:
  67. for i, p in enumerate(doc.paragraphs):
  68. if p.text.strip() == text:
  69. return i
  70. return None
  71. def find_change_log_table(doc: Document):
  72. idx = find_paragraph(doc, "更改记录")
  73. if idx is None:
  74. return None
  75. body = list(doc.element.body)
  76. pos = body.index(doc.paragraphs[idx]._element)
  77. for el in body[pos + 1 : pos + 10]:
  78. if not el.tag.endswith("tbl"):
  79. continue
  80. for table in doc.tables:
  81. if table._tbl is not el:
  82. continue
  83. header = [c.text.strip() for c in table.rows[0].cells]
  84. header_text = "".join(header)
  85. if "变更说明" in header_text and ("版本" in header_text or "日期" in header_text):
  86. return table
  87. return None
  88. def table_after_heading(doc: Document, heading: str):
  89. idx = find_paragraph(doc, heading)
  90. if idx is None:
  91. return None
  92. body = list(doc.element.body)
  93. pos = body.index(doc.paragraphs[idx]._element)
  94. for el in body[pos + 1 : pos + 6]:
  95. if el.tag.endswith("tbl"):
  96. for table in doc.tables:
  97. if table._tbl is el:
  98. return table
  99. return None
  100. def standard_version_rows(meta: dict[str, str]) -> list[tuple[str, str, str, str]]:
  101. return [
  102. (V01, meta["created"], meta["author"], NOTE_V01),
  103. (V02, meta["updated"], meta["author"], NOTE_V02),
  104. ]
  105. def fill_standard_version_table(table, meta: dict[str, str]) -> None:
  106. rows = standard_version_rows(meta)
  107. resize_table_rows(table, len(rows))
  108. headers = ["版本", "日期", "修订人", "修订说明"]
  109. for ci, val in enumerate(headers):
  110. set_cell_text(table.rows[0].cells[ci], val, bold=True)
  111. for ri, row_data in enumerate(rows, start=1):
  112. for ci, val in enumerate(row_data):
  113. set_cell_text(table.rows[ri].cells[ci], val)
  114. def fill_change_log_table(table, meta: dict[str, str]) -> None:
  115. header_cells = [c.text.strip() for c in table.rows[0].cells]
  116. if "变更说明" not in header_cells and "修订说明" not in "".join(header_cells):
  117. return
  118. rows = standard_version_rows(meta)
  119. resize_table_rows(table, len(rows))
  120. # 日期 | 版本 | 修订人/姓名 | 变更说明
  121. name_col = "姓名" if "姓名" in header_cells else "修订人"
  122. mapping = {
  123. "日期": lambda r: r[1],
  124. "版本": lambda r: r[0],
  125. name_col: lambda r: r[2],
  126. "修订人": lambda r: r[2],
  127. "姓名": lambda r: r[2],
  128. "变更说明": lambda r: r[3],
  129. "修订说明": lambda r: r[3],
  130. }
  131. for ci, title in enumerate(header_cells):
  132. if title in ("版本", "日期", "修订人", "姓名", "变更说明", "修订说明"):
  133. set_cell_text(table.rows[0].cells[ci], title, bold=True)
  134. for ri, row_data in enumerate(rows, start=1):
  135. for ci, title in enumerate(header_cells):
  136. fn = mapping.get(title)
  137. if fn:
  138. set_cell_text(table.rows[ri].cells[ci], fn(row_data))
  139. def update_cover_version(doc: Document, meta: dict[str, str]) -> int:
  140. changed = 0
  141. for p in doc.paragraphs[:25]:
  142. t = p.text.strip()
  143. if t.startswith("当前版本:"):
  144. new = f"当前版本:{V02}"
  145. if p.text != new:
  146. if p.runs:
  147. p.runs[0].text = new
  148. for r in p.runs[1:]:
  149. r.text = ""
  150. else:
  151. p.add_run(new)
  152. changed += 1
  153. elif t.startswith("版本:"):
  154. new = f"版本:{V02} 日期:{format_cn_date(meta['updated'])} 作者:{meta['author']}"
  155. if p.text.strip() != new:
  156. if p.runs:
  157. p.runs[0].text = new
  158. for r in p.runs[1:]:
  159. r.text = ""
  160. else:
  161. p.add_run(new)
  162. changed += 1
  163. if doc.tables:
  164. row0 = doc.tables[0].rows
  165. for row in row0:
  166. cells = [c.text.strip() for c in row.cells]
  167. if len(cells) >= 2 and cells[0] == "版本" and cells[1].startswith("V"):
  168. if cells[1] != V02:
  169. set_cell_text(row.cells[1], V02)
  170. changed += 1
  171. return changed
  172. def format_cn_date(iso_date: str) -> str:
  173. m = re.match(r"(\d{4})-(\d{2})-(\d{2})", iso_date)
  174. if not m:
  175. return iso_date
  176. y, mo, d = m.groups()
  177. return f"{y}年{int(mo)}月{int(d)}日"
  178. def process_file(path: Path) -> None:
  179. doc = Document(path)
  180. meta = parse_metadata(doc)
  181. meta["updated"] = normalize_date(meta["updated"])
  182. meta["created"] = normalize_date(meta["created"])
  183. cover_changes = update_cover_version(doc, meta)
  184. version_table = table_after_heading(doc, "版本记录")
  185. version_updated = False
  186. if version_table is not None:
  187. fill_standard_version_table(version_table, meta)
  188. version_updated = True
  189. change_table = find_change_log_table(doc)
  190. change_updated = False
  191. if change_table is not None:
  192. fill_change_log_table(change_table, meta)
  193. change_updated = True
  194. doc.save(path)
  195. print(
  196. f"{path.name}: cover={cover_changes} "
  197. f"版本记录={'Y' if version_updated else 'N'} "
  198. f"更改记录={'Y' if change_updated else 'N'}"
  199. )
  200. def main() -> None:
  201. for mod in ["S0", "S1", "S2", "S3", "S4"]:
  202. mod_dir = BASE / mod
  203. for path in sorted(mod_dir.glob("*.docx")):
  204. process_file(path)
  205. if __name__ == "__main__":
  206. main()