TF-IDF提取《红楼梦》回目关键词:中文古白话文本分析实战

发布时间:2026/10/16 17:50:49
TF-IDF提取《红楼梦》回目关键词:中文古白话文本分析实战 做文本分析这些年我越来越觉得中文语料的坑比想象中多。前阵子接了个活儿把《红楼梦》一百二十回按回目做核心关键词提取用来快速浏览每回到底讲了啥顺便看看能不能发现一些隐藏的叙事线索。标题里写的“基于 TF-IDF”听起来是个老掉牙的方法但真拿它去处理古白话小说尤其是《红楼梦》这种人物多、场景切换快、虚实交织的文本还是会遇到一堆预料之外的麻烦。这篇就复盘一下整个实战过程包括我踩过的坑、调过的参数以及最后怎么让结果看起来像人话而不是术语堆砌。1. 项目概述为什么选 TF-IDF 和《红楼梦》分回文本1.1 核心需求从三十万字的章回体里快速定位每回焦点《红楼梦》全书一百二十回每回少则五六千字多则上万字通读一遍费时费力。文本挖掘的第一步通常是“降维”先把每回的主要内容压缩成几个关键词让人一眼看出这回的主线是什么。比如某回如果提到“凤姐”“操办”“丧事”大概率能猜到是协理宁国府的情节如果出现“黛玉”“焚稿”“断痴情”那基本是高鹗续书里的名场面。这个需求在实际场景里很有用——做文学研究的人可以用它辅助标注做在线阅读平台的编辑可以用它生成章节摘要普通读者用它当导读也方便。但“提取核心关键词”这件事难点不在算法本身而在“什么算核心”。在《红楼梦》这种文本里高频人名、地名、器物名会淹没真正的动作和事件比如贾母、宝玉这两个词几乎每回都会出现但对“这一回”的区分度极低。所以做这件事的核心思路不是单纯统计词频而是找一个能平衡“词频”和“区分度”的指标TF-IDF就是这么个东西。1.2 方案选型为什么没用 LDA 也没用 TextRank在动手之前肯定要纠结一下方案。市面上常见的文本关键词提取无外乎三种路线统计路线TF-IDF、词频、图路线TextRank、主题模型路线LDA。我当时选 TF-IDF 而不是另外两个原因有三条。第一LDA 适合做“主题归纳”而不是“关键词提取”。LDA 输出的是词的概率分布拟合的是“这篇文章在谈论什么主题”粒度太粗。比如某回模型算出“主题1家庭事务”你还是不知道具体是哪个事件。而且 LDA 需要人为指定主题数 k调起来主观性太强不同 k 值结果天差地别不太适合做批量自动化的章节导读。第二TextRank 的效果受文本长度影响很大而且更吃图构建时的共现窗口参数。分回文本长度不一从 3000 到 9000 字都有固定窗口会把短章和长章的连边数量搞得不可比导致有些回目提取出的词特别散乱。TF-IDF 对文本长度没有那么敏感因为它本质上是在每个文档内部做词频归一化再乘逆文档频率。第三TF-IDF 在 sklearn 里实现成熟、参数透明跑批处理非常稳。配合 jieba 分词百来行代码就能出一个基线结果后续调优的空间也大。说白了先用一套可解释性强的基线方法把流程打通后面再叠加 TextRank 或者词向量做融合心里才踏实。最终我的技术栈定成了Python jieba 分词 sklearn 的 TfidfVectorizer pandas 做结果整理。数据源用的是网上公开的《红楼梦》全文 TXT按回目切分好。整个流程跑完一百二十回每个回目输出 8-10 个核心关键词大概花了不到二十分钟效果比预想的好。2. TF-IDF 核心原理与中文文本预处理2.1 一个朴素但有效的基础假设词的重要性 在本文中多 在全书中少先从原理说起。TF-IDF 由两部分组成TF 是词频IDF 是逆文档频率。前者衡量词在当前文档中出现的频繁程度后者衡量词在整个语料库中出现文档数量的倒数取对数。公式是TF-IDF(t, d) TF(t, d) × IDF(t)其中 IDF(t) log(N / (1 DF(t)))N 是总文档数DF(t) 是包含词 t 的文档数。这里的关键思想用大白话讲就是一个词如果在某一回里反复出现但在其他回目里很少出现那它大概率是这一回的特征词。反过来像“说道”“只见”“贾母”这种每回都有的词DF 值很大IDF 接近 0权重被压得很低自然排不上号。但《红楼梦》的文风造成了一个现象很多高频动词在全书里分布均匀比如“笑道”“不敢”“起身”这些词在 TF 那一步得分不低但 IDF 会把它拉下来。真正能体现“分回焦点”的往往是具体名词——人名、物品、事件词。比如“猜谜”“螃蟹”“诗社”这类在某几回里高度集中IDF 会给它们很高的权重。2.2 中文分词难点古白话、人名歧义和词表构建中文和英文最大的不同是分词。英文天然按空格切词中文需要先分词分词结果直接决定了关键词提取的上限。jieba 默认词典是面向现代汉语的处理《红楼梦》这种古白话会有几个明显问题。其一古白话专名词在默认词典里缺失比如“栊翠庵”“妙玉”“蘅芜苑”这类地名、人名jieba 可能会切成“栊翠”“庵”“妙玉”甚至“蘅芜”“苑”这种碎片导致词频统计失真。其二同一个人名可能有多种称呼。贾宝玉在文里可以是“宝玉”“宝二爷”“怡红公子”林黛玉可以是“黛玉”“林妹妹”“颦儿”。如果不去归一化这些称呼会被当成不同词各自分散 TF 值导致真正的核心人物反而不突出。其三现代汉语词汇和古白话词汇混在一起。比如“丫鬟”在文里更多写成“丫头”“吃饭”更多写成“用饭”如果不做针对性的词表调整提取结果会偏向现代词不够贴合文本风格。针对这些问题我做了三件预处理先加载 jieba 自带词典再自定义追加一批《红楼梦》专有名词包括主要人物、字、号、居所、器物名。在分词结果上做同义词归一化把“宝二爷”“怡红公子”“宝玉”统一映射成“宝玉”把“林妹妹”“颦儿”“黛玉”统一映射成“黛玉”。这里要注意不是所有词都能粗暴合并比如“凤姐”和“王熙凤”在不同章节语境里的实体指向是同一个可以合并但“宝玉”和“通灵宝玉”指向完全不同合并了会乱套所以映射表要人工盯一遍。自定义停用词表。除了常见的“的”“了”“之”还要加入大量文言虚词和叙事套话比如“话说”“且说”“却说”“只见”“不知”等。这套停用词表是我手动从结果里一遍遍挑出来的后面会细说。2.3 没有绝对通用的停用词表只有不断迭代的停用词表这里插一句我在项目里的真实感受。网上有很多公开的“中文停用词表”但直接用往往效果很一般。因为停用词是高度语料相关的对新闻语料有效的表放到古白话小说里就差劲了。我的做法是先用基础停用词表跑一遍基线结果然后把每回关键词里那些明显没信息量的词挑出来加入停用词表再重跑。这个迭代我做了四轮。第一轮发现高频干扰词有“一个”“如何”“只见”第二轮发现“因”“便”“方”“且”等文言连词混进了关键词第三轮发现一些像“老太太”“太太”“姑娘”的称谓词虽然区分度低但偶尔进 Top第四轮又清了一批“话说”“不在话下”之类的过门套话。迭代大概能让结果质量提升一个档次。解决方法不复杂就是从输出反推词表但得有点耐心一遍全跑完再一起加不要看到一个改一次那样效率太低。3. 实操过程数据清洗、分词、TF-IDF 计算与结果整理3.1 数据准备从原始文本到一百二十个独立文档这一步看起来平平无奇其实最容易出鬼。我在网上找的《红楼梦》全文 TXT 版本有各种残次品有带校注者注释的有不带注释的有章节标题格式不统一的有的还混有脂评。我的清洗流程是这样的把原文里的空格、全角空白、换行控制符统一清掉只保留中文字符和少数必要标点。用正则表达式按“第X回”切分文本。注意有些版本写作“第一回”有些写作“第一回 ”后面带空格还有些写作“卷 第X回 ”我统一用re.split(r第[一二三四五六七八九十百零]回, text)但不直接 split而是先找到所有回目标题位置再按位置切片。每回的内容里可能残留回目标题和副题比如“贾雨村风尘怀闺秀”我把它们剔除了不然回目标题里的词必然在关键词结果里刷屏。最后一共得到 120 个文档存成每回一个 txt再读入 pandas DataFrame列名叫 “chapter” 和 “content”。这里我顺手统计了一下每回字数最长的一回有 9640 字最短的 4260 字字数差距接近两倍。这个统计对后面设置 TF-IDF 参数有帮助也让我意识到长度差异确实存在后面考虑要不要用归一化策略时心里有底。3.2 分词和文本向量化代码实现与参数取舍分词和向量化我用的是 jieba 加 sklearn 的 TfidfVectorizer。核心代码不长贴一段关键实现import jieba import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer # 假设 df 已经包含 chapter 和 content 两列 def cut_words(text): return .join(jieba.lcut(text)) df[cut_content] df[content].apply(cut_words) vectorizer TfidfVectorizer( max_features3000, ngram_range(1, 2), stop_wordsstop_words_list, sublinear_tfTrue ) tfidf_matrix vectorizer.fit_transform(df[cut_content]) feature_names vectorizer.get_feature_names_out()几个参数我当时斟酌了很久逐个说一下。max_features3000的意思是只考虑全部文档中 DF 值最大的 3000 个词。这能砍掉大量只出现一两次的冷门词和分词碎片。实际上对一百二十回的小说来说去掉虚词和超低频词之后特征词总数大概八千左右3000 是个比较合适的截断点。太小会漏掉重要事件词太大会让向量变得稀疏增加噪声。ngram_range(1, 2)允许词和相邻词的组合作为特征。为什么要开 bigram因为《红楼梦》里很多关键事件是固定搭配比如“葬花”“焚稿”“螃蟹宴”单独一个“花”或“稿”信息量太小bigram 能把这些组合词抓出来。实测开 bigram 之后比如 第二十七回“滴翠亭杨妃戏彩蝶 埋香冢飞燕泣残红”核心词里出现了“葬花”这个组合效果立刻比只看单个词好。sublinear_tfTrue这个参数很多人忽略。它把 TF 做了1 log(TF)的对数放缩防止某些回里同一个词反复出现导致 TF 值爆炸。比如某回里“酒”字出现 30 次不代表它比出现 15 次的词重要两倍对数放缩让这种差距变缓更符合直觉。实测下来开了这个参数后高频词不霸榜了低频但关键的事件词能浮上来。最后是 IDF 的计算方式。sklearn 默认的 smooth_idfTrue分子分母都加平滑项避免除零。这个默认值我保留没动因为文档数固定 120平滑与否对排序影响很小。3.3 结果输出哪些词能成为真正的“回目之眼”计算完成后从 tfidf_matrix 里取每一行的 Top 词输出成表格。我把每回取前 12 个然后人工抽样检查了十几个回目的效果。先看几个示例结果回数前 8 个核心关键词第二十七回葬花、黛玉、滴翠亭、宝钗、彩蝶、红玉、凤姐、帕子第四十回刘姥姥、贾母、行令、茄鲞、鸳鸯、笑话、杯子、酒令第六十六回尤三姐、柳湘莲、剑、鸳鸯剑、退婚、尤二姐、贾琏、定礼第九十八回黛玉、宝钗、宝玉、病、泪、断痴情、紫鹃、魂归、出嫁、狠心第二十七回的结果尤其漂亮“滴翠亭”和“葬花”分列两头正好对应这一回的两条主线扑蝶和葬花。第四十回“茄鲞”“行令”直接锁定宴饮场面和那句著名的“老刘老刘食量大如牛”。第六十六回“尤三姐”“鸳鸯剑”“退婚”精准命中刚烈女子自刎的名场面。第九十八回也就是“苦绛珠魂归离恨天”那一回“魂归”“泪”“断痴情”排得很靠前这说明只要预处理到位TF-IDF 完全有能力抓住叙事的情绪高潮。但也有失败的回目。最典型的是第一回“甄士隐梦幻识通灵 贾雨村风尘怀闺秀”提取出的关键词里全是“通灵”“顽石”“甄士隐”“贾雨村”“太虚幻境”这些超高频概念和“怀闺秀”相关的“娇杏”反而被淹没了。原因也简单第一回是全书的“楔子”信息密度极高概念词一个接一个TF-IDF 在这种超长密集文本上会把每个词都分配权重反而难以聚焦。这属于方法本身的局限我用后处理做了修正后面细说。3.4 同义合并和实体归一化的实现细节刚才提了一嘴同义词归一化这里展开说说。我的做法是在分词之后、向量化之前对 cut_content 的 token 序列做一次映射替换。alias_map { 宝二爷: 宝玉, 怡红公子: 宝玉, 绛洞花主: 宝玉, 林妹妹: 黛玉, 颦儿: 黛玉, 潇湘妃子: 黛玉, 琏二爷: 贾琏, 凤辣子: 王熙凤, 老祖宗: 贾母 } def normalize_tokens(tokens): return [alias_map.get(tok, tok) for tok in tokens]这个映射表要非常谨慎。比如“潇湘妃子”是黛玉的号合并是合理的“潇湘馆”是住所不能合并。再有“袭人”和“花袭人”可以合并但“花气袭人知昼暖”里的“袭人”是动词不能合并。虽然这句话在文中出现次数不多但映射表的构建原则就是“宁可遗漏不可错并”错了会引入新的混乱。另外一些人名在不同回里以姓氏称谓出现比如“贾母”有时叫“史太君”有时叫“老太太”。“老太太”在贾府语境里基本专指贾母但偶尔也指王夫人或邢夫人这种有歧义的我就不合并只保留“贾母”和“史太君”的同义映射。做完归一化再跑 TF-IDF最直观的变化是“宝玉”“黛玉”“王熙凤”这些关键人物的排名普遍上升了因为它们的各个别名 TF 值合并在一起。但同时也要防一手归一化后某些词的整体 DF 变大IDF 变小如果某个词本来是某回的别名集中出现的比如只有某回用了“潇湘妃子”这个号合并成“黛玉”之后反而可能被稀释掉。所以后来我做了一个变通保留原始别名作为独立特征但也在结果表里标注其对应的人名这样既不破坏 IDF 的区分度又方便人眼理解。4. 结果解读技巧与常见问题排查实录4.1 从关键词序列反推叙事结构三个解读维度提取出关键词之后不是结束而是开始。单看一个回目里的关键词列表容易迷失在词海里。我总结了三个实用的解读维度。第一是“回内对比”。把每回关键词按权重从高到低排观察前三个词和后几个词的差异。如果前三个都是人物名说明这一回偏向“人物驱动”主线可能围绕关系冲突展开如果前三个里有大量器物名或地点名那这一回更可能偏向“场景驱动”。比如第十七回“大观园试才题对额”关键词里“园”“匾”“对额”“贾政”“题词”占了半席一下就能判断这是空间与建筑描写为主的回目。第二是“回间对比”。把相邻几回的关键词放一起看能看出叙事节奏的推进。比如第六十三回到第六十五回关键词从“宝玉”“夜宴”“群芳”逐步过渡到“尤二姐”“尤三姐”“贾琏”“偷娶”“退婚”这说明叙事线从大观园日常转移到宁国府风波节奏由轻松转入紧张。这种视角对文学研究很有用可以用它来给全书做“叙事段”分割。第三是“人物关联度”。我不止算关键词还顺手统计了在每一回里“关键词与人物名是否共现”。具体做法很简单就是在 TF-IDF 矩阵里挑出人物名所在的列和事件词所在的列做相关性排序。比如第四十回里“刘姥姥”和“笑话”共现的权重很高可以推断这一回的笑点主要由刘姥姥制造。这个方法有点糙但对于快速验证叙事直觉已经够用。4.2 常见坑位盘点五个几乎必踩的问题整个项目从零到一跑了七八遍踩过的坑我按频次排了序列成表格供后来人直接避雷。问题现象原因解决方案回目标题刷屏“通灵”“怀秀”跑进前 5切分时没去掉回目标题行先识别回目标题并剔除再切正文同义词分散权重“宝二爷”“怡红公子”各自低频人称、号太多做别名映射但要防歧义文言虚词残留“便”“因”“方”进入Top词基础停用词表不够古白话特化四轮迭代扩充停用词表bigram 组合太僵“了的”“之处”这类无效组合默认bigram不够或参数开太大结合自定义词典再开bigram地名与人物混淆“蘅芜苑”被切成“蘅芜”“苑”jieba 默认词典缺古白话词自定义追加专名维护词典这当中最难的其实是第一个回目标题刷屏。我最初用正则直接按“第X回”切分没处理标题行结果切出来的第一回正文开头还是“甄士隐梦幻识通灵”这七个字直接霸占当回 TF 值前列。后来我把切分逻辑改成先定位所有回目标题的起止位置然后把标题从正文里替换为“”再分词问题才消失。还有一个小坑是编码问题。很多公开 TXT 是 GBK 编码用 UTF-8 读会直接乱码。读取时我统一encodinggb18030做兼容把错误处理设成errorsignore然后再转存为 UTF-8 的干净版本。这个坑虽然小但能让人白忙活一小时。4.3 超长回目和信息密集回目的后处理方法刚才提到第一回效果不好这也是 TF-IDF 方法的典型痛点当一篇文档里的各个信息点都重要时TF-IDF 不知道该怎么“侧重”只能平均分配权重。我试过几种补救方法效果不一这里分享一条我觉得可行的路线。先做“片段滑动窗口”把每个回目的文本按 200 字一个窗口切成段分别在窗口级做 TF-IDF再把窗口排名结果做聚类聚合。这样能缓解长回目内部的语义漂移。不过 120 回的文档量不大窗口滑动后文档数膨胀到上万我当时为了跑得快只对个别超长回目做了这个处理没有全量启用。再就是加一个后处理规则从候选词里剔除“与全书所有回目共现频率超过 70%”的词。虽然这些词的 TF 可能很高但它们不可能是某回的特征词。比如“宝玉”在 120 回里出现次数为 113 回这个 DF 太高了即使它是某回的关键人物名也不具备区分度。但这种词又不能一概剔除因为有些回目里“宝玉”确实是事件核心删了就丢了主角。所以我的做法是把这些高频词降低权重而不是直接删设置一个惩罚系数DF 100 的词权重乘以 0.6DF 在 80-100 之间的乘以 0.8。实测惩罚后人物名与事件词的平衡感更好例如第一回里名场面相关的“识通灵”“怀闺秀”这类事件词排名上升了。4.4 如何选 Top N 关键词不是越多越好Top N 的数量直接关系到可读性。我最初输出 Top 20结果每回的关键词表格长得像天书第 15 个词开始出现一堆没有叙事意义的低权重词比如“众人”“一面”“一面又”看着枯燥也无助于导读。后来我做了个实验把 N 从 5 调到 20人工对比十个回目的结果发现 8-12 个是一个甜蜜区间。少于 8 个摘要粒度太粗比如某回只给了人物名事件动词没浮出来多于 12 个噪声明显上升。最终我统一输出每回前 10 个词再在权重超过平均值 1.2 倍的关键词里额外选 2 个时报词汇这个策略基本稳定。这里也要提醒N 的选取不能完全拍脑袋最好基于人工抽样看几回再定。不同语料的甜蜜区间不一样比如分析新闻标题可能 5 个就够分析学术摘要15 个也合理。关键是每回结果的“可读性一致”。5. 后续扩展从单纯关键词到章节“迷你摘要”5.1 关键词串联成句给阅读器用的浅层摘要TF-IDF 提取的关键词是碎片如果想做成读者可读的迷你摘要还有一个后处理步骤。我的想法是既然每回有 10 个核心关键词那就用这些词去原文里找片段把包含 3 个及以上关键词的句子抽出来按出现顺序拼接就能得到一段原始文本构成的迷你摘要。实操时我先按句号、问号、感叹号把每回切成句子然后筛出含核心关键词数量 3 的句子按在原文里的顺序排好。这个操作在第二十七回的效果让我很惊讶——拼接出来的句子几乎就是“黛玉葬花”和“宝钗扑蝶”的原始描写片段完全可以当导读用。sentences re.split(r[。], chapter_text) key_sentence [] for sent in sentences: hit sum(1 for word in keyword_list if word in sent) if hit 3: key_sentence.append(sent) summary 。.join(key_sentence[:5])这一招有个附带的好处它既保留了原文文风又能保证摘要内容不是模型瞎编的。对文学类文本来说保持原汁原味非常重要。后来我用这个方法给一百二十回都生成了迷你摘要人工抽查了十回基本都能覆盖该回主要情节。除了极个别回目因为原文描写分散、一句话里难凑三个关键词摘要会拼出一些跳跃感但整体可用性已经远超纯关键词列表。5.2 多方法融合TF-IDF 作为基线的意义最后说一点关于方法选型的思考。TF-IDF 这个算法 1972 年就提出来了快六十年了今天看来很简单甚至有人觉得过时。拉一个词向量模型出来可能显得更“先进”但我做这个项目的体会是在文本分析里“可解释的简单方法”往往比“不可解释的复杂模型”更值得先做。因为 TF-IDF 的每一步都能落到具体词和具体权重上出了奇怪结果可以直接反查对于古代文本这种噪声极高的语料尤其重要。我现在把整个流程沉淀成了一个复现性较强的代码库输入是任意长文本集合输出是每个文档的 Top 词表和句子级迷你摘要。后续要做的话可以在 TF-IDF 结果之上叠加预训练语言模型做关键词消歧比如把“葬花”和“黛玉”之间的语义关联进一步强化。不过那是更靠后的优化方向基线阶段我仍然推荐老老实实用 TF-IDF 把数据和预处理打磨干净毕竟后面所有花哨模型的效果都建立在这一步的地基上。按我个人的经验跑这类项目最大的收获反而不在算法本身而在对“文本差异”的敏感度。《红楼梦》每一回的语言风格、叙事密度、词场分布差异巨大而这些差异在 TF-IDF 的权重里体现得很分明。看多了结果表你对“什么词才是区分性的”会形成直觉这直觉在做任何 NLP 项目时都好用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询