| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- # -*- 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()
|