JavaScript 之 300 行代码搞定汉字转拼音

发布于 2022-05-25 18:54:10 字数 9687 浏览 1207 评论 22

有天刷掘金,看到这样一篇文章 利用 Android 源码,轻松实现汉字转拼音功能,非常感兴趣,花了两个多小时,阅读了博客和代码,算是弄懂了原理。然后就想,是不是可以从 Java 移植到 JavaScript。

本篇博客记录的就是阅读和折腾的过程,顺便提醒自己,借助现代浏览器的能力(API),几百行代码可以轻松搞定汉字转拼音。


依据本篇博客编写的汉字转拼音库 tiny-pinyin 已上线,越300行代码左右,可轻松阅读。Online demo 地址 https://creeperyang.github.io/pinyin/,可放心体验。

tiny-pinyin

一. 汉字转拼音的现状

首先应该说,汉字转拼音是个强需求,比如联系人按拼音字母排序/筛选;比如目的地(典型如机票购买)
按拼音首字母分类等等。但是这个需求的解决方案,但好像没听过什么巧妙的实现(特别是浏览器端),大概都需要一个庞大的字典。

具体到JavaScript,查查github和npm,比较优秀的处理汉字转拼音的库有pinyin
pinyinjs,可以看到,两者都自带了庞大的字典。
这些字典动辄几十上百KB(有的甚至几MB),想在浏览器端使用还是需要一些勇气的。所以当我们碰到汉字转拼音的需求,也不怪我们第一反应就是拒绝需求(或者服务端实现)。

现在,如果我告诉你可以浏览器端 300 行代码实现汉字转拼音,是不是不可置信?

二. 从安卓 4.2.2 联系人代码说起

再次强调这篇博客——利用Android源码,轻松实现汉字转拼音功能

今天和大家分享一个从Android系统源代码提取出来的汉字转成拼音实现方案,只要一个类,560多行代码就可以让你轻松实现汉字转成拼音的功能,且无需其他任何第三方依赖。

是不是打破了你的思维定势:难道有什么强大的算法可以抛弃字典?

第一遍看完博客,稍有些失望,并没有什么算法解析,只是介绍了从安卓代码发现的这几百行代码。第二遍时带着移植到JavaScript的想法阅读代码,算是弄懂了原理,于是开始了踩坑的移植之旅。

源码在 Android Git Reposities,感兴趣的可以去看看。

三. 手把手教你 300 行代码实现汉字转拼音

首先直指核心:为什么有汉字转拼音必须有庞大字典的思维定势?

因为汉字的排布和拼音并没有什么关联,比如在汉字区间 u4E00-u9FFF,前一个可能是 ha,后一个可能就是 ze,没有办法从汉字的unicode关联到拼音,所以只能有一个庞大的字典记录每个汉字(或常用汉字)的拼音。

但是,假设我们可以把所有汉字按拼音排序,比如按 'A', 'AI', 'AN', 'ANG', 'AO', 'BA',...,'ZUI', 'ZUN', 'ZUO' 排序,那么,我们只需要记住每个相同拼音的汉字队列的第一个汉字就好了。那么,所需要的字典就会很小(覆盖所有拼音即可,拼音数量本身不多)。

现在,难点就是把汉字按拼音排序了。很幸运,ICU/本地化相关的API提供了这个排序API(如果没有方便的排序/比较方法,那么本篇文章可能就不会出现了)。

所以,这就是为什么 300 行可以实现汉字转拼音:

  1. Intl.Collator API:Intl.Collator 内部实现了本土化相关的字符串排序。我们通过 Intl.Collator.prototype.compare 可以把所有汉字 基本 按照拼音来排序。
  2. 边界汉字表:记录了排序的边界点。该汉字表的每个汉字都是排序后相同拼音的汉字集合的首个汉字(Each unihans is the first one within same pinyin when collator is zh_CN)。

说到这里,可能仍然有没说清楚的地方,所以直接上一段代码:

/**
 * 说明:19968-40959,即所有汉字(4e00-9fff)的charCode
 * 
 * 输出结果(即排序)如下:
 * 
 * [{
 *   "hanzi": "阿", // 拼音 a
 *   "unicode": "u963f",
 *   "index": 0
 * },
 * {
 *   "hanzi": "锕", // 拼音 a
 *   "unicode": "u9515",
 *   "index": 1
 * },
 * ...
 * {
 *   "hanzi": "鿿",
 *   "unicode": "u9fff",
 *   "index": 20991
 * }]
 * 
 */
const fs = require('fs')
const FIRST_PINYIN_UNIHAN = 19968
const LAST_PINYIN_UNIHAN = 40959

function listAllHanziInOrder() {
  const arr = []
  for(let i = FIRST_PINYIN_UNIHAN; i <= LAST_PINYIN_UNIHAN; i++) {
    arr.push(String.fromCharCode(i))
  }
  const COLLATOR = new Intl.Collator(['zh-Hans-CN'])
  arr.sort(COLLATOR.compare)
  console.log(arr.length)
  fs.writeFileSync(`${__dirname}/sortedHanzi.json`, JSON.stringify(
    arr.map((v, i) => {
      return {
        hanzi: v,
        unicode: `\u${v.charCodeAt(0).toString(16)}`,
        index: i
      }
    }),
    null,
    '  '
  ))
  console.log('done')
}

listAllHanziInOrder()

有兴趣的同学可以执行node --icu-data-dir=node_modules/full-icu 上面的脚本.js看看,然后看看是不是得到了 基本 按照拼音排序的汉字表。

这里有几点要注意

  1. 我再次加粗了 基本 ,因为我们得到的汉字列表并没有完全按照拼音来排序,中间偶尔有一些其它拼音的汉字插入,这点在制作 边界表 时要额外注意。
  2. 上面脚本里得出的表是所有汉字的排序,其中有些和安卓代码里 HanziToPinyin.java 的表有不同,所以需要更新 HanziToPinyin.java 的表。(从Java转到JavaScript的最大的坑和工作量:更正边界表)
  3. 相信大家都看到了核心代码:const COLLATOR = new Intl.Collator(['zh-Hans-CN'])Intl.Collator(这里指定locale是中国zh-Hans-CN)正是能把汉字按拼音排序的关键,它是按locale-specific顺序,排序字符串的Internationalization API。
  4. 执行脚本时请先 npm i full-icu,这个依赖会自动安装缺失的中文支持并提示如何指定ICU数据文件来执行脚本。

1. ICU

ICU即International Components for Unicode,为应用提供 Unicode 和国际化支持。

ICU is a mature, widely used set of C/C++ and Java libraries providing Unicode and Globalization support for software applications. ICU is widely portable and gives applications the same results on all platforms and between C/C++ and Java software.

并且 ICU 提供了本地化字符串比较服务(Unicode Collation Algorithm + 本地特定的比较规则):

Collation: Compare strings according to the conventions and standards of a particular language, region or country. ICU's collation is based on the Unicode Collation Algorithm plus locale-specific comparison rules from the Common Locale Data Repository, a comprehensive source for this type of data.

想更深入了解的可以看http://site.icu-project.org/。但我们只需要知道 node/chrome 等等都是通过ICU来支持国际化,包括我们用到的根据本地惯例和规则去排序字符。

在现代浏览器上,一般ICU内置了对用户本地语言的支持,我们直接使用即可。

但对 node.js 来说,通常情况下,ICU只包含了一个子集(通常是英语),所以我们需要自行添加对中文的支持。一般来说,可以通过npm install full-icu安装full-icu来安装缺失的中文支持。(参见上面node --icu-data-dir=node_modules/full-icu)。

full-icu,更多信息可查看full-icu-npm,以及一个讨论nodejs/node#3460

同时,node ICU的跟多信息可查看https://github.com/nodejs/node/wiki/Intl

2. Intl API

上一小节应该基本讲清楚了国际化/本地化相关的知识,这里再补充一下内置API的使用。

怎么查看用户语言和Runtime是否支持这个语言?

Intl.Collator.supportedLocalesOf(array|string)返回包含支持(不用回退到默认locale)的locales的数组,参数可以是数组或字符串,为想要测试的locales(即BCP 47 language tag)。

2017-05-10 1 59 19

构造Collator对象和排序字符串

2017-05-10 2 08 08

通过 Intl.Collator.prototype.compare,我们可以按语言指定的顺序来排序字符串。而中文中,这个排序恰好绝大多数都是按拼音的顺序来的,'A', 'AI', 'AN', 'ANG', 'AO', 'BA', 'BAI', 'BAN', 'BANG', 'BAO', 'BEI', 'BEN', 'BENG', 'BI', 'BIAN', 'BIAO', 'BIE', 'BIN', 'BING', 'BO', 'BU', 'CA', 'CAI', 'CAN', ...,这正是我们上面提到的汉字转拼音的关键。

四. 边界表更正

使用与安卓代码相同的边界表,测试默认的常用汉字(6000+),得到结果如下:

2017-05-10 2 17 21

显然,这个边界表是有问题的,需要更正。我们可看到,大部分的汉字被转成了qing,可见,qing这个拼音对应的汉字有问题。

  1. 找到这个汉字,是 'u72c5'/'狅',加上前后各一个字,['u4eb2', 'u72c5', 'u828e']/["亲", "狅", "芎"]
  2. 搜索,'u72c5'/'狅'可以读qing,但现在多读kuang,这应该就是错误的原因了。
  3. 根据最初得到那张所有汉字的排序表,qing的第一个汉字是'u9751'/'靑'
  4. 改动后,转换失败的只剩104了。

2017-05-10 2 31 48

整个更新过程即如上所属:不断测试,找出错误的边界汉字并更正。

tiny-pinyin 提交历史 可看到大量的字典修正,顺便帮常用汉字拼音字典(用于测试)更正了不少拼音,花了大约有一天工作时间,算是辛苦。

此外,可看到 Node.js 上 7.x/6.x 都测试通过了,但 5.x/4.x 部分汉字转换后的拼音存在问题。这可以通过为特定版本 Node.js 更正字典来解决。最后,希望大家理解了本篇提到的汉字转拼音的原理,也欢迎大家为 tiny-pinyin 提问题。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(21

-旧情勿念。 2022-05-04 13:58:37

太机智了

《☆╮栀子花开つ》 2022-05-04 13:58:37

如果能加入声调就更好了

许一世地老天荒 2022-05-04 13:58:37

请问如果输出的结果是有个按钮把拼音放在汉字上面 像这个页面的功能一样 该怎么搞?
https://duchinese.net/lessons/307-nice-to-meet-you?from=course

帅哥哥的热头脑 2022-05-04 13:58:37

如果是多音字 要怎么处理? ‘银行’, ‘很行’, 可以是 ‘hang’, 可以是 ‘xing’

筑梦。 2022-05-04 13:58:37

如果是多音字 要怎么处理? ‘银行’, ‘很行’, 可以是 ‘hang’, 可以是 ‘xing’

不支持多音字。

茶底世界 2022-05-04 13:58:37

然后一个图片就1m了,把节省的空间又填上了

温柔少女心 2022-05-04 13:58:32

这个有拼音转换成汉字的功能吗

葬シ愛ベ 2022-05-04 13:58:01

很有意义的实现,正好用到,感谢!

一个 typo:
因为汉字的排布和拼音并有什么关联 => 因为汉字的排布和拼音并没有什么关联

逆光下的微笑 2022-05-04 13:58:01

你好,上面的示例代码中 require('fs')这个是导入一个为fs.js的文件吗?但是在down下来的源码中并没有发现这些

洒一地阳光ヽ 2022-05-04 13:57:48

我之前在翻 MDN 找 String, UTF-8 相关资料的时候浅显地知道这方法. 现在这里比较详细的介绍让我有了更系统的了解. 谢谢.

本宫微胖 2022-05-04 13:57:24

@MasterHuan 多音字就只能呵呵了... 需要多音字/音调之类等更多功能还是推荐 https://github.com/hotoo/pinyin 等。

葬心 2022-05-04 13:56:11

多音字怎么办.......

惯饮孤独 2022-05-04 13:53:15

li一类的中文转拼音有误会变成leng

枫林﹌晚霞¤ 2022-05-04 13:52:24

学到了

陌伤浅笑 2022-05-04 13:50:00

感谢分享,长见识了

倒带 2022-05-04 13:18:15

学到了

⒈起吃苦の倖褔 2022-05-04 12:28:06

@Dmmo 感谢反馈。下一步会测试各个浏览器并更正不同浏览器的边界字典。

陌伤ぢ 2022-05-04 11:23:55

Safari 测试
7KB的轻量级 汉字转拼音 库,适用于现代浏览器和 Node.js

7KB de qing liang ji han zi zhuan pin yin ku , shi yong yu xia dai liu lan qi he Node.js

“xia dai”

岁月静好 2022-05-04 09:39:40

@cssmagic 是的,浏览器 “偷偷” 提供了好多我们想不到/不知道的功能。

你另情深 2022-05-04 05:45:15

很有意思的实践,本质上是在利用系统提供的词典数据,令人眼前一亮,谢谢分享!

~没有更多了~

关于作者

够运

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文