读IKAnalyzer源码之IK启动

来源:互联网 发布:smee社gal知乎 编辑:程序博客网 时间:2024/06/05 18:27

IKAnalyzer非常流行的中文分词器,对中文切词有兴趣的朋友可以看看。


基本所有的框架都分两部分,一是:框架的初始化,也就是框架启动;二是:调用框架,让框架为我们做一些事。我们今天先来看看一下IK的初始化过程。

IKAnalyzer可以说一个非常流行的分词器了,但我觉得IKAnalyzer的代码写一般。

废话就不多说了,直接看源码吧。IKAnalyzer挂在Google上,直接到google下就好了,文档之类的都很全。

假设1,你用过IKAnalyzer,对IKAnalyzer有了解。或者,你想了解分词器。
假设2,你已经下到源码,建了一个Java SE的项目。

假如,如果你满足两个假设,那就一起来读读IKAnalyzer的源码吧。


新建一个类,主方法的内容如下:
public static void main(String[] args){IKSegmenter seg = new IKSegmenter(new StringReader("中国人民"), true);try {Lexeme lex = seg.next();while(lex != null){System.out.println(lex);lex = seg.next();}} catch (IOException e) {e.printStackTrace();}}
PS:我们从IKSegment而不是从IKAnalyzer开始,如果从IKAnalyzer这个类开始,那么我们需要再加入Lucene3.1以上(Lucene4.0以下)的包。而且,基本都上Lucene的内容,暂时不关注,可以自行了解。


强调一下,IKAnalyzer的作者非常有心,看过源码的朋友应该知道,他注释很多。先赞一个。


从IKSegment的构造方法进来,
/** * IK分词器构造函数 * @param input  * @param useSmart 为true,使用智能分词策略 *  * 非智能分词:细粒度输出所有可能的切分结果 * 智能分词: 合并数词和量词,对分词结果进行歧义判断 */public IKSegmenter(Reader input , boolean useSmart){this.input = input;this.cfg = DefaultConfig.getInstance();this.cfg.setUseSmart(useSmart);this.init();}

首先看 DefaultConfig.getInstance() 里面是这样的:
/** * 返回单例 * @return Configuration单例 */public static Configuration getInstance(){return new DefaultConfig();}
由于IKSegment的初始化实际上是IK自己做的,在IKTokenizer。因此,这种写法完全没有问题,它初始化次数绝对是串行。假如,我说假如哈。假如这个东西是并行,这个单例的写法就不合适了,会造成多次初始化。有兴趣的话要以比对着Dictionary.initial(this.cfg)看。当然,这不是我们今天关注的内容。
同时这个地方做资源初始化,也就是解析配置文件,有兴趣的朋友可以看看。


进到 this.init() 方法,代码也是中规中矩的没什么难点,但这是我们今天的关注点。从代码上可以看到,首先初始化了词典,然后初始化分词环境,……

/** * 初始化 */private void init(){//初始化词典单例Dictionary.initial(this.cfg);//初始化分词上下文this.context = new AnalyzeContext(this.cfg);//加载子分词器this.segmenters = this.loadSegmenters();//加载歧义裁决器this.arbitrator = new IKArbitrator();}

在词典的初始化过程,

/** * 加载主词典及扩展词典 */private void loadMainDict(){//建立一个主词典实例_MainDict = new DictSegment((char)0);//读取主词典文件InputStream is = this.getClass().getClassLoader().getResourceAsStream(cfg.getMainDictionary());if(is == null){throw new RuntimeException("Main Dictionary not found!!!");}try {// 这里转码,原因是我们的词典都是UTF-8,但运行环境不一定。BufferedReader br = new BufferedReader(new InputStreamReader(is , "UTF-8"), 512);String theWord = null;do {//到了这里,我建议大家先看一目词典文件(/src/org/wltea/analyzer/dic/main2012.dic)。现在,我们基本了解词典文件是怎么样的了。因此,你也就知道,它为什么是一行一行读取的了。theWord = br.readLine();if (theWord != null && !"".equals(theWord.trim())) {// toLowerCase()是规范化,毕竟索引不分大小写。_MainDict.fillSegment(theWord.trim().toLowerCase().toCharArray());}} while (theWord != null);}……}

我觉得这段,如果按下来这么写兴许更好一些,你们觉得呢。
String theWord = br.readLine();while(theWord != null){if(!theWord.trim().isEmpty()){_MainDict.fillSegment(theWord.trim().toLowerCase().toCharArray());}}

由_MainDict.fillSegment(theWord.trim().toLowerCase().toCharArray());进来,历经千辛万苦来到这里。下面的代码可以谓重中之得哈。有一个数据结构叫,字典树。不管你知不知道,它都在下面了。这里简单说一下吧,详情请登陆www.baidu.com。


字典树,跟什么B树啊、B+树啊、B-树都差不多,因此跟二叉树不大一样。它是这样的,它广度不定,小于字典字数(“一二三一”算三个字)。然后,节点带有一个状态,即是不是完整词、词性之类的。


最最重要的一点,它的字必须是重复引用的。即有一个拥有字典全部单字的集合。比如,对一个英文字词,它有一个集合放里26个字母,然后所有的节点的都从这个集合引用,从而降低内存的开销。


也就是说,它一棵广度不定、深度不定,并引用来自同一集合的对象的树。我能说的,这就这么多了,剩下的交给百度吧。


看一下下面的代码,也都比较简单,就是字典树的实现。
private synchronized void fillSegment(char[] charArray , int begin , int length , int enabled){// 装箱,然后获取字典表中的文字对象Character beginChar = new Character(charArray[begin]);Character keyChar = charMap.get(beginChar);//字典中没有该字,则将其添加入字典表if(keyChar == null){charMap.put(beginChar, beginChar);keyChar = beginChar;}//搜索当前节点的存储,查询对应keyChar的keyChar,如果没有则创建。如:已有“中国”,再来一个“中国人”的时候,它首先发现“中”已存在,那就直接,一点也不客气。若没有,那只能创建了。DictSegment ds = lookforSegment(keyChar , enabled);if(ds != null){//处理keyChar对应的segmentif(length > 1){//词元还没有完全加入词典树ds.fillSegment(charArray, begin + 1, length - 1 , enabled);}else if (length == 1){//已经是词元的最后一个char,设置当前节点状态为enabled,//enabled=1表明一个完整的词,enabled=0表示从词典中屏蔽当前词ds.nodeState = enabled;}}}

这段代码非常坑爹,但能提高效率。从这里可以知道,作者在效率方面下很大的功夫。有得有失,因此代码很复杂。中心思想是数组的效率比集合框架的效率高。具体是这样的,当前节点的宽度不大于ARRAY_LENGTH_LIMIT(默认是3)时,用数组来存;当宽度大于时,用HashMap来存。
        /** * 查找本节点下对应的keyChar的segment  *  * @param keyChar * @param create  =1如果没有找到,则创建新的segment ; =0如果没有找到,不创建,返回null * @return */private DictSegment lookforSegment(Character keyChar ,  int create){DictSegment ds = null;if(this.storeSize <= ARRAY_LENGTH_LIMIT){//获取数组容器,如果数组未创建则创建数组DictSegment[] segmentArray = getChildrenArray();//搜寻数组DictSegment keySegment = new DictSegment(keyChar);int position = Arrays.binarySearch(segmentArray, 0 , this.storeSize, keySegment);if(position >= 0){ds = segmentArray[position];}//遍历数组后没有找到对应的segmentif(ds == null && create == 1){ds = keySegment;if(this.storeSize < ARRAY_LENGTH_LIMIT){//数组容量未满,使用数组存储segmentArray[this.storeSize] = ds;//segment数目+1this.storeSize++;Arrays.sort(segmentArray , 0 , this.storeSize);}else{//数组容量已满,切换Map存储//获取Map容器,如果Map未创建,则创建MapMap<Character , DictSegment> segmentMap = getChildrenMap();//将数组中的segment迁移到Map中migrate(segmentArray ,  segmentMap);//存储新的segmentsegmentMap.put(keyChar, ds);//segment数目+1 ,  必须在释放数组前执行storeSize++ , 确保极端情况下,不会取到空的数组this.storeSize++;//释放当前的数组引用this.childrenArray = null;}}}else{//获取Map容器,如果Map未创建,则创建MapMap<Character , DictSegment> segmentMap = getChildrenMap();//搜索Mapds = (DictSegment)segmentMap.get(keyChar);if(ds == null && create == 1){//构造新的segmentds = new DictSegment(keyChar);segmentMap.put(keyChar , ds);//当前节点存储segment数目+1this.storeSize ++;}}return ds;}

接下来,还会加载量词词典及扩展词典,内容基本一样,自己看看就好了。整个过程也就优化这个位置有复杂,其它都还好,主要是要知道作者为什么要用先数组再用集合,这个地方清楚了,也就没啥问题。

后续,作者在用数组装的时候,用了折半搜索。我个人觉得,你竟然用了“折半查找”,为什么要长度设为3呢?这样首配对元素1,再配对元素2,最后配对元素3,对吧?而我们顺序查找也是这样的,所以我觉得这个参数值不大合理。或许作者有其它想法,我还没有参悟。


字典加载完了,回到IKSegmenter#init()继续下一步,即是初始化上下文环境。这个环境,分词器分词的环境,它提供分词基本条件和记录着分词状态。比如,需要分词的东西、分好词的词片、分词进到哪个位置等等等
。对上下文有兴趣的可以看看。
原创粉丝点击