最近想在站点的文章和问答等板块加上一个自动提取关键词的功能。
数据库中已经有了keyword的表,收录了一批常用的关键词,结构如下:
{
"id": "design_pattern",
"name" : "设计模式",
"otherName" : ["Design pattern"],
"fathers" : []
},
{
"id": "create_pattern",
"name" : "创建型模式",
"otherName" : [],
"fathers" : ["design_pattern"]
},
{
"id": "struct_pattern",
"name" : "结构型模式",
"otherName" : [],
"fathers" : ["design_pattern"]
},
{
"id": "behavior_pattern",
"name" : "行为模式",
"otherName" : [],
"fathers" : ["design_pattern"]
},
{
"id": "factory_method",
"name" : "工厂方法模式",
"otherName" : ["Factory Method", "虚拟构造函数", "Virtual Constructor"],
"fathers" : ["design_pattern", "create_pattern"]
},
{
"id": "abstract_factory",
"name" : "抽象工厂模式",
"otherName" : ["Abstract Factory"],
"fathers" : ["design_pattern", "create_pattern"]
},
// ...
主要实现思路如下:
分词
工具,将文章内容打散,有点类似锤子的大爆炸,首先大爆炸功能让我第一个联想到的就是jieba
这个库,github地址,当时对这个熟悉主要就是因为名字取的好,中文:结巴,233,一耳朵之后就在大脑挥之不去了。
简单找了一下,果然有node的实现版本:nodejieba
该项目主版本是以Python组件方式提供的中文分词框架,大家都知道,Python用途广泛。然而无论是前端还是后端,这个分词组件在数据分析,爬虫,搜索引擎中的关键词处理等领域都有很大帮助
结巴分词的特点:
a. 精确模式,试图将句子最精确地切开,适合文本分析; b. 全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义; c. 搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。 d. paddle模式,利用Paddle深度学习框架,训练序列标注(双向GRU)网络模型实现分词。同时支持词性标注。paddle模式使用需安装paddlepaddle-tiny,pip install paddlepaddle-tiny==1.6.1。目前paddle模式支持jieba v0.40及以上版本。
写这边文章的时候,nodejieba处于v2.5.1的版本,首先简单看了下库的README.md
和package.json
,里面有这个:
"engines": {
"node": ">= 10.20.0"
},
毕竟是一个移植的项目,所以建议node版本升至10.20.0以上,不然会有些莫名其妙的错误。
官网文档也不是很详细,简单总结了一下常用的方法,
//load参数都是可选的,如果没有对应的项则自动填充默认参数。
nodejieba.load({
dict: nodejieba.DEFAULT_DICT,
hmmDict: nodejieba.DEFAULT_HMM_DICT,
userDict: __dirname + '/testdata/userdict.utf8',
idfDict: nodejieba.DEFAULT_IDF_DICT,
stopWordDict: nodejieba.DEFAULT_STOP_WORD_DICT,
});
// 使用userDict可以加载自定义词典
nodejieba.load({
userDict: __dirname + '/testdata/userdict.utf8',
});
result = nodejieba.cut('红掌拨清波')
console.log(result);
var sentence = "我是拖拉机学院手扶拖拉机专业的。不用多久,我就会升职加薪,当上CEO,走上人生巅峰。";
var result;
// 没有主动调用nodejieba.load载入词典的时候,
// 会在第一次调用cut或者其他需要词典的函数时,自动载入默认词典。
// 词典只会被加载一次。
// 基础分词
result = nodejieba.cut(sentence);
console.log(result);
result = nodejieba.cut(sentence, true);
console.log(result);
result = nodejieba.cutHMM(sentence);
console.log(result);
result = nodejieba.cutAll(sentence);
console.log(result);
result = nodejieba.cutForSearch(sentence);
console.log(result);
result = nodejieba.tag(sentence);
console.log(result);
// 按词重返回最重要的 topN 个 词
// return值得格式为:{ word: string, weight: number }
var topN = 5;
result = nodejieba.extract(sentence, topN);
console.log(result);
result = nodejieba.cut("男默女泪");
console.log(result);
// 分词前向字典插入自定义单词
nodejieba.insertWord("男默女泪");
result = nodejieba.cut("男默女泪");
console.log(result);
result = nodejieba.cutSmall("南京市长江大桥", 3);
console.log(result);
jieba好用的地方在于可以灵活插入自定义的词典,比如文章开始的关键词列表,就可以自动生成为一个词典。
自定义词典txt文件格式目前只支持utf-8
,具体格式如下:
关键词 权重 词性
关键词 权重 词性
// 例如:
混合开发 999 n
Weex 9999 n
Flutter 9999 n
小程序 9999 n
React Native 9999 n
RN 999 n
jsBridge 9999 n
Native双向通信 999 n
Angular 9999 n
SSR 9999 n
服务端渲染 999 n
Server-Side Rendering 999 n
Eslint 9999 n
词性使用的是ICTCLAS
标准,具体类型如以下表格
代码 | 名称 | 帮助记忆的诠释 |
---|---|---|
Ag | 形语素 | 形容词性语素。形容词代码为a,语素代码g前面置以A。 |
a | 形容词 | 取英语形容词adjective的第1个字母。 |
ad | 副形词 | 直接作状语的形容词。形容词代码a和副词代码d并在一起。 |
an | 名形词 | 具有名词功能的形容词。形容词代码a和名词代码n并在一起。 |
b | 区别词 | 取汉字“别”的声母。 |
c | 连词 | 取英语连词conjunction的第1个字母。 |
Dg | 副语素 | 副词性语素。副词代码为d,语素代码g前面置以D。 |
d | 副词 | 取adverb的第2个字母,因其第1个字母已用于形容词。 |
e | 叹词 | 取英语叹词exclamation的第1个字母。 |
f | 方位词 | 取汉字“方” 的声母。 |
g | 语素 | 绝大多数语素都能作为合成词的“词根”,取汉字“根”的声母。 |
h | 前接成分 | 取英语head的第1个字母。 |
i | 成语 | 取英语成语idiom的第1个字母。 |
j | 简称略语 | 取汉字“简”的声母。 |
k | 后接成分 | |
l | 习用语 | 习用语尚未成为成语,有点“临时性”,取“临”的声母。 |
m | 数词 | 取英语numeral的第3个字母,n,u已有他用。 |
Ng | 名语素 | 名词性语素。名词代码为n,语素代码g前面置以N。 |
n | 名词 | 取英语名词noun的第1个字母。 |
nr | 人名 | 名词代码n和“人(ren)”的声母并在一起。 |
ns | 地名 | 名词代码n和处所词代码s并在一起。 |
nt | 机构团体 | “团”的声母为t,名词代码n和t并在一起。 |
nz | 其他专名 | “专”的声母的第1个字母为z,名词代码n和z并在一起。 |
o | 拟声词 | 取英语拟声词onomatopoeia的第1个字母。 |
p | 介词 | 取英语介词prepositional的第1个字母。 |
q | 量词 | 取英语quantity的第1个字母。 |
r | 代词 | 取英语代词pronoun的第2个字母,因p已用于介词。 |
s | 处所词 | 取英语space的第1个字母。 |
Tg | 时语素 | 时间词性语素。时间词代码为t,在语素的代码g前面置以T。 |
t | 时间词 | 取英语time的第1个字母。 |
u | 助词 | 取英语助词auxiliary 的第2个字母,因a已用于形容词。 |
Vg | 动语素 | 动词性语素。动词代码为v。在语素的代码g前面置以V。 |
v | 动词 | 取英语动词verb的第一个字母。 |
vd | 副动词 | 直接作状语的动词。动词和副词的代码并在一起。 |
vn | 名动词 | 指具有名词功能的动词。动词和名词的代码并在一起。 |
w | 标点符号 | |
x | 非语素字 | 非语素字只是一个符号,字母x通常用于代表未知数、符号。 |
y | 语气词 | 取汉字“语”的声母。 |
z | 状态词 | 取汉字“状”的声母的前一个字母。 |
该方法可以按某种策略来执行,例如:keyword表变更时执行,定时脚本(node-schedule
)执行
// 生成关键词字典
const generateDict = async () => {
const keywords = await Keyword.find({}, { fathers: 0, _id: 0 });
let txtData = '';
keywords &&
keywords.forEach(({ name, otherName }) => {
txtData += `${name} 9999\r\n`;
otherName.length > 0 &&
otherName.forEach((name) => {
txtData += `${name} 999\r\n`;
});
});
// 创建目录
const dictPath = path.join(__dirname, '../../dict/');
await fs.mkdirSync(dictPath, { recursive: true });
await fs.writeFileSync(path.join(dictPath, 'keyword.utf8.txt'), txtData);
};
const { load, extract } = require('nodejieba');
const userDict = path.join(__dirname, '../dict/keyword.utf8.txt');
fs.stat(userDict, (err) => {
if (!err) {
load({ userDict });
}
});
module.exports = {
...
handleKeyword: async (model) => {
const { title = '', summary } = model;
let cutStr = `${title} ${summary}`; // 提取关键词的原文本
const keywordDict = [];
extract(cutStr, 10).forEach((item) => {
keywordDict.push(item.word.toLowerCase());
});
const res = await Keyword.findAllByWordList(keywordDict);
if (res && res.length > 0) {
model.keywords = res;
}
},
...
}
findAllByWordList的实现:
findAllByWordList: function (words, cb) {
return this.find(
{
$or: [
{ id: { $in: words } },
{ name: { $in: words } },
{ otherName: { $all: words } },
],
},
{ id: 1, name: 1, _id: 0 }
)
.exec(cb)
.then((data) => data)
.catch((err) => {
console.error('KeywordSchema, findAllByWordList err', err);
});
},
三、post提交的时候执行handleKeyword
const updateData = { title, content };
handleSummary(updateData);
await handleKeyword(updateData);
const { ok } = await Post.updateOne({ _id: req.body._id }, updateData);
这里记录一下线上使用nodejieba是遇到的错误·。
背景:云服务器是阿里云的centos 7
,安装的Node.js
版本是14.16.0
本地环境调试的时候没问题,线上启动直接报错:
2021-04-15 21:01 +08:00: Error: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by /root/leedong/node_modules/_nodejieba@2.5.1@nodejieba/build/Release/nodejieba.node)
2021-04-15 21:01 +08:00: at Object.Module._extensions..node (internal/modules/cjs/loader.js:1122:18)
2021-04-15 21:01 +08:00: at Module.load (internal/modules/cjs/loader.js:928:32)
2021-04-15 21:01 +08:00: at Function.Module._load (internal/modules/cjs/loader.js:769:14)
2021-04-15 21:01 +08:00: at Module.require (internal/modules/cjs/loader.js:952:19)
2021-04-15 21:01 +08:00: at Module.Hook._require.Module.require (/usr/lib/node_modules/pm2/node_modules/require-in-the-middle/index.js:80:39)
2021-04-15 21:01 +08:00: at require (internal/modules/cjs/helpers.js:88:18)
2021-04-15 21:01 +08:00: at Object.<anonymous> (/root/leedong/node_modules/_nodejieba@2.5.1@nodejieba/index.js:4:17)
gcc
报的一个很常见的错误:
/lib64/libstdc++.so.6: version
GLIBCXX_3.4.20' not found`
Centos 7默认gcc版本为4.8,主要是目前的libstdc++.so.6没有对应的GLBCXX造成的
[root@lidong bin]# strings /usr/lib64/libstdc++.so.6 | grep GLIBC
GLIBCXX_3.4
GLIBCXX_3.4.1
GLIBCXX_3.4.2
GLIBCXX_3.4.3
GLIBCXX_3.4.4
GLIBCXX_3.4.5
GLIBCXX_3.4.6
GLIBCXX_3.4.7
GLIBCXX_3.4.8
GLIBCXX_3.4.9
GLIBCXX_3.4.10
GLIBCXX_3.4.11
GLIBCXX_3.4.12
GLIBCXX_3.4.13
GLIBCXX_3.4.14
GLIBCXX_3.4.15
GLIBCXX_3.4.16
GLIBCXX_3.4.17
GLIBCXX_3.4.18
GLIBCXX_3.4.19
GLIBC_2.3
GLIBC_2.2.5
GLIBC_2.14
GLIBC_2.4
GLIBC_2.3.2
GLIBCXX_DEBUG_MESSAGE_LENGTH
可以看到,最高版本为3.4.19,没有对应的3.4.20。
在一下本地,看有没有更高版本的libstdc++.so.6
[root@lidong bin]# find / -name "libstdc++.so*"
/usr/share/gdb/auto-load/usr/lib64/libstdc++.so.6.0.19-gdb.pyo
/usr/share/gdb/auto-load/usr/lib64/libstdc++.so.6.0.19-gdb.pyc
/usr/share/gdb/auto-load/usr/lib64/libstdc++.so.6.0.19-gdb.py
/usr/lib64/libstdc++.so.6
/usr/lib64/libstdc++.so.6.0.19
果然没有,那就去下一个,或者升级一下 gcc
的基础库文件,google了一下,果然有好心人上传了一个Centos 7 中高版本 libstdc++.so.6下载地址
下载:
wget http://www.vuln.cn/wp-content/uploads/2019/08/libstdc.so_.6.0.26.zip
安装zip解压工具并解压:
yum install -y unzip zip
unzip -d tmp/ libstdc.so_.6.0.26.zip
扔到lib64文件夹
cp ./libstdc++.so.6.0.26 /usr/lib64/
重新做一个libstdc++.so.6
链接
rm -f /usr/lib64/libstdc++.so.6
ln -s /usr/lib64/libstdc++.so.6.0.26 /usr/lib64/libstdc++.so.6
看下效果:
[root@lidong tmp]# strings /usr/lib64/libstdc++.so.6 | grep GLIBCXX
GLIBCXX_3.4
GLIBCXX_3.4.1
GLIBCXX_3.4.2
GLIBCXX_3.4.3
GLIBCXX_3.4.4
GLIBCXX_3.4.5
GLIBCXX_3.4.6
GLIBCXX_3.4.7
GLIBCXX_3.4.8
GLIBCXX_3.4.9
GLIBCXX_3.4.10
GLIBCXX_3.4.11
GLIBCXX_3.4.12
GLIBCXX_3.4.13
GLIBCXX_3.4.14
GLIBCXX_3.4.15
GLIBCXX_3.4.16
GLIBCXX_3.4.17
GLIBCXX_3.4.18
GLIBCXX_3.4.19
GLIBCXX_3.4.20
GLIBCXX_3.4.21
GLIBCXX_3.4.22
GLIBCXX_3.4.23
GLIBCXX_3.4.24
GLIBCXX_3.4.25
GLIBCXX_3.4.26
GLIBCXX_DEBUG_MESSAGE_LENGTH
感恩好心人,一直支持到了GLIBCXX_3.4.26
pm2重启下node进程,问题解决!!!