Files

8.4 KiB

修改已有tracked changes的作者和文本内容

场景

  • 合并用户在OnlyOffice中的修订(author如"华诚-Z"改为"WB")
  • 修改INS元素中的文本内容(如更新法律措辞)
  • 修改批注作者(comments.xml中的w:comment author属性)

技术实现

修改tracked change作者

from lxml import etree
import zipfile

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

# 打开docx,修改document.xml
z_in = zipfile.ZipFile('input.docx', 'r')
z_out = zipfile.ZipFile('output.docx', 'w')

# 复制非document.xml的文件
for item in z_in.namelist():
    if item != 'word/document.xml':
        z_out.writestr(item, z_in.read(item))

# 修改tracked change作者
with z_in.open('word/document.xml') as f:
    tree = etree.parse(f)
root = tree.getroot()

for elem in root.iter():
    tag = etree.QName(elem.tag).localname
    if tag in ('ins', 'del'):
        old_author = elem.get(f'{WNS}author', '')
        if old_author == '旧作者名':
            elem.set(f'{WNS}author', 'WB')

z_out.writestr('word/document.xml', etree.tostring(tree, encoding='UTF-8', xml_declaration=True, standalone=True))
z_in.close()
z_out.close()

修改INS文本内容

from copy import deepcopy
from datetime import datetime

now = datetime.now().isoformat()

# 定位特定段落中的INS元素
body = root.find(f'{WNS}body')
paras = body.findall(f'{WNS}p')
target_para = paras[12]  # 按索引定位

# 删除旧的INS元素(按作者筛选)
for ins in list(target_para.findall(f'{WNS}ins')):
    author = ins.get(f'{WNS}author', '')
    if author == '目标作者':
        target_para.remove(ins)

# 添加新的INS元素
new_ins = etree.SubElement(target_para, f'{WNS}ins')
new_ins.set(f'{WNS}author', 'WB')
new_ins.set(f'{WNS}date', now)
new_r = etree.SubElement(new_ins, f'{WNS}r')

# 从同段落的原文run复制格式
orig_runs = target_para.findall(f'{WNS}r')
if orig_runs:
    orig_rpr = orig_runs[0].find(f'{WNS}rPr')
    if orig_rpr is not None:
        new_r.append(deepcopy(orig_rpr))

new_t = etree.SubElement(new_r, f'{WNS}t')
new_t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
new_t.text = '新的插入文本'

修改批注作者

# 修改comments.xml中的作者
if 'word/comments.xml' in z_in.namelist():
    with z_in.open('word/comments.xml') as f:
        ctree = etree.parse(f)
    croot = ctree.getroot()
    for c in croot.findall(f'{WNS}comment'):
        if c.get(f'{WNS}author', '') == '旧作者名':
            c.set(f'{WNS}author', 'WB')
    z_out.writestr('word/comments.xml', etree.tostring(ctree, encoding='UTF-8', xml_declaration=True, standalone=True))

在已有WB INS元素内修改部分文本(2026-07-01 反委托代发工资协议)

当段落文本全部是WB INS(无普通w:r),需要替换其中某一句时,不能删除整个INS重建(会丢失该INS中其他文本的修订标记)。正确手法:trim原INS的w:t + addnext插入DEL/INS

old_sentence = "退回派遣员工由乙方依法自行安置处理,与甲方无涉。"
new_sentence = "派遣员工退回后由乙方依法负责安置处理。因乙方安置不当导致甲方被追究责任的,乙方应赔偿甲方因此遭受的全部损失。"

for child in list(p):
    tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
    if tag == 'ins' and child.get(f'{WNS}author') == 'WB':
        for r in child.findall(f'{WNS}r'):
            for t in r.findall(f'{WNS}t'):
                if t.text and old_sentence in t.text:
                    rpr_copy = copy.deepcopy(r.find(f'{WNS}rPr')) if r.find(f'{WNS}rPr') is not None else None
                    
                    # 1. Trim原INS文本(去掉被替换的句子)
                    t.text = t.text.replace(old_sentence, "")
                    
                    # 2. 创建DEL
                    del_elem = etree.Element(f'{WNS}del')
                    del_elem.set(f'{WNS}id', next_id())
                    del_elem.set(f'{WNS}author', 'WB')
                    del_elem.set(f'{WNS}date', rev_date)
                    del_r = etree.SubElement(del_elem, f'{WNS}r')
                    if rpr_copy: del_r.insert(0, copy.deepcopy(rpr_copy))
                    del_r.set(f'{WNS}rsidDel', rsid)
                    del_t = etree.SubElement(del_r, f'{WNS}delText')
                    del_t.set(XML_SPACE, 'preserve')
                    del_t.text = old_sentence
                    
                    # 3. 创建INS
                    ins_elem = etree.Element(f'{WNS}ins')
                    ins_elem.set(f'{WNS}id', next_id())
                    ins_elem.set(f'{WNS}author', 'WB')
                    ins_elem.set(f'{WNS}date', rev_date)
                    ins_r = etree.SubElement(ins_elem, f'{WNS}r')
                    if rpr_copy: ins_r.insert(0, copy.deepcopy(rpr_copy))
                    ins_r.set(f'{WNS}rsidR', rsid)
                    ins_t = etree.SubElement(ins_r, f'{WNS}t')
                    ins_t.set(XML_SPACE, 'preserve')
                    ins_t.text = new_sentence
                    
                    # 4. 插入到原INS之后(addnext保证顺序)
                    child.addnext(ins_elem)   # 后插的在后面
                    child.addnext(del_elem)   # 后插的在前面 → 最终: [原INS] [DEL] [INS]

关键点

  • addnext 两次:先插INS再插DEL,后插的排前面,最终顺序:[原INS(trimmed)] [DEL旧句] [INS新句]
  • 绝不能 p.remove(child) 再重建——会丢失INS中其他未改动的文本
  • rPr必须从原INS的run深拷贝,不要从全文body_rpr取(字号可能不同)

给全INS段落补充条款编号(2026-07-01)

段落所有文本都是WB INS时,编号INS插到pPr之后:

ins_num = etree.Element(f'{WNS}ins')
ins_num.set(f'{WNS}id', next_id()); ins_num.set(f'{WNS}author', 'WB'); ins_num.set(f'{WNS}date', rev_date)
ins_r = etree.SubElement(ins_num, f'{WNS}r')
ins_r.insert(0, copy.deepcopy(existing_rpr))  # 从同段落INS run深拷贝
ins_r.set(f'{WNS}rsidR', rsid)
ins_t = etree.SubElement(ins_r, f'{WNS}t')
ins_t.set(XML_SPACE, 'preserve'); ins_t.text = "第X条  "

ppr = p.find(f'{WNS}pPr')
if ppr is not None: ppr.addnext(ins_num)
else: p.insert(0, ins_num)

新INS元素eastAsia字体显式补齐

原文WB INS run可能没有显式eastAsia属性(靠docDefaults回退),但新INS run必须显式设置eastAsia=宋体,否则修订上下文中可能丢失回退。post-save sweep:

for p in body.findall(f'{WNS}p'):
    for ins in p.findall(f'{WNS}ins'):
        for r in ins.findall(f'{WNS}r'):
            text = ''.join(t.text for t in r.findall(f'{WNS}t') if t.text)
            if not any('\u4e00' <= c <= '\u9fff' for c in text): continue
            rpr = r.find(f'{WNS}rPr')
            if rpr is None:
                rpr = etree.Element(f'{WNS}rPr'); r.insert(0, rpr)
            rf = rpr.find(f'{WNS}rFonts')
            if rf is None: rf = etree.SubElement(rpr, f'{WNS}rFonts')
            if not rf.get(f'{WNS}eastAsia'): rf.set(f'{WNS}eastAsia', '宋体')
            if not rf.get(f'{WNS}ascii'): rf.set(f'{WNS}ascii', '宋体')

验证方法

# 验证所有作者已更改
content = z_out.read('word/document.xml').decode('utf-8', 'ignore')
authors = set(re.findall(r'w:author="([^"]+)"', content))
assert '旧作者名' not in authors, f"仍有旧作者: {authors}"

# 验证批注作者
if 'word/comments.xml' in z_out.namelist():
    with z_out.open('word/comments.xml') as f:
        ctree = etree.parse(f)
    for c in ctree.getroot().findall(f'{WNS}comment'):
        assert c.get(f'{WNS}author') != '旧作者名'

⚠️ 铁律

  1. 修改前必须备份原文件:覆盖含第三方修订的文件 = 不可逆丢失
  2. 只改作者名,不改文本:除非明确要求修改INS内容
  3. zipfile不能原地读写:必须先读后写临时文件,再用os.replace
  4. 保留comments.xml中的批注锚点:只改author属性,不改id/content/anchor

实证(2026-07-01 反委托代发工资协议)

华诚-Z在OnlyOffice中做了3处修订(第六条去法条引用、第七条简化纠正流程、第八条加退回员工安置)。后续制作版本时覆盖了所有中间文件,导致华诚-Z修订痕迹丢失。最终通过系统化文件扫描在/tmp/v1_doro_updated.docx中找到仍含华诚-Z作者的文件,提取修订内容后在最终版本中恢复。