/** * Copyright 2007 The Apache Software Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.paoding.analysis.knife; import java.util.HashSet; import net.paoding.analysis.dictionary.Dictionary; import net.paoding.analysis.dictionary.Hit; /** * 排列组合Knife。 * <p> * * 该Knife把遇到的非LIMIT字符视为一个单词分出。<br> * 同时如果有以该词语开头的字符串在x-for-combinatorics.dic出现也会切出 * * @author Zhiliang Wang [qieqie.wang@gmail.com] * * @since 1.0 * */ public abstract class CombinatoricsKnife implements Knife, DictionariesWare { protected Dictionary combinatoricsDictionary; protected HashSet/* <String> */noiseTable; public CombinatoricsKnife() { } public CombinatoricsKnife(String[] noiseWords) { setNoiseWords(noiseWords); } public void setNoiseWords(String[] noiseWords) { noiseTable = new HashSet/* <String> */((int) (noiseWords.length * 1.5)); for (int i = 0; i < noiseWords.length; i++) { noiseTable.add(noiseWords[i]); } } public void setDictionaries(Dictionaries dictionaries) { combinatoricsDictionary = dictionaries.getCombinatoricsDictionary(); } public int dissect(Collector collector, Beef beef, int offset) { // 当point == -1时表示本次分解没有遇到POINT性质的字符; // 如果point != -1,该值表示POINT性质字符的开始位置, // 这个位置将被返回,下一个Knife将从point位置开始分词 int point = -1; // 记录同质字符分词结束极限位置(不包括limit位置的字符)-也就是assignable方法遇到LIMIT性质的字符的位置 // 如果point==-1,limit将被返回,下一个Knife将从limit位置开始尝试分词 int limit = offset + 1; // 构建point和limit变量的值: // 往前直到遇到LIMIT字符; // 其中如果遇到第一次POINT字符,则会将它记录为point GO_UNTIL_LIMIT: while (true) { switch (assignable(beef, offset, limit)) { case LIMIT: break GO_UNTIL_LIMIT; case POINT: if (point == -1) { point = limit; } } limit++; } // 如果最后一个字符也是ASSIGNED以及POINT, // 且beef之前已经被分解了一部分(从而能够腾出空间以读入新的字符),则需要重新读入字符后再分词 if (limit == beef.length() && offset > 0) { return -offset; } // 检索是否有以该词语位前缀的词典词语 // 若有,则将它解出 int dicWordVote = -1; if (combinatoricsDictionary != null && beef.charAt(limit) > 0xFF) { dicWordVote = tryDicWord(collector, beef, offset, limit); } // 收集从offset分别到point以及limit的词 // 注意这里不收集从point到limit的词 // ->当然可能从point到limit的字符也可能是一个词,不过这不是本次分解的责任 // ->如果认为它应该是个词,那么只要配置对应的其它Knife实例,该Knife会有机会把它切出来的 // ->因为我们会返回point作为下一个Knife分词的开始。 int pointVote = collectPoint(collector, beef, offset, point, limit, dicWordVote); int limitVote = collectLimit(collector, beef, offset, point, limit, dicWordVote); return nextOffset(beef, offset, point, limit, pointVote, limitVote, dicWordVote); } /** * 通知收集从offset到第一个LIMIT字符的词,并投票下一个Knife开始的分词位置。如果不存在POINT字符,则Point的值为-1。 * <p> * * 默认方法实现:如果不存在POINT性质的字符,则直接返回不做任何切词处理。 * * @param collector * @param beef * @param offset * 本次分解的内容在beef中的开始位置 * @param point * 本次分解的内容的第一个POINT性质字符的位置,-1表示不存在该性质的字符 * @param limit * 本次分解的内容的LIMIT性质字符 * @return 投票下一个Knife开始分词的位置;-1表示弃权。默认方法实现:弃权。 */ protected int collectPoint(Collector collector, Beef beef, int offset, int point, int limit, int dicWordVote) { if (point != -1 && dicWordVote == -1) { collectIfNotNoise(collector, beef, offset, point); } return -1; } /** * 通知收集从offset到第一个LIMIT字符的词,并投票下一个Knife开始的分词位置。 * <p> * * 默认方法实现:把从offset位置到limit位置止(不包含边界)的字符串视为一个词切出。 * * @param collector * @param beef * @param offset * 本次分解的内容在beef中的开始位置 * @param point * 本次分解的内容的第一个POINT性质字符的位置,-1表示不存在该性质的字符 * @param limit * 本次分解的内容的LIMIT性质字符 * * @param dicWordVote * * @return 投票下一个Knife开始分词的位置;-1表示弃权。默认方法实现:弃权。 */ protected int collectLimit(Collector collector, Beef beef, int offset, int point, int limit, int dicWordVote) { if (dicWordVote == -1) { collectIfNotNoise(collector, beef, offset, limit); } return -1; } /** * 尝试从combinatorics字典中检索,如果存在以offset到limit位置止(不包含limit边界)字符串开始的词语,则切出该词语。 * <p> * 如没有检索到这样的词语,则本方法返回-1弃权投票下一个Knife的开始分解位置。<br> * 如果检索到这样的词语,在切出在词语的同时,投票返回这个词语的结束位置(词语本身不包含该结束位置的字符) * <p> * * (for version 2.0.4+):<br> * 本方法目前存在的局限:<br> * 如果字典中的某个词语刚好分隔在两次beef之中,比如"U"刚好是此次beef的最后字符,而"盘"是下一次beef的第一个字符,<br> * 这种情况现在 {@link CombinatoricsKnife}还没机制办法识别将之处理为一个词语 * * @param collector * @param beef * @param offset * @param limit * @return */ protected int tryDicWord(Collector collector, Beef beef, int offset, int limit) { int ret = limit; for (int end = limit + 1, count = limit - offset + 1; end <= beef .length(); end++, count++) { Hit hit = combinatoricsDictionary.search(beef, offset, count); if (hit.isUndefined()) { break; } else if (hit.isHit()) { collectIfNotNoise(collector, beef, offset, end); // 收到词语,将ret设置为该词语的end ret = end; } // gotoNextChar为true表示在词典中存在以当前词为开头的词, boolean gotoNextChar = hit.isUnclosed() && end < beef.length() && beef.charAt(end) >= hit.getNext().charAt(count); if (!gotoNextChar) { break; } } return ret <= limit ? -1 : ret; // TODO: // 存在的局限: // 刚好词语分隔在两次beef之中,比如"U"刚好是此次beef的最后字符,而"盘"是下一次beef的第一个字符 // 这种情况现在CombinatoricsKnife还没机制办法识别将之处理为一个词语 } /** * 当Knife决定切出从offset始到end位置止(不包含结束位置的字符)的词语时,本方法能够过滤掉可能是noise的词,使最终不切出。 * * @param collector * @param beef * @param offset * @param end */ protected void collectIfNotNoise(Collector collector, Beef beef, int offset, int end) { // 将offset和end之间的词(不包含end位置)创建出来给word // 如果该词语为噪音词,则重新丢弃之(设置为null), String word = beef.subSequence(offset, end).toString(); if (noiseTable != null && noiseTable.contains(word)) { word = null; } // 否则发送消息给collect方法,表示Knife新鲜出炉了一个内容为word的候选词语 // 即:最终决定是否要把这个词语通知给collector的是collect方法 if (word != null) { doCollect(collector, word, beef, offset, end); } } /** * * 当Knife决定切出从offset始到end位置止(不包含结束位置的字符)的词语时,本方法直接调用{@link #doCollect(Collector, String, Beef, int, int)}切出词语(而不过滤noise词汇) * * @param collector * @param beef * @param offset * @param end */ protected void collect(Collector collector, Beef beef, int offset, int end) { String word = beef.subSequence(offset, end).toString(); doCollect(collector, word, beef, offset, end); } /** * 收集分解出的候选词语。 默认实现是将该候选词语通知给收集器collector。<br> * 子类覆盖本方法可以更灵活地控制词语的收录,例如控制仅当word满足一些额外条件再决定是否收集,<br> * 或依上下文环境收集更多的相关词语 * * @param collector * @param word * @param beef * @param offset * @param end */ protected void doCollect(Collector collector, String word, Beef beef, int offset, int end) { collector.collect(word, offset, end); } /** * 根据字符串性质位置,以及分词结果投票,决出下一个Knife应该从哪一个位置开始探测切词 * * @param beef * @param offset * 本次分词的开始位置 * @param point * 本次分词的第一个POINT性质的字符位置,-1表示没有该性质的字符 * @param limit * 本次分词的第一个LIMIT性质的字符位置 * @param pointVote * 收集从offset到第一个POINT性质字符词汇时的投票,-1表示弃权 * @param limitVote * 收集从offset到第一个LIMIT性质字符词汇时的投票,-1表示弃权 * @param dicWordVote * 收集combinatorics词典词语时的投票,-1表示弃权 * @return */ protected int nextOffset(Beef beef, int offset, int point, int limit, int pointVote, int limitVote, int dicWordVote) { int max = pointVote > limitVote ? pointVote : limitVote; max = max > dicWordVote ? max : dicWordVote; if (max == -1) { return point != -1 ? point : limit; } else if (max > limit) { return max; } else { return limit; } } }