24如何让AI对word文档进行修订


如何让 AI 对 Word 文档进行修订(Track Changes)

日期:2026-06-29 | 场景:标书编写 —— 报价函填报

一、目标

用 AI 在 Word 文档中以**修订模式(Track Changes)**填写模板占位符,效果为:

  • 原文占位符保留不动(不删除)
  • 新填入的内容以红色下划线(<w:ins>)标注
  • 人工复核后可一键「接受所有修订」完成填写

二、尝试过程与踩坑记录

尝试 1:字符串替换 + <w:del> + <w:ins>

做法:解压 docx → 在 document.xml 中找到占位符文本(如 (采购人全称)),用正则替换为 <w:del> + <w:ins> 包裹的内容。

结果:❌ 失败。占位符被删除标记覆盖,破坏了原文格式。用户反馈:"不要删除格式上的任何东西"。

教训:修订的真正含义是插入新内容 + 保留旧内容,不是替换。用 <w:del> 删原文再 <w:ins> 加新文,等于"覆盖",不是"修订"。

尝试 2:字符串替换文本内容(保留占位符在同一个 <w:t> 之后)

做法:用字符串替换,在原始 <w:t> 标签内部,在占位符文本之前插入 <w:ins> 块。例如将 <w:t>(供应商全称)授权</w:t> 替换为 <w:ins>山西金辉</w:ins><w:t>(供应商全称)授权</w:t>

结果:❌ 部分失败。公司名插入未生效。

根因:Word OOXML 中 <w:t> 是叶节点,不能在 <w:t> 内部嵌套 <w:del><w:ins> 元素。修订标记必须是 <w:r>(run)级别的兄弟节点。实际文本是 <w:t>(供应商全称)授权</w:t>,我的正则只匹配到 <w:t>(供应商全称)</w:t>(不包含"授权"),所以没替换成功。

尝试 3:在 <w:r> 级别插入 <w:ins> 兄弟节点

做法:找到包含占位符文本的 <w:r> 行,在该行之前插入一个 <w:ins> 块(内含包含新文本的 <w:r>)。

结果:❌ 失败。格式混乱。

根因:直接在第 N 行插入新内容导致行号偏移,后续操作的索引全部错乱。而且 <w:ins> 插入后破坏了 <w:p> 内部格式属性的继承关系。

三、正确的技术方案(未验证的理想做法)

核心原则

  1. 修订标记是段落级操作<w:ins> / <w:del> 放在 <w:p> 内部、<w:r> 外部
  2. 只增不删:填充模板时只做 <w:ins>(插入),不做 <w:del>(删除)
  3. 保留全部原有 <w:r> 元素:包含占位符的原 run 不能删、不能改
  4. 插入的新文本必须复制原 run 的 <w:rPr> 格式:字体、字号、下划线、颜色等

推荐操作流程

Step 1: 解压 docx
python scripts/office/unpack.py input.docx unpacked/

Step 2: 解析 document.xml 的 DOM 树(非正则)
使用 lxml 或 ElementTree 解析 XML,定位到目标 <w:p>

Step 3: 为每个需要填写的位置,在 DOM 中操作
- 找到包含占位符文本的 <w:r> 元素
- clone 该 <w:r> 的 <w:rPr>(格式属性)
- 创建新的 <w:ins> 元素,内含新的 <w:r>(带格式 + 新文本)
- 将 <w:ins> 插入为原 <w:r> 的前一个兄弟节点
- 保持原 <w:r> 元素完全不变

Step 4: 序列化回 XML 并重新打包
python scripts/office/pack.py unpacked/ output.docx --original input.docx

为什么不能用字符串替换

方法 问题
字符串 replace 无法感知 XML 层级结构,容易破坏嵌套
正则匹配 <w:t>xxx</w:t> 叶节点内容可能跨越多行、包含空格和实体
<w:t> 内部插入 违反 OOXML Schema,Word 会忽略或报错
按行号插入 一次插入后所有后续行号失效

为什么必须解析 DOM

  • OOXML 文档中,一个占位符可能分散在多个 <w:r> 中(如 (采购人 + 全称) 各在独立 run)
  • <w:rPr> 格式属性必须精确复制,否则新插入文本会丢失字体、加粗、下划线等
  • <w:ins><w:del>w:id 必须全局唯一且从 0 递增
  • <w:ins> 必须包含完整的 <w:r> 结构,内部有自己的 <w:rPr><w:t>

最佳实践:用脚本批量处理

比人肉改 XML 更可靠的方式是写 Python 脚本,使用 lxml:

from lxml import etree

NSMAP = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}

tree = etree.parse('unpacked/word/document.xml')
root = tree.getroot()

# 遍历所有段落
for p in root.iter('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p'):
    # 找到包含占位符的 run
    for r in p.findall('.//w:r', NSMAP):
        t = r.find('w:t', NSMAP)
        if t is not None and '采购人全称' in (t.text or ''):
            # 复制格式
            rpr = r.find('w:rPr', NSMAP)
            # 创建 ins 元素
            ins = etree.SubElement(p, '{http://...}ins', 
                {'{http://...}id': '1', 
                 '{http://...}author': '标工',
                 '{http://...}date': '2026-06-29T00:00:00Z'})
            new_r = etree.SubElement(ins, '{http://...}r')
            if rpr is not None:
                new_r.append(copy.deepcopy(rpr))
            new_t = etree.SubElement(new_r, '{http://...}t')
            new_t.text = '河津市自然资源局'
            # 将 ins 移到原 r 之前
            p.insert(list(p).index(r), ins)
            break

tree.write('unpacked/word/document.xml', encoding='utf-8', xml_declaration=True)

四、当前能力边界

能力 状态
解压/打包 docx
提取 docx 文本
批注(comment)
修订标记(用 XML 字符串替换简单文本) ⚠️ 单项勉强可行
修订标记(占位符跨 <w:r> / 需保留原格式) ❌ 待实现
修订标记(批量、可靠) ❌ 需开发脚本

五、后续方向

  1. 写一个 lxml 版本的 insert_revision.py 脚本,封装"在指定文本前插入 <w:ins>"的可靠操作
  2. 将脚本纳入 bid-tech-proposal 技能的 scripts/ 目录
  3. 对于纯文本替换(不涉及占位符保留)的场景,字符串替换 + <w:ins> 仍然可行,但限制条件为:目标文本是独立 <w:r> → 直接在该 <w:r> 前插入 <w:ins> 兄弟节点

文章作者: 摸鱼
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 摸鱼 !
  目录