feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# 拆分合并的标题+正文段落为两个独立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开始递增)
|
||||
- 操作前先备份原文件
|
||||
Reference in New Issue
Block a user