一直都想着自己动手写一写中文分词,但是一直都没有动手。今天终于开始了。从最简单的开始,步步深入。希望自己最后能把分词、词性标注、命名实体识别这几块都完成。

   好了,话不多述,进入正题。

   分词最简单的思路就是查词典,确实,最开始大家都是这么做的。包括现在都有人这样做。所以分词效果的好坏最重要的是要有一部好词典,及一个好的匹配算法。

  第一步:找到好词典。

  我分词开始用的是在搜狗实验室弄到一份通词典。一共有15万七千多词;后来又找到了一个词典,有28万词。在分词中用的是28万词的词典,我想简单的应用应该够了。

  28万词的词典下载地址:http://www.xunsearch.com/scws/download.php,同时这里也实现了一个php的分词。

  第二步:确定好算法。

  这里的学习主要参考了52nlp里面的一个博客:

http://www.52nlp.cn/maximum-matching-method-of-chinese-word-segmentation

  讲得很详细,还有C++版本的分词实现。

  基于博客,我简单地用Java实现了一下:

package com.xh.seg;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.xh.dictionary.DictionaryUtil;
public class Segmentation {
    private StringBuilder sb=new StringBuilder();
    private List<String> ls=new ArrayList<String>();
    private int maxwl;//设置词的最大长度
    /*
     * @param source 待分词的句子
     * @return 分词后的结果,用空格隔开。
     * */
    public String seg_str(String source){
        sb.delete(0, sb.length());
        int len;
        String word;
        maxwl=5;
        while((len=source.length())>0){
            if(maxwl>len){
                maxwl=len;
            }
            word=source.substring(len-maxwl);
            int start=len-maxwl;
            boolean find=false;
            //从左边不断减字,直到匹配成功或者字的长度为一
            while(word.length()>1){
                if(DictionaryUtil.match(word)){
                    sb.insert(0, "\t"+word);find=true;
                    break;
                }
                ++start;
                word=source.substring(start);
            }
            if(!find){
                sb.insert(0,"\t"+ word);
            }
            source=source.substring(0, start);
        }
        return sb.toString();
    }
    /*
     * @param source 待分词的句子
     * @return 分词后的结果,用list存储。
     * */
    public List<String> seg_list(String source){
        ls.clear();
        int len;
        String word;
        maxwl=5;
        while((len=source.length())>0){
            if(maxwl>len){
                maxwl=len;
            }
            word=source.substring(len-maxwl);
            int start=len-maxwl;
            boolean find=false;
            while(word.length()>1){
                if(DictionaryUtil.match(word)){
                    ls.add(word);find=true;
                    break;
                }
                ++start;
                word=source.substring(start);
            }
            if(!find){
                ls.add(word);
            }
            source=source.substring(0, start);
        }
        Collections.reverse(ls);
        return ls;
    }
                                                                                                                                                                                                                            
}

程序里面的两个方法是一样的,只是返回结果不一样而已。

词典的加载程序如下:

package com.xh.dictionary;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class DictionaryUtil {
    static Map<String,Integer> dic=new HashMap<String,Integer>(160000);
    static{
        //loadDic("dictionary/SougouLabDic_utf8.txt");
        loadDic("dictionary/dic_utf8.txt");
    }
    public static void loadDic(String name){
        BufferedReader br=null;
        try {
            br=new BufferedReader(new InputStreamReader(new FileInputStream(DictionaryUtil.class
                    .getClassLoader()
                    .getResource("dictionary/SogouLabDic_utf8.dic")
                    .getFile()),"utf-8"));
                                                                                                                                                                                                                
            String line;
            int end;
            while((line=br.readLine())!=null){
                end=line.indexOf('\t');
                if(end!=-1)
                    dic.put(line.substring(0, end), 1);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally{try {br.close();} catch (IOException e) {e.printStackTrace();}}
        System.out.println("dic load success,have been load "+dic.size()+" words!");
    }
                                                                                                                                                                                                        
    public static boolean match(String word){
        if(dic.containsKey(word))
            return true;
        return false;
    }
}

   当时考虑了下,感觉用set会更直接,但是set是建立在map的基础上的,所以直接用map的性能应该会高一点,当然只是猜想。

   第三步:测试

   测试代码如下:

package com.xh.seg;
import org.junit.Test;
public class SegmentationTest {
    @Test
    public void testSeg_str(){
                                                                                                                                                         
        Segmentation seg=new Segmentation();
        String str[]={
                "按自己喜欢选择的,质量不错",
                "结婚的和尚未结婚的都需要登记",
                "他说的确实在理",
                "阿拉斯加遭强暴风雪袭击致xx人死亡",
                "×××生前使用过的物品 ",
                "乒乓球拍卖完了",
                "吴江西陵印刷厂"
        };
        for (String string : str) {
            System.out.println(seg.seg_list(string));
        }
    }
}


分词的结果如下:

dic load success,have been load 284646 words!

[按, 自己, 喜欢, 选择, 的, ,, 质量, 不错]

[结婚, 的, 和, 尚未, 结婚, 的, 都, 需要, 登记]

[他, 说, 的, 确实, 在理]

[阿拉斯加, 遭, 强, 暴风雪, 袭击, 致, x, x, 人, 死亡]

[×××, 生前, 使用, 过, 的, 物品,  ]

[乒乓球, 拍卖, 完了]

[吴江, 西陵, 印刷厂]


第四步:总结

除了“乒乓球拍卖完了”这句分错外,其它的都没什么问题。可见,逆向最大匹配算法在词典够完善的情况下,分词的效果还是勉强可以接受的。毕竟是这么简单的代码实现。最后还是感谢提供词典和分词思路的作者。接下来将想学习双数组Trie树结构在分词上的实现。


参考文献:

 (分词算法) http://www.52nlp.cn/maximum-matching-method-of-chinese-word-segmentation

 (词    典)http://www.xunsearch.com/scws/download.php

  (测试数据) http://my.oschina.net/jcseg/blog/135746