首页 > 代码库 > IK分词源码讲解(一)-初始篇

IK分词源码讲解(一)-初始篇

IK分词全名为IK Analyzer,是由java编写的中文分词工具包,目前在lucene以及solr中用的比较多,本系列的文章主要对ik的核心源码进行解析讲解,与大家分享,如果有错误的地方还望指教。

先来个整体概况:



其实从上面的图可以看出,真实的ik的代码其实并不多,这样给我们开始接触心里压力就小的多。

先打开IKAnalzyerDemo.java文件,先大体看看IK的工作流程

//构建IK分词器,使用smart分词模式

       Analyzer analyzer = new IKAnalyzer(true);    

       //获取LuceneTokenStream对象

        TokenStream ts = null;

       try {

           ts= analyzer.tokenStream("myfield",newStringReader("剑生杨朝来访问控制列表这是一个中文分词的例子,你可以直接运行它!IKAnalyer can analysis english text too"));

           //获取词元位置属性

           OffsetAttribute  offset = ts.addAttribute(OffsetAttribute.class);

           //获取词元文本属性

           CharTermAttribute term = ts.addAttribute(CharTermAttribute.class);

           //获取词元文本属性

           TypeAttribute type = ts.addAttribute(TypeAttribute.class); 

           //重置TokenStream(重置StringReader

           ts.reset();

           //迭代获取分词结果

           while (ts.incrementToken()) {

             System.out.println(offset.startOffset()+" - "+ offset.endOffset() +" : " + term.toString() + " | " + type.type());

           }

           //关闭TokenStream(关闭StringReader

           ts.end();   // Performend-of-stream operations, e.g. set the final offset.

 

       }catch(IOExceptione) {

           e.printStackTrace();

       }finally{

           //释放TokenStream的所有资源

           if(ts !=null){

             try {

              ts.close();

             } catch (IOException e) {

              e.printStackTrace();

             }

           }

        }

 

TokenStream的作用是从给入的文本中不断解析出Token,具体的做法是TokenStream有方法incrementToken,每次调用 将产生待分析文本的下一个Token,其实incrementToken做的事情就是填充用户所关心的若干属性,通过这些属性来反馈分析结果,因此自然而然 的一种想法是TokenStream的派生类中有若干的属性成员,每次调用incrementToken都首先清除上一次的属性信息,然后进行分析并填充 属性,这样做无可厚非,但是请考虑TokenStream流的嵌套,也就是说嵌套的内层流获取的属性将作为外层流的分析的输入,如果使用上述方法实现 TokenStream,则必然嵌套流的每层流都将有自己的属性实例,而层次之间可能会出现同样的属性,也就是说同样的属性实例在流层次中可能会有多个, 这样是没有必要的,也就是说对相同的属性在流层次中只有一个实例就可以满足分析的需求了。 

当将TokenStream所关心的属性抽象的由AttributeSource来管理时,我们在进行流的嵌套时,根据对 AttributeSource的分析可知,外层流定义自己关心的属性,并不需要在构造函数中实例化该属性,而是从AttributeSource中获取,如果存在的话,则直接返回实例,否则新建,这样在流嵌套式外层流和内存流共享AttributeSource,也就是说当外层流和内层流都关心某个属 性时,内层流首先初始化,此时他将会将该属性注册到AttributeSource中,这样在外层流初始化时将向AttributeSource获取该属 性,从而可以保证在流层次中若干层流都关心的属性只有一份实例(注意这个就好理解了)。

 

我们启用ik的所谓智能分词模式,

运行结果如图:



从上面可以看出,ik的大体流程,是先加载词库(我没有输出main2012.dic,其实它是先加载这个内置的词库,然后再来加载我们加入的扩展词库,最后再加载停用词词库),将这些词都放在字典树中。

下面介绍下最核心的分词部分

/**

     * 分词,获取下一个词元

     * @returnLexeme词元对象

     * @throwsIOException

     */

    public synchronized Lexeme next()throwsIOException{

       Lexeme l =null;

       while((l =context.getNextLexeme()) ==null ){

           /*

            * reader中读取数据,填充buffer

            * 如果reader是分次读入buffer的,那么buffer 进行移位处理

            * 移位处理上次读入的但未处理的数据

            */

           int available =context.fillBuffer(this.input);

           if(available <= 0){

              //reader已经读完

              context.reset();

              return null;

             

           }else{

              //初始化指针

              context.initCursor();

              do{

                   //遍历子分词器

                   for(ISegmenter segmenter :segmenters){

                      segmenter.analyze(context);

                   }

                   //字符缓冲区接近读完,需要读入新的字符

                   if(context.needRefillBuffer()){

                      break;

                   }

             //向前移动指针

              }while(context.moveCursor());

              //重置子分词器,为下轮循环进行初始化

              for(ISegmenter segmenter :segmenters){

                  segmenter.reset();

              }

           }

           //对分词进行歧义处理

           this.arbitrator.process(context,this.cfg.useSmart());        

           //将分词结果输出到结果集,并处理未切分的单个CJK字符

           context.outputToResult();

           //记录本次分词的缓冲区位移

           context.markBufferOffset();       

       }

       return l;

    }

 

    intavailable = context.fillBuffer(this.input);

此处是用来读取待处理的文本信息

遍历分词器,进行分词处理,这里是最核心的流程之一,将待匹配文本生成分词候选集。

总共有三种分词器CJKSegmenter(中文分词器)、CN_QuantifierSegment(数量词分词器)、LetterSegment(字母数字分词器),每种分词器的分词方法是独立的,各自生成自己的分词结果,放到分词候选集里

点击(此处)折叠或打开

for(ISegmenter segmenter : segmenters){

                        segmenter.analyze(context);

                    }

 

//对分词进行歧义处理

this.arbitrator.process(context,this.cfg.useSmart()); 

生成分词候选集之后,进行歧义处理,歧义处理方法区分智能和非智能,也就是在初始化IKSegment时传递的第二个参数IKSegmenter(Readerinput, boolean useSmart)。

其功能是根据分词候选集和歧义处理策略,生成最后的分词结果,具体策略后面介绍

 

 

//记录本次分词的缓冲区位移

           context.markBufferOffset();

IK整体来说代码量不算很高,也没有特别复杂的分词算法在里面,总体来说理解起来还是容易很多。

源码地址:http://download.csdn.net/detail/a925907195/8240641

IK分词源码讲解(一)-初始篇