| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- # -*- 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)}")
|