115 lines
5.3 KiB
Python
115 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""编号链诊断探针 — 一次性看清 docx 的自动编号/手动编号全貌。
|
|
|
|
用途:合同编号疑似错乱(重复/跳号/双号)时,动手改之前必跑此脚本。
|
|
它把三件事一次性摊开,让你判断「是我们WB改错的 / 他人修订重排的 / 还是源文件自带的潜伏自动编号」:
|
|
1. 每个段落:是否带 <w:numPr>(自动编号)、numId、ilvl、是否整段ins/del
|
|
2. numbering.xml 解析:numId→abstractNum→(numFmt, lvlText, start) ——
|
|
⚠️ start≠1 的 decimal 列表会渲染出「6、」之类的可见编号,但 run 里没有这个字!
|
|
这是最隐蔽的坑:源文件起草人给某段挂了 numId(start=6),OnlyOffice 自动显示「6、售后服务」,
|
|
而你在末尾新增条款时只数了手打的「1 2 3 4 5」,顺手编成「6」→ 与潜伏的自动6撞号。
|
|
3. 每段「接受所有修订后」的可见文本(去w:del、保w:ins),近似 OnlyOffice 接受后视图
|
|
|
|
用法: python numbering-diagnose.py <contract.docx>
|
|
.doc 先转换: soffice --headless --convert-to docx <file>.doc
|
|
"""
|
|
import sys, zipfile
|
|
from lxml import etree
|
|
|
|
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
|
|
|
|
def text_mode(p, mode):
|
|
"""mode='final': 接受所有修订后(去del,保ins). mode='orig': 修订前(去ins,保del)."""
|
|
parts = []
|
|
for node in p.iter():
|
|
if node.tag == W + 't':
|
|
anc, skip = node, False
|
|
while anc is not None:
|
|
if mode == 'final' and anc.tag == W + 'del':
|
|
skip = True; break
|
|
if mode == 'orig' and anc.tag == W + 'ins':
|
|
skip = True; break
|
|
anc = anc.getparent()
|
|
if not skip:
|
|
parts.append(node.text or '')
|
|
elif node.tag == W + 'delText' and mode == 'orig':
|
|
parts.append(node.text or '')
|
|
return ''.join(parts).strip()
|
|
|
|
def parse_numbering(z):
|
|
"""返回 numId -> (numFmt, lvlText, start) 仅 lvl0(够用于条款标题层)."""
|
|
out = {}
|
|
if 'word/numbering.xml' not in z.namelist():
|
|
return out
|
|
num = etree.fromstring(z.read('word/numbering.xml'))
|
|
n2a = {}
|
|
for n in num.findall(W + 'num'):
|
|
ab = n.find(W + 'abstractNumId')
|
|
if ab is not None:
|
|
n2a[n.get(W + 'numId')] = ab.get(W + 'val')
|
|
a2fmt = {}
|
|
for ab in num.findall(W + 'abstractNum'):
|
|
l0 = ab.find(W + 'lvl')
|
|
if l0 is not None:
|
|
fmt = l0.find(W + 'numFmt')
|
|
txt = l0.find(W + 'lvlText')
|
|
st = l0.find(W + 'start')
|
|
a2fmt[ab.get(W + 'abstractNumId')] = (
|
|
fmt.get(W + 'val') if fmt is not None else '?',
|
|
txt.get(W + 'val') if txt is not None else '',
|
|
st.get(W + 'val') if st is not None else '1',
|
|
)
|
|
for nid, aid in n2a.items():
|
|
out[nid] = a2fmt.get(aid, ('?', '', '1'))
|
|
return out
|
|
|
|
def main(path):
|
|
z = zipfile.ZipFile(path)
|
|
root = etree.fromstring(z.read('word/document.xml'))
|
|
numinfo = parse_numbering(z)
|
|
|
|
print(f"### {path}\n")
|
|
print("=== numbering.xml: numId -> (numFmt, lvlText, start) ===")
|
|
if not numinfo:
|
|
print(" (无 numbering.xml — 全文应为手动文本编号)")
|
|
for nid, (fmt, txt, st) in sorted(numinfo.items()):
|
|
warn = ' ⚠️start≠1 会渲染潜伏编号!' if (fmt == 'decimal' and st != '1') else ''
|
|
print(f" numId={nid}: fmt={fmt}, lvlText='{txt}', start={st}{warn}")
|
|
print()
|
|
print("idx | numPr(自动) | rendered | ins/del | 文本(接受修订后)")
|
|
print("-" * 92)
|
|
for i, p in enumerate(root.findall('.//' + W + 'p')):
|
|
tf = text_mode(p, 'final')
|
|
if not tf:
|
|
continue
|
|
npr = p.find('.//' + W + 'numPr')
|
|
npinfo, rendered = '—', ''
|
|
if npr is not None:
|
|
nid_el = npr.find(W + 'numId')
|
|
il_el = npr.find(W + 'ilvl')
|
|
nid = nid_el.get(W + 'val') if nid_el is not None else '?'
|
|
il = il_el.get(W + 'val') if il_el is not None else '0'
|
|
npinfo = f"numId={nid},lvl={il}"
|
|
fmt, txt, st = numinfo.get(nid, ('?', '', '1'))
|
|
if fmt == 'decimal':
|
|
rendered = (txt or '%1、').replace('%1', st) # 该项首个渲染值(近似)
|
|
elif fmt == 'bullet':
|
|
rendered = '•'
|
|
elif fmt == 'none':
|
|
rendered = '(无)'
|
|
has_ins = p.find('.//' + W + 'ins') is not None
|
|
has_del = p.find('.//' + W + 'del') is not None
|
|
mk = ('INS' if has_ins else '') + ('/' if has_ins and has_del else '') + ('DEL' if has_del else '')
|
|
print(f"{i:3d} | {npinfo:18s} | {rendered:8s} | {mk:7s} | {tf[:46]}")
|
|
print()
|
|
print("判读要点:")
|
|
print(" - rendered 列非空 = OnlyOffice 会自动加这个编号(run里没有这串字)")
|
|
print(" - 手动编号: rendered='—' 且文本以「N、」开头 = 编号是写死的文字")
|
|
print(" - 若末尾新增条款(INS)的手打编号 与 上方某段 rendered 自动编号 相同 → 撞号")
|
|
print(" 正确做法: 新增手打编号应接续【rendered 自动值】往下编, 不是接续最后一个手打数字")
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) < 2:
|
|
print(__doc__); sys.exit(1)
|
|
main(sys.argv[1])
|