IKAnalyzer源码分析—CJKSegmenter、LetterSegmenter和CN_QuantifierSegmenter
本章开始分析IKAnalyzer中的三个Segmenter,分别是CJKSegmenter、LetterSegmenter和CN_QuantifierSegmenter。LetterSegmenter用来处理英文字符和阿拉伯数字,CN_QuantifierSegmenter用来处理量词,CJKSegmenter用来处理其余的中文字符。
IKSegmenter的next函数中会依次调用每个Segmenter的analyze函数处理AnalyzeContext中的缓存数据segmentBuff。下面依次分析这三个Segmenter。
CJKSegmenter
CJKSegmenter::analyze
public void analyze(AnalyzeContext context) {
if(CharacterUtil.CHAR_USELESS != context.getCurrentCharType()){
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()){
Lexeme newLexeme = new Lexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() - hit.getBegin() + 1 , Lexeme.TYPE_CNWORD);
context.addLexeme(newLexeme);
if(!hit.isPrefix()){
this.tmpHits.remove(hit);
}
}else if(hit.isUnmatch()){
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()){
this.tmpHits.add(singleCharHit);
}
}else if(singleCharHit.isPrefix()){
this.tmpHits.add(singleCharHit);
}
}else{
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,首先遍历之前查询得到的前缀tmpHits,针对每个前缀调用matchWithHit匹配当前字符,如果前缀加上当前字符在主词典中查找到了匹配单词,则创建Lexeme并添加到AnalyzeContext中,如果没有匹配,并且原来的前缀加上当前字符不是一个新的前缀,则移除原先的前缀。
再往下处理当前字符,通过matchInMainDict函数在主词典中查找当前字符是否匹配或者是某个单词的前缀,如果匹配,则添加到AnalyzeContext中,如果是前缀,则添加到tmpHits中。
最后,如果当前字符类型为CHAR_USELESS,或者缓冲区读取完毕,则清空之前的前缀tmpHits。
matchInMainDict和matchWithHit函数一致,下面只分析matchWithHit函数。
CJKSegmenter::analyze->matchWithHit
public Hit matchWithHit(char[] charArray , int currentIndex , Hit matchedHit){
DictSegment ds = matchedHit.getMatchedDictSegment();
return ds.match(charArray, currentIndex, 1 , matchedHit);
}
Hit match(char[] charArray , int begin , int length , Hit searchHit){
if(searchHit == null){
searchHit= new Hit();
searchHit.setBegin(begin);
}else{
searchHit.setUnmatch();
}
searchHit.setEnd(begin);
Character keyChar = new Character(charArray[begin]);
DictSegment ds = null;
DictSegment[] segmentArray = this.childrenArray;
Map<Character , DictSegment> segmentMap = this.childrenMap;
if(segmentArray != null){
DictSegment keySegment = new DictSegment(keyChar);
int position = Arrays.binarySearch(segmentArray, 0 , this.storeSize , keySegment);
if(position >= 0){
ds = segmentArray[position];
}
}else if(segmentMap != null){
ds = (DictSegment)segmentMap.get(keyChar);
}
if(ds != null){
if(length > 1){
return ds.match(charArray, begin + 1 , length - 1 , searchHit);
}else if (length == 1){
if(ds.nodeState == 1){
searchHit.setMatch();
}
if(ds.hasNextNode()){
searchHit.setPrefix();
searchHit.setMatchedDictSegment(ds);
}
return searchHit;
}
}
return searchHit;
}
matchWithHit函数首先通过getMatchedDictSegment获得原来的前缀在树中对应节点上的结构DictSegment,然后在该节点下的数组segmentArray或者segmentMap中查找当前字符对应的DictSegment,如果DictSegment存在,并且还有后续字符需要查找匹配,则嵌套调用DictSegment的match函数,如果length为1,则表示不需要匹配后续字符,设置查找的节点状态为MATCH,如果还有后续的节点,则设置查找的节点为前缀,最后返回查找结果。
LetterSegmenter
LetterSegmenter::analyze
public void analyze(AnalyzeContext context) {
boolean bufferLockFlag = false;
bufferLockFlag = this.processEnglishLetter(context) || bufferLockFlag;
bufferLockFlag = this.processArabicLetter(context) || bufferLockFlag;
bufferLockFlag = this.processMixLetter(context) || bufferLockFlag;
if(bufferLockFlag){
context.lockBuffer(SEGMENTER_NAME);
}else{
context.unlockBuffer(SEGMENTER_NAME);
}
}
processEnglishLetter函数用来处理英文字符,processArabicLetter函数用来处理阿拉伯数字字符,processMixLetter函数用来处理带有连接符的数词,默认的连接符如下,
private static final char[] Letter_Connector = new char[]{'#' , '&' , '+' , '-' , '.' , '@' , '_'};
这三个函数类似,下面只分析processEnglishLetter函数。
LetterSegmenter::analyze->processEnglishLetter
private boolean processEnglishLetter(AnalyzeContext context){
boolean needLock = false;
if(this.englishStart == -1){
if(CharacterUtil.CHAR_ENGLISH == context.getCurrentCharType()){
this.englishStart = context.getCursor();
this.englishEnd = this.englishStart;
}
}else {
if(CharacterUtil.CHAR_ENGLISH == context.getCurrentCharType()){
this.englishEnd = context.getCursor();
}else{
Lexeme newLexeme = new Lexeme(context.getBufferOffset() , this.englishStart , this.englishEnd - this.englishStart + 1 , Lexeme.TYPE_ENGLISH);
context.addLexeme(newLexeme);
this.englishStart = -1;
this.englishEnd= -1;
}
}
if(context.isBufferConsumed()){
if(this.englishStart != -1 && this.englishEnd != -1){
Lexeme newLexeme = new Lexeme(context.getBufferOffset() , this.englishStart , this.englishEnd - this.englishStart + 1 , Lexeme.TYPE_ENGLISH);
context.addLexeme(newLexeme);
this.englishStart = -1;
this.englishEnd= -1;
}
}
if(this.englishStart == -1 && this.englishEnd == -1){
needLock = false;
}else{
needLock = true;
}
return needLock;
}
if部分表示该分词器重新碰见某个英文字符,else部分表示上一个字符为英文单词。如果当前字符也是英文字符,则设置englishEnd为当前缓存的指针位置,如果当前字符不是英文字符,则将之前记录的英文字符的位置信息封装进Lexeme里并重置。如果读完缓冲区了,则将已经读取的信息封装进Lexeme中,注意这里并不会出现英文单词被分割的情况,因为该函数的上层保证了每次缓冲区“快”读完时就会继续向缓冲区中填入数据。
CN_QuantifierSegmenter
CN_QuantifierSegmenter::analyze
public void analyze(AnalyzeContext context) {
this.processCNumber(context);
this.processCount(context);
if(this.nStart == -1 && this.nEnd == -1 && countHits.isEmpty()){
context.unlockBuffer(SEGMENTER_NAME);
}else{
context.lockBuffer(SEGMENTER_NAME);
}
}
processCNumber函数用来处理中文数词,processCount函数用来处理中文量词。
CN_QuantifierSegmenter::analyze->processCNumber
private void processCNumber(AnalyzeContext context){
if(nStart == -1 && nEnd == -1){
if(CharacterUtil.CHAR_CHINESE == context.getCurrentCharType()
&& ChnNumberChars.contains(context.getCurrentChar())){
nStart = context.getCursor();
nEnd = context.getCursor();
}
}else{
if(CharacterUtil.CHAR_CHINESE == context.getCurrentCharType()
&& ChnNumberChars.contains(context.getCurrentChar())){
nEnd = context.getCursor();
}else{
this.outputNumLexeme(context);
nStart = -1;
nEnd = -1;
}
}
if(context.isBufferConsumed()){
if(nStart != -1 && nEnd != -1){
outputNumLexeme(context);
nStart = -1;
nEnd = -1;
}
}
}
private void outputNumLexeme(AnalyzeContext context){
if(nStart > -1 && nEnd > -1){
Lexeme newLexeme = new Lexeme(context.getBufferOffset() , nStart , nEnd - nStart + 1 , Lexeme.TYPE_CNUM);
context.addLexeme(newLexeme);
}
}
ChnNumberChars记录了默认的中文数词字符,如下所示
private static String Chn_Num = "一二两三四五六七八九十零壹贰叁肆伍陆柒捌玖拾百千万亿拾佰仟萬億兆卅廿";
和LetterSegmenter的processEnglishLetter函数类似,如果是重新发现一个中文数词,则记录该数词字符的起始位置至nStart中。如果上一个字符为数词,并且当前字符也为数词,则记录该字符的结束位置至nEnd,如果当前字符不是数词,则根据之前记录的位置信息调用outputNumLexeme函数输出该字符。如果缓冲区读完了,并且之前有数词字符信息未处理,则也直接输出该字符。
CN_QuantifierSegmenter::analyze->processCount
private void processCount(AnalyzeContext context){
if(!this.needCountScan(context)){
return;
}
if(CharacterUtil.CHAR_CHINESE == context.getCurrentCharType()){
if(!this.countHits.isEmpty()){
Hit[] tmpArray = this.countHits.toArray(new Hit[this.countHits.size()]);
for(Hit hit : tmpArray){
hit = Dictionary.getSingleton().matchWithHit(context.getSegmentBuff(), context.getCursor() , hit);
if(hit.isMatch()){
Lexeme newLexeme = new Lexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() - hit.getBegin() + 1 , Lexeme.TYPE_COUNT);
context.addLexeme(newLexeme);
if(!hit.isPrefix()){
this.countHits.remove(hit);
}
}else if(hit.isUnmatch()){
this.countHits.remove(hit);
}
}
}
Hit singleCharHit = Dictionary.getSingleton().matchInQuantifierDict(context.getSegmentBuff(), context.getCursor(), 1);
if(singleCharHit.isMatch()){
Lexeme newLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_COUNT);
context.addLexeme(newLexeme);
if(singleCharHit.isPrefix()){
this.countHits.add(singleCharHit);
}
}else if(singleCharHit.isPrefix()){
this.countHits.add(singleCharHit);
}
}else{
this.countHits.clear();
}
if(context.isBufferConsumed()){
this.countHits.clear();
}
}
needCountScan函数用来判断是否需要扫描量词。假设需要处理量词,并且当前待处理的字符为中文字符,则遍历之前存入的前缀countHits,针对每个前缀,调用matchWithHit函数判断加上当前字符后是否是量词,如果是,则创建Lexeme并添加到AnalyzeContext中。如果加上当前字符后不是一个前缀,或者并没有找到匹配的量词,则删除之前的前缀。
再往下,通过matchInQuantifierDict函数对当前字符进行处理,matchInQuantifierDict函数根据量词字典进行匹配,如果匹配成功,则创建Lexeme并添加到AnalyzeContext中,如果该字符是量词前缀,则添加到countHits中。
最后,如果要分析的字符不是中文字符,或者缓冲区已读取完毕,则直接清空前缀列表countHits。
matchWithHit、matchInQuantifierDict函数和CJKSegmenter中的matchWithHit、matchInMainDict函数一致,这里就不往下看了。
CN_QuantifierSegmenter::analyze->processCount->needCountScan
private boolean needCountScan(AnalyzeContext context){
if((nStart != -1 && nEnd != -1 ) || !countHits.isEmpty()){
return true;
}else{
if(!context.getOrgLexemes().isEmpty()){
Lexeme l = context.getOrgLexemes().peekLast();
if(Lexeme.TYPE_CNUM == l.getLexemeType() || Lexeme.TYPE_ARABIC == l.getLexemeType()){
if(l.getBegin() + l.getLength() == context.getCursor()){
return true;
}
}
}
}
return false;
}
needCountScan函数用来判断是否需要扫描量词,如果当前正在处理中文数词或中文量词,或者在已经处理的最后一个结果为中文数词或者阿拉伯数词字符,则返回true,表示需要开始扫描量词,其余情况返回false。