文档定点无损耗修订法(.docx zipfile 手术)
适用场景
- 用户改过的 docx 视为「最终版」:禁止 python-docx
save()整表重建 / 覆盖 / 还原;只能做定点修改。 - 任何插入、修改都不得改变既有段落的行距、段距、对齐、字体字号,以及表格边框/底纹/居中、图片位置。
- 存在口径约束,如:不写具体部门名、不用表格(纯文字描述)、仅限特定某单位(如某单位)。
- 需要去 AI 味、改错别字、核对上级/董事长要求是否全覆盖。
- 源文件保留不动;删除/改名被环境安全机制拦截时改用延迟改名。
核心原则(本项目沉淀的标准)
- 排版零变动:插入/修改前后,H2 数、表格数、图片数、底纹数必须一致(用计数断言校验)。
- 定点修改:仅改指定段落/单元格,保留其格式与结构;绝不整表重建。
- 口径约束:不写具体部门名(只写"对接事项清单"类表述);不用表格,纯文字以"一是/二是…"分条;仅限指定某单位。
- 公文/安防专业语体:措辞用公文与安防专业表述,去掉 AI 味(见末尾替换清单)。
- 源文件保留 + 先备份:改前
shutil.copy2(src, src+".bak_说明");不破坏原文件。
操作流程
- 备份:
shutil.copy2(SRC, SRC+".bak_时间戳或说明")。若已存在则跳过。 - 读取:
zipfile.ZipFile(SRC).read('word/document.xml').decode('utf-8')。 - 定位锚点:用
xml.rfind('<w:t>'+标题+'</w:t>')取正文末次出现(目录缓存区也可能含章节标题文字,必须排除);插入多段时加位置断言(校验插入点前后的关键标题索引)。 - 构造片段:克隆现有同类段落/表格的完整
<w:p>/<w:tbl>结构,只替换<w:t>文本与paraId,其余样式(pStyle、spacing、rFonts、sz、ind、jc)原样保留。 - 插入:在锚点段落边界(
xml.rfind('<w:p', 0, i)找段首)前/后拼接;多段用insert_before/insert_after辅助函数。 - 写回:遍历
zin.infolist(),仅把word/document.xml换成新字节,其余成员原样writestr(保持ZIP_DEFLATED与原压缩方式)。 - 校验:
- H2 数 =xml.count('<w:pStyle w:val="4"/>')不变;
- 表格数 =xml.count('<w:tbl>')不变;
- 图片数 =xml.count('<v:imagedata') + xml.count('<w:drawing>')不变;
- 断言新增关键字存在、锚点顺序正确。
关键技术
- 绕开 python-docx:
save()会重写样式、破坏版式(行距、表格底纹、图片锚点全乱)。一律用 zipfile 改word/document.xml文本。 - 锚点取正文末次出现:
rfind而非find,避免命中目录缓存区。 - 克隆样式:新段落复制相邻同类段落的完整
<w:p>结构,仅改文字与paraId(用新十六进制避免重复)。 - 表格克隆:复制一个现有表格的 XML,改行列数与单元格文本,保持
tblStyle、单线边框、表头底纹、列宽合计不变。 - 锁文件处理:预览进程持只读共享句柄,
os.rename报WinError 32;本环境.docx删除被安全删除垫片拦截 → 用MoveFileExW(..., 0x4)(MOVEFILE_DELAY_UNTIL_REBOOT)注册重启延迟改名,不要直接删。 - TOC 刷新:在
word/settings.xml加<w:updateFields w:val="true"/>,Word 打开时按提示更新域;目录缓存区文字无需手动改。 - 定位正文子节须排除 TOC 域条目:目录条目与正文子节标题文字完全相同,且往往带
HYPERLINK/fldChar/instrText域代码。只用find/rfind按标题文字定位会命中 TOC 条目,导致锚点偏到文档前部、误把别的章节(甚至其表格)圈进本节的"正文区域"。定位函数必须同时排除instrText、fldChar、HYPERLINK(仅靠instrText不够,存在无域代码残留的 TOC 条目),只命中真正 style3/4 且无域代码的正文标题。 - 替换节前先断言不含表格:用「标题→下一标题」切出的"本节正文区域"可能内嵌
<w:tbl>。若直接整段替换会误删表格。替换前assert '<w:tbl' not in 本节区域;含表格的薄节改用「末尾追加」而非「整段替换」,保留其表格。 - 去 AI 味验证:先把
<w:t>文本抽成干净纯文本(去掉 run 内<w:br/>等空标签),再对全文做标记词扫描,避免被 XML 标签污染计数。 - 结构损坏修复(双层包裹 / 断裂):当
word/document.xml出现"幽灵段落"或"断裂"时,先用严格 XML 解析(xml.dom.minidom.parseString)判定是否 well-formed;非 well-formed 才需修(Repaired_final替换前必须再验一次)。配套辅助: true_open_positions(s):正则<w:p(?=[\s>"])取真正段落开标签(排除<w:pPr>/<w:pageBreakBefore>误匹配)。find_matching_close(opens, closes, O):事件扫描配平events=sorted([(p,1) for opens]+[(p,-1) for closes]),按深度归零找 O 的配对闭合。- 双层包裹(double-wrap):
<w:p OUTER><w:pPr><w:p INNER><w:pPr>…外层幽灵<w:p>包住内层真标题(常包 2 个内层段落)。修复:循环re.search(r'<w:p w14:paraId="[0-9A-F]{8}"><w:pPr>(<w:p w14:paraId="[0-9A-F]{8}")'),每轮取首个匹配,x = x[:O]+x[Istart:OC]+x[OC+6:](删外层<w:p…><w:pPr>与外层</w:p>),至清零。 - 断裂(break):某段落的
<w:p><w:pPr>开头被吞,仅留孤立<w:pStyle w:val="X"/>,其前邻为</w:p>或</w:tbl>。修复:匹配(</w:p>|</w:tbl>)<w:pStyle w:val="(\d+)"\s*/>,在<w:pStyle>前插入<w:p w14:paraId="新唯一ID"><w:pPr>,并把孤儿内容(从<w:pStyle>起点取到下一个真正<w:p开标签前)整体用新</w:p>闭合。必须保留<w:pStyle w:val="4"/>,否则丢 H2。新 paraId 用 8 位 HEX 且避已用集合。 - 损坏根因(脚本劈裂闭合标签):
a4[:-5]仅去掉</w:t>末 5 字符会拦腰劈开闭合标签,导致 XML 破损、段落错并、(九)重复、八·五丢失。任何"片段 + 闭合标签"拼接都必须用完整</w:t>作锚点,禁止[:-N]截断闭合标签。 - 致命正则陷阱:
<w:t[^>]*>会误吞<w:textAlignment/>:<w:t是<w:textAlignment的前缀,用<w:t[^>]*>做"替换首个<w:t>文本"会一路匹配到<w:textAlignment w:val="auto"/>,把</w:pPr>与后续 run 标签一并吃掉 → 表现为w:pPr开标签多 N、w:r闭标签多 N、w:p却平衡(最难查的一类失衡)。一律写<w:t\b[^>]*>(或<w:t(?=[ >]))。排查手法:dbg_balance.py对比手术前后各标签 open/close/selfclose 三元组,找出净增量定位到具体构造。 - minidom 校验孤立片段会假报 unbound prefix:把新造段落单独
parseString会因缺根元素w14命名空间声明而报unbound prefix,不是真损坏;必须拿整份word/document.xml校验。同时注意:原文档 TOC 域代码里可能含字面<w:p文本,会让朴素 open/close 计数差 1(已知假阴性),以parseString为准。 - 拆分"整段标题"为「短标题 + 正文段」:当某子节把整段正文(数百字)写进了 style4 标题段落且 run 级全加粗(视觉像大标题、目录显示超长),正确做法是——保留原段结构克隆出标题段(style4,只留首句作标题),再克隆一个真实正文段模板(从同章已有正文段取完整
<w:p>,含 pPr/spacing/rPr)承载剩余正文;两个新段各分配唯一paraId。标题取首句用full.find('。')切分。 - 三级重排(章→组→条):章内子节过多(>15)时,新增组层(style4,编号"一、二、三、四"),原子节降为 style5(编号按组重新起排" (一)(二)…")。实现要点:① 先算出
old→new编号映射;② 倒序删除原 22 个块再按顺序插入新块(正序插入会因字符串偏移导致定位漂移、且易重复插入产生重复paraId);③ 重排后必须同步更新正文交叉引用(如"本条规定与(十一)保安员纪律…相互衔接"→"(四)"),先扫全文交叉引用再改;④ 编号常量务必用全角括号「(一)」,写半角(一)会让断言计数为 0(最难发现的假失败)。 - 去 AI 味的两条判据:① 口号式四字对仗收尾(同频联动 / 无缝衔接 / 弹性可控 / 口径一致 / 闭环 / 赋能 / 打造 / 织密 / 筑牢)→ 换成写实动词(同步实施、衔接有序、及时调整、服务形象统一);② 括号内引号举例(如"(如"请您这边走""请出示证件")")→ 公文不用举例引语,改为"用语简明规范"。"通过/确保/全覆盖/第一时间/全程备勤/指令到位/快速响应"属公文正常词,不删。
反例避雷
- 不要用 python-docx 打开再
save()—— 会丢失/重排原格式。 - 不要把新段落插到目录缓存区(目录里也有章节标题文字)—— 必须用
rfind取正文末次出现并加断言。 - 不要整表重建来"更新"用户改过的表 —— 只做定点单元格修改。
- 不要把"通过 / 确保 / 全覆盖"等公文正常词当 AI 味删掉。
- 不要对锁定文件直接
rename/delete—— 用延迟改名。 - 不要凭记忆改 XML,先用脚本抽出目标段落核对当前实际文字,再手术。
- 不要用
a4[:-5]之类截断闭合标签再拼接 —— 会劈裂</w:t>,务必用完整闭合标签作锚点。 - 替换原文件前,先把"正文子节"标题文字拿去目录区搜一圈:若目录里也有同名字段,定位函数务必排除
HYPERLINK/fldChar/instrText,否则锚点落错位置会让替换/追加插到目录区或误圈他人表格。 - 整段替换薄节前,先验该节区域是否含
<w:tbl>;含表格就改「末尾追加」,别用替换把表格删了。 - 修复"断裂"时,切片要从 close_tag 之后(即
<w:pStyle>起点)开始取孤儿内容,不要从<w:pStyle>之后取,否则会丢失<w:pStyle w:val="4"/>这类 H2 样式、凭空少 H2。 - 替换原文件前,先确认"损坏态"已备份、Repaired 文件经严格 XML 解析 well-formed 且 H2/表格/图片计数与干净基底一致。
- 不要写
<w:t[^>]*>匹配 w:t 开标签 —— 会吞掉<w:textAlignment/>,用<w:t\b[^>]*>。 - 不要把新造片段脱离整份文档做 XML 校验 ——
unbound prefix是假警报,会误导排查方向。 - 三级重排时不要正序逐个插入 —— 用倒序删除再顺序插入,且每个新段分配唯一
paraId。 - 重编号不要漏改正文交叉引用,也不要用半角括号写" (一)"式常量。
标准作业流程(SOP 七步)
> 完整作业要求见 references/作业要求清单.md(开工前 / 口径 / 语体 / 技术红线 / 验收 / 收尾 六张表)。
- 接手确认:判断是否"用户改过的版本"——是则全程禁止生成/重建脚本,只做定点修改。
- 备份 + 建基线:
shutil.copy2(src, src+".bak_<说明>");记录 style3/4/5、<w:tbl>、<v:imagedata>+<w:drawing>、<w:t>、<w:p>六项计数;parseString确认 well_formed。 - 只读体检:dump 全文(排除 TOC 域条目)→ 查章节层级是否错位、查"整段标题"缺陷、查编号机制是否统一(自动
numPrvs 人工键入)、扫交叉引用、扫 AI 味。 - 出方案给用户确认:列清改什么、怎么改、风险点;未经确认不写盘。
- 手术:按 A 拆分整段标题 → B 编号统一 → C 三级重排(倒序删+顺序插)→ 同步交叉引用 → D 去 AI 味,每步单独
wf()校验,出错即停。 - 写盘 + 验收:
write_back只替换word/document.xml;验收清单十项全过(见作业要求清单第五节)。 - 收尾:改后重新 dump 二次复扫 AI 味;升版(封面版本号注意跨 run);更新项目日志;把本次新坑回写 SKILL.md 并递增版本号、重打 zip。
可复用模板片段
references/模板片段.py:段落样式模板h2()/body()、锚点插入insert_before()/insert_after_para()、写回write_back()、计数校验verify()、干净文本抽取clean_text()。references/结构优化脚本骨架.py:章节结构优化通用骨架 ——is_toc()/blocks()/find_real()定位三件套、set_style()/set_text()/set_para_id()(含<w:t\b词边界)、part_split()拆分整段标题、part_number()编号统一、part_regroup()三级重排(倒序删+顺序插)、part_version()升版、main()主流程。references/作业要求清单.md:六张标准化检查表(开工前/口径/语体/技术红线/验收/收尾)。
去 AI 味常用替换(本项目沉淀)
- 推进 → 实施;"持续做好…台账管理" → "建立…台账,实时更新、动态核销"
- "拉得出顶得上" → "随时可调度、可增援";"召之即来来之能战" → "指令到位、快速响应"
- "绿色出行" → "通勤交通不便"
- "垂直二楼三楼" → "二楼、三楼办公区";"地下停车" → "地下停车场"
- 删除对不存在附件的引用;逐条核对董事长要求是否全覆盖(覆盖到的在正文标注,不补的不计入)
版本记录
- v1.0.0(2026-10-20):基于「A会期间某单位物业守护安全工作方案(物业安防范围)V1.2」修订全流程沉淀。涵盖三处细节补充(物业本体守护管控细则 / 流线组织与分流原则 / 人员准入与礼仪形象要求)、全文去 AI 味核验、版本对齐、目录刷新、相关部门分工表插入等。
- v1.1.0(2026-10-21):增补「结构损坏修复」关键技术——双层包裹解包、断裂修复(孤儿
<w:pStyle>还原并保留 H2 样式)、以及a4[:-5]劈裂闭合标签的损坏根因与避雷。同版以 zipfile 纯文本手术恢复 V1.2:因旧脚本a4[:-5]误劈</w:t>致 document.xml 破损,改以干净基底bak_细节完善_et解包 4 处双层包裹 + 修复 4 处断裂 + 内嵌三处新增(四·七 / 六·七 / 八·五),落地为 well-formed,校验 688/688、H2=21、tbl=12、img=42、三处落位与顺序正确,替换原损坏文件(损坏态保留于.bak_修复前_损坏态)。 - v1.2.0(2026-10-22):增补两项定位/防误删经验——① 目录条目与正文子节标题文字相同且含
HYPERLINK/fldChar/instrText域代码,定位函数须同时排除这三者,否则锚点命中目录、误圈他节表格;② 整段替换薄节前assert '<w:tbl' not in 本节区域,含表格的薄节改「末尾追加」以保表格。本次据此对 V2.0.repaired.docx 落地:补(十一)拍照禁令、(五)(三)并入食材车证并修正其 style3→4 误用章级样式、扩写 7 个无表格薄节(四一/四二/四三/六一/六四/六五/六十三),tbl=12、img=42、paraId 唯一、well-formed 全通过;(四)(五)通道属性划分因含表格保留未替换。 - v1.3.0(2026-10-22):增补「章节结构优化」全套经验——①
<w:t[^>]*>前缀陷阱(吞<w:textAlignment/>致 w:pPr 开多、w:r 闭多的隐蔽失衡)与dbg_balance三元组排查法;② minidom 校验孤立片段的unbound prefix假警报;③ 拆分"整段标题"为短标题+克隆正文段;④ 章内三级重排(章 style3→组 style4→条 style5)的倒序删除再插入、old→new编号映射、全角括号常量、交叉引用同步;⑤ 去 AI 味两条判据(口号式四字对仗收尾 / 括号引号举例)与"公文正常词不删"白名单。本次据此对 V2.0.repaired.docx 落地:拆分原(二十)(二十一)(二十二)三节整段标题、三章子节去除numPr改人工编号并统一字号、第六章 22 条按 4 组三级重排、交叉引用(十一)→(四)、6 处去 AI 味改写;校验 style3=7 / style4=15 / style5=22 / tbl=12 / img=42 / paraId 唯一 / well-formed 全通过。 - v1.4.0(2026-10-22):标准化定型版。新增「标准作业流程(SOP 七步)」主章节;新增
references/作业要求清单.md(开工前 / 口径 / 语体 / 技术红线 / 验收十项 / 收尾 六张表,含 AI 味黑名单与公文正常词白名单);新增references/结构优化脚本骨架.py(定位三件套is_toc/blocks/find_real、set_style/set_text/set_para_id、part_split/part_number/part_regroup/part_version四大手术函数与main()流程,替换常量即可复用)。补「版本号常跨 run("版 本:V" + "2.0"),须按 run 粒度替换,直接搜 V2.0 搜不到」。实战收官:V2.0 → V3.0 全流程落地(结构优化 + 去 AI 味 + 升版 + 重命名),六项计数零变动、well_formed、paraId 唯一、testzip=None 全通过。