Java下的中文分词方案

原文地址

原文链接

前言

在博客网站构建的时候我们需要针对文章做检索,由于在检索的时候不可能检索所有文章的所有内容,所以我们采用关键词的检索,而且我们也需要将关键词放在网站的meta当中,提高网站在收索引擎中的权值。那么我们需要从文章中提取关键词的方案,首先想到的就是中文分词

由于我们查找的关键词不是一般词语,而是相对来说比较偏技术的词语,而正常的可以生成词云的单词反而不是我们需要的对象,而这类技术词库要么自己训练,要么找现有的词库。训练的话目前没有那个时间和精力,找现有词库的话,目前中文的互联网技术词库也没有找到比较合适完善的。所以我这里的处理就是,在每文章之后手动标记关键词,然后在自定义词库中添加我们标记过的关键词,最后在分词后过滤我们标记的关键词。由于个人博客涉及范围较小可以使用这种方案,但是确实不够优雅,但是这种方案好的地方就是,之后训练出词库之后可以直接代替已有词库

中文分词的方案还是蛮多的,看了一些中文分词的比较测试,比如ysctiandi等。分词的方案广义上就两种,一种是基于已有词库的分词方案,一种是机器学习的分词方案。机器学习的方案其实本质上也是对分词库的一种扩充只是实现和计算的手法不同。如果使用机器学习的方案,不自己训练的话,那么就需要调现有的api,这样的话一方面接口的稳定性不一定能很好的保障,另一方面这种api不是需要申请就是需要付费,还是挺麻烦的

在秉承着【省事】【开源】【免费】【维护中】【性能过得去】的原则我们这里调研了jcsegmynlp,未调研方案word, Ansj, jieba, HanLp,虽然没有详细调研,但是HanLp从维护性和文档详细程度以及社区活跃度都是最优的,但是目前没时间弄机器学习的东西就先不考虑这个了

mynlp

mynlp简述

官方文档,就目前的github更新情况其实并不能算是一个非常棒的方案。首先官方文档的给的资料太少,而且文档还停留在上一个大版本。所以只能是去看源码,但是从源码来看,

  • 首先是测试用例写的不够明白,不能够很简单的从测试用例中看出功能点

  • 其次是源码的注释相对较少,很多接口慢慢看才能读懂功能

  • 最后是对于集成的支持不够,比如对于忽略词文档的添加,文档和注释中都没有给自定义忽略词文档的定义方法,但是在源码中

    /**
     * 系统默认自带的停用词表
     */
    const val StopWordDictPath = "stopwords.txt"
    
    /**
     * 格式
     * + word
     * - wordb
     * 用来控制对默认停用词表的增加和删除
     */
    const val MergeStopWordDictPath = "merge_stopwords.txt"
    
    /**
     * 停用词接口
     *
     * Guice默认注入SystemStopWordDict
     *
     * @author jimichan
     */
    @ImplementedBy(SystemStopWordDict::class)
    interface StopWordDict {
        fun contains(word: String): Boolean
        fun add(word: String)
        fun remove(word: String)
        fun rebuild()
    }
    
        companion object {
    
            @JvmStatic
            fun loadSystemStopword(env: MynlpEnv): Set<String> {
    
                fun readStopDict(env: MynlpEnv): Set<String> {
                    try {
                        val resource = env.tryLoadResource(StopWordDictPath)
    
                        resource?.let { re ->
                            return re.inputStream().bufferedReader().readLines().asSequence()
                                .map { it.trim() }.filter { it.isNotBlank() }.toSet()
    
                        }
                    } catch (e: Exception) {
                        Mynlp.logger.error("", e)
                    }
    
                    return emptySet()
                }
    
                val wordSet = readStopDict(env).toMutableSet()
    
                // 如果存在merge_stopwords.txt资源的话
                val resource = env.tryLoadResource(MergeStopWordDictPath)
                resource?.let { re ->
                    re.inputStream().bufferedReader().readLines().forEach { line_ ->
                        val line = line_.trim()
                        if (line.isNotBlank()) {
                            if (line.startsWith("+")) {
                                wordSet += line.substring(1).trim()
                            } else if (line.startsWith("-")) {
                                wordSet -= line.substring(1).trim()
                            }
                        }
                    }
                }
    
                return wordSet
            }
    
        }
    

    可以大概理解【merge_stopwords.txt】这个是自定义忽略词的配置文件。但是作为配置文件只能直接写在resource的文件下,只能叫merge_stopwords.txt,所以目前来看确实没有对项目接入有特别好的支持的

mynlp使用

引包

        <!--        https://github.com/mayabot/mynlp-->

        <dependency>
            <groupId>com.mayabot.mynlp</groupId>
            <artifactId>mynlp-all</artifactId>
            <version>4.0.0</version>
        </dependency>

上个版本官方文档的基本分词对象构建使用是

 Lexer lexer = Lexers.coreBuilder().build();

在新的版本中我们应该使用,来构建

Lexer lexer = Mynlp.instance().lexerBuilder().hmm().build();

源码

//...
    fun hmm(): FluentLexerBuilder {
        pipeline.install(HmmLexerPlugin(mynlp))
        return this@FluentLexerBuilder
    }    
//...
@Deprecated(message = "使用hmm方法", replaceWith = ReplaceWith("hmm"), level = DeprecationLevel.WARNING)
    fun core(): FluentLexerBuilder {
        pipeline.install(HmmLexerPlugin(mynlp))
        return this@FluentLexerBuilder
    }
//...

解析句子我们需要添加一个spring boot start版本的推荐服务包

        String article = "我们需要添加一个spring boot start版本的推荐服务包。";
        Lexer lexer = Mynlp.instance().lexerBuilder().hmm().withPos().build();
        log.info(lexer.scan(article).toList().toString());
//[我们/r, 需要/v, 添加/v, 一个/mq, spring/x, boot/x, start/x, 版本/n, 的/u, 推荐/v, 服务/v, 包/q, 。/w]
添加自定义词汇

如果我们需要自定义添加一些字典,这里MemCustomDictionary的构建最好是在服务开始的时候,build还是需要花些许时间的

        MemCustomDictionary dictionary = new MemCustomDictionary();
        dictionary.clear();
        dictionary.addWord("spring boot start");
        dictionary.addWord("服务包");
        dictionary.rebuild();


        String article = "我们需要添加一个spring boot start版本的推荐服务包。";
        Lexer lexer = Mynlp.instance().lexerBuilder().hmm().withPos().with(new CustomDictionaryPlugin(dictionary)).build();
        log.info(lexer.scan(article).toList().toString());
//[我们/r, 需要/v, 添加/v, 一个/mq, spring boot start/nx, 版本/n, 的/u, 推荐/v, 服务包/n, 。/w]
忽略词汇

默认是不会忽略词汇的,我们需要修改默认的reader方式

        MemCustomDictionary dictionary = new MemCustomDictionary();
        dictionary.clear();
        dictionary.addWord("spring boot start");
        dictionary.addWord("服务包");
        dictionary.removeWord("的");
        dictionary.rebuild();


        String article = "我们需要添加一个spring boot start版本的推荐服务包。";
        Lexer lexer = Mynlp.instance().lexerBuilder().hmm().withPos().with(new CustomDictionaryPlugin(dictionary)).build();
        LexerReader reader = lexer.filterReader(true, true);


        log.info(reader.scan(article).toSentence().toList().toString());
//[需要/v, 添加/v, 一个/mq, spring boot start/nx, 版本/n, 推荐/v, 服务包/n]

第二种方法是使用他给的配置文件merge_stopwords.txt,用于添加默认停止词,和删除默认停止词

+ 需要
- 的

此时使用带停止词的简单解析

        String article = "我们需要添加一个spring boot start版本的推荐服务包。";
        Lexer lexer = Mynlp.instance().lexerBuilder().hmm().withPos().build();
        LexerReader reader = lexer.filterReader(true, true);
        log.info(reader.scan(article).toSentence().toList().toString());
//[添加/v, 一个/mq, spring/x, boot/x, start/x, 版本/n, 的/u, 推荐/v, 服务/v, 包/q]
//此时“需要”非停止词会被停止,“的”停止词会被解析 
关键字提取
        MemCustomDictionary dictionary = new MemCustomDictionary();
        dictionary.clear();
        dictionary.addWord("spring boot start");
        dictionary.addWord("服务包");
        dictionary.rebuild();

        String article = "我们需要添加一个spring boot start版本的推荐服务包。";

        Lexer lexer = Mynlp.instance().lexerBuilder().hmm().withPos().with(new CustomDictionaryPlugin(dictionary)).build();
        LexerReader reader = lexer.filterReader(true, true);
        KeywordSummary keywordSummary = new KeywordSummary(reader);
        log.info(keywordSummary.keyword(article, 3).toString());
//[一个, spring boot start, 版本]

结论

总体来说,做个简单的词云或者词性分析统计等等,我觉着这个库是完全够用了,目前处理和我的需求重合度不高以及文档太少之外其实在易用性上来说还是比较好的java分词方案

jcseg

jcseg简述

官方地址,其实jcseg的文档比较清晰,而且基础功能的说明也比较详细,而且最重要的一点他的【检测模式】可以直接拿到只有指定词库词语的分词。这里说一下很多分词方案都有关键词提取,但是基本就是把词云里面的词从大到小排列,对于我们这种【提取关键词】其实关键程度不在于重复次数,而是在于是否位于以训练完成的词库当中就没有太大的用处

jcseg使用

引包

     <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>jcseg-analyzer</artifactId>
            <version>2.6.3</version>
        </dependency>

具体使用方法官网二次开发文档已经给的很清楚了,我这里就不画蛇添足了,只展示下我需求(检测模式)的使用

首先看下源码的lexicon示例部分,这里有很多分词规则的示例,可以相对快速了解词法分析器的规则,然后我们写一个自己的分词库在resources下

#resouces/lexicons/lex-extend-en.lex
CJK_WORD
spring boot/n/null/null/
spring cloud/null/null/null/
spring boot start/null/null/null/

根据默认配置文件默认的最大匹配长度是7,jcseg.maxlen = 7,我们由于包含英文所以不接受他的建议所以需要修改,根据文档中提示修改步骤

* 寻找jcseg-core-{version}.jar目录下的jcseg.properties
* 如果没找到继续寻找classpath下的jcseg.properties(默认已经打包了)
* 如果没找到继续寻找user home下的jcseg.properties(除非把classpath下的jcseg.properties删除了,要不然不会到这)

我们这里将jcseg.properties拷贝到resouces目录下进行修改,此时可以使用

 SegmenterConfig config = new SegmenterConfig(true);

的方式初始化配置,修改配置的jcseg.maxlen

jcseg.maxlen = 12

配置读取后,需要读入自定义词库,这里有两种方案

  • 第一种,如果你确定不要他的词库,那么直接在jcseg.properties中将lexicon.path配置为你的文件夹地址

    lexicon.path = src/main/resources/lexicons
    

    然后再走默认配置即可

    ADictionary dic = DictionaryFactory.createSingletonDictionary(config);
    
  • 第二种,如果你可能需要到他的配置,那么需要忽略默认配置然后加载自己的配置

            ADictionary dic = DictionaryFactory.createSingletonDictionary(config, false);
            dic.loadDirectory("src/main/resources/lexicons");
    

然后再根据文章/句子删选指定关键字即可,下面放上简易代码

private final static String LEX_PATH = "src/main/resources/lexicons";

public static Set<String> getKeywordSet(String str) {
        Set<String> keywordSet = new HashSet<>();
        try {
            SegmenterConfig config = new SegmenterConfig(true);
            ADictionary dic = DictionaryFactory.createSingletonDictionary(config, false);
            dic.loadDirectory(LEX_PATH);
            ISegment seg = ISegment.DETECT.factory.create(config, dic);
            seg.reset(new StringReader(str));
            IWord word = null;
            while ((word = seg.next()) != null) {
                keywordSet.add(word.getValue());
            }
        } catch (IOException ex) {
            ex.printStackTrace();
            throw new CaskKotomiException("read parse chinese config fail");
        }
        return keywordSet;
    }

最后

其实优秀的中文分词方案还是比较多的,对于没有特别的场景需求的方案其实基本大同小异,可以参考前言中的测试方案选择适合自己的项目的实现方式。总体来说我认为mynlp更加易用,但是文档和功能较少,jcseg文档充足而且功能性较多,但相对来说没有那么易用

原文地址

原文链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值