资讯动态

用TF-IDF提取《红楼梦》分回关键词:从文本预处理到完整实战脚本

发布时间:2026/10/17 11:12:13 来源:尧图企业网站定制
1. 为什么是分回关键词而不是全书关键词手里这份 txt 版《红楼梦》一共一百二十回约七十余万字。如果只想快速知道每一回讲了什么最快的方式不是通读而是把每一回当成一个独立文档用 TF-IDF 提取这一回里最突出的核心关键词。这个任务看起来简单真正跑起来才会发现回目怎么切、人名要不要保留、停用词怎么选每一点都会影响结果。这篇实战记录就是把这套流程完整拆开从算法原理到可直接复用的脚本一次讲清楚。适合看这篇内容的人主要是三类刚接触中文文本挖掘、想找个真实项目练手的 NLP 学习者想把经典名著做成检索、导读或数字人文分析的同学以及纯粹好奇“计算机能不能自动读懂章回体小说”的读者。整个项目不需要 GPU不需要大模型一台普通电脑和一点耐心就够了。1.1 从长文本到可计算的章节单元长文本直接做关键词统计得到的结果通常很无聊。“宝玉”“黛玉”“说道”“众人”这类词会霸占前列因为它们在全书中出现次数最高。可这是全书级的答案不是分回级的答案。你问的是“第三回的重点是什么”“第六十六回的核心事件落在哪里”全书统计给不了这种粒度。章回体小说有个天然优势每一回就是一个人为设计好的叙事单元场景、人物、事件都会在这一回里集中展开。把一百二十回切成一回一个文档每一回的文本量大约是几千字TF-IDF 在这个粒度上算出来的关键词能明显反映这一回独有的重心。比如某回里突然高频出现“通灵”“石头”“还泪”大概率这一回在讲木石前盟的渊源某回里反复出现“灯谜”“爆竹”“击鼓传花”那这回的重心就是贾府宴饮游乐。这种“一换回目关键词就跟着换”的效果正是分回处理能带来的核心价值。用生活里的例子类比全书关键词相当于“年度总结报告”只能看出一年里的大盘子分回关键词相当于“每月重点摘要”能看出每个月到底发生了什么变化。对于一本叙事线索密集的长篇小说后者显然更有分析意义。1.2 TF-IDF 为什么是这个任务的首选分词之后你手里每个回目就是一堆词语。最朴素的词频统计只能回答“哪些词出现得多”回答不了“哪些词对这个回目最有辨识度”。TF-IDF 解决的正是这个问题它在词频基础上乘上一个逆文档频率如果某个词在全书绝大多数回目里都出现它会削弱这个词的权重如果某个词只出现在少数回目里这个词的权重会被放大。放到分回场景里这个逻辑非常贴切。比如“一个”“只见”“笑道”几乎每回都有区分度很低而“桃花社”“柳絮词”只在某几回集中出现那么这些词理应是那几回的关键词。TF-IDF 不需要训练不需要标签不需要外部知识库纯粹基于当前语料内部统计就能给出可解释的结果这对小说文本分析来说是很稳的起点。有人会问为什么不直接调jieba.analyse.extract_tags因为它自带的 IDF 是基于通用语料统计出来的它不认识你正在处理的是《红楼梦》内部一百二十回的横向比较。你真正想要的“跨回稀缺度”必须用这本书自己的回目语料现算。这也是这篇实战里最值得留意的一点算法的实现不是难点选对你要用的语料统计口径才是。1.3 整体流程五步走整个项目的流程可以拆成五步顺序不建议打乱分回切分把整本 txt 切成一百二十份章节文本。文本清洗去掉空白、特殊符号保留对分词有用的中文内容。分词与词性过滤用 jieba 切词再按名词、地名、动词等词性白名单做筛选。构建 TF-IDF 语料每一回作为一个独立文档统一进入向量化流程。按回输出 Top N 关键词并和回目标题相互对照验证。切分和清洗必须放在最前面否则目录、乱码、空白符都会污染后面的分词结果。词性过滤要放在分词之后因为只有先知道每个词的词性才能决定哪些保留、哪些剔除。整个过程从原始文本到最终关键词链路很清晰出问题时也方便定位到底是在哪一步出了岔子。2. 先把 120 回切开文本预处理的两个关键动作2.1 回目标题的识别与切分《红楼梦》不同的 txt 来源格式五花八门。常见的是“第一回 甄士隐梦幻识通灵”占一行但有的版本没有空格有的在“回”字后面跟着冒号还有的把章回标题和正文挤在同一行。处理方式是先用正则定位所有“第X回”出现的位置再按照位置把文本切片。正则可以写成r第[一二三四五六七八九十百零〇]回用re.finditer找到所有匹配位置。第一次跑通之前你一定会遇到一个经典问题txt 开头有完整的目录目录里有一百二十个回目正文里又出现一百二十个回目结果finditer找出来两百四十个匹配。这时候不要慌先从匹配数量上做判断如果超过一百三十个直接取最后一百二十个匹配正文部分就在那里。这算是我踩过的第一个坑。解决办法不算优雅但对大多数网络流传的 txt 版本都有效目录在前正文在后回目顺序又一致那么“最后一百二十个回目标题”基本就是正文的标题位置。核心切分逻辑是拿到每个回目标题匹配的结束位置用它作为这一回正文的起点用下一个回目匹配的起始位置作为这一回正文的终点。最后一个回目没有下一个匹配就直接取到文本结尾。切完之后最好打印回目数量和每回的标题扫一眼就能发现切歪了没有。2.2 清理非正文内容切出来的文本里通常还有大量空白、换行、全角空格以及从 PDF 转过来的乱码符号。清洗策略按优先级做三件事把\s统一去掉或替换成空格把非中文、非常用标点的字符删掉保留中文以及“。、”这类基础标点。有人会问关键词提取最终只要词标点留着有什么意义答案是标点对 TF-IDF 本身没意义但标点会影响 jieba 分词时的句子边界判断。直接全部删掉标点有些上下文语境会丢失分词结果可能更差。我习惯把正文里的英文、数字、特殊符号清除但保留中文标点等分完词之后再统一丢弃标点。章节文本里附带的诗词、对联、回末结语不要急着删。它们在《红楼梦》里往往有一定情节分量如果只保留白话叙事部分会漏掉不少有价值的词。清洗的目标是“把噪声去掉”而不是“把特色去掉”这两件事要分清楚。2.3 自定义词典与停用词表《红楼梦》的人名、地名、专有名词非常多而且很多不是标准现代汉语词。比如“通灵宝玉”“木石前盟”“太虚幻境”这类词如果不加词典jiejue 可能会拆得七零八落。解决办法是在分词之前用jieba.add_word或自定义词典文件把这些词加进去。这个步骤对最终效果的影响比我预期的大得多。停用词表也要针对文本特性重新准备。通用停用词表里的“的”“了”“在”“是”当然要过滤但《红楼梦》的白话叙述里还有一批特殊高频词比如“笑道”“说着”“只见”“一面”“因见”。这些词在每一回里都大量出现算出来的 TF 很高IDF 又压不住如果不加进停用词清单一番操作之后 Top10 里可能一半都是这种“叙事填充词”。我的建议是准备两套过滤规则。一套是通用停用词管“的、了、就、都”另一套是“白话行为词”专门放“笑道、说道、只见、一面、因着、不觉”。同时保留一个开关控制要不要把人物名词也过滤掉。分析人物出场时保留人名分析情节主线时则过滤人名两种需求要分开做。3. TF-IDF 手写一遍才知道参数在调什么3.1 词频与逆文档频率的直觉TF 是 Term Frequency度量某个词在当前文档中出现的频率。在分回场景里就是某个词在这一回里出现得有多频繁。IDF 是 Inverse Document Frequency度量某个词在语料中的稀缺程度。一个词如果在越多回目里出现它区分“这一回”和“其他回”的能力就越弱IDF 就会越小。两者相乘含义就是某个词在这个回目里出现得多同时在别的回目里很少出现它才有资格成为这一回的核心关键词。这有点像餐厅里的“招牌菜”白米饭每天人人点没有辨识度松露牛排虽然点的人少但一旦出现就能代表这家店的调性。TF-IDF 要抓的正是这种“有调性的词”。3.2 公式与平滑处理标准的 IDF 计算方式是idf(t) ln((总文档数) / (包含 t 的文档数))如果某个词每个回目都有分母等于总文档数log 结果为 0词频再高也会被完全抹掉。实际工具包里通常不会这么做而是用平滑版本idf(t) ln((1 总文档数) / (1 包含 t 的文档数)) 1这个公式的好处是即使一个词在所有文档里都出现IDF 也不会变成 0而是接近 1也就是说它还会有基础权重只是不会主导排序。对《红楼梦》这种高频重复词特别多的文本平滑处理是必须的否则“宝玉”这类贯穿全书的主角名可能直接被踢出所有关键词结果。还有一个参数值得单独说就是sublinear_tf。默认 TF 用的是原始词频一个词在某一回出现 100 次的权重会粗暴地达到出现 10 次的 10 倍。长文本里高频词差异会被线性放大导致排序不稳。开启sublinear_tfTrue之后TF 会被压缩成1 log(tf)高频词的边际收益递减低频特色词反而有更多机会浮上来。我在实际跑分回关键词时通常都会开启这个参数。3.3 手写版本的简化代码用 scikit-learn 几行就能算但为了搞清楚每一步在干嘛我建议先手写一个简化版。核心逻辑是先统计每个词在多少个文档里出现过也就是文档频率然后再对每一回计算词频乘上逆文档频率。import math from collections import Counter # docs 是经过分词后的每一回文本格式为 list[list[str]] docs [ [通灵, 石头, 仙草, 还泪], [演说, 荣府, 夫人, 投奔], # ... 一共120个列表 ] N len(docs) # 120 df Counter() for words in docs: for word in set(words): # 每个回目只统计一次 df[word] 1 def idf(word): return math.log((N 1) / (df[word] 1)) 1 def tfidf_score(words, word): tf words.count(word) / len(words) return tf * idf(word)手写版本和 scikit-learn 的输出会有细微差异因为工具包默认还会做 L2 归一化但排序趋势基本一致。把公式自己写一遍之后你再去调TfidfVectorizer的参数就大概能猜到每个参数在改什么不会只是无脑抄参数。4. 完整脚本从《红楼梦》txt 到每回 Top10 关键词4.1 环境准备整个项目只依赖三个东西Python 3.8 以上环境、jieba 分词库、scikit-learn 的文本特征提取工具。pandas 不是必须的但用来整理输出、打印对照表会很舒服。安装命令就一条pip install jieba scikit-learn pandas如果没有现成的《红楼梦》txt建议先找一个适合作文本分析的精校版最好没有大量异体字、没有几百个错别字的版本。原始文本质量会直接影响关键词质量这一步不值得省。4.2 完整可运行脚本下面这个脚本把前文说的所有环节串联起来了可以直接保存成hlm_keywords.py运行。import re import jieba import jieba.posseg as pseg from sklearn.feature_extraction.text import TfidfVectorizer # 1. 读取原始文本注意编码 with open(hlm.txt, r, encodingutf-8) as f: text f.read() # 2. 切分回目 pattern re.compile(r第[一二三四五六七八九十百零〇]回) matches list(pattern.finditer(text)) # 如果出现目录干扰取最后120个匹配 if len(matches) 130: matches matches[-120:] print(识别到回目数量, len(matches)) chapters [] for i in range(len(matches)): start matches[i].end() end matches[i 1].start() if i 1 len(matches) else len(text) title matches[i].group().strip() content text[start:end].strip() chapters.append((title, content)) # 3. 自定义词典示例按实际语料补充 user_words [通灵宝玉, 木石前盟, 太虚幻境] for w in user_words: jieba.add_word(w) # 4. 停用词表通用 白话高频词 stopwords set() # 这里补充你的停用词文件读取逻辑或者直接内嵌一个列表 stopwords.update([ 的, 了, 在, 是, 我, 你, 他, 她, 笑道, 说道, 只见, 一面, 因见, 不觉, 一时 ]) # 5. 清洗 def clean_text(s): s re.sub(r\s, , s) s re.sub(r[^\u4e00-\u9fa5。、“”‘’《》], , s) return s # 6. 分词 词性过滤 def segment(s, keep_personFalse): words [] for word, flag in pseg.cut(s): if word in stopwords: continue if len(word) 2: continue if not keep_person and flag.startswith(nr): continue if flag.startswith(n) or flag.startswith(v) or flag.startswith(ns): words.append(word) return .join(words) documents [] for title, content in chapters: cleaned clean_text(content) seg segment(cleaned, keep_personFalse) documents.append(seg) # 7. TF-IDF 向量化 vectorizer TfidfVectorizer( sublinear_tfTrue, min_df1, max_df0.8, max_features5000, token_patternr\S, ngram_range(1, 1) ) X vectorizer.fit_transform(documents) feature_names vectorizer.get_feature_names_out() # 8. 输出每回 Top10 for i in range(min(120, len(chapters))): row X[i].toarray()[0] top_idx row.argsort()[-10:][::-1] top_words [feature_names[idx] for idx in top_idx] print(chapters[i][0], -, .join(top_words))脚本里有几个细节值得注意。正则token_patternr\S是为了适配已经用空格拼接过的分词结果如果保持 scikit-learn 默认的正则中文可能被当成一个整体切不出来。max_df0.8的意思是如果一个词在超过八成的回目里都出现就把它从特征里拿掉这比单纯堆停用词表更省事。min_df1则是保留只在一回里出现的关键词允许“回特征词”存在。4.3 输出结果的验证方法脚本跑完之后先别急着分析语言风格先做三件事验证结果是不是靠谱。第一看它和回目标题是否呼应。回目是作者亲手写的章节摘要关键词提取如果再能对应上说明流程没问题。例如第一回标题里出现“通灵”关键词列表里往往也会有类似特征词。第二看相邻两回的关键词是否过度重复。如果第二回的结果和第一回几乎一样说明停用词没加够或者 IDF 的区分度被淹没了。第三把 Top10 的数量缩小成 Top5 再看一眼Top10 经常混入一些中频背景词Top5 往往更锐利。我自己习惯把回目标题打印出来跟关键词并排做一个表格肉眼扫一遍。页面上看起来一目了然哪里断词出了问题也更容易定位。5. 运行中的典型问题与排查速查表5.1 五类高频问题的定位思路跑过几次之后你会发现大部分问题并不出在 TF-IDF 本身而是出在分词和语料切分上。第一类问题是回目切分数量不对。常见原因是目录干扰或者文本里的“第X回”出现位置不标准。排查思路是先打印matches的前后几项确认到底是切成了 240 回还是 120 回。如果是目录问题用取最后 120 个匹配的办法能解决。第二类问题是人名地名被切开。比如专职名词被拆成两个普通字关键词列表里出现一堆不完整的词。排查方法是直接在结果里找有没有明显断裂的片段然后把这些词补充进user_words。自定义词典不会一次配好通常要跑两三轮才能稳定。第三类问题是 Top 关键词全是“笑道”“只见”“一时”这种词。这类词词性标注不一定是动词也不一定是名词过滤规则不一定能覆盖到需要在停用词表里手工补。建议第一次跑完结果之后把出现频率最高的 30 个词打印出来挨个看有哪些是白话连接词一次性补进停用词。第四类问题是所有回目的关键词高度重合。原因通常是max_df没设、停用词太少、或者sublinear_tf没开。调试时先开max_df0.8再补停用词最后检查min_df1是否保留了自己的专属词。第五类问题是输出里混入错别字或异体字。这种情况通常是 txt 来源本身不干净清洗步骤解决不了垃圾进垃圾出的问题换个精校版文本最直接。5.2 调参与取舍心得我把几个常用参数的推荐值整理成了一张表方便后续直接参考参数推荐值说明sublinear_tfTrue压缩高频词的线性差异长文本里很稳min_df1保留只在这一回出现的专属关键词max_df0.8淘汰超过八成回目都出现的背景词max_features5000限制特征总量防止稀疏矩阵过大token_pattern\S适配空格分词后的文本ngram_range(1,1)单关键词不推荐一上来就开二元词组这里面最容易被忽视的是max_features。不设置的话总词典可能膨胀到几万个词不仅慢结果还会大量集中在长尾噪声里。设成 5000 以后前面排名的词基本都是有效信息。现象原因解法识别到 240 回txt 开头有目录匹配数大于 130 时取最后 120 个人名被切成两半词典缺失用 add_word 补充完整词关键词全是“笑道”“说道”白话高频词未过滤补停用词表所有回结果雷同背景词区分度不够开启 max_df0.8 并补停用词结果里出现大量单字长度过滤缺失在 segment 里限制 len(word) 26. 这些关键词能做什么扩展应用与我的心法6.1 关键词的直接应用价值拿到一百二十回各自的核心关键词之后第一个能做的事是自动生成一份导读地图。每一回只要看十来个词就能大致判断它是不是你感兴趣的情节段。这比翻目录更快也比只看回目标题更接近正文的侧重点。把关键词保留人物名再跑一版可以做人物出场追踪。某角色在哪些回目里集中出现、哪些回目里完全消失从关键词表里能直接看出来。更进一步把每回的关键词集合做共现统计还能还原角色之间的同场景关系这就是简单的人物关系网络底稿。6.2 从 TF-IDF 到更高阶方法跑完 TF-IDF 之后如果想继续做深有两个方向比较自然。一个是换成 TextRank它基于词与词的共现关系迭代计算权重适合提取篇章级关键词能够利用上下文信息但计算量更大结果解释起来也不如 TF-IDF 这么直白。另一个是做 LDA 主题模型把每一回当成一篇文档自动归纳出若干个主题分布能看到“哪些回目在情节上更接近”但主题数的设定比较敏感调参成本高。我的建议是先建立 TF-IDF 基线把清洗、分词、停用词这套数据管道打磨好再升级模型。数据管道做扎实了换算法只是换一层壳数据管道没做好换十个算法结果都不会太理想。6.3 实操心得与一个小技巧我自己跑完整本书之后最大的体会是算法只要几行预处理才是重头戏。TF-IDF 的公式不会变但你在“回目怎么切、人名要不要留、停用词怎么选”上的每个决定都会成倍放大到最终结果上。最后分享一个小技巧把回目标题和每回 Top5 关键词放到同一张表里打印出来。你就知道这一回到底有没有被准确识别如果某些回的结果里混入了来回重复的动词、连接词那一首眼就能看出来再去补停用词表。这个做法不仅适用于《红楼梦》换成其他章回体小说、历年财报、政策文件只要按自然段落分组整个流程都能直接复用。数据清洗的方向会变停用词会变但“先切块再算权重再人工核验”的思路是完全一样的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑