# -*- coding: utf-8 -*- """Unify version records to V0.1 初版建立 + V0.2 增加版本号 in delivery docx.""" from __future__ import annotations import re from pathlib import Path from docx import Document from docx.shared import Pt BASE = Path( r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目管理\产互联项目管理\交付文档" ) V01 = "V0.1" V02 = "V0.2" NOTE_V01 = "初版建立" NOTE_V02 = "增加版本号" def parse_metadata(doc: Document) -> dict[str, str]: meta = {"author": "智造易项目组", "created": "2026-06-10", "updated": "2026-06-10"} for p in doc.paragraphs[:25]: t = p.text.strip() if t.startswith("文档作者:"): meta["author"] = t.split(":", 1)[1].strip() if t.startswith("创建日期:"): meta["created"] = t.split(":", 1)[1].strip() meta["_has_created"] = True elif t.startswith("更新日期:"): meta["updated"] = t.split(":", 1)[1].strip() elif t.startswith("版本:"): m = re.search(r"作者:\s*(.+)$", t) if m: meta["author"] = m.group(1).strip() m = re.search(r"日期:\s*([^作者]+?)(?:\s+作者:|$)", t) if m: d = normalize_date(m.group(1).strip()) meta["updated"] = d if not meta.get("_has_created"): meta["created"] = d if doc.tables: for row in doc.tables[0].rows: cells = [c.text.strip() for c in row.cells] if len(cells) >= 2 and cells[0] == "编制单位" and cells[1]: meta["author"] = cells[1] if len(cells) >= 2 and cells[0] == "版本" and cells[1].startswith("V"): pass return meta def normalize_date(text: str) -> str: text = text.strip() m = re.match(r"(\d{4})年(\d{1,2})月(\d{1,2})日", text) if m: y, mo, d = m.groups() return f"{y}-{int(mo):02d}-{int(d):02d}" m = re.match(r"(\d{4})/(\d{1,2})/(\d{1,2})", text) if m: y, mo, d = m.groups() return f"{y}-{int(mo):02d}-{int(d):02d}" return text def set_cell_text(cell, text: str, *, bold: bool = False) -> None: cell.text = "" run = cell.paragraphs[0].add_run(text) run.font.size = Pt(10.5) run.bold = bold def resize_table_rows(table, data_row_count: int) -> None: target = data_row_count + 1 while len(table.rows) > target: table._tbl.remove(table.rows[-1]._tr) while len(table.rows) < target: table.add_row() def find_paragraph(doc: Document, text: str) -> int | None: for i, p in enumerate(doc.paragraphs): if p.text.strip() == text: return i return None def find_change_log_table(doc: Document): idx = find_paragraph(doc, "更改记录") if idx is None: return None body = list(doc.element.body) pos = body.index(doc.paragraphs[idx]._element) for el in body[pos + 1 : pos + 10]: if not el.tag.endswith("tbl"): continue for table in doc.tables: if table._tbl is not el: continue header = [c.text.strip() for c in table.rows[0].cells] header_text = "".join(header) if "变更说明" in header_text and ("版本" in header_text or "日期" in header_text): return table return None def table_after_heading(doc: Document, heading: str): idx = find_paragraph(doc, heading) if idx is None: return None body = list(doc.element.body) pos = body.index(doc.paragraphs[idx]._element) for el in body[pos + 1 : pos + 6]: if el.tag.endswith("tbl"): for table in doc.tables: if table._tbl is el: return table return None def standard_version_rows(meta: dict[str, str]) -> list[tuple[str, str, str, str]]: return [ (V01, meta["created"], meta["author"], NOTE_V01), (V02, meta["updated"], meta["author"], NOTE_V02), ] def fill_standard_version_table(table, meta: dict[str, str]) -> None: rows = standard_version_rows(meta) resize_table_rows(table, len(rows)) headers = ["版本", "日期", "修订人", "修订说明"] for ci, val in enumerate(headers): set_cell_text(table.rows[0].cells[ci], val, bold=True) for ri, row_data in enumerate(rows, start=1): for ci, val in enumerate(row_data): set_cell_text(table.rows[ri].cells[ci], val) def fill_change_log_table(table, meta: dict[str, str]) -> None: header_cells = [c.text.strip() for c in table.rows[0].cells] if "变更说明" not in header_cells and "修订说明" not in "".join(header_cells): return rows = standard_version_rows(meta) resize_table_rows(table, len(rows)) # 日期 | 版本 | 修订人/姓名 | 变更说明 name_col = "姓名" if "姓名" in header_cells else "修订人" mapping = { "日期": lambda r: r[1], "版本": lambda r: r[0], name_col: lambda r: r[2], "修订人": lambda r: r[2], "姓名": lambda r: r[2], "变更说明": lambda r: r[3], "修订说明": lambda r: r[3], } for ci, title in enumerate(header_cells): if title in ("版本", "日期", "修订人", "姓名", "变更说明", "修订说明"): set_cell_text(table.rows[0].cells[ci], title, bold=True) for ri, row_data in enumerate(rows, start=1): for ci, title in enumerate(header_cells): fn = mapping.get(title) if fn: set_cell_text(table.rows[ri].cells[ci], fn(row_data)) def update_cover_version(doc: Document, meta: dict[str, str]) -> int: changed = 0 for p in doc.paragraphs[:25]: t = p.text.strip() if t.startswith("当前版本:"): new = f"当前版本:{V02}" if p.text != new: if p.runs: p.runs[0].text = new for r in p.runs[1:]: r.text = "" else: p.add_run(new) changed += 1 elif t.startswith("版本:"): new = f"版本:{V02} 日期:{format_cn_date(meta['updated'])} 作者:{meta['author']}" if p.text.strip() != new: if p.runs: p.runs[0].text = new for r in p.runs[1:]: r.text = "" else: p.add_run(new) changed += 1 if doc.tables: row0 = doc.tables[0].rows for row in row0: cells = [c.text.strip() for c in row.cells] if len(cells) >= 2 and cells[0] == "版本" and cells[1].startswith("V"): if cells[1] != V02: set_cell_text(row.cells[1], V02) changed += 1 return changed def format_cn_date(iso_date: str) -> str: m = re.match(r"(\d{4})-(\d{2})-(\d{2})", iso_date) if not m: return iso_date y, mo, d = m.groups() return f"{y}年{int(mo)}月{int(d)}日" def process_file(path: Path) -> None: doc = Document(path) meta = parse_metadata(doc) meta["updated"] = normalize_date(meta["updated"]) meta["created"] = normalize_date(meta["created"]) cover_changes = update_cover_version(doc, meta) version_table = table_after_heading(doc, "版本记录") version_updated = False if version_table is not None: fill_standard_version_table(version_table, meta) version_updated = True change_table = find_change_log_table(doc) change_updated = False if change_table is not None: fill_change_log_table(change_table, meta) change_updated = True doc.save(path) print( f"{path.name}: cover={cover_changes} " f"版本记录={'Y' if version_updated else 'N'} " f"更改记录={'Y' if change_updated else 'N'}" ) def main() -> None: for mod in ["S0", "S1", "S2", "S3", "S4"]: mod_dir = BASE / mod for path in sorted(mod_dir.glob("*.docx")): process_file(path) if __name__ == "__main__": main()