# -*- coding: utf-8 -*- """校验生成文件未改动原文,并通过 Word COM 取签章位真实页码,生成签章位置清单。""" import json import os import re import sys import time sys.stdout.reconfigure(encoding="utf-8", errors="replace") from docx import Document from docx.oxml.ns import qn DOC_DIR = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(DOC_DIR, "_tmp_xunbi.docx") OUT_NAME = "响应文件-2026年河南产互联制造业数据智能运营平台研发项目-北京智造易科技有限公司.docx" OUT = os.path.join(DOC_DIR, OUT_NAME) # 页码要按实际装订的总册算:承诺书、软著一览表、团队表与技术方案各章已并入其中。 # 总册在 OneDrive 里会被手工补材料(业绩扫描件、查询截图等),页码随之变化, # 因此优先取两处中较新的那一份,并在输出里注明用的是哪份。 FINAL_NAME = "响应文件(完整版)-2026年河南产互联制造业数据智能运营平台研发项目.docx" LIST_NAME = "响应文件签章位置清单.docx" LIST_PATH = os.path.join(DOC_DIR, LIST_NAME) ONEDRIVE_DIR = r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目招投标" def pick_final(): """在本地生成版与 OneDrive 手工修改版之间取较新的一份;可用 argv 直接指定。""" if len(sys.argv) > 1 and os.path.exists(sys.argv[1]): return sys.argv[1], "命令行指定" cands = [(os.path.join(DOC_DIR, FINAL_NAME), "本地生成版"), (os.path.join(ONEDRIVE_DIR, FINAL_NAME), "OneDrive 修改版")] cands = [(p, w) for p, w in cands if os.path.exists(p)] if not cands: return None, None return max(cands, key=lambda x: os.path.getmtime(x[0])) FINAL, FINAL_FROM = pick_final() # ---------------------------------------------------------------- 1) 原文校验 src = Document(SRC) def ptext(el): return "".join(n.text or "" for n in el.iter(qn("w:t"))) ch = list(src.element.body.iterchildren()) c6 = [i for i, e in enumerate(ch) if e.tag == qn("w:p") and ptext(e).strip().startswith("第六章") and "响应文件格式" in ptext(e)] c7 = [i for i, e in enumerate(ch) if e.tag == qn("w:p") and ptext(e).strip().startswith("第七章")] s = c6[-1] t = next(i for i in c7 if i > s) orig_paras = [ptext(e) for e in ch[s:t] if e.tag == qn("w:p")] gen = Document(OUT) gen_paras = [p.text for p in gen.paragraphs] # 原文中作为「填空提示」的固定文字,填写后必然改变,属预期 FILLED_KEYS = [ "项目编号:", "供应商:", "日 期:", "致:", "1、根据已收到的项目编号为", "4、一旦我公司成交", "单位名称:", "单位性质:", "地 址:", "成立时间:", "经营期限:", "姓 名:", "系", "法定代表人:", "本人", "供 应 商:", "法定代表人身份证号码:", "委托代理人:", "委托代理人身份证号码:", "供应商名称(盖章):", "日期:", "承诺单位(签章):", "年 月 日", "对第四章", "对第五章", "供应商名称:", "(采购单位)", "为营造公开", "八、本人作为", "项目", " 年 月 日", ] # 原文第 768 段的手工目录条目(无页码),已整体换成自动目录域,属预期删除 MANUAL_TOC = {"响应函及响应函附录", "法定代表人身份证明书", "法定代表人授权委托书", "资格审查资料", "商务响应文件", "技术响应文件", "其他"} # 原文漏排编号、生成时补齐前缀的段落,比对时按「原文是生成文的后缀」放行 PREFIXED = {"包括但不限于以下资料(根据评分办法要求格式自拟):"} # 原文编号重复、生成时改号的段落(原文 → 生成文) RENUMBERED = {"(一)技术要求的汇总应答": "(二)技术要求的汇总应答"} def is_fill_para(txt): return any(k and txt.strip().startswith(k) for k in FILLED_KEYS) def norm(txt): """比对时忽略空白差异:制表符与连续空格在原文/生成文件中写法不一, 仅因此报「原文缺失」属误报(如封面署名行原文用空格、模板里是制表符)。""" return re.sub(r"\s+", "", txt) # 已按填写要求填入日期的段落,原文的空日期行必然消失,属预期 DATE_LINE = re.compile(r"^年+\s*月+\s*日$") missing = [] gen_set = {norm(x) for x in gen_paras} for op in orig_paras: o = op.strip() if not o or is_fill_para(o) or DATE_LINE.match(norm(o)) or o in MANUAL_TOC: continue if o in PREFIXED and any(x.endswith(norm(o)) for x in gen_set): continue if o in RENUMBERED and norm(RENUMBERED[o]) in gen_set: continue if norm(o) not in gen_set: missing.append(o) print("=" * 66) print("【原文完整性校验】") print(f" 第六章原文非空段落:{len([x for x in orig_paras if x.strip()])}") print(f" 生成文件段落总数:{len(gen_paras)}") if missing: print(f" !以下 {len(missing)} 条原文未在生成文件中找到:") for m in missing[:20]: print(f" - {m[:70]}") else: print(" OK:所有未涉及填写的原文段落均原样保留") # 检查是否混入了非原文的新增条款(响应函应仍为 6 条) # 按「(一)响应函」到「(二)响应函附录」之间的编号段落计数; # 早先用「含我公司」筛选会漏掉以「我们」「贵方」开头的第 5、6 条,造成误报。 def _section(paras, start_key, end_key): try: a = next(i for i, x in enumerate(paras) if start_key in x) b = next(i for i, x in enumerate(paras) if i > a and end_key in x) except StopIteration: return paras return paras[a:b] resp_items = [x for x in _section(gen_paras, "(一)响应函", "(二)响应函附录") if re.match(r"^[1-9]、", x.strip())] print(f" 响应函条目数:{len(resp_items)}(原文为 6 条)") for x in resp_items: print(f" {x.strip()[:40]}") # ---------------------------------------------------------------- 2) 取页码 with open(os.path.join(DOC_DIR, "_tmp_seals.json"), encoding="utf-8") as f: seals = json.load(f) print(f"\n【页码依据】{FINAL_FROM}:{os.path.basename(FINAL or OUT)}") if FINAL: print(" 修改时间 " + time.strftime( "%Y-%m-%d %H:%M", time.localtime(os.path.getmtime(FINAL)))) pages = {} try: import win32com.client as win32 word = win32.DispatchEx("Word.Application") word.Visible = False word.DisplayAlerts = 0 d = word.Documents.Open(FINAL or OUT, ReadOnly=True) d.Repaginate() total = d.ComputeStatistics(2) # wdStatisticPages for key, *_ in seals: try: pages[key] = d.Bookmarks(key).Range.Information(3) except Exception as e: pages[key] = f"?({e.__class__.__name__})" d.Close(False) word.Quit() print(f"\n【页码提取】文档总页数:{total}") except Exception as e: total = "?" print(f"\n[Word COM 不可用] {type(e).__name__}: {e}") seals.sort(key=lambda s: (pages.get(s[0], 999) if isinstance(pages.get(s[0]), int) else 999, s[0])) for key, section, *_ in seals: print(f" {key} 第 {pages.get(key, '?')} 页 {section}") # ---------------------------------------------------------------- 3) 生成清单 from docx.enum.table import WD_TABLE_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.shared import Cm, Pt lst = Document() sec = lst.sections[0] sec.page_width, sec.page_height = Cm(29.7), Cm(21.0) # A4 横向 sec.orientation = 1 sec.left_margin = sec.right_margin = Cm(1.8) sec.top_margin = sec.bottom_margin = Cm(1.8) st = lst.styles["Normal"] st.font.name = "Times New Roman" st.font.size = Pt(10.5) st.element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") h = lst.add_paragraph() h.alignment = WD_ALIGN_PARAGRAPH.CENTER r = h.add_run("响应文件 签章位置清单") r.font.size = Pt(18) r.font.bold = True r._element.get_or_add_rPr().find(qn("w:rFonts")).set(qn("w:eastAsia"), "黑体") \ if r._element.get_or_add_rPr().find(qn("w:rFonts")) is not None else None sub = lst.add_paragraph() sub.alignment = WD_ALIGN_PARAGRAPH.CENTER rs = sub.add_run(f"{FINAL_NAME if FINAL else OUT_NAME}" f" | 共 {total} 页 | " f"下列 {len(seals)} 处均为询比文件明文要求,非明文要求者未列入") rs.font.size = Pt(9) lst.add_paragraph() tb = lst.add_table(rows=1, cols=6) tb.style = "Table Grid" tb.alignment = WD_TABLE_ALIGNMENT.CENTER hdrs = ["序号", "页码", "所在章节 / 位置", "盖章要求", "签字要求", "依据出处(询比文件原文)"] widths = [1.1, 1.8, 5.4, 3.9, 5.4, 8.5] from docx.oxml import OxmlElement tb.autofit = False _lay = OxmlElement("w:tblLayout") _lay.set(qn("w:type"), "fixed") tb._tbl.tblPr.append(_lay) for i, htxt in enumerate(hdrs): c = tb.rows[0].cells[i] c.text = "" rr = c.paragraphs[0].add_run(htxt) rr.font.bold = True rr.font.size = Pt(10) for n, (key, section, stamp, sign, basis) in enumerate(seals, 1): cells = tb.add_row().cells for i, v in enumerate([str(n), f"第 {pages.get(key, '?')} 页", section, stamp, sign, basis]): cells[i].text = "" rr = cells[i].paragraphs[0].add_run(v) rr.font.size = Pt(9.5) cells[0].paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER cells[1].paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER for row in tb.rows: for i, w in enumerate(widths): row.cells[i].width = Cm(w) lst.add_paragraph() p2 = lst.add_paragraph() r2 = p2.add_run("另:明文要求「加盖单位公章」的复印件(共 2 处)") r2.font.bold = True r2.font.size = Pt(11) for txt in [ "1. 法定代表人身份证复印件 —— 第六章「二、法定代表人身份证明书」末尾原文:" "附:法定代表人身份证复印件,加盖单位公章。", "2. 委托代理人身份证复印件 —— 第六章「三、法定代表人授权委托书」末尾原文:" "附:委托代理人身份证复印件,加盖单位公章。", ]: pp = lst.add_paragraph() pp.paragraph_format.left_indent = Pt(18) pp.add_run(txt).font.size = Pt(10) lst.save(LIST_PATH) print(f"\n[完成] {LIST_PATH}") import shutil if os.path.isdir(ONEDRIVE_DIR): shutil.copy2(LIST_PATH, os.path.join(ONEDRIVE_DIR, LIST_NAME)) print(f"[已同步] {os.path.join(ONEDRIVE_DIR, LIST_NAME)}")