# A类(手动文本编号)新增条款:克隆"真实邻居段落"而非信任库的 _title_rpr / _body_rpr 2026-06-18 赵巷镇 X线设备采购合同实战。终审字体核验抓出"新标题不加粗",根因是库提取的标题格式丢了 bold。 ## 何时用这套手法 - 合同是 **A 类**:条款标题是 run 里的**手动文本编号**(如 `8.争端的解决`、`第七条 索赔`),段落**无** numPr。 - 要新增一个带标题的条款(标题段 + 正文段),并希望格式与兄弟条款 100% 一致。 - (B 类自动编号列表项见 `auto-numbered-list-clause-insert.md`,手法不同。) ## 为什么不直接用库的 `_title_rpr` / `_body_rpr` `ContractEditor._extract_formats()`(contract_docx_lib.py ~第86-175行)用启发式认"条款标题": ```python is_clause_title = (re.match(r'^\d+[..、]\s*\S', p_text) or re.match(r'^第[一二三四五六七八九十百千\d]+条\s*\S', p_text)) and len(p_text) < 30 # 且 _is_title_style() 要 或标题字体(黑体/SimHei…) 才算 title ``` **坑**:当标题就是"8.争端的解决"(宋体 + ``,无特殊标题字体),若该段在扫描中**没被 `_is_title_style` 命中**(例如 b 标记在 bCs 旁、或正则边界),`clause_title_rpr` / `first_bold_rpr` 取空 → `_title_rpr` **回退到 `_body_rpr`(不含 bold)**。 实测后果:新标题 `8.转包与分包` 的 INS run `bold=False`,而原文兄弟标题 `9.争端的解决` `bold=True`。`validate()` 查不出(它不比 bold),只有逐段 WB INS 与"同段/同级原文"对比才抓得到。 ## 稳健手法:克隆紧邻同级原文段落 不取库的 `_title_rpr`/`_body_rpr`,改为**直接深拷贝隔壁真实条款段**的 pPr 和首个 run 的 rPr: ```python import sys, copy sys.path.insert(0, '/home/maggie/contract-work') from contract_docx_lib import ContractEditor, qn from lxml import etree ed = ContractEditor(src) # 已先做完所有 tracked_replace def find(kw): for p in ed.body.findall(qn('p')): if kw in ''.join(t.text or '' for t in p.findall(f'.//{qn("t")}')): return p return None # 克隆来源:插入点后面那条原文条款的【标题段】和它的【正文段】 title_src = find('8.争端的解决') # 兄弟条款标题(自带 + 宋体四属性 + 标题pPr缩进) body_src = find('双方如在履行合同中发生纠纷') # 兄弟条款正文(无bold + firstLine=420 缩进) def clone_as_ins(src_para, new_text): """深拷贝 src_para 的 pPr + 首run rPr,替换文本,整段(含¶)标 w:ins(author=WB)""" np = etree.Element(qn('p')) ppr = copy.deepcopy(src_para.find(qn('pPr'))) # ¶ 段落标记标插入 rprm = ppr.find(qn('rPr')) if rprm is None: rprm = etree.SubElement(ppr, qn('rPr')) insm = etree.SubElement(rprm, qn('ins')) insm.set(qn('id'), ed._next_id()); insm.set(qn('author'), 'WB'); insm.set(qn('date'), ed._revision_date) np.append(ppr) # run rPr 直接克隆兄弟段首 run(bold/字体/字号全继承,不碰库的默认值) src_r = src_para.find(qn('r')) src_rpr = copy.deepcopy(src_r.find(qn('rPr'))) if (src_r is not None and src_r.find(qn('rPr')) is not None) else None np.append(ed._mk_ins(new_text, src_rpr)) return np title_p = clone_as_ins(title_src, "8.转包与分包") body_p = clone_as_ins(body_src, "未经甲方书面同意,乙方不得将本合同项下的…连带责任。") idx = list(ed.body).index(title_src) ed.body.insert(idx, title_p) # 标题插在兄弟条款标题之前 → 成为新的"8.",兄弟顺延为"9." ed.body.insert(idx + 1, body_p) ``` ## A类手动编号的顺延(与 B 类自动顺延不同!) A 类编号是 run 里的字面文字,**不会自动顺延**。插入新"8."后,必须手动把后续所有手动编号 DEL 旧号+INS 新号(用 tracked_replace): ```python for old, new in [("8.争端的解决","9.争端的解决"), ("9.合同生效","10.合同生效"), ("9.1 本合同在…","10.1 本合同在…"), ("10.合同附件","11.合同附件"), ("10.1 配置清单","11.1 配置清单"), ..., ("12.特别约定","13.特别约定")]: ed.tracked_replace(old, new) ``` - **子编号一并顺延**(9.1/9.2→10.1/10.2,11.1-11.7→12.1-12.7)。 - 匹配串要够长以避免短串误命中(见 SKILL.md「tracked_replace 短字符串误命中」)。 ## 交付前验证(必做) 1. **bold 对照**:新标题 INS run `bold==True` 且 ==兄弟标题;新正文 INS run `bold==False` 且有正确 `firstLine` 缩进。 ```python r = p.find('.//w:ins/w:r', ns); b = r.find('w:rPr/w:b', ns) # 标题段 b is not None == 兄弟标题段 b is not None ``` 2. **WB INS 字体逐段核验(相对同段/同级原文)**:异常应为 0。原文 run 有显式宋体四属性时,克隆来的 INS 也带四属性——与原文一致即合格。 3. **接受所有修订后渲染**,确认手动编号链连续(…7、**8.转包**、9、10、10.1、10.2、11…13),无重号/跳号。 4. python-docx 能打开(XML 合法)。 ## 一句话 A 类手动编号合同新增带标题条款:**别用库的 `_title_rpr`/`_body_rpr`(启发式可能丢 bold)**,直接 `copy.deepcopy` 紧邻兄弟条款的【标题段】和【正文段】的 pPr+首run rPr,文本替换+整段标 w:ins;编号不会自动顺延,手动 tracked_replace 把后续主/子编号全部 +1。