如何提取关键词?简介如何node中jieba的使用。

最近想在站点的文章和问答等板块加上一个自动提取关键词的功能。

数据库中已经有了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"]
  },
  // ...

主要实现思路如下:

  • 根据 分词 工具,将文章内容打散,有点类似锤子的大爆炸,
  • 然后根据词语出现的频率计算出最重要的若干个词组,
  • 最后将词组在keywords数据库中进行匹配,匹配成功则自动关联

首先大爆炸功能让我第一个联想到的就是jieba这个库,github地址,当时对这个熟悉主要就是因为名字取的好,中文:结巴,233,一耳朵之后就在大脑挥之不去了。

简单找了一下,果然有node的实现版本:nodejieba

jieba简介

该项目主版本是以Python组件方式提供的中文分词框架,大家都知道,Python用途广泛。然而无论是前端还是后端,这个分词组件在数据分析,爬虫,搜索引擎中的关键词处理等领域都有很大帮助

结巴分词的特点:

  1. 支持繁体分词
  2. 支持自定义词典
  3. MIT 授权协议
  4. 支持4种分词模式
  5. 4种分词模式:

a. 精确模式,试图将句子最精确地切开,适合文本分析; b. 全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义; c. 搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。 d. paddle模式,利用Paddle深度学习框架,训练序列标注(双向GRU)网络模型实现分词。同时支持词性标注。paddle模式使用需安装paddlepaddle-tiny,pip install paddlepaddle-tiny==1.6.1。目前paddle模式支持jieba v0.40及以上版本。

nodejieba

写这边文章的时候,nodejieba处于v2.5.1的版本,首先简单看了下库的README.mdpackage.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);
};

二、在公共方法处引入nodejieba,导入自定义词典

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进程,问题解决!!!

WRITTEN BY

lidong

鄂ICP备20003892号 Copyright © 2017-2023 leedong.cn

ABOUT ME

Hello,这里是「我的心情永不立冬」
一个想到什么就做什么的个人站点,所有内容纯主观、有偏见