一、首先简单的了解两个问题:
1.Trie树的简单实现
http://xiaozhou09.iteye.com/blog/1927348
2.最大正向匹配的一个简单例子
//正向匹配的一个例子 public void wordSegment(String sentence){ int strLength=sentence.length();//计算传入字符串的长度 int maxLength=12;//最大匹配的长度 int start=0,end=0;//start 是开始位置,end为结束位置 boolean isFind=false;//是否存在这个词 while(start<strLength){ //字符串结尾的地方 int N=start+maxLength>strLength?strLength:start+maxLength; //存在首先失败的 isFind=false; String temp=""; for(end=N;end>start;end--){ temp=sentence.substring(start, end); //字典判断是否包含这个词 if(hs.contains(temp)){ isFind=true; break; } } //找不到以这个字开头的词 if(!isFind){ end+=1; temp=sentence.substring(start,end); } System.out.println("开始位置:"+start+"==="+temp+"==="+"结束位置:"+end); start=end; } }
二、IK的实现思路简单分析 我看的不是最新的,版本名字是:IK Analyzer 2012_u5_source
1.对于文本的处理一个Read一边读取一边处理速度这样肯定是最快的
问题: 对于读取最后的边界的处理 ================================================== 是否要进行读取Read的next的时候 /** * 判断segmentBuff是否需要读取新数据 * * 满足一下条件时, * 1.available == BUFF_SIZE 表示buffer满载 * 2.buffIndex < available - 1 && buffIndex > available - BUFF_EXHAUST_CRITICAL表示当前指针处于临界区内 * 3.!context.isBufferLocked()表示没有segmenter在占用buffer * 要中断当前循环(buffer要进行移位,并再读取数据的操作) * @return */ boolean needRefillBuffer(){ return this.available == BUFF_SIZE && this.cursor < this.available - 1 && this.cursor > this.available - BUFF_EXHAUST_CRITICAL && !this.isBufferLocked(); } 我认为第二个条件的处理很关键
2.IK在处理前的操作
Context属性长度一致: //字符窜读取缓冲 private char[] segmentBuff; //字符类型数组 private int[] charTypes; ========================= 属性实例化: /** * 初始化buff指针,处理第一个字符 */ void initCursor(){ this.cursor = 0; this.segmentBuff[this.cursor] = CharacterUtil.regularize(this.segmentBuff[this.cursor]); this.charTypes[this.cursor] = CharacterUtil.identifyCharType(this.segmentBuff[this.cursor]); } /** * 指针+1 * 成功返回 true; 指针已经到了buff尾部,不能前进,返回false * 并处理当前字符 */ boolean moveCursor(){ if(this.cursor < this.available - 1){ 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; } }
3.IK单词的识别,首先看下对数字、单词、数字字母组合等
其中的数字识别: 属性: /* * 阿拉伯数字起始位置 */ private int arabicStart; /* * 阿拉伯数字结束位置 */ private int arabicEnd; ============================== 方法: /** * 处理阿拉伯数字输出 * @param context * @return */ private boolean processArabicLetter(AnalyzeContext context){ boolean needLock = false; if(this.arabicStart == -1){//当前的分词器尚未开始处理数字字符 if(CharacterUtil.CHAR_ARABIC == context.getCurrentCharType()){ //记录起始指针的位置,标明分词器进入处理状态 this.arabicStart = context.getCursor(); this.arabicEnd = this.arabicStart; } }else {//当前的分词器正在处理数字字符 if(CharacterUtil.CHAR_ARABIC == context.getCurrentCharType()){ //记录当前指针位置为结束位置 this.arabicEnd = context.getCursor(); }else if(CharacterUtil.CHAR_USELESS == context.getCurrentCharType() && this.isNumConnector(context.getCurrentChar())){ //不输出数字,但不标记结束 }else{ ////遇到非Arabic字符,输出词元 //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! //识别产生一个词元了 Lexeme newLexeme = new Lexeme(context.getBufferOffset() , this.arabicStart , this.arabicEnd - this.arabicStart + 1 , Lexeme.TYPE_ARABIC); context.addLexeme(newLexeme); this.arabicStart = -1; this.arabicEnd = -1; } } //判断缓冲区是否已经读完 if(context.isBufferConsumed()){ if(this.arabicStart != -1 && this.arabicEnd != -1){ //生成已切分的词元 Lexeme newLexeme = new Lexeme(context.getBufferOffset() , this.arabicStart , this.arabicEnd - this.arabicStart + 1 , Lexeme.TYPE_ARABIC); context.addLexeme(newLexeme); this.arabicStart = -1; this.arabicEnd = -1; } } //判断是否锁定缓冲区 if(this.arabicStart == -1 && this.arabicEnd == -1){ //对缓冲区解锁 needLock = false; }else{ needLock = true; } System.out.println("数字:"+arabicStart+" "+arabicEnd); return needLock; } /** * 判断是否是字母连接符号 * @param input * @return */ private boolean isLetterConnector(char input){ int index = Arrays.binarySearch(Letter_Connector, input); return index >= 0; } /** * 判断是否是数字连接符号 * @param input * @return */ private boolean isNumConnector(char input){ int index = Arrays.binarySearch(Num_Connector, input); return index >= 0; }
4.中文词组的识别要依靠字典了,怎么去识别?
//处理的时匹配是否可能有一个词组就是根的子节点是否包含它,有的话存放到一个List中 //下一次遍历List中存储的跟当前的链接在一起判断1.完全匹配 产生一个新单词 // 2.是前缀的话 继续保留List中 下次处理 // 这样识别的了字典里面的存在当前匹配的所有的词 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()){ //输出当前的词 Lexeme newLexeme = new Lexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() - hit.getBegin() + 1 , Lexeme.TYPE_CNWORD); context.addLexeme(newLexeme); System.out.println("真正的匹配 "+new String(context.getSegmentBuff()).substring(hit.getBegin(),context.getCursor()+1)); if(!hit.isPrefix()){//不是词前缀,hit不需要继续匹配,移除 this.tmpHits.remove(hit); } }else if(hit.isUnmatch()){ //hit不是词,移除 this.tmpHits.remove(hit); } } } //System.out.println("first"); //********************************* //再对当前指针位置的字符进行单字匹配 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); //System.out.println(" "+context.getSegmentBuff()[context.getCursor()]); //同时也是词前缀 if(singleCharHit.isPrefix()){ //前缀匹配则放入hit列表 this.tmpHits.add(singleCharHit); } System.out.println("匹配"+context.getSegmentBuff()[context.getCursor()]); }else if(singleCharHit.isPrefix()){//首字为词前缀 //前缀匹配则放入hit列表 this.tmpHits.add(singleCharHit); System.out.println("前缀 "+context.getSegmentBuff()[context.getCursor()]); } }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); } }
5.粒度实现后奇异消除的实现
1.首先第一轮的是有顺序的排列 /* * 词元在排序集合中的比较算法 * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(Lexeme other) { //起始位置优先 if(this.begin < other.getBegin()){ return -1; }else if(this.begin == other.getBegin()){ //词元长度优先 if(this.length > other.getLength()){ return -1; }else if(this.length == other.getLength()){ return 0; }else {//this.length < other.getLength() return 1; } }else{//this.begin > other.getBegin() return 1; } } 2.把有歧义的最大长度串单独一个链出来 3.然后每一个歧义的词都作为一个一次存在的实现这个歧义链的一种情况 4.自动实现的排序取第一个最合适的 public int compareTo(LexemePath o) { //比较有效文本长度 if(this.payloadLength > o.payloadLength){ return -1; }else if(this.payloadLength < o.payloadLength){ return 1; }else{ //比较词元个数,越少越好 if(this.size() < o.size()){ return -1; }else if (this.size() > o.size()){ return 1; }else{ //路径跨度越大越好 if(this.getPathLength() > o.getPathLength()){ return -1; }else if(this.getPathLength() < o.getPathLength()){ return 1; }else { //根据统计学结论,逆向切分概率高于正向切分,因此位置越靠后的优先 if(this.pathEnd > o.pathEnd){ return -1; }else if(pathEnd < o.pathEnd){ return 1; }else{ //词长越平均越好 if(this.getXWeight() > o.getXWeight()){ return -1; }else if(this.getXWeight() < o.getXWeight()){ return 1; }else { //词元位置权重比较 if(this.getPWeight() > o.getPWeight()){ return -1; }else if(this.getPWeight() < o.getPWeight()){ return 1; } } } } } } return 0; } ----------------------------------------------------------------------- 粒度细分的取舍。 取可能出现一些确实歧义的 不又想实现搜索出来
6.自定义字典