翻呗 —— 你身边的翻译助手

内容中心 › 排版

排版

译文 PDF 复制出来的字,粘到别处就变怪字:CJK 兼容区踩坑记录

作者:翻呗运营2026-11-06阅读 1评论 0

摘要 做 PDF 翻译的质量验收时,遇到一个很难一眼看出来的问题。
本文原发于 CSDN(2026-11-06),同步发布于本站内容中心;站内版本保留全文,格式按本站排版重排。

做 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)

用你自己的文档验一遍文里的做法可以直接复用,换成你的文件同样成立。
上传文档,开始翻译

客户评论(0)

还没有评论,欢迎分享你的实测体验。
评论先审后发 · 仅登录客户可评