Nodejs 中文分詞常用模塊用法分析
???
???
???????????????(生活只有在平淡無味的人看來才是空虛而平淡無味的。 —— 車爾尼雪夫斯基)
???
???
????????????????
中文分詞器
引用百度的說明 ~~
中文分詞就是將連續(xù)的字序列按照一定的規(guī)范重新組合成詞序列的過程。我們知道,在英文的行文中,單詞之間是以空格作為自然分界符的,而中文只是字、句和段能通過明顯的分界符來簡單劃界,唯獨詞沒有一個形式上的分界符,雖然英文也同樣存在短語的劃分問題,不過在詞這一層上,中文比之英文要復(fù)雜得多、困難得多
- 與英文為代表的拉丁語系語言相比,英文以空格作為天然的分隔符,而中文由于繼承自古代漢語的傳統(tǒng),詞語之間沒有分隔。 古代漢語中除了連綿詞和人名地名等,詞通常就是單個漢字,所以當(dāng)時沒有分詞書寫的必要。而現(xiàn)代漢語中雙字或多字詞居多,一個字不再等同于一個詞。
- 在中文里,“詞”和“詞組”邊界模糊
現(xiàn)代漢語的基本表達單元雖然為“詞”,且以雙字或者多字詞居多,但由于人們認識水平的不同,對詞和短語的邊界很難去區(qū)分。
例如:“對隨地吐痰者給予處罰”,“隨地吐痰者”本身是一個詞還是一個短語,不同的人會有不同的標準,同樣的“海上”“酒廠”等等,即使是同一個人也可能做出不同判斷,如果漢語真的要分詞書寫,必然會出現(xiàn)混亂,難度很大。
中文分詞的方法其實不局限于中文應(yīng)用,也被應(yīng)用到英文處理,如手寫識別,單詞之間的空格就不很清楚,中文分詞方法可以幫助判別英文單詞的邊界
NodeJS中的中文分詞器
mmseg-node
https://github.com/zzdhidden/mmseg-node
一個基于 libmmseg 的 NodeJS 驅(qū)動
安裝流程
sudo apt-get install make gcc g++ automake libtool wget http://www.coreseek.cn/uploads/csft/3.2/mmseg-3.2.14.tar.gz sudo tar zxvf mmseg-3.2.14.tar.gz cd mmseg-3.2.14 ./bootstrap ./configure make && make install npm install mmseg
示例代碼
const mmseg = require("mmseg");
const q = mmseg.open('/usr/local/etc/');
console.log(q.segmentSync("我是中文分詞"));從該包的下載量和issue的解決程度來看,不推薦使用,很容易碰到問題,而沒有得到有效的解決
nseg
https://github.com/mountain/nseg
Chih-Hao Tsai發(fā)明的MMSG,一種非常流行的中文分詞算法。許多實現(xiàn)可在不同的平臺上使用,包括Python,Java等
安裝流程
npm install nseg
示例代碼
const dict = require('../data/dict'),
freq = require('../data/freq'),
date = require('../lex/datetime'),
sina = require('../lex/sina');
const opts = {
dict: dict,
freq: freq,
lexers: [date, sina],
};
const nseg = require('nseg').normal(opts);
nseg('研究生源計劃', (result) => {
console.log(result);
});同mmseg一樣,雖然在其他環(huán)境(比如Java,Python)有著較為成熟的應(yīng)用和場景,但包的維護者重心可能不在nodejs上,所以還是不太建議使用…
node-segment
https://github.com/leizongmin/node-segment
以盤古分詞組件中的詞庫為基礎(chǔ), 算法設(shè)計也部分參考了盤古分詞組件中的算法
具有以下特點
- 純JavaScript編寫,可以在任何支持ECMAScript5的引擎上執(zhí)行(需要稍微修改部分代碼)
- 基于詞性進行聯(lián)想識別
- 可使用JavaScript編寫自定義的分詞模塊
安裝流程
npm install segment --save
示例代碼
const Segment = require('segment');
const segment = new Segment();
segment.useDefault(); // 載入默認詞典
segment.loadDict('test.text'); // 載入字典,詳見dicts目錄,或者是自定義字典文件的絕對路徑
const text = "歡迎來到CSDN開發(fā)者博客論壇~~";
console.log(segment.doSegment(text, {
stripPunctuation: true //去除標點符號
}));
定制化示例文件 .text
0x00100000代表16進制的詞性,但在返回時轉(zhuǎn)換為了十進制來標識
- 名詞 十進制:1048576 十六進制:0x00100000
- 動詞 十進制:4096 十六進制:0x00001000
- 機構(gòu)團體 十進制:20 十六進制:0x00000020
- 時間 十進制:16384 十六進制:0x00004000
- 人名 十進制:128 十六進制:0x00000080
- 標點符號 十進制:2048 十六進制:0x00000800
- …
101代表了這個詞語的權(quán)重,在后續(xù)匹配詞語時,若匹配到近似詞,則以權(quán)重優(yōu)先匹配
架構(gòu)師|0x00100000|101 程序員|0x00100000|101 運維工程師|0x00100000|101
該包使用純JavaScript編寫,github上的star量有1k,但通過查看issue,該包的維護著似乎已經(jīng)不太關(guān)心這個包了。但如果對性能要求沒那么高,且多關(guān)注內(nèi)存,使用得當(dāng)?shù)脑?,還是可以考慮的
nodejieba
https://github.com/yanyiwu/nodejieba
NodeJieba是"結(jié)巴(jieba)"中文分詞的 Node.js 版本實現(xiàn), 由CppJieba提供底層分詞算法實現(xiàn), 是兼具高性能和易用性兩者的 Node.js 中文分詞組件
具有以下特點
- 詞典載入方式靈活,無需配置詞典路徑也可使用,需要定制自己的詞典路徑時也可靈活定制。
- 底層算法實現(xiàn)是C++,性能高效。
- 支持多種分詞算法,各種分詞算法見CppJieba的README.md介紹。
- 支持動態(tài)補充詞庫。
安裝流程
npm install nodejieba
但在安裝該包的時候可能會遇到npm下載權(quán)限的問題,因為底層使用了CppJieba,當(dāng)使用sudo npm安裝時會提示權(quán)限不足

github issue問題
npm 官方解釋
解決辦法
- 單獨下載該包文件
sudo npm install nodejieba --unsafe-perm- 將該包緩存至全局
sudo npm install nodejieba -g --unsafe-perm- 配置npm
sudo su -
npm config set unsafe-perm- 切換至root身份運行
sudo su -
npm i
示例代碼
jieba詞性
- 獲取文本分詞標注結(jié)果
- n 名詞
- v 動詞
- nt 機構(gòu)名稱
- t 時間
- nr 人名
…
示例自定義文件 .utf-8
客戶模板 n 客戶狀態(tài) n 客戶手機號 n 自定義 n 自定義字段 n 云通訊 n
const nodejieba = require("nodejieba");
jieba.load({ userDict: `self.utf-8`}); // 自定義詞典
const text = '紅掌撥清波';
const tags = ['n', 'v', 'nt', 'nr', 't'];
const docuemnt = jieba.tag(text).filter((v) => tags.includes(v.tag)).map((v) => v.word);
console.log(docuemnt);
目前NodeJS中性能最強的分詞器,且github上star量達到2k+,但開發(fā)者可能會遇到安裝的問題,因為底層依賴了python g++,如果版本過低或者過高,在安裝時會比較棘手。但如果能夠?qū)C器環(huán)境配置好的話,還是推薦使用該工具的,畢竟性能很強~~
性能對比
起初在使用nodejieba時,遇到了很多安裝的問題,比如npm權(quán)限,g++版本不匹配等等,期間也嘗試了node-segment,但因為服務(wù)對性能的要求較高,所以還是選擇克服機器配置的問題,使用了nodejieba

node-sgement和nodejieba的性能對比以及代碼分析
示例代碼
'use strict';
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const Segment = require('segment');
const segment = new Segment();
const nodejieba = require('nodejieba');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
segment.useDefault();
segment.loadDict('test.text');
app.post('/', function (req, res, next) {
const text = req.body.text;
const type = req.body.type;
const num = req.body.num;
let str = '';
let array = [];
for (let index = 0; index < num; index++) {
str += text;
}
const tags = ['n', 'v', 'nt', 'nr', 't'];
if (type === 'nodejieba') {
array = nodejieba.tag(str).filter((v) => tags.includes(v.tag)).map((v) => v.word)
} else {
array = segment.doSegment(str).map((v) => v.w);
}
console.log(array);
res.send(array);
});
app.listen(3000);示例請求文件 .text
{
"text": "生活只有在平淡無味的人看來才是空虛而平淡無味的。",
"type": "segment",
"num": 1
}
- 使用apach bench進行測試
測試方法:ab -n 1000 -p post.txt -c 200 -T application/json localhost:3000
- -n 請求總數(shù)
- -p post請求文件
- -c 并發(fā)量
- -T 請求格式
…
- 請求返回參數(shù)說明
- Document Path: /phpinfo.php #測試的頁面
- Document Length: 50797 bytes #頁面大小
- Concurrency Level: 200 #測試的并發(fā)數(shù)
- Time taken for tests: 11.846 seconds #整個測試持續(xù)的時間
- Complete requests: 1000 #完成的請求數(shù)量
- Failed requests: 0 #失敗的請求數(shù)量
- Write errors: 0
- Total transferred: 204586997 bytes #整個過程中的網(wǎng)絡(luò)傳輸量
- HTML transferred: 203479961 bytes #整個過程中的HTML內(nèi)容傳輸量
- Requests per second: 337.67 [#/sec] (mean) #最重要的指標之一,每秒處理請求數(shù)
- Time per request: 2961.449 [ms] (mean) #最重要的指標之二,平均等待時長
- Time per request: 2.961 [ms] (mean, across all concurrent requests) #每個連接請求實際運行時間的平均值
- Transfer rate: 16866.07 [Kbytes/sec] received #平均每秒網(wǎng)絡(luò)上的流量,可以幫助排除是否存在網(wǎng)絡(luò)流量過大導(dǎo)致響應(yīng)時間延長的問題
- …
- 測試版本
- node v12
- segment v0.1.3
- nodejieba v2.4.1
- 測試結(jié)果
| 分詞類型 | 字符長度 | 請求總數(shù) | 并發(fā)條數(shù) | 平均每秒處理 | 平均等待時長(ms) | 是否包含分隔符(標點符號等) |
|---|---|---|---|---|---|---|
| segment | 60 | 1000 | 20 | 129.47 | 154.474 | 是 |
| nodejieba | 60 | 1000 | 20 | 2662.83 | 7.511 | 是 |
| segment | 60 | 1000 | 50 | 137.51 | 363.598 | 是 |
| nodejieba | 60 | 1000 | 50 | 2660.41 | 17.222 | 是 |
| segment | 60 | 1000 | 100 | 134.56 | 753.688 | 是 |
| nodejieba | 60 | 1000 | 100 | 2774.89 | 36.038 | 是 |
| segment | 60 | 1000 | 200 | 134.05 | 1491.937 | 是 |
| nodejieba | 60 | 1000 | 200 | 2846.30 | 70.267 | 是 |
| ---- | ---- | — | – | – | – | – |
| segment | 180 | 1000 | 200 | 67.40 | 2967.466 | 是 |
| nodejieba | 180 | 1000 | 200 | 2390.53 | 83.664 | 是 |
| segment | 360 | 1000 | 200 | 42.55 | 4700.340 | 是 |
| nodejieba | 360 | 1000 | 200 | 1801.97 | 110.989 | 是 |
| ---- | ---- | — | – | – | – | – |
| segment | 60 | 1000 | 200 | 88.26 | 2265.952 | 否 |
| nodejieba | 60 | 1000 | 200 | 2860.95 | 69.907 | 否 |
| segment | 60 | 200 | 50 | 101.18 | 494.188 | 否 |
| nodejieba | 60 | 200 | 50 | 2688.39 | 18.598 | 否 |
| segment | 120 | 200 | 50 | 1.73 | 28933.129 | 否 |
| nodejieba | 120 | 200 | 50 | 1551.11 | 32.235 | 否 |
| segment | 360 | 200 | 50 | error(棧溢出) | error(棧溢出) | 否 |
| nodejieba | 360 | 200 | 50 | 1117.46 | 44.744 | 否 |
- 測試結(jié)果點評
- 從測試結(jié)果來看,nodejieba是node-sgement的20-30倍,這得益于nodejieba依賴的CppJieba,而node-sgement使用的純JS編寫,所以性能要遜色很多
- nodejieba和node-sgement在處理分詞的請求都比較穩(wěn)定,但面對長文本,即便性能很強的nodejieba依然會有性能瓶頸問題
- 面對不包含標點符號的文本,nodejieba和node-sgement性能都會有所影響
- node-sgement在對長文本(360個無標點符號的字符)進行分詞時會導(dǎo)致棧溢出
- 測試過程說明
查看node-sgement源碼發(fā)現(xiàn) 作者建議不要對較長且無任何標點符號的文本進行分詞的是發(fā)生在字典分詞階段
因為分詞是通過空格換行,通配符,特殊字符進行分離處理的(正是因為有了分離,所以每次分詞的文本不會很長)
而segment處理一串字符是通過循環(huán)每一個字符進行遞增匹配的,從而導(dǎo)致匹配的的次數(shù)是成倍的增長
…一開始想問原作者沒有特殊符號的長文導(dǎo)致時間倍增的具體原因,但是他可能很久沒有維護這個包了,就沒有提供有用的信息,然后只能看源碼去了解了

- node-segment 分詞邏輯
- 例子 你好我是七陌,循環(huán)次數(shù)是6+5+4+3+2+1 = (1+n)*n/2
- 第一輪對比
你
你好
你好我
…- 第二輪對比
好
好我
好我是
好我是七
…
一共分為12個分詞模塊
- 6個分詞模塊
url識別
通配符識別
標點符號
字母/數(shù)字識別
字典識別- 6個優(yōu)化模塊
中文姓
人名
中文人名
日期時間
詞典
郵箱地址
- 整體執(zhí)行邏輯
每次分詞會把紅框中的所有模塊文件執(zhí)行一遍
- 將(空格/換行)進行分組,用于后續(xù)循環(huán)匹配字符和詞性,然后進行六個階段的分詞計算
- url識別 地址匹配,https/http… 如果命中則進行轉(zhuǎn)換,并返回數(shù)據(jù)和詞性,每次從當(dāng)前下標開始截取協(xié)議名稱字符數(shù),如果是http則是7個。將截取的字符進行對比,直到循環(huán)非url字符時結(jié)束。此時將這段字符的前后分成三段返回
- 通配符識別 通過循環(huán)遍歷將命中的通配符返回,并在上層匹配通配符詞性
- 標點符號 和通配符識別邏輯相同
- 字母/數(shù)字識別 將匹配的英文和數(shù)字字符使用ASCII轉(zhuǎn)換,如果命中ASCII區(qū)間,則將區(qū)間對應(yīng)的16進制轉(zhuǎn)換為10進制作為詞性并分離返回,分離后的結(jié)構(gòu)和請求協(xié)議的結(jié)構(gòu)相同
- 字典識別 將字符串的字符循環(huán)和每一個字典的內(nèi)的字符進行包含性對比
第一次先從0下標開始查找,如果未命中則字符串截取數(shù)++,如果都沒有命中就將對比下標向前進一位,其中重復(fù)命中的會經(jīng)過一層優(yōu)化(去重打分),留下詞頻率最高,未識別詞最少的等等- 中文人名識別
- 人名優(yōu)化
…
總結(jié)
當(dāng)然是推薦使用nodejieba了,畢竟是目前nodejs性能最強的分詞,但對機器環(huán)境依賴較為苛刻,所以建議使用docker將分詞服務(wù)單獨部署,進行環(huán)境依賴的隔離。
但如果更改機器配置對其他服務(wù)影響較大,且服務(wù)對性能要求沒那么高的話,可以考慮使用node-segment,然后加一些避免棧溢出的代碼,效果還是不錯的~~
PS:筆者比較喜歡實用nodejieba分詞工具,但是針對某些領(lǐng)域(如:區(qū)塊鏈相關(guān))的專業(yè)術(shù)語會出現(xiàn)分詞錯誤的情況,這個時候就需要去尋找相關(guān)的詞庫并手動加載進來使用。
相關(guān)文章
詳解如何在Node.js中執(zhí)行CPU密集型任務(wù)
Node.js通常被認為不適合CPU密集型應(yīng)用程序,Node.js的工作原理使其在處理I/O密集型任務(wù)時大放異彩,雖然執(zhí)行CPU密集型任務(wù)肯定不是Node的主要使用場景,但是我們依舊有方法來改善這些問題,本文詳細給大家介紹了如何在Node.js中執(zhí)行CPU密集型任務(wù)2023-12-12
用nodejs實現(xiàn)json和jsonp服務(wù)的方法
本篇文章主要介紹了用nodejs實現(xiàn)json和jsonp服務(wù)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
express.js如何做mysql注入與node-mysql中防止SQL注入方法解析
這篇文章主要介紹了express.js如何做mysql注入與node-mysql中防止SQL注入方法,結(jié)合實例形式分析了express框架使用mysql數(shù)據(jù)庫過程中SQL注入的原理與防范技巧,需要的朋友可以參考下2023-05-05
nodejs連接mongodb數(shù)據(jù)庫實現(xiàn)增刪改查
本篇文章主要結(jié)合了nodejs操作mongodb數(shù)據(jù)庫實現(xiàn)增刪改查,包括對數(shù)據(jù)庫的增加,刪除,查找和更新,有興趣的可以了解一下。2016-12-12


