Lucene5学习之使用MMSeg4j分词器

MMSeg4j是一款中文分词器,详细介绍如下:

1、mmseg4j 用 Chih-Hao Tsai 的 MMSeg 算法(http://technology.chtsai.org/mmseg/ )实现的中文分词器,并实现 lucene 的 analyzer 和 solr 的TokenizerFactory 以方便在Lucene和Solr中使用。

2、MMSeg 算法有两种分词方法:Simple和Complex,都是基于正向最大匹配。Complex 加了四个规则过虑。官方说:词语的正确识别率达到了 98.41%。mmseg4j 已经实现了这两种分词算法。

1.5版的分词速度simple算法是 1100kb/s左右、complex算法是 700kb/s左右,(测试机:AMD athlon 64 2800+ 1G内存 xp)。

1.6版在complex基础上实现了最多分词(max-word)。“很好听” -> "很好|好听"; “*” -> "中华|华人|共和|国"; “中国人民银行” -> "中国|人民|银行"。

1.7-beta 版, 目前 complex 1200kb/s左右, simple 1900kb/s左右, 但内存开销了50M左右. 上几个版都是在10M左右

可惜的是,MMSeg4j最新版1.9.1不支持Lucene5.0,于是我就修改了它的源码将它升级咯,使其支持Lucene5.x,至于我是怎样修改,这里就不一一说明的,我把我修改过的MMSeg4j最新源码上传到了我的百度网盘,现分享给你们咯:

mmseg4j-1.9.1源码

mmseg4j-1.9.2源码(支持Lucene5.x)

下面是一个MMSeg4j分词器简单使用示例:

Java代码Lucene5学习之使用MMSeg4j分词器
  1. packagecom.chenlb.mmseg4j.analysis;
  2. importjava.io.IOException;
  3. importorg.apache.lucene.analysis.Analyzer;
  4. importorg.apache.lucene.analysis.TokenStream;
  5. importorg.apache.lucene.analysis.tokenattributes.CharTermAttribute;
  6. importorg.apache.lucene.analysis.tokenattributes.OffsetAttribute;
  7. importorg.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
  8. importorg.apache.lucene.analysis.tokenattributes.TypeAttribute;
  9. importorg.junit.Assert;
  10. importorg.junit.Before;
  11. importorg.junit.Ignore;
  12. importorg.junit.Test;
  13. /**
  14. *MMSegAnalyzer分词器测试
  15. *@authorLanxiaowei
  16. *
  17. */
  18. publicclassMMSegAnalyzerTest{
  19. Stringtxt="";
  20. @Before
  21. publicvoidbefore()throwsException{
  22. txt="京华时报2009年1月23日报道昨天,受一股来自中西伯利亚的强冷空气影响,本市出现大风降温天气,白天最高气温只有零下7摄氏度,同时伴有6到7级的偏北风。";
  23. txt="2009年ゥスぁま是中ABcc国абвгαβγδ首次,我的ⅠⅡⅢ在chenёlbēū全国ㄦ范围ㄚㄞㄢ内①ē②㈠㈩⒈⒑发行地方*债券,";
  24. txt="大S小3U盘浙BU盘T恤T台A股牛B";
  25. }
  26. @Test
  27. //@Ignore
  28. publicvoidtestSimple()throwsIOException{
  29. Analyzeranalyzer=newSimpleAnalyzer();
  30. displayTokens(analyzer,txt);
  31. }
  32. @Test
  33. @Ignore
  34. publicvoidtestComplex()throwsIOException{
  35. //txt="1999年12345日报道了一条新闻,2000年中法国足球比赛";
  36. /*txt="第一卷云天落日圆第一节偷欢不成倒大霉";
  37. txt="中国人民银行";
  38. txt="我们";
  39. txt="工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作";*/
  40. //ComplexSeg.setShowChunk(true);
  41. Analyzeranalyzer=newComplexAnalyzer();
  42. displayTokens(analyzer,txt);
  43. }
  44. @Test
  45. @Ignore
  46. publicvoidtestMaxWord()throwsIOException{
  47. //txt="1999年12345日报道了一条新闻,2000年中法国足球比赛";
  48. //txt="第一卷云天落日圆第一节偷欢不成倒大霉";
  49. //txt="中国人民银行";
  50. //txt="下一个为什么";
  51. //txt="我们家门前的大水沟很难过";
  52. //ComplexSeg.setShowChunk(true);
  53. Analyzeranalyzer=newMaxWordAnalyzer();
  54. displayTokens(analyzer,txt);
  55. }
  56. /*@Test
  57. publicvoidtestCutLeeterDigitFilter(){
  58. StringmyTxt="mb991chcq40-519txmmseg4j";
  59. List<String>words=toWords(myTxt,newMMSegAnalyzer(""){
  60. @Override
  61. protectedTokenStreamComponentscreateComponents(Stringtext){
  62. Readerreader=newBufferedReader(newStringReader(text));
  63. Tokenizert=newMMSegTokenizer(newSeg(),reader);
  64. returnnewTokenStreamComponents(t,newCutLetterDigitFilter(t));
  65. }
  66. });
  67. //Assert.assertArrayEquals("CutLeeterDigitFilterfail",words.toArray(newString[words.size()]),"mb991chcq40519txmmseg4j".split(""));
  68. for(Stringword:words){
  69. System.out.println(word);
  70. }
  71. }*/
  72. publicstaticvoiddisplayTokens(Analyzeranalyzer,Stringtext)throwsIOException{
  73. TokenStreamtokenStream=analyzer.tokenStream("text",text);
  74. displayTokens(tokenStream);
  75. }
  76. publicstaticvoiddisplayTokens(TokenStreamtokenStream)throwsIOException{
  77. OffsetAttributeoffsetAttribute=tokenStream.addAttribute(OffsetAttribute.class);
  78. PositionIncrementAttributepositionIncrementAttribute=tokenStream.addAttribute(PositionIncrementAttribute.class);
  79. CharTermAttributecharTermAttribute=tokenStream.addAttribute(CharTermAttribute.class);
  80. TypeAttributetypeAttribute=tokenStream.addAttribute(TypeAttribute.class);
  81. tokenStream.reset();
  82. intposition=0;
  83. while(tokenStream.incrementToken()){
  84. intincrement=positionIncrementAttribute.getPositionIncrement();
  85. if(increment>0){
  86. position=position+increment;
  87. System.out.print(position+":");
  88. }
  89. intstartOffset=offsetAttribute.startOffset();
  90. intendOffset=offsetAttribute.endOffset();
  91. Stringterm=charTermAttribute.toString();
  92. System.out.println("["+term+"]"+":("+startOffset+"-->"+endOffset+"):"+typeAttribute.type());
  93. }
  94. }
  95. /**
  96. *断言分词结果
  97. *@paramanalyzer
  98. *@paramtext源字符串
  99. *@paramexpecteds期望分词后结果
  100. *@throwsIOException
  101. */
  102. publicstaticvoidassertAnalyzerTo(Analyzeranalyzer,Stringtext,String[]expecteds)throwsIOException{
  103. TokenStreamtokenStream=analyzer.tokenStream("text",text);
  104. CharTermAttributecharTermAttribute=tokenStream.addAttribute(CharTermAttribute.class);
  105. for(Stringexpected:expecteds){
  106. Assert.assertTrue(tokenStream.incrementToken());
  107. Assert.assertEquals(expected,charTermAttribute.toString());
  108. }
  109. Assert.assertFalse(tokenStream.incrementToken());
  110. tokenStream.close();
  111. }
  112. }

mmseg4j分词器有3个字典文件,如图:
Lucene5学习之使用MMSeg4j分词器
chars.dic是汉字字典文件,里面有12638个汉字

units.dic里是中文单位词语,如小时,分钟,米,厘米等等,具体自己打开看看就明白了

words.dic就是用户自定义字典文件,比如:么么哒,T恤,牛B等这些词,放在这个字典文件里,分词器就能把它当作一个词

我们在使用mmseg4j分词器时,是这样用的:

Java代码Lucene5学习之使用MMSeg4j分词器
  1. Analyzeranalyzer=newSimpleAnalyzer();

查看SimpleAnalyzer的构造函数,

Java代码Lucene5学习之使用MMSeg4j分词器
  1. publicSimpleAnalyzer(){
  2. super();
  3. }

调用的是父类MMSegAnalyzer的无参构造函数,接着查看MMSegAnalyzer类的无参构造函数:

Java代码Lucene5学习之使用MMSeg4j分词器
  1. publicMMSegAnalyzer(){
  2. dic=Dictionary.getInstance();
  3. }

你会发现是通过Dictionary.getInstance()单实例模式去加载字典文件的,接着查看getInstance方法,
Lucene5学习之使用MMSeg4j分词器
这里的代码注释写的很清楚,告诉了我们字典文件的加载逻辑。

File path = getDefalutPath();用来获取默认的字典文件路径,

然后根据字典文件路径调用getInstance(path)方法去加载字典文件,接着查看该方法,

Lucene5学习之使用MMSeg4j分词器
先从缓存dics里去字典文件,如果缓存里没有找到,则才会根据字典文件路径去加载,然后把加载到的字典文件放入缓存dics即dics.put(),

接着看看Dictionary字典是如何初始化的,查看Dictionary的构造函数源码:
Lucene5学习之使用MMSeg4j分词器
你会发现内部实际是通过调用init(path);方法进行字典初始化的,继续查阅init方法,
Lucene5学习之使用MMSeg4j分词器
内部又是调用的reload方法加载的字典,继续跟踪至reload方法,
Lucene5学习之使用MMSeg4j分词器
内部通过loadDic去加载words和chars两个字典文件,通过loadUnit方法去加载units字典文件,wordsLastTime是用来存放每个字典文件的最后一次修改时间,引入这个map的目的是为了实现字典文件重新加载,通过字典文件的最后一次修改时间来判定文件是否修改过,如果这个map里不存在某字典文件的最后一次修改时间,则表明该字典文件是新加入的,需要重新加载至内存,这是loadDic方法的源码:

Java代码Lucene5学习之使用MMSeg4j分词器
  1. privateMap<Character,CharNode>loadDic(FilewordsPath)throwsIOException{
  2. InputStreamcharsIn=null;
  3. FilecharsFile=newFile(wordsPath,"chars.dic");
  4. if(charsFile.exists()){
  5. charsIn=newFileInputStream(charsFile);
  6. addLastTime(charsFile);//chars.dic也检测是否变更
  7. }else{//从jar里加载
  8. charsIn=this.getClass().getResourceAsStream("/data/chars.dic");
  9. charsFile=newFile(this.getClass().getResource("/data/chars.dic").getFile());//onlyforlog
  10. }
  11. finalMap<Character,CharNode>dic=newHashMap<Character,CharNode>();
  12. intlineNum=0;
  13. longs=now();
  14. longss=s;
  15. lineNum=load(charsIn,newFileLoading(){//单个字的
  16. publicvoidrow(Stringline,intn){
  17. if(line.length()<1){
  18. return;
  19. }
  20. String[]w=line.split("");
  21. CharNodecn=newCharNode();
  22. switch(w.length){
  23. case2:
  24. try{
  25. cn.setFreq((int)(Math.log(Integer.parseInt(w[1]))*100));//字频计算出*度
  26. }catch(NumberFormatExceptione){
  27. //eat...
  28. }
  29. case1:
  30. dic.put(w[0].charAt(0),cn);
  31. }
  32. }
  33. });
  34. log.info("charsloadedtime="+(now()-s)+"ms,line="+lineNum+",onfile="+charsFile);
  35. //tryloadwords.dicinjar
  36. InputStreamwordsDicIn=this.getClass().getResourceAsStream("/data/words.dic");
  37. if(wordsDicIn!=null){
  38. FilewordsDic=newFile(this.getClass().getResource("/data/words.dic").getFile());
  39. loadWord(wordsDicIn,dic,wordsDic);
  40. }
  41. File[]words=listWordsFiles();//只要wordsXXX.dic的文件
  42. if(words!=null){//扩展词库目录
  43. for(FilewordsFile:words){
  44. loadWord(newFileInputStream(wordsFile),dic,wordsFile);
  45. addLastTime(wordsFile);//用于检测是否修改
  46. }
  47. }
  48. log.info("loadalldicusetime="+(now()-ss)+"ms");
  49. returndic;
  50. }

大致逻辑就是先加载chars.dic再加载words.dic,最后加载用户自定义字典文件,注意用户自定义字典文件命名需要以words开头且文件名后缀必须为.dic,查找所有用户自定义字典文件是这句代码:

Java代码Lucene5学习之使用MMSeg4j分词器
  1. File[]words=listWordsFiles();


Lucene5学习之使用MMSeg4j分词器
注意:dicPath.listFiles表示查找dicPath目录下所有文件,dicPath即我们的words.dic字典文件的所在路径,而重载的accept的意思我想大家都懂的,关键点我用红色方框标注出来了,这句代码意思就是查找words.dic字典文件所在文件夹下的以words开头的dic字典文件,包含子文件夹里的字典文件(即递归查找,你懂的)。看到这里,我想至于如何自定义用户自定义字典文件,大家都不言自明了。为了照顾小白,我还是说清楚点吧,自定义用户字典文件方法步骤如下:

如果你想把屌丝,高富帅 当作一个词,那你首先需要新建一个.dic文件,注意dic文件必须是无BOM的UTF-8编码的文件(切记!!!!!!),且自定义字典文件命名需要符合上面说过的那种固定格式,不知道的请看上面那张图,看仔细点,然后一行一个词,你懂的,然后把你自定义的字典文件复制到classPath下的data文件夹下,如果你是简单的Java project,那么就在src下新建一个data包,然后 把你自定义字典文件copy到data包下,如果你是Maven Project,那就在src/main/sources包下新建一个package 名字叫data,同理把你自定义字典文件复制到data包下即可。这样你的自定义词就能被分词器正确切分啦!

mmseg4j就说这么多了吧,mmseg4j我修改过的最新源码上面有贴出百度网盘下载地址,自己去下载,jar包在target目录下,如图:
Lucene5学习之使用MMSeg4j分词器
Lucene5学习之使用MMSeg4j分词器
Lucene5学习之使用MMSeg4j分词器

从我提供的下载地址下载的最新源码包里有打包好的jar包,如图去找就行了,当然为了方便你们,我待会儿也会在底下的附件里将打包的jar包上传上去。

OK,打完收工!!!!如果你还有什么问题,请QQ上联系我(QQ:7-3-6-0-3-1-3-0-5),或者加我的Java技术群跟我们一起交流学习,我会非常的欢迎的。群号: