内容中心 › 排版
做 PDF 翻译的质量验收时,遇到一个很难一眼看出来的问题。
译文页面看起来完全正常,公式、表格、字号都对。用阅读器选中一段,复制到编辑器里,字也都在。但拿这段复制出来的文本去做术语统计,结果总是对不上:明明数了 97 个 attention 对应的「注意力」,程序只认出 60 多个。
一开始以为是编码问题,或者是 PDF 文字层被拆成了碎块。查到最后,问题出在一批长得一模一样、但码位完全不同的汉字上。
素材还是 arXiv:1706.03762 那篇 15 页的 Transformer 论文,译文同为 15 页。下面每个数字都从两份 PDF 的文字层直接提取,没有二次排版。
抽取工具用 PyMuPDF 的 get_text(),术语统计先做一遍大小写归一,再数子串出现次数。
import fitz, unicodedata
doc = fitz.open('translated.pdf')
raw = ''.join(p.get_text() for p in doc)
n_raw = raw.count('注意力') # 62
n_nfkc = unicodedata.normalize('NFKC', raw).count('注意力') # 96
同一份文件,归一化前后差了 34 次。
把对不上的字符单独打出来看:
bad = [c for c in raw if 0xF900 <= ord(c) <= 0xFAFF]
from collections import Counter
print(Counter(bad).most_common(8))
# [('', 12), ('', 7), ('', 5), ...]
print([(c, hex(ord(c)), unicodedata.normalize('NFKC', c)) for c in set(bad)][:5])
# [('', '0xf98a', '注'), ('', '0xf9dd', '意'), ...]
这些字符落在 U+F900–U+FAFF,CJK 兼容表意文字区。它们在屏幕上和常用汉字长得一样,甚至部分字体共用一个字形,但 Unicode 码位不同。PDF 里的 ToUnicode 映射如果指到了兼容区,复制出来的字符串就带着这批「长得一样但不是同一个字」的字符。
不是翻译引擎翻错,也不是编码写坏。
PDF 的中文文字层依赖字体里的 cmap / ToUnicode 表来做「字形 → Unicode」的反查。某些中文字体的 cmap 里,同一个字形既映射到基本区(U+4E00–U+9FFF),也映射到兼容区。生成 PDF 时如果取了后一条映射,页面渲染照常正确,文字层里却留下兼容区码位。
再叠一层:这类字符在部分编辑器、终端里显示成空白或方框,日志里看不出异常。所以这个问题往往到「拿文本做下游处理」时才暴露。
| 场景 | 表现 | 后果 |
|---|---|---|
| 术语一致性统计 | 命中数偏低 | 误判「术语没统一」,反过来去改翻译逻辑 |
| 全文检索 / 索引 | 搜常用字搜不到 | 明明页面上有,检索结果为 0 |
| 复制粘贴到别处 | Word / 输入法里出现怪字 | 用户直接感知,且很难自己定位原因 |
| 前后文本 diff | 视觉相同却整行标红 | 版式验收假失败 |
第四类最麻烦。做译文与原文的逐段比对时,如果一边来自原始文本、一边来自 PDF 文字层,兼容区字符会让整段被判为「完全不同」,把真正的排版问题淹没掉。
最省事的做法是在所有文本处理的入口统一做 NFKC,而不是在每个统计函数里单独兜底:
def clean(s: str) -> str:
# NFKC 会把兼容表意文字折叠回基本区,同时处理全角字母、罗马数字等兼容字符
return unicodedata.normalize('NFKC', s)
text = clean(raw)
几个实测结论:
1. NFKC 够用,不需要自建映射表。 兼容区汉字在 Unicode 里有标准分解,NFKC 会折叠回基本区。自己维护「怪字 → 正字」字典反而容易漏。
2. 别用 NFC / NFD。 它们只做组合字符的规范组合,不处理兼容等价。实测 NFC 之后 count 仍是 62。
3. 只对「从 PDF 提取的文本」归一,不要对译文源文件归一。 源文件本身是干净的,多余归一化可能影响公式里的全角符号排版语义。
4. 验收脚本加一条断言:抽取后若 0xF900 <= ord(c) <= 0xFAFF 的字符数 > 0,打印警告并计入报告。这样下次换字体或换生成链路时能立刻发现。
很多工具介绍里会写「译文可复制、可搜索」。经过这件事之后,我觉得这个说法得收一收:
所以更准确的表述是「文字层可提取,下游做术语统计前需 NFKC 归一化」。差一个限定词,验收时省掉一次误判。
---
口径说明:原文与译文均为 15 页,文字层用 PyMuPDF get_text() 提取;计数为子串出现次数,未做分词;兼容区字符统计范围 U+F900–U+FAFF。
本文用到的在线文档翻译服务与版式说明:[整份 PDF 保留公式与表格的做法](https://trans.sunnytranslation.com/pdf-fanyi)