最近HanLP希望支持拼音与繁体功能,所以学习了几个开源的Java实现,优化后集成进来。
开源项目地址:https://github.com/hankcs/HanLP
原理
这是GitHub上星星最多一个,主要原理就是利用一张HashTable将字与拼音一一对应起来。同时,在扫描的时候也会将当前汉字依次与后面的3个、2个、1个汉字组合,判断下是否存在多音字词组。也就是说,它最多支持4字词的多音字校正。同时,顺序扫描并且组合的话,复杂度的常数项有点高(大约是O(4n))。再乘上哈希表的复杂度,就是一个愚蠢、低效的实现。
词典格式
jpinyin中一共有3个表,分别是:
chinese.db 简繁词表
一共大约两千个汉字的简繁对应:

事实上,汉字的简繁对应并非严格的一对一,比如“皇后”的繁体应该是“皇后”,而“以后”的繁体应该是“以後”,两者并不相同。季先生着重谈到当年简化汉字时,也说把“皇后”的后与“以后”的“后”弄成一个字是遗憾。
所以这个词典就是个垃圾。
pinyin.db 汉字读音表
一共大约两万个汉字与它们的读音,支持多音字:

mutil_pinyin.db 多音词组
有些词语中的某个字读音与常用读音不同,比如鸭绿江。一共大约八百个:

这些词典都是以zip形式的Property储存的,而Property其实就是一个HashMap,所以这些词典可以视作哈希表。
算法
算法没什么可说的,基本步骤是:
-
先统统以字为单位转为简体
-
从前往后扫描,先尝试多音字识别处理(将当前汉字依次与后面的3个、2个、1个汉字组合,判断下是否存在多音字词组),如果没有查到多音词组,则以字为单位查询读音,取第一个(也就是说多音字并没有利用到)。
评价
个人评价极低,不支持简繁体分歧,在多音字的处理上没有用到更高效的算法,乏善可陈,渣渣。
这个项目是一个基本包,封装了大多数nlp项目中常用工具,其中就有简繁转换与拼音模块。
词典格式
fan2jian.dic 简繁体分歧词典
这是一个繁体到简体的词典,词汇量大约在五千左右,但是包含了一些汉字的简繁对照,所以真实词汇量会小很多:

pinyin.dic 拼音字词词典
这是一个汉字与词语到拼音的词典,词汇量在20万左右,同样包含汉字的拼音,所以真实词汇量会小很多:

算法
算法与词典在内存中的数据结构有很大关系,ansj这次使用了二分trie树来储存这些词典。
简繁转换算法
使用了二分trie树的前缀查询算法,比Hash表高效。关于二分trie树的更多讲解,请参考:《Trie树分词》。
值得注意的是,这里的繁转简词典是fan2jian.dic所示,简转繁词典则是fan2jian.dic的前后两个词串逆转过来合成的,这样做很聪明。当然,会损失一些词语,比如:
- 乙太網 以太网
- 乙太網路 以太网
不过,ansj的词典还是弱了一点,把“皇后”转成了“皇後”。
拼音算法
拼音词典,储存采用了一个叫做SmartForest的结构。
- /**
- * 一个小树,和Forest的区别是.这个在首字也是用二分查找,做过一次优化.达到到达一定量级自动扩展为hash定位 在ansj分词中这个应用是在自适应分词
- *
- * @author ansj
- */
- public class SmartForest<T> implements Comparable<SmartForest<T>>
SmartForest依然是一棵trie树,只不过,当如果数组内元素接近于最大值直接数组定位(Forest则永远是二分定位):
添加:
- // 如果数组内元素接近于最大值直接数组定位,rate是内存和速度的一个平衡
- if (branches != null && branches.length >= MAX_SIZE * rate)
- {
- SmartForest<T>[] tempBranches = new SmartForest[MAX_SIZE];
- for (SmartForest<T> b : branches)
- {
- tempBranches[b.getC()] = b;
- }
- tempBranches[branch.getC()] = branch;
- branches = null;
- branches = tempBranches;
- }
- else
- {
- SmartForest<T>[] newBranches = new SmartForest[branches.length + 1];
- int insert = -(bs + 1);
- System.arraycopy(this.branches, 0, newBranches, 0, insert);
- System.arraycopy(branches, insert, newBranches, insert + 1, branches.length - insert);
- newBranches[insert] = branch;
- this.branches = newBranches;
- }
查找:
- public int get(char c)
- {
- if (branches == null)
- return -1;
- if (branches.length == MAX_SIZE)
- {
- return c;
- }
- int i = Arrays.binarySearch(this.branches, new SmartForest<T>(c));
- return i;
- }
其他的并没有特别的,依然是《Trie树分词》的那一套逻辑。
评价
算法给好评,词典给中评。
这是一套名不见经传的类库,作者在介绍中说“中文相关工具包,目前提供中文简繁体互转,以及中文转拼音。未来会提供中文分词。”,不清楚是否会履行诺言。
词典格式
pinyin.txt 汉字拼音字典
这是单个汉字与拼音的对照词典,大约有两万个常用与罕见的汉字:

polyphone.txt 多音词词典
与jpinyin类似,是异读词的集合,大约有一万词汇量:

非常全面,像这个“鱼丽于罶”还是第一次见到,我读书少,你们不要骗我 。
unknown.txt 未知读音的字的词典(这个名字好长我自己起的)
一些奇怪的汉字,可能是韩国或日本的汉字:

simp.txt trad.txt 简繁汉字对应词典
两个词典合起来就是简繁汉字对应词典了,作者把它们拆开了。
 
simplified.txt 繁简分歧词表
- ##### 繁简分歧词表 #####
-
- # 计算机
- 印表機=打印机
- 記憶體=内存
- 乙太網=以太网
- 乙太網路=以太网
- 游標=光标
- 光碟=光盘
- 光碟機=光驱
- 軟碟機=软驱
- 匯流排=总线
- 碟片=盘片
- 硬體=硬件
- 硬碟=硬盘
- 磁碟=磁盘
- 磁軌=磁道
- 通信埠=端口
- 連接埠=端口
- 介面=接口
- 運算元=算子
- 演算法=算法
traditional.txt 简繁分歧词表
- ##### 简繁分歧词表 #####
-
- # 计算机
- 打印机=印表機
- 内存=記憶體
- 以太网=乙太網
- 光标=游標
- 光盘=光碟
- 光驱=光碟機
- 软驱=軟碟機
- 总线=匯流排
- 盘片=碟片
- 硬件=硬體
- 硅谷=矽谷
- 硬盘=硬碟
- 磁盘=磁碟
- 磁道=磁軌
- 端口=通信埠
- 接口=介面
- 算子=運算元
- 算法=演算法
- 芯片=晶片
算法
作者实现了一棵基于哈希表的trie树,速度估计勉勉强强,内存估计够呛。
简繁转换
依然是《Trie树分词》的那一套逻辑,先从分歧词表查,查不到再从单字简繁对照表中查。
汉字转拼音
与简繁转换的算法相同,先从多音词词表中查,查不到的话从单字读音表中查。
评价
算法还行吧,词典特别棒,有“皇后”这种分歧词。算法给4颗星,词典给好评。
HanLP
正在集百家之长,准备用trie树和chinese-utils的词典做一个无限趋近完美的类库!
简繁转换
- HanLP.Config.enableDebug();
- System.out.println(HanLP.convertToSimplifiedChinese("「以後等妳當上皇后,就能買士多啤梨慶祝了」"));
- System.out.println(HanLP.convertToTraditionalChinese("“以后等你当上皇后,就能买草莓庆祝了”"));
输出

汉字转拼音
HanLP不仅支持基础的汉字转拼音,还支持声母、韵母、音调、音标和输入法首字母与首声母功能。HanLP能够识别多音字,也能给繁体中文注拼音。最重要的是,HanLP采用了双数组trie树和压缩的词典格式(现在HanLP已经升级到Aho Corasick自动机结合DoubleArrayTrie极速多模式匹配,性能大幅提升!),能够提供毫秒级的响应速度!
- String text = "重载不是重担," + HanLP.convertToTraditionalChinese("以后爱皇后");
- List<Pinyin> pinyinList = PinyinDictionary.convertToPinyin(text);
- System.out.print("原文,");
- for (char c : text.toCharArray())
- {
- System.out.printf("%c,", c);
- }
- System.out.println();
-
- System.out.print("拼音(数字音调),");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin);
- }
- System.out.println();
-
- System.out.print("拼音(符号音调),");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getPinyinWithToneMark());
- }
- System.out.println();
-
- System.out.print("拼音(无音调),");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getPinyinWithoutTone());
- }
- System.out.println();
-
- System.out.print("声调,");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getTone());
- }
- System.out.println();
-
- System.out.print("声母,");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getShengmu());
- }
- System.out.println();
-
- System.out.print("韵母,");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getYunmu());
- }
- System.out.println();
-
- System.out.print("输入法头,");
- for (Pinyin pinyin : pinyinList)
- {
- System.out.printf("%s,", pinyin.getHead());
- }
- System.out.println();
输出
原文 |
重 |
载 |
不 |
是 |
重 |
担 |
, |
以 |
後 |
愛 |
皇 |
后 |
拼音(数字音调) |
chong2 |
zai3 |
bu4 |
shi4 |
zhong4 |
dan1 |
none5 |
yi3 |
hou4 |
ai4 |
huang2 |
hou4 |
拼音(符号音调) |
chóng |
z?i |
bù |
shì |
zhòng |
dān |
none |
y? |
hòu |
ài |
huáng |
hòu |
拼音(无音调) |
chong |
zai |
bu |
shi |
zhong |
dan |
none |
yi |
hou |
ai |
huang |
hou |
声调 |
2 |
3 |
4 |
4 |
4 |
1 |
5 |
3 |
4 |
4 |
2 |
4 |
声母 |
ch |
z |
b |
sh |
zh |
d |
none |
y |
h |
none |
h |
h |
韵母 |
ong |
ai |
u |
i |
ong |
an |
none |
i |
ou |
ai |
uang |
ou |
输入法头 |
ch |
z |
b |
sh |
zh |
d |
none |
y |
h |
a |
h |
h |
如果上表有些字符显示异常,请看下图:

或者:

开源项目
本文代码已集成到HanLP中开源:http://www./nlp/hanlp.html
知识共享署名-非商业性使用-相同方式共享:码农场 ? 汉字转拼音与简繁转换的Java实现
|