Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
creeperyang opened this issue May 9, 2017 · 27 comments
Open

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

creeperyang opened this issue May 9, 2017 · 27 comments

Comments

@creeperyang
Copy link
Owner

creeperyang commented May 9, 2017

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

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


2017/05/12 更新:

依据本篇博客编写的汉字转拼音库 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 提问题。

@creeperyang creeperyang changed the title 500行JavaScript代码搞定汉字转拼音 JavaScript之300行代码搞定汉字转拼音 May 12, 2017
@cssmagic
Copy link

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

@creeperyang
Copy link
Owner Author

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

@Dmmo
Copy link

Dmmo commented May 15, 2017

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”

@creeperyang
Copy link
Owner Author

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

@QuXiaoMing
Copy link

学到了

@WannTonn
Copy link

感谢分享,长见识了

@warpcgd
Copy link

warpcgd commented May 16, 2017

学到了

@lly0122
Copy link

lly0122 commented May 16, 2017

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

@MasterHuan
Copy link

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

@creeperyang
Copy link
Owner Author

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

@iugo
Copy link

iugo commented Jul 25, 2017

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

@huntbao
Copy link

huntbao commented Aug 30, 2017

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

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

@Jiguangjie
Copy link

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

@polunzh
Copy link

polunzh commented Aug 8, 2018

@Jiguangjie
Copy link

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

@ibufu
Copy link

ibufu commented Nov 6, 2018

学习了

@pluveto
Copy link

pluveto commented Feb 27, 2020

太机智了

@arkilis
Copy link

arkilis commented Jun 1, 2020

如果能加入声调就更好了

@yyz20002008
Copy link

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

@chuanxinwong
Copy link

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

@creeperyang
Copy link
Owner Author

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

不支持多音字。

@wizardforcel
Copy link

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

@aaroncastle
Copy link

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

这个页面是通过vue实现的,可以通过mouseover事件 + 双向绑定实现。

@weihong1028
Copy link

weihong1028 commented Dec 27, 2023 via email

@18boys
Copy link

18boys commented Dec 27, 2023 via email

@echodis
Copy link

echodis commented Dec 27, 2023 via email

@Sevneoy
Copy link

Sevneoy commented Dec 27, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests