feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# tracked_replace 跨 w:ins 元素失败的处理
|
||||
|
||||
## 症状
|
||||
|
||||
`tracked_replace(old, new)` 抛出 `ValueError: Element is not a child of this node`,
|
||||
发生在 `contract_docx_lib.py` 第368行 `parent.remove(runs[idx])`。
|
||||
|
||||
## 根因
|
||||
|
||||
合同已经过上一轮 workflow 修订,原文段落中插入了 `w:ins(author="WB")` 元素。
|
||||
目标匹配文本跨越了 `w:r` 和 `w:ins` 边界:
|
||||
|
||||
```
|
||||
w:r: "...若甲方在双"
|
||||
w:ins(author="WB"): "方"
|
||||
w:r: "核对消费金额时未提出异议的..."
|
||||
```
|
||||
|
||||
`tracked_replace` 把 `w:r` 和 `w:ins` 下的 `w:r` 都收集到 `runs` 列表,
|
||||
但 `parent.remove(runs[idx])` 时,`w:ins` 内的 `w:r` 的 parent 是 `w:ins`,不是 `w:p` → 报错。
|
||||
|
||||
## 判别
|
||||
|
||||
修改前先遍历目标段落的子元素,看是否有 `w:ins` 分割了匹配文本:
|
||||
|
||||
```python
|
||||
for elem in paragraph:
|
||||
tag = elem.tag.split('}')[-1]
|
||||
if tag in ('r', 'ins', 'del'):
|
||||
print(f" {tag}: '{''.join(t.text or '' for t in elem.findall('.//{W}t'))}'")
|
||||
```
|
||||
|
||||
## 修法:zipfile+lxml 直接操作
|
||||
|
||||
不用 `tracked_replace`,改用 zipfile+lxml 四步操作:
|
||||
|
||||
### 步骤1:裁掉第一段 run 中跨越的部分
|
||||
|
||||
```python
|
||||
# 如:w:r 末尾是 "...核对确认,若甲方在双" → 裁掉 "若甲方在双"
|
||||
assert r1_t.text.endswith('若甲方在双')
|
||||
r1_t.text = r1_t.text[:-6] # 移除6个字符
|
||||
```
|
||||
|
||||
### 步骤2:创建 DEL 包裹被裁掉的文字
|
||||
|
||||
```python
|
||||
del_elem = etree.Element(qn('del'))
|
||||
del_elem.set(qn('id'), str(next_del_id))
|
||||
del_elem.set(qn('author'), 'WB')
|
||||
del_elem.set(qn('date'), revision_date)
|
||||
|
||||
del_run = etree.SubElement(del_elem, qn('r'))
|
||||
del_rpr = etree.SubElement(del_run, qn('rPr'))
|
||||
# 从被裁 run 复制 rPr
|
||||
for child in r1_elem.find(qn('rPr')):
|
||||
del_rpr.append(copy.deepcopy(child))
|
||||
del_text = etree.SubElement(del_run, qn('delText'))
|
||||
del_text.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
|
||||
del_text.text = '若甲方在双方' # 被裁文字 + w:ins 内容
|
||||
```
|
||||
|
||||
### 步骤3:移除原来的 w:ins 元素
|
||||
|
||||
```python
|
||||
p.remove(ins_elem)
|
||||
```
|
||||
|
||||
### 步骤4:DEL旧文字 + INS新文字
|
||||
|
||||
```python
|
||||
# DEL 旧文字(第三段 run 的完整内容)
|
||||
del_elem2 = ... # 同上模式,delText = r3_t.text
|
||||
# INS 新文字
|
||||
ins_elem = etree.Element(qn('ins'))
|
||||
ins_elem.set(qn('id'), str(next_ins_id))
|
||||
ins_elem.set(qn('author'), 'WB')
|
||||
ins_elem.set(qn('date'), revision_date)
|
||||
ins_run = etree.SubElement(ins_elem, qn('r'))
|
||||
# 从原 run 复制 rPr
|
||||
ins_rpr = etree.SubElement(ins_run, qn('rPr'))
|
||||
for child in r3_elem.find(qn('rPr')):
|
||||
ins_rpr.append(copy.deepcopy(child))
|
||||
ins_t = etree.SubElement(ins_run, qn('t'))
|
||||
ins_t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
|
||||
ins_t.text = new_text
|
||||
|
||||
# 替换:在 r3 位置前插入 DEL 和 INS,再删除 r3
|
||||
r3_pos = list(p).index(r3_elem)
|
||||
p.insert(r3_pos, del_elem2)
|
||||
p.insert(r3_pos + 1, ins_elem)
|
||||
p.remove(r3_elem)
|
||||
```
|
||||
|
||||
### 步骤5:写回
|
||||
|
||||
```python
|
||||
doc_xml_modified = etree.tostring(tree, encoding='UTF-8', xml_declaration=True)
|
||||
with zipfile.ZipFile(src, 'r') as zf:
|
||||
file_data = {f: zf.read(f) for f in zf.namelist()}
|
||||
with zipfile.ZipFile(src, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for f, data in file_data.items():
|
||||
zf.writestr(f, doc_xml_modified if f == 'word/document.xml' else data)
|
||||
```
|
||||
|
||||
## 易错点
|
||||
|
||||
### 1. 中文切片长度
|
||||
`[:-3]` 移除3个**字符**(不是字节)。"但本" = 2个中文字符 → 用 `[:-2]`。
|
||||
用 `[:-3]` 会多切一个字(如把";"也切掉)。
|
||||
|
||||
**⚠️ 全角标点也是1个字符(2026-06-26 健康积分兑换协议教训)**:`(四)` 是3个字符——`(`(U+FF08全角左括号=1字)、`四`(1字)、`)`(U+FF09全角右括号=1字)。用 `[4:]` 切片会多切掉1个中文字符,导致 DEL 文本缺字("甲方"→"方")。**判别**:数切片偏移时,全角括号/标点(()、【】、《》、。,!?等)每符1字,不因"看起来宽"就计为多个。切片前 `print(repr(text[:10]))` 确认边界。
|
||||
|
||||
### 2. 旧文末尾与新文开头重复
|
||||
原文 run 裁掉部分后,末尾可能与 INS 新文字开头重复。
|
||||
如:原文末尾是"核对确认",新文字开头也是"核对确认" → 出现"核对确认核对确认"。
|
||||
**修法**:从 INS 新文字中去掉重复前缀。
|
||||
|
||||
### 3. 标点符号归属
|
||||
裁掉 run 末尾文字时,确保标点符号(;。等)留在正确位置。
|
||||
如原文"费用;但本"裁掉"但本"后应为"费用;"(保留分号)。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `python-docx` 能打开(`Document(out)` 不抛异常)
|
||||
2. `wb-ins-font-verify.py` 所有 WB INS 字体一致
|
||||
3. 用 `ContractEditor.get_para_text()` 读段落文本,确认无重复/缺字
|
||||
Reference in New Issue
Block a user