# 拆分合并的标题+正文段落为两个独立INS段落

## 场景

Reviewer发现新增条款的标题和正文被合并在一个`<w:p>`段落中（通过`<w:t>`内的换行符分隔），要求拆分为两个独立段落——标题段和正文段，各有独立的格式。

## 判别

- 目标段落是一个`<w:p>`，内含一个`<w:ins author="WB">`，`<w:ins>`内只有一个`<w:r>`，`<w:t>`文本包含换行符（`\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开始递增）
- 操作前先备份原文件