IK分词源码分析连载(二)--子分词器

转载请注明出处:
http://blog.chinaunix.net/uid-20761674-id-3424176.html
 
第一篇文章 IK分词源码分析连载(一)--主流程   概要描述了IK分词的主要流程及其功能,该进入细节部分了,这次详细介绍IK里的三个分词器:CJKSegmenter(中文分词),CN_QuantifierSegmenter(数量词分词),LetterSegmenter(字母分词)。
这三个分词器的代码很类似,思路都是一样的,采用字典树(CJK使用)或其他简单数据结构(CN_QuantifierSegmenter和LetterSegmenter)匹配文本中的当前字符,将匹配到的字符加入到分词候选集

这里详细介绍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);
    }
}
<span style="font-family:幼圆;font-size:18px;"></span>

逐句来分析:

  • 判断字符类型,字符类型是在移动文本指针的时候计算出来的,包括中文、韩文、日文、字母、数字、其他等
  • 这里的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来存数下级节点分支
  • 匹配时每次只处理一个字符
  • 字典树原理请google trie树
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中移除该字符

if(!this.tmpHits.isEmpty()

  • 简单总结这个过程:每次匹配完一个字符,向后移动文本指针,继续调用analyze()函数匹配字符,若匹配到词典中的词,加入到分词候选集

核心思路其实很简单,CN_QuantifierSegmenter和LetterSegmenter的分词代码不再详细描述,代码很CJK很类似,主要区别如下:

1. CN_QuantifierSegmenter的词典来源两个地方:1.quantifier.dic文件,包含量词 2.数词直接写到ChnNumberChars类中了,内容如下:"一二两三四五六七八九十零壹贰叁肆伍陆柒捌玖拾百千万亿拾佰仟萬億兆卅廿"

2. LetterSegmenter分别有三个类似的处理器:字母、数字、字母和数字的组合。

3. 处理的基本思路就是匹配连续的相同类型字符,直到出现不同类型字符为止,切出一个词,比如LetterSegmenter对字串"中文abc英文"的处理方式就是匹配出连续的字母子串abc,切为一个词,切词结果为中文 abc 英文

 
切词不难理解吧,后面进入另一个核心内容:歧义处理
 
IK分词源码分析连载(三)--歧义处理






你可能感兴趣的:(IK分词源码分析连载(二)--子分词器)