# ContractEditor 原文Run属性污染诊断与修复 ## 2026-07-13 洋励合同实证 ### 问题描述 ContractEditor(contract_docx_lib.py)在处理文档时,不仅给WB INS runs添加多余属性,还会**修改原文runs**的rPr——给本来靠docDefaults/style继承的orig runs添加显式eastAsia/cs/sz。 ### 典型污染模式 | 属性 | 原文(待审查) | 被污染后(交付物中的orig run) | WB INS run | |------|--------------|-------------------------------|-----------| | eastAsia | None (继承minorEastAsia) | **宋体** (被加) | None | | cs | None | **宋体** (被加) | None | | sz | None (继承docDefaults=22) | **21** (被加且值错) | None | | ascii | 宋体 | 宋体 | None | | hint | eastAsia (部分有) | eastAsia | None | ### 后果 1. 原文所有文字从11pt(docDefaults sz=22)变成10.5pt(显式sz=21) — 整体缩小0.5pt 2. INS文字没有任何属性 → 走docDefaults 11pt → 与被改小的原文不一致 3. 字体验证脚本(wb-ins-font-verify.py)报INS缺属性,但实际问题是orig被污染 ### 诊断步骤 ```bash # 1. 读原文代表性段落run rPr python3 -c " import zipfile from lxml import etree WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}' with zipfile.ZipFile('原文.docx', 'r') as z: ... # 检查: eastAsia=None? sz=None? # 如果是 → 原文靠继承 # 2. 读交付物同段落orig run rPr # 检查: 是否多了eastAsia/cs/sz? # 如果是 → 被污染 ``` ### 修复代码模板 ```python import zipfile, tempfile, shutil from lxml import etree WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}' with zipfile.ZipFile(filepath, 'r') as z: all_files = {n: z.read(n) for n in z.namelist()} tree = etree.fromstring(all_files['word/document.xml']) body = tree.find(f'{WNS}body') # Step 1: Strip contaminated attributes from ALL orig runs for p in body.findall(f'{WNS}p'): for r in p.findall(f'{WNS}r'): # Only direct child runs (not inside ins/del) rpr = r.find(f'{WNS}rPr') if rpr is None: continue rf = rpr.find(f'{WNS}rFonts') if rf is not None: # Strip eastAsia (original didn't have it) if f'{WNS}eastAsia' in rf.attrib: del rf.attrib[f'{WNS}eastAsia'] # Strip cs (original didn't have it) if f'{WNS}cs' in rf.attrib: del rf.attrib[f'{WNS}cs'] # Strip sz (original relies on docDefaults) sz = rpr.find(f'{WNS}sz') if sz is not None: rpr.remove(sz) # Step 2: Fix INS runs to match REAL original format for ins in body.findall(f'.//{WNS}ins'): if ins.get(f'{WNS}author') != 'WB': continue for r in ins.findall(f'{WNS}r'): rpr = r.find(f'{WNS}rPr') if rpr is None: continue rf = rpr.find(f'{WNS}rFonts') if rf is None: rf = etree.SubElement(rpr, f'{WNS}rFonts') # Match real original: ascii=宋体, hAnsi=宋体, NO eastAsia rf.set(f'{WNS}ascii', '宋体') rf.set(f'{WNS}hAnsi', '宋体') if f'{WNS}eastAsia' in rf.attrib: del rf.attrib[f'{WNS}eastAsia'] # Remove sz (let it inherit) sz = rpr.find(f'{WNS}sz') if sz is not None: rpr.remove(sz) # Step 3: Per-paragraph hint matching for p in body.findall(f'{WNS}p'): # Get orig run's hint orig_hint = None for r in p.findall(f'{WNS}r'): rpr = r.find(f'{WNS}rPr') if rpr is not None: rf = rpr.find(f'{WNS}rFonts') orig_hint = rf.get(f'{WNS}hint') if rf is not None else None break # Apply to INS runs in same paragraph for ins in p.findall(f'.//{WNS}ins'): if ins.get(f'{WNS}author') != 'WB': continue for r in ins.findall(f'{WNS}r'): rpr = r.find(f'{WNS}rPr') if rpr is None: continue rf = rpr.find(f'{WNS}rFonts') if rf is None: continue if orig_hint: rf.set(f'{WNS}hint', orig_hint) elif f'{WNS}hint' in rf.attrib: del rf.attrib[f'{WNS}hint'] ``` ### 注意事项 1. **必须对比原文确定被污染了哪些属性** — 不同合同模板的原文属性不同 2. **不是所有合同都有此问题** — 取决于原文是否靠继承(有显式属性的不会被"污染",因为值相同) 3. **Step 1必须在Step 2之前** — 否则wb-ins-font-verify仍会报INS与(被污染的)orig不一致 4. **hint要逐段处理** — 同一文档不同段落的orig runs可能有的有hint有的没有