Skip to content

技术文档:统计高频词

需求描述

上传小说时可以手动创建标签,便于后续搜索。批量上传时,也可以从文本中提取高频词并用作候选标签。

技术基础

自然语言处理中,分词和停用词过滤是常见的文本预处理步骤,可辅助关键词提取;它们本身并不等同于完整的 NLP 分析。

分词(Tokenization)

分词是将文本拆分为词语或符号的过程,可用于文本统计和后续分析。其用途包括:

  1. 为关键词提取和词频统计提供基本单位。
  2. 配合清洗规则过滤标点符号和无关字符。
  3. 便于后续文本分析。

停用词(Stopwords)

停用词通常指在特定任务中区分度较低的常见词语,例如部分介词、连词和冠词。是否将某个词视为停用词取决于语料和任务。过滤停用词可以:

  1. 减少词频统计中常见虚词的干扰。
  2. 减少后续处理的数据量。

分词和停用词的关系

分词通常先于停用词过滤。过滤后再结合词频等规则提取候选关键词;是否适用于特定语料仍需验证。

实例

例如,我们有一个文本数据:"我爱吃苹果,苹果是我的最爱。"

  1. 分词:我们可以将文本数据分解成单个词语,例如:"我"、"爱"、"吃"、"苹果"、"是"、"我的"、"最爱"。
  2. 停用词:我们可以过滤掉一些停用词,例如:"我"、"是"、"我的",因为它们对文本的含义没有贡献。
  3. 提取关键词:我们可以提取剩余的关键词,例如:"爱"、"吃"、"苹果"、"最爱"。

这个例子展示了分词和停用词过滤的基本流程,并不代表仅凭词频就能准确识别主题关键词。

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 脚本,并在成功取得输出后将生成的标签写入数据库。调用时应处理进程退出码、超时和标准错误输出。