简介:这是一份面向Java开发者的中文分词工具包,将Python版jieba分词的核心能力移植到Java环境,适用于文本挖掘、搜索引擎索引构建、词频统计等场景。压缩包共30个文件,包含10个Java源文件、11个编译后的class文件,以及词典txt、Eclipse工程配置(project/classpath/xml/prefs)等,整体大小约4.17MB,项目结构完整,可直接导入IDE使用。已有2210人学习下载。包内自带精确模式、全模式、搜索引擎模式三种分词策略,主入口位于run包下的test程序,运行即可观察分词效果;同时保留词典文件dict.txt与prob_emit.txt,便于按需扩展或更新词库。开发者可参考源码了解jieba基于概率模型的词典切分与Viterbi算法实现,稍作修改即可集成到搜索、推荐、舆情分析等业务中。
1. jieba分词 Java 版:中文搜索、舆情、标签系统落地前,先过这一关
做搜索联想词、舆情词频统计、合同条款拆分,这些需求的第一步几乎都是同一件事:把一段中文切成有意义的词。jieba分词 Java 版把 Python 生态里最经典的那套词法方案原样搬到了 JVM,内置几十万条词典,用 DAG 加最大概率路径选出最优切分,再用 HMM 兜底识别新人名和新词。它不重、不依赖外部服务,引入一个 jar 包就能跑,SEARCH 和 INDEX 两种模式分别对应展示和索引两个场景。适合中小型系统快速接入,也适合不想为分词引入全套 NLP 框架的团队。下面按原理、接入、特征、避坑、封装这条线,把它一次讲透。
2. 先立原理再动手:前缀树、DAG、HMM 在分词里各干多少活
2.1 词典加载与 trie 树:分词准不准,词库占七成
jieba分词 Java 版的根基是一份 UTF-8 编码的词典文件,每一行固定三列:词语、词频、词性。内置词典大致长这样:
$ head -3 jieba.dict.utf8 的 3188252 uj 了 883634 ul 我 281135 rr三列分别是词本身、语料库统计出来的出现次数、以及北大词性标注集里的词性代码。uj是助词,ul是时态助词,rr是人称代词。词频不是摆设,后面 DAG 做最大概率路径全靠它排序。
启动时,WordDictionary 会把这份词典读进一棵 trie 树。每个字是树上的一个节点,词条末尾节点保存词频和词性,匹配一个词的时间复杂度只跟词长有关,和词典总词条数无关。这也是为什么它加载要等几秒,一旦加载完,单次分词非常快。
我自己在服务里观察过,默认词典加载完成后,JVM 堆里光词典结构就要占掉一百多兆。如果你还要追加几万条领域词,内存占用会继续往上走。这一点提前评估,别等上线了再调堆。
2.2 DAG 与最大概率路径:分词结果不是拍脑袋选出来的
分词器拿到一句话时,会从每个字的位置出发,去 trie 树里查所有可能的词,把这些候选词连成一张有向无环图。以"南京市长江大桥"为例:
- 词典命中的候选词包括:南京、南京市、京市、长江、长江大桥、大桥;
- 图上存在多条从起点到终点的路径;
- 每条路径的总概率由路径上所有词的词频取对数后累加得到;
- 最后用动态规划找出全局最大概率路径,输出这条路径上的词。
精确模式最终输出"南京市 / 长江大桥",全模式则会把所有候选词都倒出来。Java 版的 SEARCH 模式接近精确模式,INDEX 模式则会在 SEARCH 结果基础上,对长词再做一次全切分,得到"南京 / 南京市 / 京市 / 长江 / 长江大桥 / 大桥"这类候选碎片。索引场景需要碎片化召回,所以用 INDEX;给人看的结果用 SEARCH。
很多刚入门的同学会误以为分词就是把词典里的词拿去匹配一遍,其实匹配只是第一步,真正决定输出的是概率路径计算。这也是为什么往自定义词典里加词时,词频这一列不能随便填——它直接影响最优路径选择。
2.3 HMM 兜底:没见过的新词,靠猜也要猜出来
词典再大也覆盖不了所有人名、地名和网络新词。jieba 的做法是引入一个隐马尔可夫模型来兜底:对于词典未登录的字串,分词器把它们看成 B、E、M、S 四种状态,分别代表词首、词尾、词中、单字,然后用 viterbi 算法解码出最可能的状态序列,状态连续的串就被拼成一个新词。
这个机制让分词器具备了一定的"猜词"能力,比如"躺平""内卷"这类词刚流行、还没进词典时,HMM 有一定概率切对。代价是结果不稳定:同一个词在不同上下文里可能一会儿被切出来,一会儿被拆开。Java 版没有像 Python 版那样把 HMM 开关直接暴露成参数,你控制它的方式只能是让词在词典里先命中。想保证某个词必不被拆,就把它写进自定义词典并给足词频。这也是后面避坑章节反复强调的一句话:词典优先,HMM 只是兜底。
3. Maven 接入到第一行输出:SEARCH 与 INDEX 到底差在哪
3.1 依赖坐标与版本选择
Java 生态接一个库,第一步永远是 Maven 坐标。jieba分词 Java 版在中央仓库里的坐标是稳定存在的,版本虽然停在 1.0.2 很久没更新,但胜在足够稳定,生产环境大量项目都在用:
<dependency> <groupId>com.huaban</groupId> <artifactId>jieba-analysis</artifactId> <version>1.0.2</version> </dependency>引入后就能直接用 JiebaSegmenter 和 WordDictionary。选型时要注意,这个库的更新节奏很慢,不要指望它持续跟进新词和算法升级。如果你的项目对分词准确率要求极高,可以对比 HanLP 那一类更重的方案;如果只是想快速给搜索或标签系统补上中文分词能力,它足够胜任。
3.2 第一段代码:把一次完整分词跑通
新建一个类,直接写一个 main 方法就能看到效果:
import com.huaban.analysis.jieba.JiebaSegmenter; import com.huaban.analysis.jieba.SegToken; public class FirstDemo { public static void main(String[] args) { JiebaSegmenter segmenter = new JiebaSegmenter(); String text = "我爱自然语言处理,也爱用jieba做中文分词"; for (SegToken token : segmenter.process(text, JiebaSegmenter.SegMode.SEARCH)) { System.out.println(token.word + "\t" + token.startOffset + "-" + token.endOffset); } } }JiebaSegmenter 是入口类,process 接收两个参数:待分词的文本、分词模式。SegToken 里除了 word,还带 startOffset 和 endOffset,表示这个词在原句中的起止位置。这个偏移量很实用,做高亮、做实体回标都要靠它。SEARCH 模式输出贴近人读习惯的完整词,适合直接展示。
再看 INDEX 模式,把 SegMode 换成 INDEX:
for (SegToken token : segmenter.process(text, JiebaSegmenter.SegMode.INDEX)) { System.out.println(token.word); }同一个句子里,"自然语言处理"这个词在 SEARCH 模式下作为一个整体输出,在 INDEX 模式下还会额外给出"自然 / 语言 / 处理"这些碎片。搜索引擎建索引时靠这些碎片提高召回率,但字段存储量和索引体积都会变大。所以我的习惯是:展示层用 SEARCH,索引层用 INDEX,两个模式各司其职。
3.3 自定义词典:把领域词塞进分词器
通用词典永远跟不上业务词汇。你做的系统里如果有产品名、人名、地名,必须建一份自定义词典。加载方式如下:
import com.huaban.analysis.jieba.WordDictionary; import java.io.File; WordDictionary.getInstance().init(new File("dict/user.dict").getAbsolutePath());init 方法接收的是文件路径,推荐用绝对路径,避免工作目录变化导致找不到文件。这个方法必须在任何 process 调用之前执行,否则词典没装进去,效果等于没配。自定义词条格式和内置词典完全一致,每行三列:
某跨平台系统 10 nz 结巴分词 20 n词频这一列是玄学,给太高会让这个词在任何上下文里都强行走同一条路径,给太低又可能被 HMM 拆掉。我一般先给 10 到 50,跑一轮回归语料再微调。第三列词性不填也能加载,但后续做词性标注时会拿到 null,后面避坑章节细说。
3.4 停用词过滤:分词后第一时间清场
分词结果里往往夹杂大量"的、了、是、在"这类功能词,直接做词频统计会被它们刷屏。通常的接法是分词之后立刻接一层停用词过滤:
import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; Set<String> stopWords = new HashSet<>(Arrays.asList("的", "了", "是", "在", "和", "也")); List<String> words = segmenter.process(text, JiebaSegmenter.SegMode.SEARCH) .stream() .map(t -> t.word) .filter(w -> !stopWords.contains(w)) .collect(Collectors.toList());停用词表不要硬编码在业务代码里,建议放到独立配置文件中,不同场景差异很大:舆情场景要保留"举报、投诉"这类词,搜索场景可能需要把语气词全部干掉。网上有现成的中文停用词表,拿下来按业务增删就行。
4. 词性标注与关键词提取:让分词结果变成能上线的特征
4.1 PosTagger 词性标注:一句话拿回词性序列
分词只是拿到词,标签系统、关键词提取还想要词性。jieba分词 Java 版提供了 PosTagger,内部先分词再标注词性,所以不需要重复调用 process:
import com.huaban.analysis.jieba.pos.PosTagger; import com.huaban.analysis.jieba.pos.Token; PosTagger posTagger = new PosTagger(); List<Token> tokens = posTagger.tag("今天天气不错,适合出去跑步"); for (Token token : tokens) { System.out.println(token.getWord() + "\t" + token.getNature()); }输出大致是:
今天 t 天气 n 不错 a 适合 v 出去 v 跑步 v词性代码遵循北大词性标注集:n 是名词,v 是动词,a 是形容词,t 是时间词,nr 是人名,ns 是地名,nt 是机构名,nz 是其他专名。看到 n 开头的词性,基本都是名词家族。
PosTagger 实例可以复用,构造开销不小,别在每次请求里 new 一个。它标注的准确率依赖词典里的词性列,如果你自定义词典第三列是空的,这部分词就会返回 null,后面做过滤时要判空。
4.2 用词性做标签抽取:只留名词和动词
内容标签系统一般只关心名词和动词,形容词和语气词贡献很弱。过滤逻辑可以直接用词性前缀:
List<String> tags = tokens.stream() .filter(t -> t.getNature() != null && t.getNature().startsWith("n")) .map(Token::getWord) .collect(Collectors.toList());用 startsWith("n") 而不是 equals("n"),是为了把 nr、ns、nt、nz 全部带进来。比如"张伟"标注为 nr,如果只判断 equals("n") 就会漏掉。动词要参与的话,把条件改成 n 或者 v 前缀。
这里有个实际经验:标签抽取时不要光看词性,要结合词频和词长。单字名词如"人、事、物"做标签太宽泛,一般过滤掉。过滤词长小于 2 的词,标签质量会明显提升。
4.3 简易关键词:词频统计加词性权重,取 TopN
Python 版 jieba 自带关键词提取函数,Java 版没有现成 API。自己实现也不复杂,核心思路是:名词动词加分,高频词加分,最后排序取 TopN:
Map<String, Integer> freq = new HashMap<>(); for (Token token : tokens) { String nature = token.getNature(); if (nature != null && (nature.startsWith("n") || nature.startsWith("v"))) { freq.merge(token.getWord(), 1, Integer::sum); } } List<Map.Entry<String, Integer>> top = new ArrayList<>(freq.entrySet()); top.sort((a, b) -> b.getValue() - a.getValue()); for (int i = 0; i < Math.min(10, top.size()); i++) { System.out.println(top.get(i).getKey() + "\t" + top.get(i).getValue()); }这套逻辑适合单条短文本,比如文章的标题、商品名称。对长文档或者一批文档做关键词,词频就不够用了,得上 TF-IDF,核心差别在于 IDF 部分要让"在很多文档里都出现的词"降权。我在实际项目里是先把 jieba 的分词结果落成词频表,再交给下游的 TF-IDF 任务去跑,分词器只负责把词切对、把词性标对,特征计算另做。这样职责单一,出问题也好排查。
5. 避坑指南:自定义词典失效、OOM、切分翻车,一次说清
5.1 自定义词典加了却不生效:先查 UTF-8 编码和词频
现象:user.dict 里明明写了"躺平",分词结果还是输出"躺 / 平"。
原因通常是三选一:文件不是 UTF-8 编码,Windows 下保存时还带了 BOM,Java 读到词首乱码导致匹配失败;词频列填了 1,最大概率路径算下来不选它;init 方法在 process 之后才执行,词典实际没加载进去。
解决:文件统一转成 UTF-8 无 BOM 格式,用 head 或 Notepad++ 确认编码;词频从 10 起步;在应用启动类里用静态代码块先调用 init,确保任何请求进来之前词典已就位。特别提醒,改了 user.dict 后要重启进程,词典不会自动热加载。
5.2 人名被切成单字:把常用姓名表写进词典
现象:"张伟说"分词输出"张 / 伟 / 说",标签系统里人名直接丢了。
原因:内置词典没有收录"张伟",HMM 把每个字都当成了独立单字。HMM 是兜底方案,能猜对一部分新词,但不是稳定的人名词库。
解决:自己维护一份业务相关的姓名词表,词性写成 nr,词频 15 左右。词性列一定要写 nr,否则后面按人名词性过滤时照样漏。线上系统的人名词表应该跟着用户注册数据走,新用户进来就同步更新词典,而不是靠内置词库猜。
5.3 加载即 OOM:词典在内存里很占地方,堆给太小必翻车
现象:Spring Boot 启动时,WordDictionary 初始化阶段直接抛 java.lang.OutOfMemoryError,或者频繁 Full GC,接口响应从毫秒级变成秒级。
原因:内置词典加 trie 树节点加词性词典同时常驻内存,分词时还要为 DAG 分配临时对象。默认堆内存只有 256M,非常容易不够用。
解决:启动参数把堆调到至少 512M,容器部署时留足余量:
java -Xms512m -Xmx1g -jar your-service.jar-Xms 是初始堆大小,-Xmx 是最大堆大小。我一般把两个值设成一样,避免运行期堆伸缩带来的停顿。容器环境记得把内存 limit 留到 1.5G 到 2G,不然 JVM 和操作系统抢内存,大概率被 kill。
5.4 INDEX 模式当精确模式用:索引体积直接翻倍
现象:同一个标题,SEARCH 模式切出 40 个词,INDEX 模式切出 80 个词,搜索引擎索引体积涨了 60%。
原因:INDEX 模式会对长度超过阈值的长词做二次全切分,设计目标就是产出尽可能多的候选路径,提高召回率。它不是"更精确"的模式,反而更碎。
解决:展示场景用 SEARCH,索引字段用 INDEX。如果索引系统只允许存一个分词字段,优先选 SEARCH,查询时用短语匹配兜底。把 INDEX 的输出直接展示给用户,阅读体验会很差。
5.5 自定义词词性标注返回 null:第三列没填或填错
现象:调用 PosTagger.tag 之后,业务代码遍历 Token 取词性,某个词打印出来是 null,直接按词性过滤时还容易触发空指针。
原因:自定义词典里那行词只有两列,词性列缺失;或者填了非标准词性代码,标注模块不认识。
解决:自定义词条严格写满三列:
某跨平台系统 10 nz 结巴分词 20 n词性代码按北大词性标注集来填,拿不准的统一填 nz。下游代码里对 nature 做判断前先判空,避免一条脏数据拖垮整个标签流程。我后来在加载自定义词典的代码里加了一段校验逻辑,遍历词条检查列数和词性合法性,格式不对直接启动失败,绝不带病上线。
6. 进阶:把分词器封装成可并发调用的服务,并留好后路
6.1 单例复用与并发安全
JiebaSegmenter 本身不持有每请求的状态,词典加载完成后,多个线程共享同一个实例是安全且高效的。实际项目里我通常这样封装:
public class SegmentService { private static final JiebaSegmenter SEGMENTER = new JiebaSegmenter(); public List<String> cut(String text) { return SEGMENTER.process(text, JiebaSegmenter.SegMode.SEARCH) .stream() .map(t -> t.word) .collect(Collectors.toList()); } }静态单例保证全进程只有一份分词器,不重复加载词典。真正要注意的是词典热更新场景,WordDictionary 是全局单例,更新期间并发请求可能读到半初始化状态,所以更新操作要做好互斥,或者干脆用 volatile 持有分词器实例,更新时整体替换。
6.2 词库热更新:给词典留一颗后悔药
自定义词典不可能一次写对,线上跑一段时间必然要加词。常见做法是定时检查词典文件的修改时间,变了就重新 init:
public class DictReloader { private volatile long lastModified; public void reloadIfNeeded(File dictFile) { long modified = dictFile.lastModified(); if (modified != lastModified) { WordDictionary.getInstance().init(dictFile.getAbsolutePath()); lastModified = modified; } } }reload 完成后,我会强制跑一遍回归用例:旧词必须还在,新词必须切出来,关键业务句子的分词结果不能出现明显倒退。这件事后来成了我每次上线分词相关改动前的固定动作,顺序是:查编码、查词频、查堆内存、跑回归。这套流程救了我好几次,希望帮到你。
本文还有配套的精品资源,点击获取