技术文档:统计高频词¶
需求描述¶
上传小说时可以手动创建标签,便于后续搜索。批量上传时,也可以从文本中提取高频词并用作候选标签。
技术基础¶
自然语言处理中,分词和停用词过滤是常见的文本预处理步骤,可辅助关键词提取;它们本身并不等同于完整的 NLP 分析。
分词(Tokenization)
分词是将文本拆分为词语或符号的过程,可用于文本统计和后续分析。其用途包括:
- 为关键词提取和词频统计提供基本单位。
- 配合清洗规则过滤标点符号和无关字符。
- 便于后续文本分析。
停用词(Stopwords)
停用词通常指在特定任务中区分度较低的常见词语,例如部分介词、连词和冠词。是否将某个词视为停用词取决于语料和任务。过滤停用词可以:
- 减少词频统计中常见虚词的干扰。
- 减少后续处理的数据量。
分词和停用词的关系
分词通常先于停用词过滤。过滤后再结合词频等规则提取候选关键词;是否适用于特定语料仍需验证。
实例
例如,我们有一个文本数据:"我爱吃苹果,苹果是我的最爱。"
- 分词:我们可以将文本数据分解成单个词语,例如:"我"、"爱"、"吃"、"苹果"、"是"、"我的"、"最爱"。
- 停用词:我们可以过滤掉一些停用词,例如:"我"、"是"、"我的",因为它们对文本的含义没有贡献。
- 提取关键词:我们可以提取剩余的关键词,例如:"爱"、"吃"、"苹果"、"最爱"。
这个例子展示了分词和停用词过滤的基本流程,并不代表仅凭词频就能准确识别主题关键词。
js 实现技术调研¶
词频统计可通过 Python 的 jieba 等库完成。项目后端基于 Express,因此最初调研了 Node.js 生态中的分词库。选择第三方库时应核对其维护状态、平台兼容性和实际 API,不能只依据名称判断。
曾测试可用(原生扩展,安装受平台环境影响):https://www.npmjs.com/package/nodejieba
曾测试可用:https://www.npmjs.com/package/analytics-node
未采用:https://www.npmjs.com/package/jieba
未采用:https://www.npmjs.com/package/node-segment
曾测试可用:https://www.npmjs.com/package/stopword
未采用:https://www.npmjs.com/package/stopwords
分词库 nodejieba¶
nodejieba 是结巴中文分词的 Node.js 实现,底层包含 C++ 扩展,支持自定义词典。由于包含原生扩展,安装和运行兼容性取决于 Node.js 版本及操作系统环境。
let nodejieba = require("nodejieba");
let result = nodejieba.cut("南京市长江大桥");
自定义词典可通过库提供的词典加载接口配置。直接覆盖 node_modules 中的默认词典不推荐,因为重新安装依赖时更改会丢失:
旧版示例路径:node_modules/nodejieba/submodules/cppjieba/dict/user.dict.utf8。
停用词库 stopword¶
stopword is a module for node and the browser that allows you to strip stopwords from an input text.
默认词库主要面向英文;中文停用词需要单独提供。
const { removeStopwords } = require("stopword");
const oldString = "a really Interesting string with some words".split(" ");
const newString = removeStopwords(oldString);
// newString is now [ 'really', 'Interesting', 'string', 'words' ]
中文需要单独配置停用词表。先对中文文本分词,再将词语数组交给停用词过滤函数:
// https://github.com/fergiemcdowall/stopword?tab=readme-ov-file
const { removeStopwords, zho } = require("stopword");
const words = nodejieba.cut(chineseText);
// words 是一个字符串数组,先经过分词库进行切分处理
const results = removeStopwords(words, zho);
代码实例¶
const nodejieba = require("nodejieba");
const { removeStopwords, zho } = require("stopword");
function getTags(text) {
let chineseText = text.replace(/[a-zA-Z\n\r\t\f\v]/g, " ");
const words = nodejieba.cut(chineseText);
const results = removeStopwords(words, zho);
const doubleWord = results.filter((item) => {
return item.length > 1;
});
const wordFrequency = {};
for (const word of doubleWord) {
if (wordFrequency[word]) {
wordFrequency[word]++;
} else {
wordFrequency[word] = 1;
}
}
const sortedWords = Object.keys(wordFrequency).sort(
(a, b) => wordFrequency[b] - wordFrequency[a]
);
const top10Words = sortedWords.slice(0, 10);
return top10Words.join(",");
}
存在的问题¶
nodejieba 依赖原生 C++ 扩展,部分服务器环境中可能无法安装或编译。相关问题记录见:https://github.com/Novel-Reader/Genshin-novel-reader-express-server/issues/42 。
Python 实现技术调研¶
由于 nodejieba 存在环境兼容性问题,项目改为调研使用 Python 实现相同功能。
以下为使用 jieba 和本地中文停用词表的示例:
import jieba
import re
from collections import Counter
from urllib.request import urlopen
from io import StringIO
import gzip
import sys
def analyze_word_frequency(text, top_n=10):
"""
分析文本的词频并返回 Top N关键词
:param text: 待分析的文本内容
:param top_n: 返回的关键词数量,默认10
:return: 包含关键词和词频的字典列表
"""
cleaned_text = re.sub(r'[^\w\s]', '', text) # 保留字母、数字、下划线、汉字和空格
cleaned_text = re.sub(r'\s+', ' ', cleaned_text).strip() # 合并多个空格为一个
words = jieba.cut(cleaned_text)
with open('./src/utils/python/cn_stopwords.txt', encoding='utf-8') as f:
stopwords = set(line.strip() for line in f if line.strip())
filtered_words = [word for word in words if word.strip() and word not in stopwords and len(word) > 1]
word_counts = Counter(filtered_words)
top_words = word_counts.most_common(top_n)
return ','.join([word for word, count in top_words])
if __name__ == "__main__":
# 从 Node.js 获取2个参数
result = analyze_word_frequency(sys.argv[1], int(sys.argv[2]))
# 这里先把结果打印出来,然后 python.stdout.on 获取数据,这个不是函数,所以不能直接 return data
print(result)
可以通过 Node.js 子进程调用 Python 脚本,并在成功取得输出后将生成的标签写入数据库。调用时应处理进程退出码、超时和标准错误输出。