# Multi-Party Revision Reconciliation (多方修订对账) ## 场景 合同经多方修订(如WB、华诚-Z、Adon hase等不同修订人),需要: 1. 核对叠加修订后的最终效果是否符合协商一致的商业条件 2. 对比最终版与模板的差异 3. 评估差异对特定方权利义务的影响 4. 统一修订人署名 ## 技术方法 ### 1. 提取带修订标记的全文(含作者归属) ```python from docx import Document from lxml import etree ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'} W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}' doc = Document('contract.docx') body = doc.element.body # 统计修订作者 authors = set() for elem in body.iter(): author = elem.get(f'{W}author') if author: authors.add(author) # 分作者统计插入/删除 ins_by_author = {} del_by_author = {} for ins in body.findall(f'.//{W}ins'): a = ins.get(f'{W}author', 'unknown') ins_by_author[a] = ins_by_author.get(a, 0) + 1 for d in body.findall(f'.//{W}del'): a = d.get(f'{W}author', 'unknown') del_by_author[a] = del_by_author.get(a, 0) + 1 ``` ### 2. 带修订标记的文本提取 格式:`[+作者: 插入文本]` / `[-作者: 删除文本]` / 普通文本 ```python def get_text_with_revisions(body): result = [] for para in body.findall(f'.//{W}p'): para_text = [] for elem in para.iter(): if elem.tag == f'{W}ins': author = elem.get(f'{W}author', '?') texts = [t.text for t in elem.findall(f'.//{W}t') if t.text] if texts: para_text.append(f"[+{author}: {''.join(texts)}]") elif elem.tag == f'{W}del': author = elem.get(f'{W}author', '?') texts = [t.text for t in elem.findall(f'.//{W}delText') if t.text] if texts: para_text.append(f"[-{author}: {''.join(texts)}]") elif elem.tag == f'{W}t': in_revision = False p = elem while p is not None: if p.tag in [f'{W}ins', f'{W}del']: in_revision = True break p = p.getparent() if not in_revision and elem.text: para_text.append(elem.text) if para_text: result.append(''.join(para_text)) return result ``` ### 3. 三版对比分析 对比维度: - **模板** → 我方标准条款(基准线) - **对方修订版** → 对方修改后接受所有修订的版本(ins=0, del=0 说明已全部接受) - **当前叠加版** → 在WB修订基础上加入另一方修改 ### 4. 协商一致核对表 按商业条件逐项核对: | 协商条件 | 条款位置 | 当前版本内容 | 是否符合 | |---|---|---|---| | 费用40/60/60/80/80 | 3.3条 | 具体金额 | ✅/❌ | | 违约责任按我方 | 第4条 | ... | ✅/⚠️ | ### 5. 模板偏差影响评估 对每处偏差评级: - ✅ 对己方有利(扩大了己方权利/对方义务) - ⚠️ 中等影响(条件调整但不改变核心权利义务) - ❗ 重要(实质性削弱己方权利/扩大己方义务/给对方逃出合同的通道) ## 修订人统一规则 当需要将多个修订人统一为一个(如统一为WB): ### 处理优先级 1. A修订了B的修订 → **以A为准**(接受A对B的修改) 2. A和B各自独立修订 → 两者都保留,统一署名 ### 核心技术挑战:嵌套修订 **关键结构:B(华诚-Z)在A(WB)的 `w:ins` 内部做了 `w:del`** XML表现为: ```xml 个月未给乙方安排工作的, 拍摄 ``` 含义:WB插入了"个月未给乙方安排拍摄工作的,",华诚-Z在WB的插入中删除了"拍摄",最终效果="个月未给乙方安排工作的,"。 ### 三步统一流程(2026-07-03 MCN模特合同实证) **Step 1: 接受嵌套删除**(B对A修订的修改) ```python def accept_nested_deletions(body, inner_author='华诚-Z', outer_author='WB'): """接受inner_author对outer_author修订的修改(删除嵌套del元素)""" for ins_elem in body.findall(f'.//{W}ins'): if ins_elem.get(f'{W}author') != outer_author: continue # 找到outer_author的ins内部,inner_author做的del for del_elem in ins_elem.findall(f'.//{W}del'): if del_elem.get(f'{W}author') == inner_author: parent = del_elem.getparent() parent.remove(del_elem) accept_nested_deletions(body) ``` **Step 2: 清除空壳元素** 接受嵌套删除后,某些WB的ins可能变空(内容全被华诚-Z删了,华诚-Z在旁边插入了替代文本): ```python def remove_empty_ins(body): """删除没有任何文本内容的ins元素""" for ins_elem in body.findall(f'.//{W}ins'): has_text = False for t in ins_elem.findall(f'.//{W}t'): if t.text and t.text.strip(): has_text = True break if not has_text: parent = ins_elem.getparent() if parent is not None: parent.remove(ins_elem) remove_empty_ins(body) ``` **Step 3: 统一作者名** ```python def rename_author(body, old_author, new_author): """修改所有修订元素的author属性""" count = 0 for elem in body.iter(): if elem.get(f'{W}author') == old_author: elem.set(f'{W}author', new_author) count += 1 return count count = rename_author(body, '华诚-Z', 'WB') ``` ### 在统一后的文件上追加模板修改 如果发现与模板有偏差需要修正(如恢复模板中的固定金额违约金选项、自动续约条件等),在已统一的文件上新增tracked changes: ```python from copy import deepcopy def get_rPr_from_run(run): """获取run的格式属性用于新增修订""" rpr = run.find(f'{W}rPr') return deepcopy(rpr) if rpr is not None else None def make_ins(text, rPr=None, author='WB', date='2026-07-03T06:00:00Z'): """创建tracked insertion""" ins = etree.Element(f'{W}ins') ins.set(f'{W}id', str(abs(hash(text)) % 100000)) ins.set(f'{W}author', author) ins.set(f'{W}date', date) r = etree.SubElement(ins, f'{W}r') if rPr is not None: r.append(deepcopy(rPr)) t = etree.SubElement(r, f'{W}t') t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') t.text = text return ins def make_del(text, rPr=None, author='WB', date='2026-07-03T06:00:00Z'): """创建tracked deletion""" d = etree.Element(f'{W}del') d.set(f'{W}id', str(abs(hash(text + 'del')) % 100000)) d.set(f'{W}author', author) d.set(f'{W}date', date) r = etree.SubElement(d, f'{W}r') if rPr is not None: r.append(deepcopy(rPr)) dt = etree.SubElement(r, f'{W}delText') dt.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') dt.text = text return d # 修改模式:找到目标run → 替换为del(旧) + ins(新) target_run = ... # 找到包含目标文本的w:r元素 rPr = get_rPr_from_run(target_run) old_text = target_run.find(f'.//{W}t').text del_elem = make_del(old_text, rPr) ins_elem = make_ins(new_text, rPr) idx = list(para).index(target_run) para.remove(target_run) para.insert(idx, del_elem) para.insert(idx + 1, ins_elem) ``` ### 恢复已被删除的内容(撤回WB之前的del) 当需要把WB之前删除的内容恢复回来(因为模板中有这个内容): ```python # 找到WB的del元素 for child in para: if child.tag == f'{W}del' and child.get(f'{W}author') == 'WB': del_texts = ''.join(dt.text for dt in child.findall(f'.//{W}delText') if dt.text) if '目标文本' in del_texts: # 策略:删除这个del,插入一个ins代替(因为原文已经"被删"了) inner_r = child.find(f'.//{W}r') inner_rPr = get_rPr_from_run(inner_r) ins_restore = make_ins(del_texts, inner_rPr) idx = list(para).index(child) para.remove(child) para.insert(idx, ins_restore) break ``` ## 验证步骤 统一修订人后必须验证: ```python # 1. 确认只剩一个作者 authors = set() for elem in body.iter(): a = elem.get(f'{W}author') if a: authors.add(a) assert authors == {'WB'}, f"Unexpected authors: {authors}" # 2. 获取接受所有修订后的最终文本 def get_accepted_text(para): texts = [] for elem in para.iter(): if elem.tag == f'{W}t': in_del = False p = elem while p is not None: if p.tag == f'{W}del': in_del = True break p = p.getparent() if not in_del and elem.text: texts.append(elem.text) return ''.join(texts) # 3. 逐条核对关键条款的最终文本 ``` ## 输出格式 报告分三部分: 1. **协商一致核对** — 逐项确认商业条件是否落实(✅/⚠️/❌) 2. **模板差异表** — 与标准模板的偏差点 + 对己方权利义务的影响评估 3. **结论与建议** — 哪些已符合、哪些需关注、是否需要进一步协商 ## 注意事项 - 对方修订版如果修订已全部接受(ins=0, del=0),说明是"接受所有修订后"的干净版本 - 比较时需要分别提取:(1)当前版本接受所有修订后的最终文本;(2)带修订标记的过程文本 - 对方修改了措辞但实质不变的情况(如"整容"→"形象变化"),需评估措辞变化是否改变法律效果——措辞更宽泛时分析对哪方有利 - 费用条款的结构性改写(如合并年度/拆分年度)要验证数学正确性(年总额×期数=合同总额) - 处理段落29-31连读场景:华诚-Z可能将原来的多个年度段落(第三、四年/第五年分开写)合并为一个(第四、第五年),导致中间段落被del清空,需要把前后段落连起来读才能看到完整的费用结构 - **修订人统一后的检查清单**:作者唯一性、ins/del总数合理、关键条款最终文本正确、文件可正常打开