# -*- coding: utf-8 -*- """把承诺书、软著一览表、团队表与技术/实施方案各章并入响应文件,成一册交付。 响应文件由 _gen_bid_response.py 生成,其中留有 @@MERGE:key@@ 占位段;本脚本用 Word 定位每个占位段,就地插入对应文件内容,然后统一页码、刷新目录并导出 PDF。 用法: python doc/_gen_bid_final.py 合并全部 python doc/_gen_bid_final.py --list 只列出占位标记与对应文件,不改文档 """ import glob import io import os import shutil import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") HERE = os.path.dirname(os.path.abspath(__file__)) SYNC_DIR = r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目招投标" MERGE_DIR = os.path.join(HERE, "_bid_merge_parts") # Word 常量 WD_STORY = 6 WD_SECTION_BREAK_NEXT_PAGE = 2 WD_SECTION_BREAK_CONTINUOUS = 3 WD_FIND_STOP = 0 WD_COLLAPSE_START = 1 WD_FORMAT_DOCX = 16 WD_EXPORT_PDF = 17 WD_HEADER_FOOTER_PRIMARY = 1 WD_STAT_PAGES = 2 # 占位标记 → (待插入文件, 分节方式) # 承诺书、一览表、团队表前面已有带段前分页的组织标题,用「连续」分节符可与标题 # 同页,不再多出一页只有标题的空页;各章无组织标题,用「下一页」分节符另起页。 PARTS = [ ("invoice", "承诺书-能够提供增值税专用发票.docx", "cont"), ("consortium", "承诺书-不组成联合体及中选后不转包.docx", "cont"), ("credit", "承诺书-资格条件及信用状况.docx", "cont"), ("swtable", "软件著作权登记证书一览表.docx", "cont"), ("team", "项目团队岗位职责与人员配备表.docx", "cont"), ] for _no in range(4, 13): _hit = glob.glob(os.path.join(MERGE_DIR, f"六3-{_no:02d} *.docx")) PARTS.append((f"ch{_no:02d}", os.path.join("_bid_merge_parts", os.path.basename(_hit[0])) if _hit else None, "next")) def resolve(): out = [] for key, rel, brk in PARTS: if rel is None: raise SystemExit(f"缺少 {key} 对应文件(_bid_merge_parts 未生成?)") path = os.path.join(HERE, rel) if not os.path.exists(path): raise SystemExit(f"缺少待并入文件:{rel}") out.append((key, path, brk)) return out def response_doc(): hits = glob.glob(os.path.join(HERE, "响应文件-*研发项目*.docx")) if not hits: raise SystemExit("未找到响应文件,请先运行 _gen_bid_response.py") return hits[0] def insert_at_marker(word, doc, key, path, brk): """定位 @@MERGE:key@@ 所在段落,整段删除后在原位插入外部文件。 必须先插分节符:被插入文件自带的节属性(页边距、横向页)会作用到它之前的 内容,不隔开会把响应文件原有版式一起改掉。标记段整段删除,否则残留的空段 落会各自占掉一页。 """ sel = word.Selection sel.HomeKey(WD_STORY) f = sel.Find f.ClearFormatting() f.Text = f"@@MERGE:{key}@@" f.Forward = True f.Wrap = WD_FIND_STOP f.MatchCase = True if not f.Execute(): raise SystemExit(f"文档中找不到占位标记 {key}") sel.Paragraphs(1).Range.Select() sel.Delete() sel.InsertBreak(WD_SECTION_BREAK_CONTINUOUS if brk == "cont" else WD_SECTION_BREAK_NEXT_PAGE) sel.InsertFile(path) return True def unify_page_numbers(doc): """全册连续编号:各节页脚继承首节,且除首节外都不重启编号。""" for i in range(2, doc.Sections.Count + 1): sec = doc.Sections(i) ft = sec.Footers(WD_HEADER_FOOTER_PRIMARY) ft.LinkToPrevious = True ft.PageNumbers.RestartNumberingAtSection = False pn = doc.Sections(1).Footers(WD_HEADER_FOOTER_PRIMARY).PageNumbers pn.RestartNumberingAtSection = True pn.StartingNumber = 1 def main(): parts = resolve() if "--list" in sys.argv: for key, path, brk in parts: print(f" {key:<10} [{brk}] → {os.path.relpath(path, HERE)}") return src = response_doc() out = os.path.join(HERE, "响应文件(完整版)-" "2026年河南产互联制造业数据智能运营平台研发项目.docx") pdf = os.path.join(HERE, "_bid_preview", os.path.basename(out)[:-5] + ".pdf") os.makedirs(os.path.dirname(pdf), exist_ok=True) for f in (out, pdf): if os.path.exists(f): os.remove(f) shutil.copy2(src, out) import win32com.client as win32 word = None for attempt in range(4): try: word = win32.gencache.EnsureDispatch("Word.Application") word.Visible = False word.DisplayAlerts = 0 break except Exception as e: # Word COM 偶发拒绝调用 print(f" 启动 Word 重试 {attempt + 1}/4:{e}") time.sleep(4) if word is None: raise SystemExit("无法启动 Word") try: doc = word.Documents.Open(out) doc.Activate() for key, path, brk in parts: insert_at_marker(word, doc, key, path, brk) print(f" 已并入 {key:<10} {os.path.basename(path)}") unify_page_numbers(doc) for _ in range(3): # 目录页数变化会反过来影响页码,多刷几遍 if doc.TablesOfContents.Count: doc.TablesOfContents(1).Update() doc.Fields.Update() doc.Repaginate() pages = doc.ComputeStatistics(WD_STAT_PAGES) toc_items = (doc.TablesOfContents(1).Range.Paragraphs.Count if doc.TablesOfContents.Count else 0) secs = doc.Sections.Count doc.SaveAs2(out, FileFormat=WD_FORMAT_DOCX) doc.ExportAsFixedFormat(pdf, WD_EXPORT_PDF) doc.Close(False) finally: word.Quit() size = os.path.getsize(out) / 1024 / 1024 print(f"\n[完成] {os.path.basename(out)}") print(f" {pages} 页 {secs} 节 目录 {toc_items} 条 {size:.1f} MB") if os.path.isdir(SYNC_DIR): dst = os.path.join(SYNC_DIR, os.path.basename(out)) # 交付目录里的总册会被手工补材料(业绩扫描件、查询截图等)。若那份比本次 # 生成的更新,直接覆盖会丢掉手工内容,故先备份再同步。 if os.path.exists(dst) and os.path.getmtime(dst) > os.path.getmtime(src): bak = dst[:-5] + time.strftime("_手工修改备份_%m%d%H%M") + ".docx" shutil.copy2(dst, bak) print(f"[注意] 交付目录那份比生成源更新,疑似手工改过,已备份为\n" f" {os.path.basename(bak)}") shutil.copy2(out, dst) print(f"[已同步] {SYNC_DIR}") if __name__ == "__main__": main()