同一个字,两种念法
两岸汉语读音差异的一次数据考古
「茜」在字典里只有 qiàn。可现实里,宋茜读 xī,茜茜公主也读 xī。
那个 xī 从哪来?为什么台湾字典里查不到?为了回答这一个问题,我花了几天时间,把两部字典的全部条目做了一遍机器比对。
过程中挖出的东西,比答案本身有意思得多——包括一个查了半年都没人发现的线上事故,
以及一句关于「自动化能做到什么」的教训。
一、从一个缺音说起
起因很小。
汉字卡的构建脚本里有一张人工维护的表,叫 READINGS_OVERRIDE,专门处理两岸读音分歧。里面躺着 10 个字:迹、企、菌、崖、发、括、携、堤、血、熟。
它们长这样:
"企": ["qǐ", "qì(台)"], # 大陆企鹅的 qǐ,台湾企业的 qì
"血": ["xuè", "xiě"], # 大陆血液的 xuè,台湾血球的 xiě
意思很清楚:萌典是台湾教育部的字典,收的是台湾读音。要做一个给大陆用户看的卡面,就得把大陆读音补上,台湾读音打上「(台)」的标记保留。
某天我查「茜」,发现卡面上只有 qiàn。
xī 呢?
宋茜、刘茜茜、奥地利皇后茜茜(Sissi)——这个音在现实里天天被用到,字典里却查不到。对用户来说,这解释不通。
于是我做了件很笨的事:写脚本把萌典和另一部大陆来源的字典做全量比对,想搞清楚到底有多少字「缺音」。
二、两岸为什么会念得不一样
在讲数据之前,得先说清一件事:读音差异不是「谁对谁错」,而是历史沉淀。
1. 文白异读:同一个字,两套系统
汉字里有一批字,从古到今就是两套读法并行。
最有名的是「行」:走路的行读 xíng,做买卖的行读 háng。「行当」「行家」「银行」都是 háng——因为「行」本来的意思就是「十字路口」,后来引申成「排成行列」,再引申成「行业」。
血也一样。上古音里血是入声(入声在普通话里已经消失了),读入声的字派生出两个读法:一个带 -s 尾,一个带 -t 尾。血保留了 xiě(书面)和 xuè(口语),到了台湾变成 xiě 和 xuè——恰好各取一个 mainland 的一半。
这类差异是最"讲道理"的:两边读音都能从汉字自己的历史里推出来,谁也没错。
2. 文言音的残留
还有一类差异来自更早的时期。
「之 zhū」这种读音,源自先秦的声母系统,现代普通话已经不用了,但台湾的文言传统保留了它。「亨 xiǎng」「仇 jū」「亏 yú」也属这类——它们在台湾字典里有,在大陆字典里往往没有,或者收作次要读音。
这类差异对普通用户几乎没有价值,但对读古书的人有用。
3. 破读字:为了区分词义而分音
「相」在「照相」里读 xiàng,在「相互」里读 xiāng。这类分音不是为了区分古今,而是为了在口语里区分不同的词。
两岸的破读习惯不完全一样,于是同一个字在两边的主读音可能不同。
4. 最复杂的一层:不同的审音机构
这一层最容易被忽略,也最容易出错。
大陆有《普通话异读词审音表》(1985 年发布,后有修订),台湾有教育部的《重編國語辭典修訂本》。两者是两个机构、两套标准,覆盖范围不同。
关键在于:审音表明确「不审专有名词」。所以「茜」在人名里的 xī,从一开始就不在审音表的管辖范围内——它是在日常使用中约定俗成形成的,不是有机构认定的。这个音在大陆是"事实上的标准",但在规范文件里找不到它。
我这次做卡面时遇到的「茜 xī」,就属于这一类。
5. 轻声与儿化:读音差异里最日常的部分
如果只看字典里的单字读音,会低估两岸差异的真实规模。真正让人「听不懂」的往往不是单字的读音,而是轻声和儿化。
实测几个词:
| 词 | 萌典(台湾) | 大陆通行 |
|---|---|---|
| 舒服 | shū fu | shū fú |
| 明白 | míng bai | míng bái |
| 东西 | dōng xi | dōng xī |
| 意思 | yì si | yì sī |
台湾保留了更多中古汉语的轻声习惯(尤其是「子」「头」类词缀的轻声化),大陆则在普通话规范化过程中,把一批原本读轻声的字重新归入本调。
有意思的是,萌典把这些轻声读法都当作正式的读音收了,不是记在注释里。因此做跨海数据合并时,这批差异会以「读音条目」的形式出现,而不是「可选读法」——如果按单字去比对,根本看不出来。
6. 还有一层:简体字
这一层与技术实现直接相关,后面会详细讲。
简繁合并让一些原本不同的字变成了同一个简化字。最典型的是「朴」:
- 「朴」(U+6734):读 piáo(朴姓)、pò(朴树)
- 「樸」(U+6A38):读 pǔ(质朴、朴素)
繁体里这是两个字,简化合并成了「朴」。所以在大陆,「朴」这个词同时承载了两边的读音。
这类差异对做工具的人最麻烦:你不能假设「简体字查到繁体字」是一对一的关系。
三、第一次踩坑:以为找到了正确的数据源
要判断「哪些读音大陆有、萌典没有」,需要两部字典:一部台湾的(萌典现成),一部大陆的。
我先看的是手边的 makemeahanzi-dictionary.txt——名字听起来很通用,还带拼音字段,应该正好是大陆数据。
拿它和萌典一比:
茜 → 只有 qiàn(没有 xī)
迹 → jī(这是台湾音,大陆读 jì)
它是空的。
原来 makemeahanzi 的拼音和萌典同源。用它和萌典比对,等于拿同一份数据减去自己,差集必然是空。
这是第一个教训:数据源的"名字"不说明它是什么。你以为的对照,可能根本不是对照组。
真正的大陆来源应该是 mozillazg/pinyin-data——它整理自汉典和《现代汉语词典》,确实是大陆规范。这才是对的对照组。
四、第二个坑:变调混进了读音表
拿到正确的数据源,跑完比对,第一次结果差点让我下错结论。
跑出来 3275 个「大陆有、萌典没有」的读音。排在最前面的几个:
一 → 大陆 yī,yí,yì 萌典 yī
七 → 大陆 qī,qí 萌典 qī
九 → 大陆 jiǔ,jiū 萌典 jiǔ
也 → 大陆 yě,yí 萌典 yě
「一」有三个读音?「九」多一个 jiū?
那些是变调,不是读音。
普通话有变调规则:两个三声字相连,前一个变二声;「一」和「不」在四声前要变。这些音在真实语境里确实会被念出来,但它们不是字典里该单独列出的读音。
问题在于,pinyin-data 没给变调做标记,混在正常读音里。得自己判断。
我在这里踩了三个坑才做对:
第一个,正则表达式的字符类写成了 [a-z],匹配不了带声调符号的字母。qiū 里的 ū 不在 [a-z] 里,结果 20924 个字只解析出 99 个。
第二个,判断三声时只写了 ǎ,漏了 ě ǐ ǒ(也 yí 因此漏网)。三声有六种韵尾:ǎ ě ǐ ǒ ǔ ǚ。
第三个,我假设「第一个读音是基准音」,结果「丙」的语料里把 bìng(变调)排在第一位。改用不依赖顺序的判据才解决——只要存在同音节的三声读音,那个读音就是变调形式。
变调过滤掉之后,候选从 3275 降到 2584。
但这还是太多,而且明显还有噪音。
五、第三个坑:以为按词频排序能解决
剩下的噪音主要有三种:
| 类型 | 例子 | 问题 |
|---|---|---|
| 文言/旧读 | 之 zhū、亏 yú、亨 xiǎng、仇 jū | 学习者永远不会遇到 |
| 轻声变体 | 道 dǎo(对 dào)、别 biè(对 bié) | 不是独立读音 |
| 数据噪音 | 能 xióng、那 nǎi | 源数据本身的问题 |
我的想法是:既然要判断「学习者会不会遇到这个音」,那就按字频排序——高频字排在前面,低频字沉底。人工核对时只看前面一批就够了。
于是我下载了一份中文词频表(5 万词),给候选字打分。
结果更糟了。
排名前列的是:
的 +dī 是 +tí 道 +dǎo
有 +wěi 能 +xióng +nái +nài
因为高频字的读音条目本来就多,噪音也被一起顶上来了。「的」「是」「有」这些字的读音列表里塞满了各种变体和罕见读音,按词频排反而让它们排在最前面。
我试过三条自动化路径——按常用度筛、按差集直接用、按词频排序——全部失败。
最终的结论是:自动化只能当候选生成器,最终必须人工核。 而且这里的「人工核」不是可选步骤,是唯一可行的路径。
六、意外发现:萌典的底子远比预期扎实
既然自动分诊走不通,我换了个笨办法:手挑一批「传统认知里的两岸多音重灾区」,逐字核对萌典到底收了什么。
给 萌典有 gěi,jǐ ✓ 已齐
谁 萌典有 shéi,shuí ✓ 已齐
薄 萌典有 bó,bò,báo ✓ 已齐
差 萌典有 chā,chà,chāi,cī ✓ 已齐
卡 萌典有 kǎ,qiǎ ✓ 已齐
落 萌典有 là,lào,luò ✓ 已齐
秘 萌典有 bì,mì ✓ 已齐
携 萌典有 xié,xī ✓ 已齐
括 萌典有 kuò,guā ✓ 已齐
23 个候选里,16 个萌典本来就全收。
我原本的假设("萌典常只收 gěi"、"萌典把 qiǎ 另立或偏废")全都不成立。
更值得说的是,萌典对两岸差异的兼容度远超预期:
- 垃圾:萌典收 lè, sè(台湾读音),大陆的 lā jī 另在别处
- 削弱:萌典收 xuè,大陆读 xuē
- 舒服:萌典收 shū fu(台湾轻声),大陆读 shū fú
- 明白:萌典收 míng bai,大陆读 míng bái
萌典不只收台湾读音,它把两岸的读法都收了,只是排序上以台湾为主。
这直接改变了工作量的判断:原本准备补 20–30 个字,实测只需要 2 个。
七、朴:不是缺读音,是差一个声调
这两个字里,第一个就有意思。
朴在萌典里的读音是 pú(二声),大陆标准是 pǔ(三声)。
第一反应是「萌典缺 pǔ,得补」。但写代码时发现,现有逻辑处理不了——它靠前缀匹配把释义分组贴上新标签,而 ú(U+00FA) 和 ǔ(U+01D4) 是两个完全不同的 Unicode 字符,startswith("pú") 对 "pǔ" 根本不成立。
这不是「缺一个读音」,而是「同一个音节,声调不同」——和血(xuè/xiě)、熟(shú/shóu)同类,但更隐蔽,因为字形看起来一模一样。
顺带一提,我原本打算用简体繁体的映射来解决(因为「朴」在繁体里是「樸」)。实测发现没必要:萌典的「朴」条目自己就有「質樸」释义,而「樸」只有「未加工成器的木材」——加进去只会多一条噪音。
一个假设,两次实测,都推翻了。
这里值得多说一句「朴」的编码问题,因为它是整件事里最反直觉的一处:
污 U+6C61 ← 简体字形
汙 U+6C59 ← 台湾正体
着 U+7740 ← 简体字形
著 U+8457 ← 台湾正体
污和汙不是繁简关系,是两个完全不同的码位。它们看起来像简繁对应,但 Unicode 层面毫无关系——因为简化字方案里,「污」这个字形被用来承载了台湾写作「汙」的那个字,同时又保留了原义。
这带来一个实际后果:任何基于「简体 → 繁体」的自动映射,都可能对这类字失效。必须用「台湾正体」专属的转换规则,而不能图省事用通用规则。
至于朴,萌典的读音列表是 pò, piáo, pú——它其实把台湾那边的读音给全了,只是 mainland 要的 pǔ 没有。所以真正的工作只是把标签改对,内容不用造。
八、茜:唯一需要「造」数据的地方
茜是真缺。萌典只有 qiàn,xī 完全没有——不是声调问题,是这个读音在台湾根本不用。
如果只把 xī 加到读音列表里,卡片上会出现这样:
拼音:qiàn xī
释义:
qiàn 植物名。茜草科……
xī (空)
有音无义,比缺音更糟。 用户点进去发现没有解释,只会以为字典出错。
所以必须手工补一条释义:
xī —— 外国女子名字的译音用字,今亦多用于人名。
例:茜茜(Sissi)
这是全库唯一一个需要「造」数据的地方。原因也很清楚:它是大陆独有的读音,而台湾字典的编写范围里根本没有这类用法。
九、抽象出来的三件事
排查完,剩下两字,但踩过的坑里有三样东西值得留下来。
1. 繁简映射:不能只用通用繁体
这是本次最大的收获,也是一个查了半年都没人发现的线上事故。
萌典收的是台湾正体,而常用的简繁转换工具默认给的是通用繁体——后者经常给出异体字。
异体字在萌典里只有一条「『X』的異體字」的指针。而构建脚本本来就有一条防御规则:剔除没有实质释义的条目。
两条规则叠加,结果是:这个字在卡片上释义全空。
全量扫描发现 22 个 GB2312 一级常用字中招:
为 众 伪 启 着 污 痹 钵 狸 硷 …
「为」「众」「伪」「启」「着」「污」——都是每天被查几百次的字。
修法是让候选链优先用「台湾正体」转换,通用繁体作备选。全量 9534 字实测:两种转换结果不同 42 字,修好 17 个,弄坏 0 个。
上线的第二天,「为」恢复 12 组释义,「着」恢复 25 组。
这个 bug 的隐蔽之处在于:它不会报错、不会崩溃,只会让内容静悄悄地消失。 如果没有全量扫描去数「哪些字释义为空」,永远不会有人发现。
2. 补丁机制:三种形态,不是三种技巧
原来那张表只能做一件事:给读音重新排序、给台湾读音加「(台)」标记。
但实际需要处理的形态有三类:
| 形态 | 例子 | 处理 |
|---|---|---|
| 主次颠倒 | 企 qǐ/qì | 排序 + 标注 |
| 同音节声调不同 | 朴 pú/pǔ | 显式声调映射 |
| 萌典完全没有 | 茜 xī | 补义项 |
第三类和前两类的性质完全不同——前两类只是改标签,第三类要新增内容。
把它们混在一个机制里,要么表达不了,要么写出别扭的特例代码。分开之后,每种机制都很直白,而且都能单独测试。
3. 自动化:明确它做不到什么
这轮我试了三条自动化路径,全部失败。但失败得很有价值——每一次失败都让我更清楚「人工该做什么」。
| 尝试 | 结果 | 教训 |
|---|---|---|
| 按常用度筛范围 | 把「茜」自己筛掉了 | 范围不是越窄越好 |
| 差集直接当候选 | 1056 条里可用不到 10% | 召回高 ≠ 可用 |
| 按词频排序 | 高频字噪音更多,更糟 | 排序会放大噪音 |
最后我用了最笨的办法:手挑 23 个候选字,逐字核对萌典的实际收录。
结果推翻了我原本的假设——萌典早就收了 16 个,工作量从「20–30 个字」降到「2 个字」。
如果我没做这次核对,就会照着「20–30 个」的预期去改表,最后给 16 个字加上不必要的覆盖,反而污染了数据。
十、一些反直觉的数字
整理下来有几个数字值得单独说:
萌典的两岸兼容度很高
手册上常见的「两岸读音差异 100 例」,萌典覆盖了其中绝大多数。实测:
| 差异类型 | 萌典的收录情况 |
|---|---|
| 台湾独有音(企 qì、菌 jùn、崖 yái、迹 jī) | ✓ 收了,但排在大陆音之后 |
| 台湾音 + 大陆音并存(血 xiě,xuè / 熟 shóu,shú) | ✓ 两个都收 |
| 台湾特有词读法(垃圾 lèsè / 削弱 xuè) | ✓ 收了(在台湾语境内正确) |
| 大陆独有音(茜 xī) | ✗ 完全没有 |
萌典的立场不是「只讲台湾话」,而是「以台湾规范为主,兼容大陆读法」。这个判断如果早点知道,工作量会直接砍掉八成。
真正需要人工判断的极少
在 1056 个一级字缺音候选里,能用的不到 10%。剩下的九成:
文言/旧读:之 zhū、亏 yú、亨 xiǎng、仇 jū
轻声变体:道 dǎo(对 dào)、别 biè(对 bié)
源数据噪音:能 xióng、那 nǎi、她 chí
自动化适合「找出来」,不适合「定下来」
差集能高召回地找出所有可能有问题的地方(2584 条)。但决定「这个差异该不该补」需要语言学判断——「之 zhū 是不是真的存在」「能 xióng 是不是录入错误」,机器回答不了。
我试过用词频给候选打分,结果更糟:高频字的读音条目本来就多,噪音被一起顶到了最前面。排名前列的是「的 dī」「能 xióng」这类。
萌典有 14 个常用字根本没收录
僳、勋、叁、墒、彝、挎、狸、硷、耪、茬、菏、酞、钎、锨——这些是萌典的覆盖边界,不是技术缺陷。台湾用不同字形(「叁」写作「參」),或者属于大陆简化新字。
要补就得找第四个数据源。而从使用频率看,这些字极罕见——为一亿个用户里可能查一次的字引入一条新数据链,性价比很低。
十一、回到最初的问题
现在回答开头那个问题:茜的 xī 从哪来?
它是大陆在几十年的使用中约定俗成形成的读音——受外来人名(茜茜公主 Sissi)影响,也被人名(宋茜)强化。这个读音在大陆是事实上的标准,但不在《普通话异读词审音表》里,因为审音表不审人名和译名。
台湾字典不收它,是因为台湾没有这种用法习惯——不是它「错了」,是它「不需要」。
所以「缺音」这个说法其实不太准确。准确的说法是:这个读音超出了台湾字典的收录范围。
至于为什么「为」这种字的释义会是空的——那跟语言学毫无关系,纯粹是一个转换器的默认行为撞上了一部字典的编排体例。
两件事,一个是语言学问题,一个是工程问题。但它们都藏在同一个「查不到字」的体验背后。
附:这次沉淀下来的三个工具
写这篇的过程中顺手做了三个脚本,都留在项目里:
check_common_hanzi.py—— 扫全量字卡,找「有拼音没释义」的空洞字check_reading_gaps.py—— 核对指定一批字在萌典的收录情况diff_readings.py—— 两部字典的读音差集(仅用于排查,不参与构建)
第一个特别有用:改完构建跑一次,对比基线数字,就能知道有没有引入回归。这次它一跑就报出 22 个空洞字——其中「为」「众」「伪」「启」「着」「污」都上线了半年没人发现。
静默的内容缺失,只有靠这种全量扫描才能发现。 这是整个教训里最实用的一条。
尾声:两类问题,藏在同一个体验背后
现在回答开头那个问题:茜的 xī 从哪来?
它是大陆在几十年的使用中约定俗成形成的读音——受外来人名(茜茜公主 Sissi)影响,也被人名(宋茜)强化。这个读音在大陆是事实上的标准,但不在《普通话异读词审音表》里,因为审音表不审人名和译名。
台湾字典不收它,是因为台湾没有这种用法习惯——不是它「错了」,是它「不需要」。
所以「缺音」这个说法其实不太准确。准确的说法是:这个读音超出了台湾字典的收录范围。
至于为什么「为」这种字的释义会是空的——那跟语言学毫无关系,纯粹是一个转换器的默认行为撞上了一部字典的编排体例。
两件事,一个是语言学问题,一个是工程问题。但它们都藏在同一个「查不到字」的体验背后。
而要把这两件事分开,唯一的办法就是——去数一遍。看看到底有多少字是这样,有多少不是。