# 修改已有tracked changes的作者和文本内容 ## 场景 - 合并用户在OnlyOffice中的修订(author如"华诚-Z"改为"WB") - 修改INS元素中的文本内容(如更新法律措辞) - 修改批注作者(comments.xml中的w:comment author属性) ## 技术实现 ### 修改tracked change作者 ```python 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文本内容 ```python 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 = '新的插入文本' ``` ### 修改批注作者 ```python # 修改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**。 ```python 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之后: ```python 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: ```python 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', '宋体') ``` ## 验证方法 ```python # 验证所有作者已更改 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作者的文件,提取修订内容后在最终版本中恢复。