一、 IK分词初始化
初始化最主要的工作就是读入词典,并将这些词放入内存字典树
1.main2012.dic(关键词)2.quantifier.dic(量词)3.stopword.dic(停用词)4.ext.dic(扩展词,可选)
http://www.voidcn.com/article/p-cawzdpor-no.html
二、 匹配
- 主流程
主要的就是ik.next()方法:
1) 读入待匹配的文本
2) 初始化文本指针,指向文本中的第一个字符
3) 遍历分词器,进行分词处理,这里是最核心的流程之一,将待匹配文本生成分词候选集。
——子分词器
4) 处理完一个字符之后,文本指针后移一位,直到处理完所有待匹配文本
——歧义处理
5) 生成分词候选集之后,进行歧义处理,歧义处理方法区分智能和非智能。
其功能是根据分词候选集和歧义处理策略,生成最后的分词结果
- IK的子分词器
子分词器才是真正的分词类,IK里面有三个子分词器
i. CJKSegmenter(中文分词),
ii. CN_QuantifierSegmenter(数量词分词),
iii.LetterSegmenter(字母分词)。
主分词器IKSegmentation遍历这三个分词器对文本输入流进行的分词处理。每种分词器的分词方法是独立的,
生成自己的分词结果,放到分词候选集里
这三个分词器的代码很类似,思路都是一样的,采用字典树(CJK使用)或其他简单数据结构(CN_QuantifierSegmenter和LetterSegmenter)匹配文本中的当前字符,将匹配到的字符加入到分词候选集
其他两个词典与其主要区别如下:
-
CN_QuantifierSegmenter的词典来源两个地方:1.quantifier.dic文件,包含量词 2.数词直接写到ChnNumberChars类中了,内容如下:"一二两三四五六七八九十零壹贰叁肆伍陆柒捌玖拾百千万亿拾佰仟萬億兆卅廿"
- LetterSegmenter分别有三个类似的处理器:字母、数字、字母和数字的组合。
处理的基本思路就是匹配连续的相同类型字符,直到出现不同类型字符为止,切出一个词,比如LetterSegmenter对字串"中文abc英文"的处理方式就是匹配出连续的字母子串abc,切为一个词,切词结果为中文 abc 英文
Thread1——main(String[])
Thread2——IKSegmenter.next()
测试类中IKSegmenter ik=new IKSegmenter(sr, false); //true代表调用IKSegmenter()构造函数时使用智能分词 调用IKSegmenter
IKSegmenter.next()主流程最主要的就是.next()方法:
//遍历子分词器 for(ISegmenter segmenter : segmenters){segmenter.analyze(context);}
子分词器for的第一次循环:处理英文,字母,阿拉伯字母,混合字母的analyze()
子分词器for的第二次循环:处理中文数词,中文量词的analyze()
Thread3——CJKSegmenter.analyze(AnalyzeContext)
子分词器for的第三次循环:中文-日韩文子分词器的analyze()
重置子分词器for(ISegmenter segmenter : segmenters){ segmenter.reset();}
歧义处理this.arbitrator.process(context, this.cfg.useSmart());
smart模式岐义消除算法:
public intcompareTo(LexemePatho) {
规则1:比较有效文本长度
L31:{的,确实,在理}
L32:{的确,实,在理}
L33:{的确,实在,理}
规则2: //比较词元个数,越少越好
规则3: //路径跨度越大越好
规则4: //根据统计学结论,逆向切分概率高于正向切分,因此位置越靠后的优先(从代码中看来
没有发现其具体实际意义)
规则5: //词长越平均越好(词元长度相乘)
规则6: //词元位置权重比较(词元长度积),含义是选取长的词元位置在后的集合
L31:{的,确实,在理}11+22+3*2=11
L32:{的确,实,在理} 12+21+3*2=10
L33:{的确,实在,理} 12+22+3*1=9
最后的分词结果:张三,说,的,确实,在理
总体思路是这样:
从三个子分词器(见前一篇文章)得到的分词结果中取出不相交的分词块,假如分词结果为abcd(abcd代表词),abcd是按其在文本中出现的位置排序的,从前到后。假如a与b相交,b与c相交,c与d不相交,则将分词结果切成abc和d两个块分别处理(的确,确实,代表相交)
如果选择了useSmart(智能分词),则从相交的块中选举一个相对最优的分词结果输出,这是由judge()完成的
如果没有选择useSmart,则输出所有分词结果,包括相交的结果
词典的存储采用了前缀树数据结构,比如词典中有6个词,"一丁点 / 一列 / 远程 / 远程教学 / 玉女 / 玉女心经",则词典对应的数据结构如下图。除了根节点,任意一个子节点包括了二个数据项:nodeChar表对应的字符,nodeState表从根节点到本节点是否是一个完整的词。
这里详细介绍CJKSegmenter,其他两个分词器只说明和CJKSegment的不同即可
public void analyze(AnalyzeContext context) { if(CharacterUtil.CHAR_USELESS != context.getCurrentCharType()){ //优先处理tmpHits中的hit if(!this.tmpHits.isEmpty()){ //处理词段队列 Hit[] tmpArray = this.tmpHits.toArray(new Hit[this.tmpHits.size()]); for(Hit hit : tmpArray){ hit = Dictionary.getSingleton().matchWithHit(context.getSegmentBuff(), context.getCursor() , hit); if(hit.isMatch()){ char[] date = context.getSegmentBuff(); //输出当前的词 Lexeme newLexeme = new Lexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() - hit.getBegin() + 1 , Lexeme.TYPE_CNWORD); context.addLexeme(newLexeme); if(!hit.isPrefix()){//不是词前缀,hit不需要继续匹配,移除 this.tmpHits.remove(hit); } }else if(hit.isUnmatch()){ //hit不是词,移除 this.tmpHits.remove(hit); } } } //********************************* //再对当前指针位置的字符进行单字匹配 Hit singleCharHit = Dictionary.getSingleton().matchInMainDict(context.getSegmentBuff(), context.getCursor(), 1); if(singleCharHit.isMatch()){//首字成词 //输出当前的词 Lexeme newLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_CNWORD); context.addLexeme(newLexeme); //同时也是词前缀 if(singleCharHit.isPrefix()){ //前缀匹配则放入hit列表 this.tmpHits.add(singleCharHit); } }else if(singleCharHit.isPrefix()){//首字为词前缀 //前缀匹配则放入hit列表 this.tmpHits.add(singleCharHit); } }else{ //遇到CHAR_USELESS字符 //清空队列 this.tmpHits.clear(); } //判断缓冲区是否已经读完 if(context.isBufferConsumed()){ //清空队列 this.tmpHits.clear(); } //判断是否锁定缓冲区 if(this.tmpHits.size() == 0){ context.unlockBuffer(SEGMENTER_NAME); }else{ context.lockBuffer(SEGMENTER_NAME); } }
逐句来分析:
判断字符类型,字符类型是在移动文本指针的时候计算出来的,包括中文、韩文、日文、字母、数字、其他等
这里的CHAR_USELESS就是其他类型字符,如果字符类型无法识别,则跳过下面的匹配步骤
if(CharacterUtil.CHAR_USELESS != context.getCurrentCharType())
这里先跳过第一个if条件,看后面的代码。matchInMainDict的作用就是将待匹配的当前字符放到由main2012.dic词典生成的词典数中进行比较
Hit singleCharHit = Dictionary.getSingleton().matchInMainDict(context.getSegmentBuff(), context.getCursor(), 1);
比较的方式是很典型的字典树,字典树中的每个节点用DictSegmenter表示,每个节点的下一级节点分支使用Array或者Map来表示,dictSegmenter类表示如下:
nodechar表示当前节点存储的2字节字符
nodeStatus表示该节点中的字符是否是词典中某个词的结尾字符
根据下级节点数量,会选择array或者map来存数下级节点分支
匹配时每次只处理一个字符
字典树原理
public class DictSegment implements Comparable<DictSegment>{ //公用字典表,存储汉字 private static final Map<Character , Character> charMap = new HashMap<Character , Character>(16 , 0.95f); //数组大小上限 private static final int ARRAY_LENGTH_LIMIT = 3; //Map存储结构 private Map<Character , DictSegment> childrenMap; //数组方式存储结构 private DictSegment[] childrenArray; //当前节点上存储的字符 //private Character nodeChar; public Character nodeChar; //当前节点存储的Segment数目 //storeSize <=ARRAY_LENGTH_LIMIT ,使用数组存储, storeSize >ARRAY_LENGTH_LIMIT ,则使用Map存储 private int storeSize = 0; //当前DictSegment状态 ,默认 0 , 1表示从根节点到当前节点的路径表示一个词 private int nodeState = 0;
回到analyze函数
这句代码表示当前字符已经匹配,并且匹配到词典中某个单个字符的词的,简单点说,就是命中了词典中的某个单字词
addLexeme,将匹配到的词加入到分词候选集中
如果匹配到的词是其他词的前缀,后面还需继续匹配,将其加入到tmpHits列表中
if(singleCharHit.isMatch()){//首字成词 //输出当前的词 Lexeme newLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_CNWORD); context.addLexeme(newLexeme); //同时也是词前缀 if(singleCharHit.isPrefix()){ //前缀匹配则放入hit列表 this.tmpHits.add(singleCharHit); } 没有匹配到词,仅前缀匹配,同样加入到tmpHits列表中 点击(此处)折叠或打开 else if(singleCharHit.isPrefix()){//首字为词前缀 //前缀匹配则放入hit列表 this.tmpHits.add(singleCharHit); }
接下来可以回到刚才我们跳过的那一大段if语句中了
很明显,tmpHits不为空,说明上面的代码匹配到了某个词的前缀
代码就不再帖一遍了,这里的功能就是将之前已经前缀匹配的字符取出,判断其和当前字符组合起来,是否还能继续匹配到词典中的词或词前缀,如果匹配到词尾,加入分词候选集。如果仍为前缀,下轮继续匹配
注意这里匹配出的都是两字以上的词,单字的词已经在上面的代码中匹配了
如果没法继续向下匹配了,从tmpHits中移除该字符
简单总结这个过程:每次匹配完一个字符,向后移动文本指针,继续调用analyze()函数匹配字符,若匹配到词典中的词,加入到分词候选集
核心思路其实很简单,CN_QuantifierSegmenter和LetterSegmenter的分词代码不再详细描述,代码很CJK很类似,主要区别如下:
- CN_QuantifierSegmenter的词典来源两个地方:1.quantifier.dic文件,包含量词 2.数词直接写到ChnNumberChars类中了,内容如下:"一二两三四五六七八九十零壹贰叁肆伍陆柒捌玖拾百千万亿拾佰仟萬億兆卅廿"
- LetterSegmenter分别有三个类似的处理器:字母、数字、字母和数字的组合。
- 处理的基本思路就是匹配连续的相同类型字符,直到出现不同类型字符为止,切出一个词,比如LetterSegmenter对字串"中文abc英文"的处理方式就是匹配出连续的字母子串abc,切为一个词,切词结果为中文 abc 英文
引用自:
https://www.cnblogs.com/sunshineKID/p/3437958.html
https://blog.csdn.net/zhaojianting/article/details/75502457
http://blog.163.com/liaoxiangui@126/blog/static/7956964020130299518177/
http://blog.chinaunix.net/uid-20761674-id-3424176.html