| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- # -*- coding: utf-8 -*-
- """把各章 Word 合成完整的《附件五 技术方案》《附件六 实施与服务方案》。
- 生成封面 + 自动目录,正文页码从 1 起连续编号(封面与目录不编号),
- 并同步导出 PDF 便于打印前校版。
- 用法:
- python doc/_gen_bid_merge.py 合并两个附件
- python doc/_gen_bid_merge.py 五 只合并附件五
- """
- import io
- import os
- import shutil
- import sys
- import time
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
- from docx.enum.text import WD_ALIGN_PARAGRAPH # noqa: E402
- from docx.oxml import OxmlElement # noqa: E402
- from docx.oxml.ns import qn # noqa: E402
- from docx.shared import Pt # noqa: E402
- from _bid_docx_kit import HEI, SONG, TITLE_C, _font, _spacing, new_doc # noqa
- 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\项目\项目招投标"
- PROJECT = "2026 年河南产互联制造业数据智能运营平台研发项目"
- PROJECT_NO = "0635-202611FW0036"
- BUYER = "河南产业互联网联合发展有限公司"
- COMPANY = "北京智造易科技有限公司"
- SUBMIT_DATE = "2026 年 8 月 5 日"
- ANNEX = {
- "五": ("附件五 技术方案", "技术方案", [
- (1, "项目需求理解及综合分析"),
- (2, "系统功能展示"),
- (3, "系统建设方案"),
- (4, "智能诊断与分析方案"),
- (5, "UAT 测试与质量保障"),
- ]),
- "六": ("附件六 实施与服务方案", "实施与服务方案", [
- (1, "质量与工期保障"),
- (2, "应急支撑方案"),
- (3, "售后服务方案"),
- (4, "项目培训方案"),
- ]),
- }
- # Word 常量
- WD_SECTION_BREAK_NEXT_PAGE = 2
- WD_STORY = 6
- WD_FORMAT_DOCX = 16
- WD_EXPORT_PDF = 17
- WD_HEADER_FOOTER_PRIMARY = 1
- # ----------------------------------------------------------------- 前置页
- def _center(doc, text, name_cn, pt, bold=False, color=None, before=0, after=0):
- p = doc.add_paragraph()
- p.alignment = WD_ALIGN_PARAGRAPH.CENTER
- _spacing(p, before=before, after=after, line=1.5)
- _font(p.add_run(text), name_cn, pt, bold=bold, color=color)
- return p
- def _clear_footer(sec):
- """封面与目录不编页码:断开继承并清空页脚。"""
- sec.footer.is_linked_to_previous = False
- for extra in list(sec.footer.paragraphs[1:]):
- extra._p.getparent().remove(extra._p)
- p = sec.footer.paragraphs[0]
- for r in list(p.runs):
- r._element.getparent().remove(r._element)
- def _toc_field(doc):
- """插入 TOC 域;合并后由 Word 计算条目与页码。"""
- p = doc.add_paragraph()
- _spacing(p, line=1.5)
- for kind, payload in (
- ("begin", None),
- # 只列到二级:三级标题过百条会撑成 7 页目录,评审翻检反而不便
- ("instr", ' TOC \\o "1-2" \\h \\z \\u '),
- ("separate", None),
- ("text", "右键选择“更新域”可刷新目录。"),
- ("end", None)):
- r = OxmlElement("w:r")
- if kind == "instr":
- it = OxmlElement("w:instrText")
- it.set(qn("xml:space"), "preserve")
- it.text = payload
- r.append(it)
- elif kind == "text":
- t = OxmlElement("w:t")
- t.text = payload
- r.append(t)
- else:
- fc = OxmlElement("w:fldChar")
- fc.set(qn("w:fldCharType"), kind)
- r.append(fc)
- p._p.append(r)
- for run in p.runs:
- _font(run, SONG, 12)
- def build_front(att):
- """封面 + 目录,两节独立且均不编页码。"""
- title, _, _ = ANNEX[att]
- doc = new_doc()
- _clear_footer(doc.sections[0])
- _center(doc, "响应文件", HEI, 20, color=TITLE_C, before=170, after=10)
- _center(doc, title, HEI, 28, color=TITLE_C, after=30)
- _center(doc, PROJECT, SONG, 15, bold=True, after=6)
- _center(doc, f"项目编号:{PROJECT_NO}", SONG, 13, after=48)
- _center(doc, f"采购人:{BUYER}", SONG, 14, after=8)
- _center(doc, f"供应商:{COMPANY}", SONG, 14, bold=True, after=8)
- _center(doc, SUBMIT_DATE, SONG, 14, after=0)
- from docx.enum.section import WD_SECTION
- sec = doc.add_section(WD_SECTION.NEW_PAGE)
- _clear_footer(sec)
- _center(doc, "目 录", HEI, 18, color=TITLE_C, before=6, after=18)
- _toc_field(doc)
- tmp = os.path.join(HERE, f"_tmp_front_{att}.docx")
- doc.save(tmp)
- return tmp
- # ----------------------------------------------------------------- 合并
- def chapter_paths(att):
- _, kind, chapters = ANNEX[att]
- out = []
- for no, name in chapters:
- p = os.path.join(HERE, f"附件{att}-{kind}-第{no}章 {name}.docx")
- if not os.path.exists(p):
- raise SystemExit(f"缺少章节文档:{os.path.basename(p)}")
- out.append(p)
- return out
- def merge(att, word):
- front = build_front(att)
- paths = chapter_paths(att)
- doc = word.Documents.Open(front)
- doc.Activate()
- sel = word.Selection
- for p in paths:
- sel.EndKey(WD_STORY)
- sel.InsertBreak(WD_SECTION_BREAK_NEXT_PAGE)
- sel.EndKey(WD_STORY)
- sel.InsertFile(p)
- # 封面、目录不编号;正文首节起从 1 连续编号
- body_start = 3
- for i in range(body_start, doc.Sections.Count + 1):
- pn = doc.Sections(i).Footers(WD_HEADER_FOOTER_PRIMARY).PageNumbers
- pn.RestartNumberingAtSection = (i == body_start)
- if i == body_start:
- pn.StartingNumber = 1
- for _ in range(2): # 目录页数变化会影响自身,刷两遍
- if doc.TablesOfContents.Count:
- doc.TablesOfContents(1).Update()
- doc.Fields.Update()
- title = ANNEX[att][0].replace(" ", "-")
- out = os.path.join(HERE, f"{title}(完整版).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)
- doc.SaveAs2(out, FileFormat=WD_FORMAT_DOCX)
- doc.ExportAsFixedFormat(pdf, WD_EXPORT_PDF)
- pages = doc.ComputeStatistics(2) # wdStatisticPages
- toc_items = (doc.TablesOfContents(1).Range.Paragraphs.Count
- if doc.TablesOfContents.Count else 0)
- doc.Close(False)
- os.remove(front)
- print(f"[完成] {os.path.basename(out)} 共 {pages} 页 目录 {toc_items} 条")
- if os.path.isdir(SYNC_DIR):
- shutil.copy2(out, os.path.join(SYNC_DIR, os.path.basename(out)))
- return out, pdf
- def main():
- want = [a for a in sys.argv[1:] if a in ANNEX] or list(ANNEX)
- 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:
- for att in want:
- merge(att, word)
- finally:
- word.Quit()
- if __name__ == "__main__":
- main()
|