[Data Mining] 文本分词小程序

这篇博客主要是记录本学期的一门课上写的小程序,课程是《数据挖掘应用与技术》,程序是文本分类,老师本来的要求比较简单,不过写着写着就超过了老师的要求,所以这里记录一下。

1 分词算法

1.1 流程图
首先是一个简单的流程图
[Data Mining] 文本分词小程序_第1张图片

1.2 正逆向匹配算法
本系统采用的分词算法是基于词典的分词算法,它的主要思想是基于字符串匹配的机械分词,即按照一定的策略将待分词的汉字串与一个“充分大的”分词词典中的词条进行匹配,若在词典中找到某个字符串,则匹配成功,即识别出一个词。因此根据文本扫描的方向和匹配原则,可以分为正向最大匹配、逆向最大匹配、逐词匹配、最少切分、全切分等匹配方法。而在实际应用中,常常将上述方法结合起来。
(1)正向最大匹配算法
正向最大匹配算法是最基本的机械匹配算法之一,它的基本思想是
A)从左往右取不超过词典最大长度的汉字作为匹配字段;
B)查询词典并进行匹配,若能匹配,则将这个匹配字段作为一个词切分出来;
C)若不能匹配,则将这个匹配字段的最后一个字去掉,剩下的字符串作为新的匹配字段,进行再次匹配;
D)循环操作,直到匹配字段字数为零为止;
E)重复正向最大匹配过程,直到切分出所有词为止。
(2)逆向最大匹配算法
逆向最大匹配算法原理类似于正向最大匹配算法,它的具体原理是:
A)从右往左取不超过词典最大长度的汉字作为匹配字段;
B)查询词典并进行匹配,若能匹配,则将这个匹配字段作为一个词切分出来;
C)若不能匹配,则将这个匹配字段的最前一个字去掉,剩下的字符串作为新的匹配字段,进行再次匹配;
D)循环操作,直到匹配字段字数为零为止;
E)重复逆向最大匹配过程,直到切分出所有词为止。
正向最大匹配算法和逆向最大匹配算法的区别在于正向最大匹配算法是从左往右依次取匹配字段,而逆向最大匹配算法则是从右往左依次取匹配字段,二者的匹配方法相同,但方向不同。

1.3 贪吃蛇算法(减少歧义)
PS:这个算法是参考另外一个博客上的,但是时间有些久了,如果原作者看到请跟我联系,我会加上原文引用

由于正向最大匹配算法和逆向最大匹配算法均存在歧义问题,为了减少歧义分词的出现,本程序将同时采用正向最大匹配算法和逆向最大匹配算法。首先对待处理的文档进行正向最大匹配算法分词,接着用逆向最大匹配算法得到逆向的分词结果,紧接着对得到的两种分词结果进行对比。如果两种算法得到的分词数组一样,那么说明分词结果不存在歧义;如果得到的分词结果不一致,那么就说明分词存在歧义现象。这时候就必须对结果进行歧义消除。
常见的歧义消除方法是,从正反向的分词结果中:选择分词数量较少的那个 或者 选择单字较少的那个 或者 选择分词长度方差最小的那个。而本算法是运用游戏贪食蛇的原理进行歧义的消除,这里称为“贪食蛇法”。具体思路是这样的:要进行分词的字符串,就是食物。有2个贪吃蛇,一个从左向右吃;另一个从右向左吃。只要左右分词结果有歧义,2条蛇就咬下一口。贪吃蛇吃下去的字符串,就变成分词。 如果左右分词一直有歧义,两条蛇就一直吃。两条蛇吃到字符串中间交汇时,就肯定不会有歧义了。这时候,左边贪吃蛇肚子里的分词,中间没有歧义的分词,右边贪吃蛇肚子里的分词,3个一拼,就是最终的分词结果。通俗地说,对字符串进行左右(正反)分词,如果有歧义,就截取字符串两头的第一个分词,作为最终分词结果的两头。两头被各吃一口的字符串,就变短了。这个变短的字符串,重新分词,再次比较左右分词有没有歧义。如果一直有歧义,就一直循环。直到没有歧义,或者字符串不断被从两头截短,最后什么也不剩下,也就不可能有歧义了。这个方法,就是把正向分词结果的左边,和反向分词结果的右边,不断的向中间进行拼接,最终得到分词结果。

1.4 结果处理
这个地方主要包括几个方面:
第一个是标点符号的去除,我的方法是用正则表达式来进行,效果比较好。
第二个是停用词的去除,也就是去掉一些十分常见的词语,这个步骤是为课程剩余实验做准备的
第三个是姓名的划分,首先根据百家姓来查找可能存在的姓名,然后匹配后面的字,一般来说名字如果是三个字,后面两个字通常不是词组,根据这个就可以分辨出大多数的名字。

1.5 准确率测试
根据别人提供的一个人工划分的标准划分文本,我写了比对算法,成功算出了分词的准确率

2、 代码

这个是主要算法实现的代码,其余还有相应的调用,具体请参看我的github代码库:https://github.com/GumpCode/DataMining/tree/master/WordCut

package Gump.DataMing;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 基于正逆双向最大匹配分词算法实现 
 * 
 */
public class TextManipulation {

    /**
     * 存储分词词典
     */
    private Map DictMap = new HashMap();
    private List StopList = new ArrayList();
    private List NameList = new ArrayList();
    private List NoiseList = new ArrayList();

    private String DictPath = "./dictionary/dic.txt";
    private String StopWordPath = "./dictionary/stopwords.txt";
    private String NameWordPath = "./dictionary/surname.txt";
    private String NoiseWordPath = "./dictionary/noise.txt";

    /**
     * 最大切词长度,即为五个汉字
     */
    private int MAX_LENGTH = 5;

    /**
     * 构造方法中读取分词词典,并存储到map中
     * 
     * @throws IOException
     */
    public TextManipulation() throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(DictPath));
        String line = null;
        while ((line = br.readLine()) != null) {
            String[] temp = line.split(" ");
            line = temp[0].trim();
            DictMap.put(line, 0);
        }
        br.close();
    }

    /**
     * 读取停词词典,并存储到map中
     * @throws Exception
     */
    private void getStopWord() throws Exception{
        BufferedReader br = new BufferedReader(new FileReader(StopWordPath));
        String line = null;
        while ((line = br.readLine()) != null) {
            String[] temp = line.split(" ");
            line = temp[0].trim();
            StopList.add(line);
        }
        br.close();
    }


    private void getNameWord() throws Exception{
        BufferedReader br = new BufferedReader(new FileReader(NameWordPath));
        String line = null;
        while ((line = br.readLine()) != null) {
            NameList.add(line);
        }
        br.close();
    }

    private void getNoiseWord() throws Exception{
        BufferedReader br = new BufferedReader(new FileReader(NoiseWordPath));
        String line = null;
        while ((line = br.readLine()) != null) {
            NoiseList.add(line);
        }
        br.close();
    }

    /**
     * 读取输入文本文件
     * @param FilePath
     * @return
     * @throws Exception
     */
    private static String readFile(String FilePath) throws Exception{
        BufferedReader br=new BufferedReader(new FileReader(FilePath)); 
        StringBuffer word = new StringBuffer();
        String txtLine = null;
        while((txtLine = br.readLine()) !=null){
            word.append(txtLine.trim().replaceAll(" ", "")+".");
        }
        br.close();
        return word.toString();
    }   

    /**
     * 设置最大切词长度
     * 
     * @param max
     *            最大切词长度
     */
    public void setMaxLength(int max) {
        this.MAX_LENGTH = max;
    }

    /**
     * 获取当前最大切词长度,默认为5(5个汉字)
     * 
     * @return 当前最大切词长度
     */
    public int getMaxLength() {
        return this.MAX_LENGTH;
    }

    /**
     * 最大匹配分词算法
     * 
     * @param SplitStr
     *            待切分的字符串
     * @param leftToRight
     *            切分方向,true为从左向右,false为从右向左
     * @return 切分的字符串
     */
    public List Split(String SplitStr, boolean leftToRight) {
        // 如果带切分字符串为空则返回空
        if (SplitStr.isEmpty())
            return null;
        // 储存正向匹配分割字符串
        List leftWords = new ArrayList();
        // 储存负向匹配分割字符串
        List rightWords = new ArrayList();
        // 用于取切分的字串
        String word = null;
        // 取词的长度,初始化设置为最大值
        int wordLength = MAX_LENGTH;
        // 分词操作中处于字串当前位置
        int position = 0;
        // 已经处理字符串的长度
        int length = 0;
        // 去掉字符串中多余的空格
        SplitStr = SplitStr.trim().replaceAll("\\s+", "");
        // 当待切分字符串没有被切分完时循环切分
        while (length < SplitStr.length()) {
            // 如果还没切分的字符串长度小于最大值,让取词词长等于该词本身长度
            if (SplitStr.length() - length < MAX_LENGTH)
                wordLength = SplitStr.length() - length;
            // 否则取默认值
            else
                wordLength = MAX_LENGTH;
            // 如果是正向最大匹配,从SplitStr的position处开始切割
            if (leftToRight) {
                position = length;
                word = SplitStr.substring(position, position + wordLength);
            }
            // 如果是逆向最大匹配,从SplitStr末尾开始切割
            else {
                position = SplitStr.length() - length;
                word = SplitStr.substring(position - wordLength, position);
            }
            // 从当前位置开始切割指定长度的字符串
            // word = SplitStr.substring(position, position + wordLength);

            // 如果分词词典里面没有切割出来的字符串,舍去一个字符
            while (!DictMap.containsKey(word)) {
                // 如果是单字,退出循环
                if (word.length() == 1) {
                    // 如果是字母或是数字要将连续的字母或者数字分在一起
                    if (word.matches("[a-zA-z0-9]")) {
                        // 如果是正向匹配直接循环将后续连续字符加起来
                        if (leftToRight) {
                            for (int i = SplitStr.indexOf(word, position) + 1; i < SplitStr
                                    .length(); i++) {
                                if ((SplitStr.charAt(i) >= '0' && SplitStr
                                        .charAt(i) <= '9')
                                        || (SplitStr.charAt(i) >= 'A' && SplitStr
                                                .charAt(i) <= 'Z')
                                        || (SplitStr.charAt(i) >= 'a' && SplitStr
                                                .charAt(i) <= 'z')) {
                                    word += SplitStr.charAt(i);
                                } else
                                    break;
                            }
                        } else {
                            // 如果是逆向匹配,从当前位置之前的连续数字、字母字符加起来并翻转
                            for (int i = SplitStr.indexOf(word, position - 1) - 1; i >= 0; i--) {
                                if ((SplitStr.charAt(i) >= '0' && SplitStr
                                        .charAt(i) <= '9')
                                        || (SplitStr.charAt(i) >= 'A' && SplitStr
                                                .charAt(i) <= 'Z')
                                        || (SplitStr.charAt(i) >= 'a' && SplitStr
                                                .charAt(i) <= 'z')) {
                                    word += SplitStr.charAt(i);
                                    if (i == 0) {
                                        StringBuffer sb = new StringBuffer(word);
                                        word = sb.reverse().toString();
                                    }
                                } else {
                                    // 翻转操作
                                    StringBuffer sb = new StringBuffer(word);
                                    word = sb.reverse().toString();
                                    break;
                                }
                            }
                        }
                    }
                    break;
                }
                // 如果是正向最大匹配,舍去最后一个字符
                if (leftToRight)
                    word = word.substring(0, word.length() - 1);
                // 否则舍去第一个字符
                else
                    word = word.substring(1);
            }
            // 将切分出来的字符串存到指定的表中
            if (leftToRight)
                leftWords.add(word);
            else
                rightWords.add(word);
            // 已处理字符串增加
            length += word.length();
        }
        // 如果是逆向最大匹配,要把表中的字符串调整为正向
        if (!leftToRight) {
            for (int i = rightWords.size() - 1; i >= 0; i--) {
                leftWords.add(rightWords.get(i));
            }
        }
        // 返回切分结果
        return leftWords;
    }

    /**
     * 判断两个集合是否相等
     * 
     * @param list1
     *            集合1
     * @param list2
     *            集合2
     * @return 如果相等则返回true,否则为false
     */
    public boolean isEqual(List list1, List list2) {
        if (list1.isEmpty() && list2.isEmpty())
            return false;
        if (list1.size() != list2.size())
            return false;
        for (int i = 0; i < list1.size(); i++) {
            if (!list1.get(i).equals(list2.get(i)))
                return false;
        }
        return true;
    }

    /**
     * 判别分词歧义函数
     * 
     * @param inputStr
     *            待切分字符串
     * @return 分词结果
     */
    public List resultWord(String inputStr) {
        // 分词结果
        List result = new ArrayList();
        // “左贪吃蛇”分词结果
        List resultLeft = new ArrayList();
        // “中贪吃蛇”(分歧部分)分词结果
        List resultMiddle = new ArrayList();
        // “右贪吃蛇”分词结果
        List resultRight = new ArrayList();
        // 正向最大匹配分词结果
        List left = new ArrayList();
        // 逆向最大匹配分词结果
        List right = new ArrayList();
        left = Split(inputStr, true);
        right = Split(inputStr, false);
        // 判断两头的分词拼接,是否已经在输入字符串的中间交汇,只要没有交汇,就不停循环
        while (left.get(0).length() + right.get(right.size() - 1).length() < inputStr
                .length()) {
            // 如果正逆向分词结果相等,那么取正向结果跳出循环
            if (isEqual(left, right)) {
                resultMiddle = left;
                break;
            }
            // 如果正反向分词结果不同,则取分词数量较少的那个,不用再循环
            if (left.size() != right.size()) {
                resultMiddle = left.size() < right.size() ? left : right;
                break;
            }
            // 如果以上条件都不符合,那么实行“贪吃蛇”算法
            // 让“左贪吃蛇”吃下正向分词结果的第一个词
            resultLeft.add(left.get(0));
            // 让“右贪吃蛇”吃下逆向分词结果的最后一个词
            resultRight.add(right.get(right.size() - 1));
            // 去掉被“贪吃蛇”吃掉的词语
            inputStr = inputStr.substring(left.get(0).length());
            inputStr = inputStr.substring(0,
                    inputStr.length() - right.get(right.size() - 1).length());
            // 清理之前正逆向分词结果,防止造成干扰
            left.clear();
            right.clear();
            // 对没被吃掉的字符串重新开始分词
            left = Split(inputStr, true);
            right = Split(inputStr, false);
        }
        // 循环结束,说明要么分词没有歧义了,要么"贪吃蛇"从两头吃到中间交汇了
        // 如果是在中间交汇,交汇时的分词结果,还要进行以下判断:
        // 如果中间交汇有重叠了:
        // 正向第一个分词的长度 + 反向最后一个分词的长度 > 输入字符串总长度,就直接取正向的
        if (left.get(0).length() + right.get(right.size() - 1).length() > inputStr
                .length())
            resultMiddle = left;
        // 如果中间交汇,刚好吃完,没有重叠:
        // 正向第一个分词 + 反向最后一个分词的长度 = 输入字符串总长度,那么正反向一拼即可
        if (left.get(0).length() + right.get(right.size() - 1).length() == inputStr
                .length()) {
            resultMiddle.add(left.get(0));
            resultMiddle.add(right.get(right.size() - 1));
        }
        // 将没有歧义的分词结果添加到最终结果result中
        for (String string : resultLeft) {
            result.add(string);
        }
        for (String string : resultMiddle) {
            result.add(string);
        }
        // “右贪吃蛇”存储的分词要调整为正向
        for (int i = resultRight.size() - 1; i >= 0; i--) {
            result.add(resultRight.get(i));
        }
        return result;
    }

    /**
     * 将一段话分割成若干句话,分别进行分词
     * 
     * @param inputStr
     *            待分割的一段话
     * @return 这段话的分词结果
     */
    public List resultSplit(String inputStr) {
        // 用于存储最终分词结果
        List result = new ArrayList();
        // 如果遇到标点就分割成若干句话
        String regex = "[,。;!?]";
        String[] st = inputStr.split(regex);
        // 将每一句话的分词结果添加到最终分词结果中
        for (String stri : st) {
            List list = resultWord(stri);
            result.addAll(list);
        }
        return result;
    }

    /**
     * 对初步分词的结果进行去停词
     * @param result
     * @return
     */
    public List cutStopWord(List result){
        for(int i=result.size()-1; i >= 0; i--){
            for(String word:StopList){
                if(word.equals(result.get(i))){
                    result.remove(i);
                }
            }
        }
        return result;
    }

    /**
     * 对初步分词的结果进行去标点符号
     * @param result
     * @return
     */
    private List cutMark(List result){
        List newlist = new ArrayList();
        for (int i = 0; i < result.size(); i++) {
            String temp = result.get(i);
            String flag = temp.replaceAll("[\\pP\\p{Punct}]", "");// 正则匹配标点符号
            //flag = flag.replaceAll(" ", "");
            if ((!flag.equals("")) && (!flag.equals(" "))) {
                newlist.add(flag);
            }
        }
        return newlist;
    }

    private List getName(List unNameList){
        for(int i=0; ifor(int j=0; jif(unNameList.get(i).equals(NameList.get(j))){
                    for(int k=1; k<3; k++){
                        if(unNameList.get(i+1).length() == 1){
                            boolean flag = false;
                            for(String word:NoiseList){
                                if(unNameList.get(i+1).equals(word)){
                                    flag = true;
                                }
                            }
                            if(flag == true){
                                break;
                            }
                            unNameList.set(i, unNameList.get(i)+unNameList.get(i+1));
                            unNameList.remove(i+1);
                        }
                    }
                }
            }
        }
        return unNameList;
    }

    /**
     * 对输入文件进行分词,可选是否去停词
     * @param FilePath
     * @param NoStop  
     *              true:去停词
     *              false: 不去停词
     * @return
     * @throws Exception
     */
    public List WordCut(String FilePath, boolean NoStop) throws Exception{
        String wordString = readFile(FilePath);
        List resultList;
        List unMergeName;
        getStopWord();
        getNameWord();
        getNoiseWord();
        if(!NoStop){
            unMergeName = resultWord(wordString);
            resultList = getName(unMergeName);
        } else {
            List rawList = resultWord(wordString);
            unMergeName = getName(rawList);
            resultList = cutStopWord(unMergeName);
        }
        List returnList = cutMark(resultList);
        return returnList;
    }
}

Edited by Gump,转载请注明出处:
http://blog.csdn.net/GumpCode/article/details/50590651

你可能感兴趣的:([Data,Mining]数据挖掘)