简介:这是一份基于Python的文本挖掘高分课程项目,以日本推理作家东野圭吾小说集为语料,完整覆盖文本读取、清洗、分词、词频统计、情感分析与可视化展示等典型环节,适合数据挖掘、大数据、信息管理等相关专业学生用于期末大作业、课程设计或毕业设计参考。项目代码注释详细,关键步骤留有说明,新手也能较快理解并复现;压缩包大小约25.78MB,主要包含可直接运行的Python源码和配套文档说明,文档对环境配置、依赖安装及操作流程均有交代。整体系统功能设计完整,界面与结果展示直观,项目经过调试可稳定运行,可作为作业直接部署或扩展改造。目前已有519人学习/下载,完成思路与实现方式具有较高参考价值,是撰写同类文本挖掘任务的实用范本。
1. 基于 Python 数据挖掘大作业:东野圭吾小说集文本挖掘能帮你交出一份高分项目
期末前一周,我把“东野圭吾小说集文本挖掘”写成了 98 分的 Python 大作业。这套项目不是单纯做词频统计,而是从语料清洗、jieba 分词、词云展示一路做到 LDA 主题分析和情感曲线,源码带注释,还配了文档说明。
当时我拿到需求心里没底:txt 乱码、章节目录混在正文、人名被切得稀碎,这些坑全是逐条踩过才填平的。期末大作业、课程设计、毕设要交一个能跑、能讲、有输出的数据挖掘项目,这套资源很合适;新手按文档能跑通,熟手能改参数分析自己想看的小说。下面我把实现思路、关键代码、参数含义和踩坑点拆开讲,方便你判断这套资源值不值得下。
2. 把小说文本变成干净语料:编码识别、正则清洗与去重
拿到文本挖掘项目,我给自己定下的第一条规矩:先清理语料,再谈模型。直接对原始 txt 跑 jieba,词频统计结果里前十名多半是“的、了、我、他、在、不”,正文里的“第X章”也会混进来,主题模型就更不用想,基本被停用词淹没。这个项目里的小说文本我拆包之后是散装的 txt,每个文件一本或一卷,命名还算规整,但内容质量并不平均,有爬虫站加的页脚、错别字、全角空格,还有重复段落。所以第二章先解决三件事,编码、清洗、去重,这三件事不做扎实,后面所有统计都是沙地上盖房子。
2.1 先摸清语料结构:目录安排与编码探测
第一步不是直接读文件,而是搞清楚目录结构和文件编码。历史教训:我用默认 utf-8 打开一个 GBK 文件,打印出来全是“锟斤拷”,后面清洗逻辑再好也没有用。常见做法是先写一个探测脚本,遍历目录,读取每个 txt 的前几千字节,交给 chardet 判断编码,再把结果写到一个对照表里,这样不用在清洗阶段反复试。
import os import chardet novel_dir = "novels/" for fname in sorted(os.listdir(novel_dir)): if not fname.endswith(".txt"): continue path = os.path.join(novel_dir, fname) with open(path, "rb") as f: sample = f.read(5000) result = chardet.detect(sample) print(f"{fname}: {result['encoding']} | 置信度 {result['confidence']}")这里用二进制方式读取,避免文件内容里夹杂非法字符时直接触发 UnicodeDecodeError;只读 5000 字节,也就是几页内容,足够判断绝大多数文本编码,又不至于把一个 20MB 的大文件整个读进内存。chardet.detect 返回的字典里 encoding 和 confidence 最重要,confidence 接近 1 时可以直接信。如果结果显示 gb2312,统一按 gbk 读也问题不大,因为爬虫站的 txt 实际编码经常在 GBK 和 GB2312 之间混用。
探测出来后,最好把每个文件的编码记下来,或者在读取时直接做一次统一转换。我会把所有文件重新存成 utf-8 的干净副本,放到clean/目录,这样后面的处理脚本不用反复判断编码,也方便搬进 Jupyter Notebook。这个过程用循环跑一遍就行。
import os import chardet novel_dir = "novels/" clean_dir = "clean/" os.makedirs(clean_dir, exist_ok=True) for fname in sorted(os.listdir(novel_dir)): if not fname.endswith(".txt"): continue path = os.path.join(novel_dir, fname) with open(path, "rb") as f: raw = f.read() enc = chardet.detect(raw)["encoding"] or "utf-8" if enc.lower() in ["gb2312", "gbk"]: enc = "gbk" text = raw.decode(enc, errors="ignore") out_path = os.path.join(clean_dir, fname) with open(out_path, "w", encoding="utf-8") as f: f.write(text)写入时统一用 utf-8,因为这是跨平台最稳的编码。errors="ignore"会丢掉无法解码的字节,可能损失个别符号,但能保住正文主体。这里有个细节:如果源文件是带 BOM 的 utf-8,直接按 utf-8 读会把 BOM 变成行首的不可见字符,之后正则行匹配会莫名失败。所以探测到是 utf-8 时,还要看一眼文件头是不是\xef\xbb\xbf,是的话就用utf-8-sig解码。这个检查代码很少,但能避免一种很难发现的脏数据。
2.2 正则清洗正文:去章节名、去转义字符、合并断行
编码统一之后,文本里还带着爬虫站的噪声。我习惯按行清洗,因为小说正文和掐头去尾的杂质基本都是按行出现的。最常见的杂质有四类:章节标题、空行、页脚广告、全角空格。正则在这一步是主角,但用不好会误伤正文。
import re def clean_novel_text(text): lines = text.splitlines() cleaned = [] for line in lines: line = line.strip() # 去掉全角空格和行首尾空白 line = line.replace("\u3000", " ") # 跳过明显的章节标题,例如“第一章 白夜行”或“Chapter 1” if re.match(r"^第[0-9一二三四五六七八九十百千零]+\s*[章回节]", line): continue if re.match(r"^Chapter\s+\d+", line, re.IGNORECASE): continue # 清理常见页脚和广告痕迹 if "手机用户" in line or "txt下载" in line.lower(): continue # 过滤太短的无意义行 if len(line) < 2: continue cleaned.append(line) return "\n".join(cleaned)这里的关键是正则做行首匹配,而不是全局替换。我见过有人用re.sub(r'第.*章', '', text),结果把正文里“第一个人”截掉,数据直接废了。re.match从行首开始匹配,后面用\s*吸收空格,再限定落在“章/回/节”这几个字上,误伤概率小很多。如果下载的文本里是“第一话”这种字眼,把结尾改成[章回节话]就行。页脚过滤行是个黑匣子,代码里写死了一部分,自己拿到项目后要按文本里实际出现的噪声调整。re.IGNORECASE用于容忍 Chapter 的大小写变体,否则大写开头的章节头会漏掉。
还有一类噪声是英文书名号和链接,可以在跑清洗前用re.sub(r'http\S+', '', line)提前清掉。清洗粒度要讲平衡:东野圭吾的原文里偶尔有英文短句,直接全部过滤到中英文标点之外可能会丢信息,所以这一版清洗只删非文本符号,不删字母数字。清洗后保存的是纯文本正文,后续分词脚本只依赖这一份中间文件,改参数时不用重新跑全量清洗。
2.3 去重与抽样:用集合做归一化,判断是否重复
爬虫类 txt 经常出现重复段落,尤其是同一本书被合并了好几个版本。去重我按行做,但不会直接拿原文行去重,因为行尾空格、全角半角不一致会导致明明一样的内容被当成两行。先把行归一化,再去查集合,既快又不会误杀太多。
def dedup_lines(text): seen = set() unique = [] for line in text.splitlines(): line = line.strip() if not line: continue norm = re.sub(r"[\s\u3000]+", "", line) norm = norm.lower() if norm in seen: continue seen.add(norm) unique.append(line) return "\n".join(unique)归一化规则是去掉所有空白和全角空格并转小写,这样“Hello World”和“hello world”会判定重复。代价是英文大小写区别被抹掉了,但英文在小说正文里占比低,可以接受。如果把seen换成Counter,还能统计重复次数,大作业报告里能多写一小节“数据情况”,导师会觉得你考虑了数据质量。
如果重复发生在不同文件的整段文本,行级去重就无能为力了。常见做法是计算每段文本的 simhash 或 MinHash,但这属于超纲操作,期末大作业不要求。我一般用最土的办法:把两个文件各自的分词结果做个 top 100 交集,超过一半就怀疑是重复文本,人工抽查。这个办法很糙,但能拦住最明显的重复。清洗和去重完的语料,我会按文件统计一次字符数和行数,记录下来。后面报告里写“总语料 X 万字,去重 Y 条”,这个数字是实打实算出来的,不是拍脑袋。
3. jieba 分词与停用词过滤:词频统计的细节和参数
语料干净后,真正的文本挖掘才刚刚开始。东野圭吾小说最让人头疼的是人名,比如“加贺恭一郎”“汤川学”,默认 jieba 词典很可能把它们拆成单字或两字词。分词结果一旦不对,后面词频、词云、主题模型全都不对。所以这一章先把分词调好,再做词频统计,最后落到能写进报告的结果文件。
3.1 自定义词典的重要性:把人名地名救回来
在项目里我维护了一个user_dict.txt,格式是 jieba 自定义词典标准格式:每行一个词,后面跟词频和词性,中间用空格分隔。词频数字不是越大越好,几百就够,作用只是告诉分词器“这是一个完整概念”。词性可填可不填,填了可以在后续做词性过滤时用。这个文件是分词环节最重要的配置,没有它,整本小说的角色名都会被切碎。
import jieba jieba.load_userdict("user_dict.txt") print(jieba.lcut("加贺恭一郎继续调查白夜行里的真相"))load_userdict加载后整个进程都生效,不用对每句重复添加。如果只是临时调试一个词,用jieba.add_word('白夜行', freq=100)更方便,但正式脚本里建议统一放到 user_dict.txt 里,方便评审老师看。我这份 user_dict.txt 里大概收了三五十个人名和书名,都是从东野圭吾小说角色列表里整理出来的,比如加贺恭一郎、汤川学、桐原亮司、唐泽雪穗。词频都给的 100,词性用 nz,也就是专有名词。
加载词典后,最好抽样打印几十句分词结果,肉眼看人名是否完整。这一步不是玄学,因为 jieba 是基于统计成词的,词典只是提高优先级,不是强制切分。遇到确实切不开的词,可以用jieba.suggest_freq调整单个词的频率。我很少调,因为大作业不需要做到十全十美,能稳住主要人名就够了。
# 如果某个词始终被拆分,可以单独调整频率 jieba.suggest_freq("桐原亮司", tune=True) print(jieba.lcut("桐原亮司和唐泽雪穗"))suggest_freq会让 jieba 重新计算该词在统计模型里的优先级,tune=True表示根据当前句子动态调整。这里的“桐原亮司”如果不做处理,很容易被切成“桐原/亮司”,加载词典后基本能保住。
3.2 停用词表不是越全越好:过滤与保留的平衡
停用词表是文本挖掘最容易翻车的地方。网上流传很多停用词表,哈工大版、百度版、机器之心版,各有侧重。它们用来过滤“的、了、把、被”这类高频虚词非常有效,但直接套到小说上有个问题:很多常见停用词在小说里是有叙事作用的。比如“夜”“人”“手”,在普通语料里是停用词候选,但在东野圭吾的文本里可能是高频词,删掉之后主题模型就会把“推理”变得更加模糊。所以我的做法是:先加载通用停用词表,再根据本书语料把误杀的词捞回来。
stopwords = set() with open("stopwords.txt", encoding="utf-8") as f: for line in f: w = line.strip() if w: stopwords.add(w) # 根据语料复查后,把部分词从停用词表中豁免 for w in ["人", "夜", "手", "世界", "女人"]: stopwords.discard(w)这里用discard而不是remove,因为不确定通用表里是否包含这个词,discard不存在时不会抛异常,脚本跑起来更稳定。过滤逻辑里,除了停用词,还会过滤纯数字、纯标点和单字符。注意单字符不是绝对要过滤,比如“夜”在这套小说里单字也有意义,如果你强行过滤单字,词云里就少了氛围词。我把单字过滤写成一个参数keep_single_char,方便在复现时切换。
def tokenize(text, keep_single_char=False): words = jieba.cut(text) result = [] for w in words: w = w.strip() if not w: continue if w in stopwords: continue if w.isdigit(): continue if not keep_single_char and len(w) == 1: continue result.append(w) return resultjieba.cut默认用精确模式,HMM参数默认 True,对新词识别有帮助。如果你希望结果完全可复现,建议把 jieba 版本锁死,因为不同版本词典更新会影响切分。项目说明里一般会写依赖版本,比如 jieba==0.42.1,固定版本是文本挖掘的后悔药,改版本前先备份当前结果。
3.3 词频统计与 Top N 输出:从 Counter 到 DataFrame
过滤完之后,词频统计就变得很简单。我用 Counter 累计所有小说分词结果,再取 Top N 做展示。对大作业来说 Counter 已经足够快,语料十几万字秒出,不需要上 Spark 之类的重型工具。
from collections import Counter counter = Counter() for doc in docs: # docs 是每一部小说的分词结果,list of list counter.update(tokenize(" ".join(doc))) top = counter.most_common(50) for rank, (word, count) in enumerate(top, 1): print(rank, word, count)update接收可迭代对象,所以把每部小说的分词结果先拼成字符串再喂进去也行,但直接列表更省内存。most_common(50)的 50 不是固定值,词云用 top 200,报告表格用 top 30,避免前 50 里全是同一批虚词。如果看到 Top 1 是“没有”,别急着调停用词表,先确认过滤条件里的长度限制是否生效。
把结果存成 CSV 也是给大作业写报告用的。Excel 打开不能乱码,所以编码用utf-8-sig而不是utf-8。这一点很多新手没注意,脚本在 PyCharm 里打印正常,一写到 CSV 就乱。
import pandas as pd df = pd.DataFrame(top, columns=["word", "count"]) df.to_csv("top_words.csv", index=False, encoding="utf-8-sig")index=False去掉 DataFrame 默认的序号列;编码utf-8-sig会写一个 BOM,Excel 看到 BOM 才知道用 UTF-8 解析,否则默认按本地编码打开,中文直接变乱码。这个细节在评审演示时能救你一命。另外,词频表里除了统计词频,我还留了一列“是否在停用词表内”,用布尔值标记,这样导师能看出你不仅会跑 Counter,还考虑过停用词对结果的影响。
4. 从 LDA 主题模型到情感曲线:代码实现与可视化
词频统计只是文本挖掘的及格线,导师通常最关心的更深一层问题是:这些小说的主题是什么,角色和情节的情绪有没有走向。我在这个项目里加了 LDA 主题模型和情感曲线,两部分各解决一个问题:前者把几十万字的语料压缩成几个可解释的主题词;后者把整本的叙事节奏量化出来。这一章代码不长,但参数设置直接影响结果,值得细看。
4.1 工具选型:gensim 还是 sklearn
LDA 的实现里 gensim 最常被提到,但大作业环境里 sklearn 更省心。gensim 的 LdaModel 对输入格式要求多,语料需要经过 Dictionary、bow_corpus 两轮转换,版本 3.x 和 4.x 的 API 还变过,踩坑成本高。sklearn 的 LatentDirichletAllocation 接口类似普通分类器,输入就是一个文档-词频矩阵,更适合快速出结果。所以我选了 sklearn。
from sklearn.feature_extraction.text import CountVectorizer from sklearn.decomposition import LatentDirichletAllocation vectorizer = CountVectorizer( tokenizer=lambda x: x.split(), max_features=5000, min_df=2, max_df=0.8 ) doc_term = vectorizer.fit_transform([" ".join(words) for words in docs]) lda = LatentDirichletAllocation( n_components=5, random_state=42, max_iter=100, learning_method="batch" ) lda.fit(doc_term)这里tokenizer直接按空格切词,是因为前面的 tokenize 函数已经把句子切成词并过滤干净了,原始文本并没有传进来。max_features=5000限制词表大小,词表越大训练越慢,3000 到 5000 对大作业够了。min_df=2表示至少在 2 个文档中出现的词才保留,去掉只在某一本里冒一次的噪声词;max_df=0.8过滤掉出现在 80% 以上文档里的词,这类词往往是通用词,会干扰主题区分。learning_method="batch"比在线更新更稳定,代价是内存稍高,几本小说完全在意料之中。random_state=42固定随机种子,否则 LDA 结果每次跑都可能不一样,导师问起来很难解释。
4.2 主题数怎么选:perplexity 与主题连贯性
LDA 的 n_components 是文本挖掘里最像玄学的参数。官方文档说困惑度越低越好,实际跑下来却发现主题数越大困惑度越小,8 个主题的方案困惑度最低,但主题词读起来一片混沌,根本分不出哪是哪。所以我的习惯是:把困惑度当参考,把主题词可读性当最终标准。
for n_topics in [3, 5, 8, 10]: lda_model = LatentDirichletAllocation( n_components=n_topics, random_state=42, max_iter=100 ) lda_model.fit(doc_term) perplexity = lda_model.perplexity(doc_term) print(f"主题数 {n_topics}: 困惑度 {perplexity:.2f}") for idx, topic in enumerate(lda_model.components_): top_words = [vectorizer.get_feature_names_out()[i] for i in topic.argsort()[:-9:-1]] print(f" Topic {idx}: {' '.join(top_words)}")lda_model.components_是每个主题的词分布,形状是(n_topics, n_words)。argsort()[:-9:-1]取每个主题权重最高的 8 个词索引,再通过get_feature_names_out()还原成词。运行后如果 3 个主题分别是“案件调查”“人物关系”“情感纠葛”,5 个主题还能多分出一个“家庭/童年”,说明主题数合适;如果 5 个主题里有两个主题词几乎重合,就要把主题数调回 3 或 4。不要迷信困惑度的拐点,主题数选择最终是解释性问题。跑出来的主题词我会整理成一张表,放在报告里,每个主题取 8 个词加一段人工命名,比如“主题 2:亲子关系”。
4.3 情感曲线:SnowNLP 打分与窗口平滑
情感曲线是我觉得这套项目里最出彩的部分。东野圭吾的小说整体都偏暗,但亮司和雪穗那条线到后半段情绪张力完全不一样。用情感分数量化出来,比口头描述更有说服力。SnowNLP 是基于评论语料训练的情感模型,给小说打分有偏差,但作为大作业完全够用,重点在展示趋势。
import re from snownlp import SnowNLP def chapter_sentiment(chapter_text): # 按句子切分,避免长文本造成计算过慢 sentences = re.split(r"[。!?\n]", chapter_text) scores = [] for s in sentences: s = s.strip() if len(s) < 2: continue try: scores.append(SnowNLP(s).sentiments) except Exception: # SnowNLP 对个别特殊字符串会抛异常,直接跳过 continue if not scores: return 0.5 return sum(scores) / len(scores)按句子切分而不是整章直接打分,是因为 SnowNLP 内部对长文本的处理时间不是线性的,传一整章进去又慢又容易出边界问题。先过滤长度小于 2 的句子,再逐句打分取平均,得到一个 0 到 1 的章节情感分。0.5 算中性,越接近 0 越负向,越接近 1 越正向。try/except不是多此一举,SnowNLP 在输入只有标点或特殊字符时会在内部抛 UnicodeEncodeError,这种错误不影响整体结果,直接跳过即可。
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] plt.rcParams["axes.unicode_minus"] = False plt.plot(range(len(scores)), scores, marker="o", markersize=3, linewidth=1.2) plt.axhline(0.5, color="gray", linestyle="--", label="中性线") plt.xlabel("章节") plt.ylabel("情感分") plt.title("东野圭吾小说情感曲线") plt.legend() plt.show()font.sans-serif必须放在画图前,否则 matplotlib 找不到中文字体,标题变成方块。axes.unicode_minus=False让负号正常显示,否则情感分低于 0 时,刻度上的负号会变成方框。排序靠前的章节序号不一定对应下载文件顺序,跑之前先确认章节列表是按时间顺序排的。情感曲线画完后,我还会用移动平均做一次平滑,方法是把前后 5 个章节的分数平均,作为当前点的输出值,这样曲线不会因为某一章特别短而剧烈抖动。
5. 复现时最容易翻车的五个坑:环境、编码与展示排查
这套项目我在提交前前后后跑过不下十遍,真正让我翻车的往往是环境、编码和可视化这三类。下面把最典型的五个坑按现象、原因、解决三部分写清楚,你复现时命中任何一个都能快速定位,不用把整个流程重新排查一遍。
5.1 环境与依赖:装错包、版本不一致、字体缺失
坑 1:import jieba直接报 ModuleNotFoundError
现象:下载项目后在终端运行python main.py,立刻提示ModuleNotFoundError: No module named 'jieba'。
原因:这个项目依赖 jieba、chardet、snownlp、scikit-learn、wordcloud 等第三方库,压缩包不会把依赖打包进去,需要自己安装。很多新手用pip install jieba后仍然报错,是因为系统里同时存在 Python2 和 Python3,或者装了多个虚拟环境,pip 装到另一个环境去了。
解决:使用python -m pip而不是裸pip,这样装到的是当前python解释器对应的环境。
python -m pip install -r requirements.txt如果找不到 requirements.txt,就手动安装:
python -m pip install jieba chardet snownlp scikit-learn gensim matplotlib wordcloud pandas装完用python -c "import jieba; print(jieba.__version__)"确认版本。如果输出正常,再跑主脚本。多环境共存的机器上,这一步能省下半小时。
坑 2:wordcloud 生成词云时中文全部变成方块
现象:wordcloud 跑出图片很快,词云轮廓正常,但所有中文文字都显示为小方块。
原因:wordcloud 自带英文字体,如果让 WordCloud 自己选字体,中文字符没有对应字形,就会渲染成方块。必须在 WordCloud 里显式指定中文字体文件路径。
解决:找一个系统的中文字体文件,Windows 常用C:/Windows/Fonts/simhei.ttf,macOS 常用/System/Library/Fonts/PingFang.ttc,Linux 常见/usr/share/fonts/wqy-microhei/wqy-microhei.ttc。代码里这样指定:
from wordcloud import WordCloud wc = WordCloud(font_path="simhei.ttf", width=800, height=600)把字体路径写成一个独立变量,方便切换系统。最好把字体文件复制到项目根目录,这样换机器跑也不会因为系统缺字体而失败。
5.2 编码与统计结果:乱码、正则误删、LDA 不稳定
坑 3:读入 txt 后输出一堆“锟斤拷”
现象:脚本能跑,所有 print 和写出的文件里中文全成了乱码,尤其 Windows 系统高发。
原因:下载的原始 txt 是 GBK/GB2312 编码,但读取时强制用了 utf-8,解码出来的字节流对不上,Python 在终端和文件写入时再转码就变成了“锟斤拷”。
解决:回到第二章的编码探测步骤,先用 chardet 探测,再按探测结果解码。写入结果文件时统一用encoding="utf-8-sig"。如果探测结果是 GB2312,统一改成 GBK 再试一次,GB2312 编码的文本用 GBK 解码通常更稳。我习惯把一个文件的读取编码和写入编码都打印出来,方便复查。
坑 4:正则清洗误删了正文里的关键句子
现象:清洗后检查语料,发现小说正文出现大面积断行,甚至“第一个人”这种词被删掉。
原因:用re.sub(r'第.*章', '', text)这类全局正则会匹配从“第”到“章”之间的任意内容,遇到“第一个人”也会匹配到一个片段,正则没限定行首,导致正文被切片。
解决:改成逐行读取并用re.match(r'^第[0-9一二三四五六七八九十百千零]+\s*[章回节]', line)判断,确认匹配的是行首的章节标题。清洗后要随机抽样 10 到 20 个段落人工看一眼,不要相信正则跑完就万事大吉。文本清洗本来就是手工活和数据工程的重合地带,想靠一条正则吃遍所有 txt 是不可能的。
坑 5:LDA 两次运行结果完全不一样
现象:同一次报告里跑两次 LDA,第一次 Topic 1 是“案件、调查、线索”,第二次 Topic 1 变成了“母亲、回忆、房间”,没法解释。
原因:LDA 是随机算法,初始节点是随机选择的,不固定随机种子,每次都会得到不同结果。
解决:在 LatentDirichletAllocation 中设置random_state=42,并且把max_iter设成相同值。固定之后,同一份语料在同一版本环境下结果可复现。如果用的是 gensim,注意 gensim 4.x 的random_state参数写法,直接查官方文档对应版本。提交报告前,我会把 LDA 参数和随机种子也写进报告附录,导师看到这一步就知道你懂可复现性的重要性。
6. 把词云做成风格画像:验证文本挖掘结果的通用技巧
词云是文本挖掘大作业里最直观的加分项,但只给一张词云图还不够,说清楚词云背后的验证逻辑才真正有价值。我处理这套项目时养成了一个习惯:跑完主流程,再拿一个对比语料做同样的处理,把东野圭吾的 top 词和对比文本的 top 词做重合度分析。如果重合率太高,说明你的停用词表还不够猛;如果重合率明显低,说明这套流程确实捕捉到了文本风格。
from wordcloud import WordCloud wc = WordCloud( font_path="simhei.ttf", width=800, height=600, background_color="white", max_words=200, collocations=False ).generate(" ".join(all_words)) wc.to_file("wordcloud.png")max_words=200表示只展示权重最高的 200 个词,词云不是越满越好,太满会糊;collocations=False让 wordcloud 不要自动组合二元词组,否则“白夜/行”这类相邻词可能被拼成“白夜行”,和 jieba 词典冲突。生成词云后,对比语料也跑一遍同样的词频统计,然后算重合率:
top_a = set([w for w, _ in counter_a.most_common(100)]) top_b = set([w for w, _ in counter_b.most_common(100)]) overlap = len(top_a & top_b) / 100 print(f"重合率 {overlap:.2%}")这里的 100 是 top 词数量,你可以跑 50、100、200 三个粒度,把结果画成一条折线,报告里能多一张图。对比语料随便选一个风格差异大的文本就行,我习惯用一本同类型的推理小说或一本文艺小说。把词云图和对照表放到实验部分,前面再放上 LDA 主题词和情感曲线,整个项目的证据链就完整了。
这套资源的压缩包里,代码和文档是配套的,docs 目录里有环境说明和运行步骤,下载后先按文档过一遍再改参数,比直接敲代码稳得多。从那以后,我每次做文本挖掘项目都会强制走一遍固定流程:先探测编码、固定随机种子、锁定 jieba 版本,再做任何统计。只要这三件事做在前面,后面翻车的概率直接砍掉一大半。希望帮到你。
本文还有配套的精品资源,点击获取