# 拆分合并的标题+正文段落为两个独立INS段落 ## 场景 Reviewer发现新增条款的标题和正文被合并在一个``段落中(通过``内的换行符分隔),要求拆分为两个独立段落——标题段和正文段,各有独立的格式。 ## 判别 - 目标段落是一个``,内含一个``,``内只有一个``,``文本包含换行符(`\n`)分隔标题和正文 - 标题格式要求:参照原文同级标题段落(如"第四条"或"第六条") - 正文格式要求:参照原文同层级正文段落(如"一、施工期限") ## 操作步骤 ### 1. 读取原文并定位目标段落 ```python import zipfile from lxml import etree import copy W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' with zipfile.ZipFile(docx_path, 'r') as zf: doc_xml = etree.parse(zf.open('word/document.xml')) all_files = {name: zf.read(name) for name in zf.namelist()} body = doc_xml.getroot().find(f'{{{W}}}body') paragraphs = list(body.findall(f'{{{W}}}p')) # 找到目标段落 for i, p in enumerate(paragraphs): texts = [] for elem in p.iter(f'{{{W}}}t'): texts.append(elem.text or '') full_text = ''.join(texts) if '第五条' in full_text and '转包' in full_text: target_idx = i break ``` ### 2. 提取标题和正文文本 ```python full_text = '' for elem in target_p.iter(f'{{{W}}}t'): full_text += elem.text or '' lines = full_text.split('\n') title_text = lines[0].strip() # "第五条 转包与分包" body_text = '\n'.join(lines[1:]).strip() # 正文内容 ``` ### 3. 找到参照段落并克隆pPr 标题段pPr从紧邻的同级标题段落克隆(如"第六条"),正文段pPr从同层级正文段落克隆(如"一、施工期限")。 ```python # 标题参照段落(如"第六条") ref_title_p = paragraphs[24] # 原文"第六条"的索引 ref_title_pPr = ref_title_p.find(f'{{{W}}}pPr') title_pPr = copy.deepcopy(ref_title_pPr) # 正文参照段落(如"一、施工期限") ref_body_p = paragraphs[13] # 原文"一、施工期限"的索引 ref_body_pPr = ref_body_p.find(f'{{{W}}}pPr') body_pPr = copy.deepcopy(ref_body_pPr) ``` ### 4. 构建标题段落 ```python title_p = etree.Element(f'{{{W}}}p', nsmap=target_p.nsmap) title_p.append(title_pPr) # 标题rPr:黑体四属性 + hint=eastAsia + sz=24 + bold title_rPr = etree.Element(f'{{{W}}}rPr') rFonts = etree.SubElement(title_rPr, f'{{{W}}}rFonts') for attr in ['ascii', 'hAnsi', 'eastAsia', 'cs']: rFonts.set(f'{{{W}}}{attr}', '黑体') rFonts.set(f'{{{W}}}hint', 'eastAsia') etree.SubElement(title_rPr, f'{{{W}}}spacing').set(f'{{{W}}}val', '-6') etree.SubElement(title_rPr, f'{{{W}}}sz').set(f'{{{W}}}val', '24') etree.SubElement(title_rPr, f'{{{W}}}szCs').set(f'{{{W}}}val', '24') etree.SubElement(title_rPr, f'{{{W}}}b') # 加粗 title_ins = etree.SubElement(title_p, f'{{{W}}}ins') title_ins.set(f'{{{W}}}id', str(new_ins_id)) title_ins.set(f'{{{W}}}author', 'WB') title_ins.set(f'{{{W}}}date', '2026-06-26T14:00:00Z') title_r = etree.SubElement(title_ins, f'{{{W}}}r') title_r.set(f'{{{W}}}rsidR', '00AA0001') title_r.append(copy.deepcopy(title_rPr)) title_t = etree.SubElement(title_r, f'{{{W}}}t') title_t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') title_t.text = title_text ``` ### 5. 构建正文段落 ```python body_p = etree.Element(f'{{{W}}}p', nsmap=target_p.nsmap) body_p.append(body_pPr) # 正文rPr:宋体四属性 + hint=eastAsia + sz=21 body_rPr = etree.Element(f'{{{W}}}rPr') body_rFonts = etree.SubElement(body_rPr, f'{{{W}}}rFonts') for attr in ['ascii', 'hAnsi', 'eastAsia', 'cs']: body_rFonts.set(f'{{{W}}}{attr}', '宋体') body_rFonts.set(f'{{{W}}}hint', 'eastAsia') etree.SubElement(body_rPr, f'{{{W}}}spacing').set(f'{{{W}}}val', '-4') etree.SubElement(body_rPr, f'{{{W}}}sz').set(f'{{{W}}}val', '21') body_ins = etree.SubElement(body_p, f'{{{W}}}ins') body_ins.set(f'{{{W}}}id', str(new_ins_id + 1)) body_ins.set(f'{{{W}}}author', 'WB') body_ins.set(f'{{{W}}}date', '2026-06-26T14:00:00Z') body_r = etree.SubElement(body_ins, f'{{{W}}}r') body_r.set(f'{{{W}}}rsidR', '00AA0001') body_r.append(copy.deepcopy(body_rPr)) body_t = etree.SubElement(body_r, f'{{{W}}}t') body_t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') body_t.text = body_text ``` ### 6. 插入并删除原段落(⚠️ addprevious顺序陷阱) **关键**:`addprevious`将元素插入到目标元素的**紧邻前一个**位置。要得到 [title, body, old_p] 的顺序,必须: ```python old_p = paragraphs[target_idx] old_p.addprevious(body_p) # 先插入body → 顺序: body, old_p body_p.addprevious(title_p) # 再在body前插入title → 顺序: title, body, old_p body.remove(old_p) # 删除原段落 → 顺序: title, body, ... ``` **错误做法**(会导致顺序反转): ```python # ❌ 错误:title在body之后 old_p.addprevious(title_p) # title, old_p old_p.addprevious(body_p) # title, body, old_p ← 看起来对但实际是 body, title, old_p ``` 原理:`addprevious`始终插入到目标元素的紧邻前一个位置。`old_p.addprevious(body_p)` 后 body_p 是 old_p 的前一个兄弟;`old_p.addprevious(title_p)` 后 title_p 成为 old_p 的前一个兄弟,body_p 被推到 title_p 之前。 ### 7. 保存 ```python new_doc_xml = etree.tostring(doc_xml.getroot(), xml_declaration=True, encoding='UTF-8', standalone=True) with zipfile.ZipFile(docx_path, 'w', zipfile.ZIP_DEFLATED) as zf_out: for name, data in all_files.items(): if name == 'word/document.xml': zf_out.writestr(name, new_doc_xml) else: zf_out.writestr(name, data) ``` ## 验证 1. **段落顺序**:确认 [title_idx] 是标题文本,[title_idx+1] 是正文文本 2. **字体属性**:标题 rPr 含 rFonts四属性(黑体) + hint=eastAsia + sz=24 + bold;正文 rPr 含 rFonts四属性(宋体) + hint=eastAsia + sz=21 3. **INS属性**:author=WB, 有 rsidR, 有唯一id 4. **OnlyOffice渲染**:x2t渲染为PDF,pdftotext确认标题和正文各占一行,正文有缩进 5. **validate()**:运行ContractEditor的validate(),区分预存误报和本轮新增问题 ## 注意事项 - 标题和正文的pPr应从**紧邻的原文同级段落**克隆,而非从目标段落自身克隆 - rFonts必须设置四属性(ascii, hAnsi, eastAsia, cs),仅设hint=eastAsia是不够的 - 标题的bold属性按照reviewer的指令设置(注意:原文标题可能不加粗,但reviewer可能要求加粗) - 两个INS段落使用不同的id(从文档中max_ins_id+1开始递增) - 操作前先备份原文件