docs(architecture): 完成技术验证 S1–S4,确定 FR-M2-05/06 目标值
- spikes/:Word 转 PDF(Gotenberg)、格式识别、原句坐标、DeepSeek 抽取四项验证代码与两版提示词;样本派生产物和 .env 不进 git - tech-validation.md:完整结论。四项都可行:WPS 字体字符集要修正并改用行高调整过的字体;格式识别不需要 Docling;表格内切句待改进;抽取需第三道依据校验与规则兜底 - architecture.md:§3.5 加预处理与字体包,§3.6 证据校验改为三道,§10 回填结论 - 03 功能需求与 decisions.md(TV1):召回 ≥ 99%、采纳准确率 ≥ 95%、页码 100%、高亮正文 ≥ 98% / 表格 ≥ 95% Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
30
spikes/s1-convert/convert.py
Normal file
30
spikes/s1-convert/convert.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""S1:用 Gotenberg 把 10 份样本转成 PDF,记录页数、耗时、是否失败。
|
||||
用法:python -I convert.py <配置名> <Gotenberg 地址> [预处理后的 DOCX 目录]
|
||||
"""
|
||||
import json, sys, time, pathlib
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
import httpx, pypdfium2 as pdfium
|
||||
from common import IDS, sample_file, sniff
|
||||
|
||||
cfg, base = sys.argv[1], sys.argv[2]
|
||||
src_dir = pathlib.Path(sys.argv[3]) if len(sys.argv) > 3 else None # 预处理后的 DOCX 目录
|
||||
out = pathlib.Path(__file__).parent / "out" / cfg
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
rows = {}
|
||||
with httpx.Client(timeout=600) as c:
|
||||
for sid in IDS:
|
||||
f = (src_dir / f"{sid}.docx") if src_dir else sample_file(sid)
|
||||
ext = sniff(f) # 按真实格式给扩展名,LibreOffice 据此选过滤器
|
||||
t = time.monotonic()
|
||||
r = c.post(f"{base}/forms/libreoffice/convert",
|
||||
files={"files": (f"{sid}.{ext}", f.read_bytes())})
|
||||
dt = round(time.monotonic() - t, 1)
|
||||
if r.status_code != 200:
|
||||
rows[sid] = {"ok": False, "status": r.status_code, "err": r.text[:200], "sec": dt}
|
||||
else:
|
||||
pdf = out / f"{sid}.pdf"
|
||||
pdf.write_bytes(r.content)
|
||||
n = len(pdfium.PdfDocument(str(pdf)))
|
||||
rows[sid] = {"ok": True, "pages": n, "sec": dt}
|
||||
print(cfg, sid, rows[sid], flush=True)
|
||||
(out / "result.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1))
|
||||
31
spikes/s1-convert/fix_charset.py
Normal file
31
spikes/s1-convert/fix_charset.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""修正 WPS 写错的字体字符集:中文字体(名字含汉字,或是常见中文字体英文名)被标成 SYMBOL(02) 时改成 GB2312(86)。
|
||||
输入、输出都是 DOCX。用法:python -I fix_charset.py in.docx out.docx"""
|
||||
import re, sys, zipfile
|
||||
|
||||
CJK_LATIN = {"SimSun", "NSimSun", "SimHei", "FangSong", "KaiTi", "Microsoft YaHei", "DengXian"}
|
||||
|
||||
def is_cjk_font(name: str) -> bool:
|
||||
return bool(re.search(r"[一-鿿]", name)) or name in CJK_LATIN
|
||||
|
||||
def fix_font_table(xml: str) -> tuple[str, list[str]]:
|
||||
fixed = []
|
||||
def repl(m):
|
||||
name, body = m.group(1), m.group(2)
|
||||
if is_cjk_font(name) and re.search(r'w:charset w:val="02"', body):
|
||||
fixed.append(name)
|
||||
body = body.replace('w:charset w:val="02"', 'w:charset w:val="86"')
|
||||
return f'<w:font w:name="{name}">{body}</w:font>'
|
||||
return re.sub(r'<w:font w:name="([^"]+)">(.*?)</w:font>', repl, xml, flags=re.S), fixed
|
||||
|
||||
def main(src, dst):
|
||||
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
|
||||
for item in zin.infolist():
|
||||
data = zin.read(item.filename)
|
||||
if item.filename == "word/fontTable.xml":
|
||||
xml, fixed = fix_font_table(data.decode("utf-8"))
|
||||
data = xml.encode("utf-8")
|
||||
print("fixed:", fixed)
|
||||
zout.writestr(item, data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1], sys.argv[2])
|
||||
32
spikes/s1-convert/fonts-B.conf
Normal file
32
spikes/s1-convert/fonts-B.conf
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
<match target="pattern"><test name="family" qual="any"><string>宋体</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>SimSun</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>新宋体</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>NSimSun</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>仿宋</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>FangSong</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>仿宋_GB2312</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>FangSong_GB2312</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>楷体</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>KaiTi</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>楷体_GB2312</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>KaiTi_GB2312</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>华文楷体</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>华文宋体</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>华文仿宋</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正仿宋_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正楷体_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正小标宋简体</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正小标宋_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正书宋_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Serif CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>黑体</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Sans CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>SimHei</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Sans CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>微软雅黑</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Sans CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>Microsoft YaHei</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Sans CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正黑体_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Sans CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>华文黑体</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Sans CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>等线</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Sans CJK SC</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>DengXian</string></test><edit name="family" mode="assign" binding="strong"><string>Noto Sans CJK SC</string></edit></match>
|
||||
</fontconfig>
|
||||
32
spikes/s1-convert/fonts-C.conf
Normal file
32
spikes/s1-convert/fonts-C.conf
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
<match target="pattern"><test name="family" qual="any"><string>宋体</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>SimSun</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>新宋体</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>NSimSun</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>仿宋</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>FangSong</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>仿宋_GB2312</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>FangSong_GB2312</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>楷体</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>KaiTi</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>楷体_GB2312</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>KaiTi_GB2312</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>华文楷体</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>华文宋体</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>华文仿宋</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正仿宋_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正楷体_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正小标宋简体</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正小标宋_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正书宋_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>BD Song</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>黑体</string></test><edit name="family" mode="assign" binding="strong"><string>BD Hei</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>SimHei</string></test><edit name="family" mode="assign" binding="strong"><string>BD Hei</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>微软雅黑</string></test><edit name="family" mode="assign" binding="strong"><string>BD Hei</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>Microsoft YaHei</string></test><edit name="family" mode="assign" binding="strong"><string>BD Hei</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>方正黑体_GBK</string></test><edit name="family" mode="assign" binding="strong"><string>BD Hei</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>华文黑体</string></test><edit name="family" mode="assign" binding="strong"><string>BD Hei</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>等线</string></test><edit name="family" mode="assign" binding="strong"><string>BD Hei</string></edit></match>
|
||||
<match target="pattern"><test name="family" qual="any"><string>DengXian</string></test><edit name="family" mode="assign" binding="strong"><string>BD Hei</string></edit></match>
|
||||
</fontconfig>
|
||||
17
spikes/s1-convert/fonts_used.py
Normal file
17
spikes/s1-convert/fonts_used.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""统计样本用到的字体(东亚字体),决定字体替换表要覆盖哪些。"""
|
||||
import collections, re, sys, zipfile, pathlib
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
from common import IDS, sample_file, sniff
|
||||
tot = collections.Counter()
|
||||
for sid in IDS:
|
||||
f = sample_file(sid)
|
||||
if sniff(f) != "docx":
|
||||
continue
|
||||
with zipfile.ZipFile(f) as z:
|
||||
xml = z.read("word/document.xml").decode("utf-8", "replace")
|
||||
sty = z.read("word/styles.xml").decode("utf-8", "replace") if "word/styles.xml" in z.namelist() else ""
|
||||
c = collections.Counter(re.findall(r'w:eastAsia="([^"]+)"', xml + sty))
|
||||
c.update(re.findall(r'w:ascii="([^"]+)"', xml + sty))
|
||||
tot.update(c)
|
||||
print(sid, c.most_common(6))
|
||||
print("TOTAL", tot.most_common(15))
|
||||
15
spikes/s1-convert/linestats.py
Normal file
15
spikes/s1-convert/linestats.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""统计 PDF 正文页的行距和每页行数,找页数偏差的来源。"""
|
||||
import statistics, sys, pdfplumber
|
||||
for path in sys.argv[1:]:
|
||||
pitches, counts, sizes, fonts = [], [], [], {}
|
||||
with pdfplumber.open(path) as pdf:
|
||||
for page in pdf.pages[5:25]:
|
||||
ys = sorted({round(c["top"], 1) for c in page.chars if c["text"].strip()})
|
||||
if len(ys) < 10:
|
||||
continue
|
||||
counts.append(len(ys))
|
||||
pitches += [b - a for a, b in zip(ys, ys[1:]) if 8 < b - a < 40]
|
||||
for c in page.chars:
|
||||
sizes.append(round(c["size"], 1)); fonts[c["fontname"].split("+")[-1]] = fonts.get(c["fontname"].split("+")[-1], 0) + 1
|
||||
print(path.split("/out/")[-1], "lines/page", statistics.median(counts), "pitch", statistics.median(pitches),
|
||||
"size", statistics.mode(sizes), sorted(fonts.items(), key=lambda x: -x[1])[:3])
|
||||
30
spikes/s1-convert/make_fonts.py
Normal file
30
spikes/s1-convert/make_fonts.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""从 Noto CJK SC 克隆出行高接近宋体 / 黑体的字体(OFL 允许修改,改名后仍按 OFL 分发)。
|
||||
字形不变,只改垂直度量:ascent + descent = EM 倍数(参数 total),lineGap = 0。
|
||||
用法:python -I make_fonts.py <源目录> <输出目录> <total,如 1.0>"""
|
||||
import sys, pathlib
|
||||
from fontTools.ttLib import TTCollection
|
||||
|
||||
src, dst, total = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]), float(sys.argv[3])
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
plan = [("NotoSerifCJK-Regular.ttc", "BD Song", "Regular"), ("NotoSerifCJK-Bold.ttc", "BD Song", "Bold"),
|
||||
("NotoSansCJK-Regular.ttc", "BD Hei", "Regular"), ("NotoSansCJK-Bold.ttc", "BD Hei", "Bold")]
|
||||
for fname, family, style in plan:
|
||||
coll = TTCollection(str(src / fname))
|
||||
font = next(f for f in coll.fonts if "SC" in f["name"].getDebugName(1))
|
||||
upm = font["head"].unitsPerEm
|
||||
asc = round(upm * total * 0.86); desc = round(upm * total) - asc # 与宋体相同的 0.86 / 0.14 比例
|
||||
font["hhea"].ascent, font["hhea"].descent, font["hhea"].lineGap = asc, -desc, 0
|
||||
os2 = font["OS/2"]
|
||||
os2.sTypoAscender, os2.sTypoDescender, os2.sTypoLineGap = asc, -desc, 0
|
||||
os2.usWinAscent, os2.usWinDescent = asc, desc
|
||||
name = font["name"]
|
||||
full = f"{family} {style}" if style != "Regular" else family
|
||||
for rec in list(name.names):
|
||||
if rec.nameID in (1, 3, 4, 6, 16, 17, 21, 22):
|
||||
name.removeNames(nameID=rec.nameID)
|
||||
for nid, val in ((1, family), (2, style), (3, f"{family.replace(' ', '')}-{style};{total}"),
|
||||
(4, full), (6, f"{family.replace(' ', '')}-{style}")):
|
||||
name.setName(val, nid, 3, 1, 0x409); name.setName(val, nid, 1, 0, 0)
|
||||
out = dst / f"{family.replace(' ', '')}-{style}.otf"
|
||||
font.save(str(out))
|
||||
print(out.name, upm, asc, desc)
|
||||
50
spikes/s1-convert/toc_truth.py
Normal file
50
spikes/s1-convert/toc_truth.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""S1 替代真值:DOCX 目录里缓存的页码(生成文档的软件排版时写入)。
|
||||
比较「各标题之间相隔的页数」,消除页码起始值的影响。"""
|
||||
import json, re, sys, zipfile, pathlib
|
||||
import pdfplumber
|
||||
|
||||
OUT = pathlib.Path(__file__).parent / "out"
|
||||
norm = lambda s: re.sub(r"[\s ·.…\-—_]", "", s)
|
||||
|
||||
def toc_entries(docx):
|
||||
x = zipfile.ZipFile(docx).read("word/document.xml").decode()
|
||||
ents = []
|
||||
for p in re.findall(r"<w:p[ >].*?</w:p>", x, re.S):
|
||||
if "PAGEREF" not in p:
|
||||
continue
|
||||
# 段落里 PAGEREF 字段之前的文字是标题,字段结果是缓存页码
|
||||
before, after = p.split("PAGEREF", 1)
|
||||
title = norm("".join(re.findall(r"<w:t[^>]*>([^<]*)</w:t>", before)))
|
||||
num = re.findall(r"<w:t[^>]*>(\d+)</w:t>", after)
|
||||
if title and num:
|
||||
ents.append((title, int(num[0])))
|
||||
return ents
|
||||
|
||||
def heading_pages(pdf_path, titles):
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
texts = [norm(pg.extract_text() or "") for pg in pdf.pages]
|
||||
found = {}
|
||||
for t in titles:
|
||||
key = t[:18]
|
||||
hits = [i for i, tx in enumerate(texts) if key in tx]
|
||||
# 跳过目录页本身:取最后一个出现位置之前、且不在前 6 页的第一个命中
|
||||
body = [i for i in hits if i >= 4]
|
||||
found[t] = body[0] + 1 if body else None
|
||||
return found
|
||||
|
||||
rows = {}
|
||||
for sid in ["S03", "S04", "S06"]:
|
||||
ents = toc_entries(OUT / "fixed-docx" / f"{sid}.docx")
|
||||
rows[sid] = {"entries": len(ents)}
|
||||
for cfg in ["A-fixed", "B-fixed", "C10-fixed", "C13-fixed"]:
|
||||
hp = heading_pages(OUT / cfg / f"{sid}.pdf", [t for t, _ in ents])
|
||||
pairs = [(n, hp[t]) for t, n in ents if hp[t]]
|
||||
if len(pairs) < 2:
|
||||
rows[sid][cfg] = None; continue
|
||||
(c0, p0), (c1, p1) = pairs[0], pairs[-1]
|
||||
span_t, span_p = c1 - c0, p1 - p0
|
||||
dev = [abs((p - p0) - (c - c0)) for c, p in pairs]
|
||||
rows[sid][cfg] = {"matched": len(pairs), "span_truth": span_t, "span_pdf": span_p,
|
||||
"span_err_pct": round(100 * (span_p - span_t) / span_t, 1), "max_dev_pages": max(dev)}
|
||||
print(sid, json.dumps(rows[sid], ensure_ascii=False))
|
||||
(OUT / "toc_truth.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1))
|
||||
32
spikes/s1-convert/truth.py
Normal file
32
spikes/s1-convert/truth.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""S1 真值:Word 最后保存时记录的页数。DOCX 读 docProps/app.xml 的 <Pages>,旧版 DOC 读 OLE SummaryInformation 的 PIDSI_PAGECOUNT。"""
|
||||
import json, re, sys, zipfile, pathlib
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
import olefile
|
||||
from common import IDS, sample_file, sniff
|
||||
|
||||
OUT = pathlib.Path(__file__).parent / "out"
|
||||
rows = {}
|
||||
for sid in IDS:
|
||||
f = sample_file(sid)
|
||||
kind = sniff(f)
|
||||
info = {"kind": kind, "pages": None, "app": None, "ole_objects": 0}
|
||||
if kind == "docx":
|
||||
with zipfile.ZipFile(f) as z:
|
||||
names = z.namelist()
|
||||
if "docProps/app.xml" in names:
|
||||
app = z.read("docProps/app.xml").decode("utf-8", "replace")
|
||||
m = re.search(r"<Pages>(\d+)</Pages>", app)
|
||||
info["pages"] = int(m.group(1)) if m else None
|
||||
a = re.search(r"<Application>([^<]*)</Application>", app)
|
||||
info["app"] = a.group(1) if a else None
|
||||
info["ole_objects"] = sum(1 for n in names if n.startswith("word/embeddings/"))
|
||||
info["has_ole_xml"] = b"<o:OLEObject" in z.read("word/document.xml")
|
||||
elif kind == "doc":
|
||||
ole = olefile.OleFileIO(str(f))
|
||||
meta = ole.get_metadata()
|
||||
info["pages"] = meta.num_pages
|
||||
info["app"] = (meta.creating_application or b"").decode("gbk", "replace") if isinstance(meta.creating_application, bytes) else meta.creating_application
|
||||
info["ole_objects"] = sum(1 for e in ole.listdir() if e and e[0] == "ObjectPool")
|
||||
rows[sid] = info
|
||||
print(sid, info)
|
||||
(OUT / "truth.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1))
|
||||
Reference in New Issue
Block a user