5.4 KiB
5.4 KiB
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行)用启发式认"条款标题":
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() 要 <w:b/> 或标题字体(黑体/SimHei…) 才算 title
坑:当标题就是"8.争端的解决"(宋体 + <w:b/>,无特殊标题字体),若该段在扫描中没被 _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:
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.争端的解决') # 兄弟条款标题(自带 <w:b/> + 宋体四属性 + 标题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):
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 短字符串误命中」)。
交付前验证(必做)
- bold 对照:新标题 INS run
bold==True且 ==兄弟标题;新正文 INS runbold==False且有正确firstLine缩进。r = p.find('.//w:ins/w:r', ns); b = r.find('w:rPr/w:b', ns) # 标题段 b is not None == 兄弟标题段 b is not None - WB INS 字体逐段核验(相对同段/同级原文):异常应为 0。原文 run 有显式宋体四属性时,克隆来的 INS 也带四属性——与原文一致即合格。
- 接受所有修订后渲染,确认手动编号链连续(…7、8.转包、9、10、10.1、10.2、11…13),无重号/跳号。
- python-docx 能打开(XML 合法)。
一句话
A 类手动编号合同新增带标题条款:别用库的 _title_rpr/_body_rpr(启发式可能丢 bold),直接 copy.deepcopy 紧邻兄弟条款的【标题段】和【正文段】的 pPr+首run rPr,文本替换+整段标 w:ins;编号不会自动顺延,手动 tracked_replace 把后续主/子编号全部 +1。