nodejieba-plus 3.5.10 → 3.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,7 @@
27
27
  + **支持包含空格的关键词**(如 "Open Claw")。
28
28
  + **支持无空格版本匹配**(如 "OpenClaw" 可匹配 "Open Claw")。
29
29
  + **支持英文大小写不敏感匹配**(如 "open claw"、"OPEN CLAW" 都可匹配 "Open Claw")。
30
+ + **支持批量加载用户词典**(字符串数组、单个字符串、Buffer 格式)。
30
31
 
31
32
  对实现细节感兴趣的请看如下博文:
32
33
 
@@ -188,6 +189,55 @@ console.log(nodejieba.cut("男默女泪"));
188
189
  // ["男默女泪"]
189
190
  ```
190
191
 
192
+ ### 批量加载用户词典(新功能)
193
+
194
+ 支持通过字符串数组、Set、单个字符串或 Buffer 批量加载用户词典:
195
+
196
+ ```js
197
+ var nodejieba = require("nodejieba");
198
+ nodejieba.load();
199
+
200
+ // 方式1:使用字符串数组
201
+ nodejieba.loadUserDict(["云计算", "人工智能 1000 nz", "大数据"]);
202
+
203
+ // 方式2:使用 Set 集合(自动去重)
204
+ const dictSet = new Set();
205
+ dictSet.add("云计算");
206
+ dictSet.add("人工智能 1000 nz");
207
+ dictSet.add("大数据");
208
+ dictSet.add("云计算"); // 重复添加会被自动去重
209
+ nodejieba.loadUserDict(dictSet);
210
+
211
+ // 方式3:使用单个字符串
212
+ nodejieba.loadUserDict("区块链");
213
+
214
+ // 方式4:使用 Buffer
215
+ const dictBuffer = Buffer.from("新词1\n新词2 100 n\n新词3 nz");
216
+ nodejieba.loadUserDict(dictBuffer);
217
+
218
+ // 分词时会识别用户词典中的词
219
+ var result = nodejieba.cut("云计算和大数据是人工智能的基础");
220
+ console.log(result); // ['云计算', '和', '大数据', '是', '人工智能', '的', '基础']
221
+ ```
222
+
223
+ #### 词典条目格式
224
+
225
+ 词典条目支持以下格式:
226
+
227
+ ```
228
+ # 仅关键词
229
+ 云计算
230
+
231
+ # 关键词 + 词频
232
+ 人工智能 1000
233
+
234
+ # 关键词 + 词性标签
235
+ 区块链 nz
236
+
237
+ # 关键词 + 词频 + 词性标签
238
+ 大数据 500 n
239
+ ```
240
+
191
241
  ### 包含空格的关键词(新功能)
192
242
 
193
243
  支持在自定义词典中使用包含空格的关键词,且支持无空格版本匹配和大小写不敏感匹配。
Binary file
package/index.js CHANGED
@@ -73,5 +73,23 @@ wrapWithDictLoad("tag");
73
73
  wrapWithDictLoad("extract");
74
74
  wrapWithDictLoad("textRankExtract");
75
75
  wrapWithDictLoad("insertWord");
76
+ wrapWithDictLoad("loadUserDict");
77
+
78
+ // 保存原始的 loadUserDict 函数
79
+ var _loadUserDict = exports.loadUserDict;
80
+
81
+ // 重写 loadUserDict 以支持 Set 格式
82
+ exports.loadUserDict = function (dict) {
83
+ if (!isDictLoaded) {
84
+ exports.load();
85
+ }
86
+
87
+ // 如果是 Set 对象,转换为数组
88
+ if (dict instanceof Set) {
89
+ dict = Array.from(dict);
90
+ }
91
+
92
+ return _loadUserDict.call(this, dict);
93
+ };
76
94
 
77
95
  module.exports = exports;
package/lib/nodejieba.cpp CHANGED
@@ -6,6 +6,8 @@
6
6
  #include "cppjieba/KeywordExtractor.hpp"
7
7
  #include "cppjieba/TextRankExtractor.hpp"
8
8
 
9
+ #include <sstream>
10
+
9
11
  NodeJieba::NodeJieba(Napi::Env env, Napi::Object exports) {
10
12
  DefineAddon(exports, {
11
13
  InstanceMethod("load", &NodeJieba::load),
@@ -17,7 +19,8 @@ NodeJieba::NodeJieba(Napi::Env env, Napi::Object exports) {
17
19
  InstanceMethod("tag", &NodeJieba::tag),
18
20
  InstanceMethod("extract", &NodeJieba::extract),
19
21
  InstanceMethod("textRankExtract", &NodeJieba::textRankExtract),
20
- InstanceMethod("insertWord", &NodeJieba::insertWord)
22
+ InstanceMethod("insertWord", &NodeJieba::insertWord),
23
+ InstanceMethod("loadUserDict", &NodeJieba::loadUserDict)
21
24
  });
22
25
  }
23
26
 
@@ -216,3 +219,49 @@ Napi::Value NodeJieba::textRankExtract(const Napi::CallbackInfo& info) {
216
219
  WrapPairVector(info.Env(), words, outArray);
217
220
  return outArray;
218
221
  }
222
+
223
+ Napi::Value NodeJieba::loadUserDict(const Napi::CallbackInfo& info) {
224
+ if (info.Length() < 1) {
225
+ return Napi::Boolean::New(info.Env(), false);
226
+ }
227
+
228
+ if( !_jieba_handle ){
229
+ Napi::Error::New(info.Env(), "Before calling any other function you have to call load() first").ThrowAsJavaScriptException();
230
+ }
231
+
232
+ // 支持传入字符串数组、单个字符串或 Buffer
233
+ if (info[0].IsArray()) {
234
+ Napi::Array arr = info[0].As<Napi::Array>();
235
+ std::vector<std::string> buf;
236
+ for (size_t i = 0; i < arr.Length(); i++) {
237
+ Napi::Value val = arr[i];
238
+ if (val.IsString()) {
239
+ buf.push_back(val.As<Napi::String>().Utf8Value());
240
+ }
241
+ }
242
+ _jieba_handle->LoadUserDict(buf);
243
+ } else if (info[0].IsString()) {
244
+ // 支持传入单个词典条目字符串
245
+ std::string line = info[0].As<Napi::String>().Utf8Value();
246
+ std::vector<std::string> buf;
247
+ buf.push_back(line);
248
+ _jieba_handle->LoadUserDict(buf);
249
+ } else if (info[0].IsBuffer()) {
250
+ // 支持传入 Buffer,将其转换为字符串并按行分割
251
+ Napi::Buffer<char> buffer = info[0].As<Napi::Buffer<char>>();
252
+ std::string content(buffer.Data(), buffer.Length());
253
+ std::vector<std::string> buf;
254
+ std::istringstream iss(content);
255
+ std::string line;
256
+ while (std::getline(iss, line)) {
257
+ if (!line.empty()) {
258
+ buf.push_back(line);
259
+ }
260
+ }
261
+ _jieba_handle->LoadUserDict(buf);
262
+ } else {
263
+ return Napi::Boolean::New(info.Env(), false);
264
+ }
265
+
266
+ return Napi::Boolean::New(info.Env(), true);
267
+ }
package/lib/nodejieba.h CHANGED
@@ -20,6 +20,7 @@ private:
20
20
  Napi::Value extract(const Napi::CallbackInfo& info);
21
21
  Napi::Value textRankExtract(const Napi::CallbackInfo& info);
22
22
  Napi::Value insertWord(const Napi::CallbackInfo& info);
23
+ Napi::Value loadUserDict(const Napi::CallbackInfo& info);
23
24
 
24
25
  cppjieba::Jieba* _jieba_handle{nullptr};
25
26
  cppjieba::TextRankExtractor* _text_rank_extractor_handle{nullptr};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodejieba-plus",
3
3
  "description": "chinese word segmentation for node",
4
- "version": "3.5.10",
4
+ "version": "3.5.12",
5
5
  "author": "Yanyi Wu <wuyanyi09@foxmail.com>",
6
6
  "maintainers": [
7
7
  "Yanyi Wu <wuyanyi09@foxmail.com>"
@@ -33,7 +33,7 @@
33
33
  "typescript": "^5.0.4"
34
34
  },
35
35
  "scripts": {
36
- "test": "mocha --timeout 10s -R spec test/test.js && mocha --timeout 10s -R spec test/load_dict_test.js && mocha --timeout 10s -R spec test/missing_binding_test.js",
36
+ "test": "mocha --timeout 10s -R spec test/test.js && mocha --timeout 10s -R spec test/load_dict_test.js && mocha --timeout 10s -R spec test/load_user_dict_test.js && mocha --timeout 10s -R spec test/missing_binding_test.js",
37
37
  "install": "npx @mapbox/node-pre-gyp install --fallback-to-build",
38
38
  "rebuild": "npx @mapbox/node-pre-gyp rebuild"
39
39
  },
@@ -10,7 +10,6 @@
10
10
  #include <stdint.h>
11
11
  #include <cmath>
12
12
  #include <limits>
13
- #include <algorithm>
14
13
  #include "limonp/StringUtil.hpp"
15
14
  #include "limonp/Logging.hpp"
16
15
  #include "Unicode.hpp"
@@ -113,97 +112,26 @@ class DictTrie {
113
112
  vector<string> buf;
114
113
  DictUnit node_info;
115
114
  Split(line, buf, " ");
116
-
117
- string word;
118
- string tag = UNKNOWN_TAG;
119
- double weight = user_word_default_weight_;
120
- bool hasSpace = false;
121
-
122
- // 处理包含空格的关键词
123
- // 格式可能是: "word" 或 "word tag" 或 "word freq tag"
124
- // 其中 word 本身可能包含空格
125
- if (buf.size() == 1) {
126
- // 只有关键词,无词频和标签
127
- word = buf[0];
128
- } else if (buf.size() == 2) {
129
- // 可能是 "word tag" "word1 word2"
130
- // 检查第二个字段是否为数字(词频)
131
- bool isNumber = true;
132
- for (char c : buf[1]) {
133
- if (!isdigit(c)) {
134
- isNumber = false;
135
- break;
115
+ if(buf.size() == 1){
116
+ MakeNodeInfo(node_info,
117
+ buf[0],
118
+ user_word_default_weight_,
119
+ UNKNOWN_TAG);
120
+ } else if (buf.size() == 2) {
121
+ MakeNodeInfo(node_info,
122
+ buf[0],
123
+ user_word_default_weight_,
124
+ buf[1]);
125
+ } else if (buf.size() == 3) {
126
+ int freq = atoi(buf[1].c_str());
127
+ assert(freq_sum_ > 0.0);
128
+ double weight = log(1.0 * freq / freq_sum_);
129
+ MakeNodeInfo(node_info, buf[0], weight, buf[2]);
136
130
  }
137
- }
138
- if (isNumber) {
139
- // "word freq" 格式,无标签
140
- int freq = atoi(buf[1].c_str());
141
- assert(freq_sum_ > 0.0);
142
- weight = log(1.0 * freq / freq_sum_);
143
- word = buf[0];
144
- } else {
145
- // "word tag" 格式
146
- word = buf[0];
147
- tag = buf[1];
148
- }
149
- } else {
150
- // 检查最后两个字段:可能是 "... word freq tag" 或 "... word1 word2 tag" 等
151
- // 倒数第二个如果是数字,则认为是词频,最后一个是标签
152
- // 否则认为只有最后一个是标签,前面都是关键词
153
- bool isFreq = true;
154
- for (char c : buf[buf.size() - 2]) {
155
- if (!isdigit(c)) {
156
- isFreq = false;
157
- break;
158
- }
159
- }
160
-
161
- if (isFreq) {
162
- // 格式: "word... freq tag"
163
- int freq = atoi(buf[buf.size() - 2].c_str());
164
- assert(freq_sum_ > 0.0);
165
- weight = log(1.0 * freq / freq_sum_);
166
- // 前面的所有部分(除了最后两个)组成关键词
167
- for (size_t i = 0; i < buf.size() - 2; ++i) {
168
- if (i > 0) word += " ";
169
- word += buf[i];
170
- }
171
- tag = buf[buf.size() - 1];
172
- } else {
173
- // 格式: "word... tag" (无词频)
174
- // 前面的所有部分(除了最后一个)组成关键词
175
- for (size_t i = 0; i < buf.size() - 1; ++i) {
176
- if (i > 0) word += " ";
177
- word += buf[i];
178
- }
179
- tag = buf[buf.size() - 1];
180
- }
181
- }
182
-
183
- // 检查词中是否包含空格
184
- hasSpace = (word.find(' ') != string::npos);
185
-
186
- // 添加原始词(包含空格版本)
187
- MakeNodeInfo(node_info, word, weight, tag);
188
- static_node_infos_.push_back(node_info);
189
- if (node_info.word.size() == 1) {
190
- user_dict_single_chinese_word_.insert(node_info.word[0]);
191
- }
192
-
193
- // 如果词包含空格,同时添加无空格版本
194
- if (hasSpace) {
195
- string wordNoSpace = word;
196
- // 移除所有空格
197
- wordNoSpace.erase(remove(wordNoSpace.begin(), wordNoSpace.end(), ' '), wordNoSpace.end());
198
- if (!wordNoSpace.empty() && wordNoSpace != word) {
199
- DictUnit node_info_no_space;
200
- MakeNodeInfo(node_info_no_space, wordNoSpace, weight, tag);
201
- static_node_infos_.push_back(node_info_no_space);
202
- if (node_info_no_space.word.size() == 1) {
203
- user_dict_single_chinese_word_.insert(node_info_no_space.word[0]);
131
+ static_node_infos_.push_back(node_info);
132
+ if (node_info.word.size() == 1) {
133
+ user_dict_single_chinese_word_.insert(node_info.word[0]);
204
134
  }
205
- }
206
- }
207
135
  }
208
136
 
209
137
  void LoadUserDict(const vector<string>& buf) {
@@ -287,36 +215,12 @@ class DictTrie {
287
215
  DictUnit node_info;
288
216
  while (getline(ifs, line)) {
289
217
  Split(line, buf, " ");
290
- // 支持包含空格的关键词
291
- // 格式: "word weight tag" 或 "word1 word2 weight tag" 等
292
- // 最后两个字段是 weight 和 tag,前面的都是关键词
293
- if (buf.size() >= DICT_COLUMN_NUM) {
294
- // 组合前面的字段作为关键词
295
- string word;
296
- for (size_t i = 0; i < buf.size() - 2; ++i) {
297
- if (i > 0) word += " ";
298
- word += buf[i];
299
- }
300
- double weight = atof(buf[buf.size() - 2].c_str());
301
- string tag = buf[buf.size() - 1];
302
-
303
- // 添加原始词(包含空格版本)
304
- MakeNodeInfo(node_info, word, weight, tag);
305
- static_node_infos_.push_back(node_info);
306
-
307
- // 如果词包含空格,同时添加无空格版本
308
- if (word.find(' ') != string::npos) {
309
- string wordNoSpace = word;
310
- wordNoSpace.erase(remove(wordNoSpace.begin(), wordNoSpace.end(), ' '), wordNoSpace.end());
311
- if (!wordNoSpace.empty() && wordNoSpace != word) {
312
- DictUnit node_info_no_space;
313
- MakeNodeInfo(node_info_no_space, wordNoSpace, weight, tag);
314
- static_node_infos_.push_back(node_info_no_space);
315
- }
316
- }
317
- } else {
318
- XCHECK(buf.size() == DICT_COLUMN_NUM) << "split result illegal, line:" << line;
319
- }
218
+ XCHECK(buf.size() == DICT_COLUMN_NUM) << "split result illegal, line:" << line;
219
+ MakeNodeInfo(node_info,
220
+ buf[0],
221
+ atof(buf[1].c_str()),
222
+ buf[2]);
223
+ static_node_infos_.push_back(node_info);
320
224
  }
321
225
  }
322
226
 
@@ -8,9 +8,7 @@
8
8
 
9
9
  namespace cppjieba {
10
10
 
11
- // 修改分隔符,移除空格,只保留其他分隔符
12
- // 这样英文单词之间的空格不会被当作分隔符
13
- const char* const SPECIAL_SEPARATORS = "\t\n\xEF\xBC\x8C\xE3\x80\x82";
11
+ const char* const SPECIAL_SEPARATORS = " \t\n\xEF\xBC\x8C\xE3\x80\x82";
14
12
 
15
13
  using namespace limonp;
16
14
 
@@ -92,10 +92,6 @@ inline RuneStrLite DecodeUTF8ToRune(const char* str, size_t len) {
92
92
  if (!(str[0] & 0x80)) { // 0xxxxxxx
93
93
  // 7bit, total 7bit
94
94
  rp.rune = (uint8_t)(str[0]) & 0x7f;
95
- // 将大写英文字母转换为小写,实现大小写不敏感匹配
96
- if (rp.rune >= 'A' && rp.rune <= 'Z') {
97
- rp.rune = rp.rune - 'A' + 'a';
98
- }
99
95
  rp.len = 1;
100
96
  } else if ((uint8_t)str[0] <= 0xdf && 1 < len) {
101
97
  // 110xxxxxx
@@ -0,0 +1,77 @@
1
+ var should = require("should");
2
+ var nodejieba = require("../index.js");
3
+
4
+ describe("nodejieba.loadUserDict", function() {
5
+
6
+ // 确保在测试前加载词典
7
+ before(function() {
8
+ nodejieba.load();
9
+ });
10
+
11
+ it("nodejieba.loadUserDict with string array should return true", function() {
12
+ // 使用字符串数组加载用户词典
13
+ var dictLines = [
14
+ "测试新词",
15
+ "自定义词汇 10 n"
16
+ ];
17
+ var loadResult = nodejieba.loadUserDict(dictLines);
18
+ loadResult.should.eql(true);
19
+ });
20
+
21
+ it("nodejieba.loadUserDict with single string should return true", function() {
22
+ // 加载单个词典条目
23
+ var loadResult = nodejieba.loadUserDict("单个词");
24
+ loadResult.should.eql(true);
25
+ });
26
+
27
+ it("nodejieba.loadUserDict with Buffer should return true", function() {
28
+ // 使用 Buffer 加载用户词典
29
+ var dictContent = Buffer.from("Buffer词 100 nz\n另一个词");
30
+ var loadResult = nodejieba.loadUserDict(dictContent);
31
+ loadResult.should.eql(true);
32
+ });
33
+
34
+ it("nodejieba.loadUserDict should return false when no argument", function() {
35
+ var loadResult = nodejieba.loadUserDict();
36
+ loadResult.should.eql(false);
37
+ });
38
+
39
+ it("nodejieba.loadUserDict with tag should return true", function() {
40
+ // 测试带标签的词典条目
41
+ var dictLines = [
42
+ "技术术语 500 nz"
43
+ ];
44
+ var result = nodejieba.loadUserDict(dictLines);
45
+ result.should.eql(true);
46
+ });
47
+
48
+ it("nodejieba.loadUserDict should work after loading words", function() {
49
+ // 先加载一些用户词典 - 使用比较独特的词确保测试准确性
50
+ nodejieba.loadUserDict(["云计算"]);
51
+ nodejieba.loadUserDict("人工智能 1000 nz");
52
+
53
+ // 验证这些词被正确识别
54
+ var result = nodejieba.cut("云计算是人工智能的基础");
55
+ result.should.containEql('云计算');
56
+ result.should.containEql('人工智能');
57
+ });
58
+
59
+ it("nodejieba.loadUserDict with Set should return true", function() {
60
+ const dictSet = new Set();
61
+ dictSet.add("非常独特的测试词123");
62
+ dictSet.add("另一个独特测试词 100 n");
63
+
64
+ var loadResult = nodejieba.loadUserDict(dictSet);
65
+ loadResult.should.eql(true);
66
+ });
67
+
68
+ it("nodejieba.loadUserDict with Set should automatically deduplicate", function() {
69
+ const dictSet = new Set();
70
+ dictSet.add("去重专用测试词");
71
+ dictSet.add("去重专用测试词"); // 重复添加
72
+ dictSet.add("去重专用测试词"); // 再次重复添加
73
+
74
+ var loadResult = nodejieba.loadUserDict(dictSet);
75
+ loadResult.should.eql(true);
76
+ });
77
+ });
package/types/index.d.ts CHANGED
@@ -27,4 +27,5 @@ declare module "nodejieba" {
27
27
  export function textRankExtract(sentence: string, threshold: number): ExtractResult[];
28
28
  export function insertWord(word: string, tag?: string): boolean;
29
29
  export function cutSmall(sentence: string, small: number): string[];
30
+ export function loadUserDict(dict: string | string[] | Set<string> | Buffer): boolean;
30
31
  }