IK里的分词器主要是三个分词器:CJKSegmenter(中文分词),CN_QuantifierSegmenter(数量词分词),LetterSegmenter(字母分词)。这三个分词器都继承了ISegmenter接口,思路相差不大,其中采用的结构也比较容易理解,采用字典树(CJK使用)或其他简单数据结构(CN_QuantifierSegmenter和LetterSegmenter)匹配文本中的当前字符,将匹配到的字符加入到分词候选集
其中CJKSegmenter中的核心代码analyze方法的代码如下:
public void analyze(AnalyzeContext
if(CharacterUtil.CHAR_USELESS !=context.getCurrentCharType()){
//优先处理tmpHits中的hit
if(!this.tmpHits.isEmpty()){
//处理词段队列
Hit[] tmpArray =this.tmpHits.toArray(newHit[this.tmpHits.size()]);
for(Hit
Dictionary.getSingleton().matchWithHit(context.getSegmentBuff(),context.getCursor() , hit);
if(hit.isMatch()){
//输出当前的词
Lexeme newLexeme = newLexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() -hit.getBegin() + 1 ,Lexeme.TYPE_CNWORD);
context.addLexeme(newLexeme);
if(!hit.isPrefix()){//不是词前缀,hit不需要继续匹配,移除
this.tmpHits.remove(hit);
}
}elseif(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);
}
}
CharacterUtil.CHAR_USELESS!= context.getCurrentCharType()
context.getCurrentCharType()此方法是用来判断字符的类型,包括字符的中文、字母、数字等。判断是方法是通过移动文本的指针计算出来。
具体方法在AnalyzeContext.java中
/**
* 初始化buff指针,处理第一个字符
*/
voidinitCursor(){
this.cursor
this.segmentBuff[this.cursor] =CharacterUtil.regularize(this.segmentBuff[this.cursor]);
this.charTypes[this.cursor] =CharacterUtil.identifyCharType(this.segmentBuff[this.cursor]);
}
/**
* 指针+1
* 成功返回 true;指针已经到了buff尾部,不能前进,返回false
* 并处理当前字符
*/
booleanmoveCursor(){
if(this.cursor<this.available
this.cursor++;
this.segmentBuff[this.cursor] =CharacterUtil.regularize(this.segmentBuff[this.cursor]);
this.charTypes[this.cursor] =CharacterUtil.identifyCharType(this.segmentBuff[this.cursor]);
return true;
}else{
return false;
}
}
通过移动指针然后通过CharacterUtil.identifyCharType方法来进行判断
其中CharacterUtil.identifyCharType方法的代码如下,着实有点无语,很容易理解了
/**
* 识别字符类型
* @param input
* @returnint CharacterUtil定义的字符类型常量
*/
static int identifyCharType(char input){
if(input >='0' && input <= '9'){
returnCHAR_ARABIC;
}else if((input >= 'a' && input <= 'z')
'A'&& input <='Z')){
returnCHAR_ENGLISH;
}else {
Character.UnicodeBlock ub =Character.UnicodeBlock.of(input);
if(ub ==Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A){
//目前已知的中文字符UTF-8集合
returnCHAR_CHINESE;
}else if(ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS//全角数字字符和日韩字符
//韩文字符集
Character.UnicodeBlock.HANGUL_SYLLABLES
Character.UnicodeBlock.HANGUL_JAMO
Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO
//日文字符集
Character.UnicodeBlock.HIRAGANA//平假名
Character.UnicodeBlock.KATAKANA//片假名
Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS){
returnCHAR_OTHER_CJK;
}
}
//其他的不做处理的字符
returnCHAR_USELESS;
}
继续往下讲
CharacterUtil.CHAR_USELESS指的是其他类型,说白了就是没识别出来,那么就跳过此步。
//再对当前指针位置的字符进行单字匹配
Hit singleCharHit= Dictionary.getSingleton().matchInMainDict(context.getSegmentBuff(),context.getCursor(),1);
这里进行单字匹配,其中matchInMainDict就是将待匹配的字符在由main2.12.dic词典生成的词典树中进行匹配。比较的方式是很典型的字典树,字典树中的每个节点用DictSegmenter表示,每个节点的下一级节点分支使用Array或者Map来表示,dictSegmenter类表示如下:
//公用字典表,存储汉字
private static final Map<Character , Character>charMap= newHashMap<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;
//当前节点存储的Segment数目
//storeSize <=ARRAY_LENGTH_LIMIT ,使用数组存储, storeSize >ARRAY_LENGTH_LIMIT ,则使用Map存储
private int storeSize
//当前DictSegment状态 ,默认 0 , 1表示从根节点到当前节点的路径表示一个词 1表明一个完整的词,enabled=0表示从词典中屏蔽当前词
private int nodeState
回到analyze继续讲解
if(singleCharHit.isMatch()){//首字成词
//首字成词
//输出当前的词
LexemenewLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_CNWORD);
context.addLexeme(newLexeme);
//同时也是词前缀
if(singleCharHit.isPrefix()){
//前缀匹配则放入hit列表
this.tmpHits.add(singleCharHit);
}
singleCharHit.isMatch()这句代码表示当前字符已经匹配,并且匹配到词典中某个单个字符的词的,简单点说,就是命中了词典中的某个单字词
addLexeme,将匹配到的词加入到分词候选集中
如果匹配到的词是其他词的前缀,后面还需继续匹配,将其加入到tmpHits列表中
很明显,tmpHits不为空,说明上面的代码匹配到了某个词的前缀,这里的功能就是将之前已经前缀匹配的字符取出,判断其和当前字符组合起来,是否还能继续匹配到词典中的词或词前缀,如果匹配到词尾,加入分词候选集。如果仍为前缀,下轮继续匹配注意这里匹配出的都是两字以上的词,单字的词已经在上面的代码中匹配了如果没法继续向下匹配了,从tmpHits中移除该字符
分词的过程总结来就是一个个字符匹配,匹配完一个字符后,指针向后移动,然后继续调用analyze的函数来继续处理字符。最后将所有匹配到的分词结果放到分词候选集中。
另外的两个切词中:
CN_QuantifierSegmenter的词主要来自于quantifier.dic这个内置的词典还有代码中嵌入的中文数词
private static String Chn_Num ="一二两三四五六七八九十零壹贰叁肆伍陆柒捌玖拾百千万亿拾佰仟萬億兆卅廿";//Cnum
LetterSegmenter主要处理字母、阿拉伯数字还有字母跟阿拉伯数字数字的组合
public voidanalyze(AnalyzeContextcontext) {
boolean bufferLockFlag =false;
//处理英文字母
bufferLockFlag= this.processEnglishLetter(context)|| bufferLockFlag;
//处理阿拉伯字母
bufferLockFlag= this.processArabicLetter(context)|| bufferLockFlag;
//处理混合字母(这个要放最后处理,可以通过QuickSortSet排除重复)
bufferLockFlag= this.processMixLetter(context)|| bufferLockFlag;
//判断是否锁定缓冲区
if(bufferLockFlag){
context.lockBuffer(SEGMENTER_NAME);
}else{
//对缓冲区解锁
context.unlockBuffer(SEGMENTER_NAME);
}
}
处理的方式就是对连续的并且类型相同的字符继续处理,比如处理英文的时候会匹配出连续的字母串来切分为一个词。