简介:围绕东野圭吾小说集展开的Python文本挖掘期末大作业项目,面向数据挖掘课程设计、毕业设计及需要完整实战案例的初学者。项目利用真实小说语料,涵盖文本预处理、词频统计、情感分析等常见挖掘流程,代码附详细注释,新手也能读懂并二次开发。压缩包采用zip格式,大小约25.78MB,内含Python源码和配套文档说明,文件组织清晰、部署简单,经过严格调试可直接运行。目前已有518人浏览学习,适合作为课程作业或毕业设计的参考范本。通过该项目,读者可以系统掌握从文本获取、清洗、分词到特征提取、可视化展示的完整分析思路,并借助代码注释理解每一步的实现逻辑,快速提升文本挖掘实战能力,也可为后续毕业设计提供可复用的代码框架。
1. 东野圭吾小说集文本挖掘:一门把三百本小说变成数据的必修课
如果期末大作业只让你“用Python做个数据分析”,而你又恰好是个推理小说迷,那拿东野圭吾的小说集下手几乎是作弊级的选择。它同时满足三个硬条件:语料获取门槛低、文本特征足够鲜明、可做的分析维度极多。这不只是“把小说做成词云”的玩具项目,而是从爬虫、文本清洗、分词、TF-IDF关键词提取、LDA主题模型到情感分析的一整条数据挖掘流水线,能完整覆盖课程要求的“数据获取—预处理—特征工程—建模分析—可视化”全链路。
这门课的核心难点从来不是“有哪些算法”,而是“拿到一堆txt之后,第一步该干嘛、参数调到什么程度、结果怎么解读才不像是凑字数”。文本挖掘项目的通病是:爬虫代码能跑到凌晨三点,最后却只交出一张词云图,拿了个及格分。所以这里要讲的,不是怎么把爬虫写得多优雅,而是怎么组织代码结构、选对分析路径、避开那些让老师一眼看穿“这作业在水”的坑。
适合谁来读?正在为Python数据挖掘课设发愁的同学,以及想用文本挖掘方向作为毕业设计起点的朋友。这篇笔记会从数据采集一路讲到结果解读,中间穿插具体的代码和参数,每个环节都给出现在就能抄的版本。
2. 数据从哪来:构建东野圭吾作品语料库的三种方案与选型
2.1 为什么语料获取方案直接决定项目成败
文本挖掘的头号天坑不是算法,是数据。用错语料,后面所有分析全是空中楼阁。对于东野圭吾作品,业内最常见的做法有三个:从公开电子书网站爬取、使用GitHub上已有的爬虫项目二次开发、手动下载txt后本地整理。第一个方案最“像”数据挖掘,能展示爬虫能力,但反爬策略、乱码、版权风险三者要同时面对;第二个方案效率最高,很多开源项目已经把《白夜行》《嫌疑人X的献身》等核心书目整理成了干净的txt,但风险是拿到的是别人处理过的数据,作业答辩时一问三不知;第三个方案最笨但最可控,适合只交代码不交数据的作业场景。
我的建议是走“半自动”路线:自己写爬虫,但目标站选那些结构简单、无JS渲染的公开文本站,同时把爬虫和解析逻辑做成独立模块,方便在答辩时清晰讲出“数据是怎么来的”。这既保留了完整的技术链路,又不需要硬刚反爬。
2.2 爬虫模块的具体实现:请求、解析、落地
公开文本站的页面结构一般规律性极强:小说目录页是一堆链接,每个链接指向一个正文页,正文页里存在一个包含所有段落的div容器。写代码时不要用Selenium那种重武器,requests加BeautifulSoup就够了,处理速度更快也更稳。核心代码长这样:
import requests from bs4 import BeautifulSoup import time import os HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } def fetch_novel_text(url: str) -> str: """ 获取单个小说正文页的纯文本 常见页面结构: <div id="content"> ... 段落 ... </div> """ try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = resp.apparent_encoding # 关键: 多数站点是gbk或utf-8,apparent_encoding更稳 resp.raise_for_status() except Exception as e: print(f"[错误] 请求失败 {url}: {e}") return "" soup = BeautifulSoup(resp.text, "html.parser") content_div = soup.find("div", id="content") if content_div is None: # 有的站点用class而不是id content_div = soup.find("div", class_="showtxt") if content_div is None: return "" # 把div里的<br/>换成换行符,再抽纯文本 for br in content_div.find_all("br"): br.replace_with("\n") text = content_div.get_text("\n", strip=True) return text这段代码注意三个地方:请求头的User-Agent必须伪装成浏览器,否则很多站点直接拒绝连接;编码解析用apparent_encoding而不是写死utf-8,有些老牌小说站还在用GBK编码,硬解析会出现全篇乱码,血泪经验;BeautifulSoup的get_text第二参数用了\n,为的是把段间分隔符保留下来,后面做分句、段落统计还有用。
2.3 语料库的本地组织方式:文件命名与元数据
爬下来之后不要一把梭存成一个大txt。正确的做法是“一本书一个目录,一个章节一个文件”,同时写一个meta.json记录书目、作者、爬取时间、字数。原因是后面做分析时,你可能要按作品维度统计,也可能要按总语料跑模型,分级存储都方便。命名格式我一般用作品名_章节号.txt,比如白夜行_01.txt,一目了然,也方便在数据清洗时报错时快速定位。
3. 文本清洗与中文分词:让隐形规则变得可见的中文NLP预处理
3.1 清洗掉什么:不只去杂音,还要做内容规整
小说文本比新闻语料脏得多。网文站的正文里常见前缀比如“笔趣阁手机版阅读网址”,章末常见“本章完,本章正在努力更新中”,还有乱入的HTML实体如 。这些都要清洗,否则词频统计时会跑出一堆莫名其妙的高频词。另一个重点是全半角统一和标点归一:中文引号、英文引号、空格混在一起时,分句正则很难写。我用的清洗策略分四步:去HTML标签、去广告正则、标点替换、空白压缩。代码如下:
import re def clean_text(raw: str) -> str: """ 清洗小说正文 1. 去掉HTML标签残留 2. 删除站内广告行 3. 统一标点与空白 """ # 去掉残留标签 text = re.sub(r"<[^>]+>", "", raw) # 删除常见广告行: 包含"手机用户" "章节" "网址"且长度短于20 lines = [line.strip() for line in text.split("\n") if line.strip()] filtered = [] for line in lines: if ("手机" in line and "网址" in line) or len(line) < 5: continue filtered.append(line) text = "\n".join(filtered) # 英文标点 -> 中文标点, 方便后续按中文标点分句 text = re.sub(r"[,,]", ",", text) text = re.sub(r"[。.]", "。", text) # 连续空白折叠 text = re.sub(r"\s+", " ", text) return text这里特别说明为什么第2步过滤了长度小于5的行:很多文本站的标题行混在正文里,清洗它比后面分词时特殊处理要省事得多。至于标点归一,是因为中文NLP的分句与情感分析都强依赖标点质量,后面要用的SnowNLP如果输入里冒出一堆英文逗号,情感分数会失真。
3.2 分词:jieba不是装上就能用,词典和停用词才是关键
中文分词用的是jieba,这没有讨论空间。但有三个操作不做,分词效果会直线下滑。第一,加载自定义词典,把人名(东野圭吾作品里的大量人名如“雪穗”“亮司”“石神”“汤川学”)让分词器认成单独词汇;第二,配置停用词表,把“的”
“了”“是”“在”这些无意义虚词和小说套话“说”“道”“nbsp”全部过滤;第三,注意模式选择。jieba.lcut()默认精确模式适合关键词抽取,但lcut_for_search()适合构建词共现网络时使用,模式不同结果差很多。配置代码如下:
import jieba import jieba.analyse # 自定义词典: 每行一个词, 可以有词频和词性 custom_words = ["雪穗", "亮司", "石神", "汤川学", "靖子", "桐原"] for w in custom_words: jieba.add_word(w, freq=9999) # 高频强制成词 # 停用词表: 直接用文本列表, 不需要额外文件 stopwords = set([ "的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这", "那" ]) def tokenize(text: str) -> list: tokens = jieba.lcut(text) # 过滤: 停用词 + 单字 + 空白 tokens = [t.strip() for t in tokens if t.strip() and t not in stopwords and len(t) >= 2] return tokens参数说明:freq=9999是强制手段,让jieba在计算最大概率路径时优先把这些人名当作独立词;停用词表的长度直接影响后续所有统计指标的质地。注意停用词里保留了“没有”,因为推理小说里“没有”常出现在关键线索语境中,情感分析和主题分析都有参考价值,这点和一些通用停用词表不同。
3.3 分词效果验证:三个必须检查的指标
分词做完不能直接进入下一步。准备工作:随机挑3个章节,手工统计三件事——人名是否被切开(切开了说明词典没加载)、数字是否被误切(东野圭吾作品里年份、日期非常多)、重复词是否合理(高频词前20名是否全是“房间”“时间”“警官”这类实体,如果出现“的”这种停用词,说明过滤没生效)。这一环节耗时十分钟,但能避免后面主题模型跑出“的”“了”两个主题的尴尬。
4. 统计分析与主题建模:从词频到“这本书在反复讲什么”
4.1 TF-IDF关键词提取:用对比消除“杂志感”
词频统计是最基础的一层,但如果只看词频,得到的是全书的高频名词,比如“时间”“房间”“警察”,这些东西什么都没解释。TF-IDF的意义是它有对比度:它回答的不是“这个词出现了多少次”,而是“这个词相对于普通文本有多特殊”。举一个直观例子,“刑警”在东野圭吾所有书里都高频,没有区分度;而“花冈”只出现在《白夜行》里,IDF值拉满。用jieba内置方法做TF-IDF成本极低:
from jieba.analyse import extract_tags def extract_keywords(text: str, top_k: int = 20) -> list: # 允许自定义词性过滤, 默认提取名词、动词、形容词 tags = extract_tags(text, topK=top_k, withWeight=True) return tags # 对《白夜行》全书文本提取关键词 with open("data/白夜行_all.txt", "r", encoding="utf-8") as f: whole_text = f.read() keywords = extract_keywords(whole_text, 30) for word, weight in keywords: print(f"{word}: {weight:.4f}")注意extract_tags的withWeight参数会返回TF-IDF权重,这是答辩时的加分项:直接展示“为什么是这些词而不是那些词”,权重排序比只贴词频图要有说服力得多。提取出的关键词可以直接用于绘制词云,也可以作为后续LDA的输入特征。
4.2 LDA主题模型:主题数怎么定,结果怎么解释
LDA是文本挖掘课程里的重头戏,也是最容易翻车的环节:主题数K定错了,出来全是一团浆糊;语料不够长,主题词列表像是随机抽样。东野圭吾的小说语料体量够大(几十万字),但判断主题数K才是真正的玄学。基本的经验法则:先用语料的文本长度除以200个词作为粗略基准,再跑一个困惑度曲线。gclda或gensim都行,gensim更常用。
from gensim import corpora, models def train_lda(tokenized_docs: list, num_topics: int = 6) -> models.LdaModel: """ tokenized_docs: 每本书的token列表, 外层是书, 内层是词 """ dictionary = corpora.Dictionary(tokenized_docs) # 极端词过滤: 出现频率过低(1次)和过高(超过50%语料)的词都去掉 dictionary.filter_extremes(no_below=2, no_above=0.5) corpus = [dictionary.doc2bow(doc) for doc in tokenized_docs] lda_model = models.LdaModel( corpus=corpus, id2word=dictionary, num_topics=num_topics, random_state=42, # 固定随机种子, 保证结果可复现 passes=10, # 迭代轮数, 语料不大时10轮足够 alpha="auto", # 自动学习主题稀疏度 per_word_topics=True ) return lda_model, dictionary, corpus # 书级别粒度: 每本书当一个文档 novel_tokens = [] # 在预处理阶段构建 model, dict_, corpus = train_lda(novel_tokens, num_topics=5) for idx, topic in model.print_topics(num_words=8): print(f"主题{idx}: {topic}")关键参数说明:random_state必须固定,否则每次运行主题词都在变,答辩时如果前一次跑出来的结果和后一次对不上,老师会质疑代码稳定性;filter_extremes的no_above=0.5是经验值,对小说语料来说,超过一半文档都含有的词基本是功能性词汇,不该作为主题特征;passes不是越大越好,超过15轮后困惑度下降趋于平坦,耗时反而线性增加。
LDA的结果解释才是真正的难点。一次典型输出的5个主题里,可能其中一个主题词是“尸体”“房间”“警察”“调查”“嫌疑”,那这就是推理探案主题;另一个主题是“母亲”“女儿”“家庭”“回忆”,这就是家庭伦理主题。答辩时别只念词表,要往上抽象一层:东野圭吾作品反复出现的不是“犯罪”一个极端,而是“犯罪现场和家庭日常成为一体两面”,这种解读才撑起高分。
4.3 共现网络:人物关系可视化的替代方案
课程作业通常不要求做网络分析,但如果你想拿优秀或者冲一下加分项,人物共现网是性价比最高的进阶方向。做法很直接:把全书按句切分,统计同一句中出现的人名对次数,构造成共现矩阵。然后在networkx里画图,节点大小设成度中心性,节点颜色标社区。不需要写多复杂的代码,但有一件事务必注意:一定用清洗后、加入了自定义词典的分词结果,否则“雪穗”被切成“雪”
和“穗”两个字,共现矩阵会完全失真。
5. 情感分析与可视化:把“推理小说文本”画成一张能讲故事的图
5.1 基于SnowNLP的情感倾向:一条能走的捷径,但要踩对参数
中文情感分析库SnowNLP可以用来分析段落级或者章节级的情感倾向。推理小说的情感曲线天然是“低开—俯冲—回弹”的结构,画出来会非常好看。但SnowNLP的默认模型是在电商评论上训练的,直接用于文学作品会有明显偏置:大量中性描述被判成负面。缓解手段有两个:只有两种切实有效的思路。其一,不按句子跑情感,而是按章节聚合;其二,在分析前先做一次“情感段修正”预处理。直接上代码:
from snownlp import SnowNLP def sentiment_by_chapter(chapter_path: str) -> float: with open(chapter_path, "r", encoding="utf-8") as f: text = f.read() # 按句子切分 sentences = re.split(r"[。!?!?]", text) scores = [] for sent in sentences: if len(sent.strip()) < 5: continue try: s = SnowNLP(sent) scores.append(s.sentiments) except Exception as e: continue if not scores: return 0.5 # 段落级聚合: 中位数对离群句更鲁棒 return sorted(scores)[len(scores) // 2]为什么用中位数而不是均值:因为推理小说里每个章节都有极端情绪句(比如命案发生的段落),均值会被少数几句拉得忽高忽低,画出来的曲线毛刺严重,中位数能反映一个章节的整体情绪基调,画出的曲线更平滑也更“像那么回事”。
5.2 可视化组合:词云、柱状图、情感曲线,一图胜千言
可视化的核心任务是让老师一分钟内看懂你做了什么。推荐三件套:全书词云图、TF-IDF权重Top30柱状图、章节情感曲线图。词云用wordcloud库配中文字体(这个坑每年都有无数人踩,Windows下不指定font_path,中文全变方块),柱状图用matplotlib,情感曲线用pandas加matplotlib双轴。代码结构如下:
import matplotlib.pyplot as plt from wordcloud import WordCloud def draw_wordcloud(tokens: list, save_path: str = "wordcloud.png"): text_for_cloud = " ".join(tokens) # 注意font_path必须指到中文字体文件路径 wc = WordCloud( font_path="C:/Windows/Fonts/simhei.ttf", # 黑体 width=1200, height=800, background_color="white", max_words=300, collocations=False # 不显示二元词组 ).generate(text_for_cloud) plt.figure(figsize=(10, 6)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.savefig(save_path, dpi=150, bbox_inches="tight")一个小细节:collocations=False在WordCloud 2.0以上版本才有效,这个参数是控制“要不要把相邻词合成词组”,默认True,但中文语料里它经常把“雪穗”和“亮司”合成“雪穗亮司”,词云里就会冒出一个假的实体词,必须关掉。
5.3 文档怎么写:从数据到故事的组装逻辑
文档(就是那份说明文档)的结构比代码还重要。一份高分文档的叙事线是:先讲东野圭吾作品为什么值得做文本挖掘(语料独特、主题丰富、情感曲线有辨识度),再给数据流程图,然后是每一步的数据样例和结果截图,最后是分析发现——注意这里要写“发现”,不是“结论”。“发现”是开放式的、有观察细节的;“结论”是闭合的、裁判式的。比如“《白夜行》情感曲线在第3章和第11章出现两个深谷,对应的正是两起核心案件的核心现场”,这就比“本书情感偏负面”好一万倍。
6. 踩坑记录:五个让文本挖掘项目半夜重跑的问题与排查路径
6.1 乱码:不是解压就能解决的事
现象:打开爬下来的txt,满屏“锟斤拷”或“�”。原因:请求时没有做编码探测,直接按utf-8解码,而目标站实际是gbk;或者用apparent_encoding时因为页面里有个别乱码字节导致探测错误。解决:先用requests.utils.get_encoding_from_headers(resp)看响应头,再用resp.apparent_encoding做兜底,两个都不行时,用resp.content.decode("gbk", errors="ignore")手动指定。注意errors="ignore"会静默丢弃无法解码的字节,适合小说正文,不适合需要严格保真的场景。
6.2 jieba分词把主角名切得稀碎
现象:Person名字“汤川学”被分成“汤川”和“学”,或者“雪穗”和“亮司”根本不出现。原因:自定义词典没有正确加载,或者词典里的词频太低,竞争不过jieba的HMM识别。解决:先把jieba.add_word改成jieba.add_word(word, freq=10000),把词频顶到极高;再用jieba.suggest_freq手动调整。如果还不行,检查词典文件是不是保存成了带BOM的UTF-8格式,Python读取时会读入\ufeff导致匹配失败。
6.3 LDA每跑一次结果都不一样
现象:两次运行,主题词排序完全变了,甚至主题数都好像不一样。原因:LDA采样的初始状态是随机的,不固定种子的情况下,结果天然不稳定。解决:在LdaModel初始化时传入random_state=42,同时把passes固定。此外语料顺序也影响结果,构建corpus前可以先给文档列表排序,保证每次运行输入顺序一致。
6.4 情感曲线像过山车,完全看不出规律
现象:每一章的情感分数在0.2到0.8之间乱跳,曲线毛刺多到无法解读。原因:细节决定成败,按句子跑情感时,推理小说里的疑问句和祈使句会被SnowNLP误判;章节太长时正负句抵消严重。解决:换成分段聚合,按每两百词一个窗口计算平均情感,再对整个章节做滑动平均,曲线立刻变干净。
6.5 词云图中文全变成方块
现象:词云上全是整整齐齐的方块,一个汉字都没有。原因:wordcloud默认使用的DroidSansMono字体不支持中文。解决:指定font_path指向系统中文字体,最常见的是Windows下的C:/Windows/Fonts/simhei.ttf或simsun.ttc。注意检查路径是否存在,有些精简版Windows系统没装黑体。
7. 进阶玩法与答辩准备:文本挖掘项目如何从“完成”变“优秀”
到这里,一个能拿高分的作业已经成型。但如果你不满足于“不挂科”,想在答辩时让老师眼前一亮,可以做三件性价比极高的事。第一,把5个主题的生成过程做成“主题—代表作品—代表段落”的对照表,展示时直接放作品里的原文段落,让老师看到算法结果是可以回译回文本的,这比一百页PPT都管用。第二,做一个简单的“作品相似度”矩阵,先做主题向量然后算余弦相似度,这也是一个可视化亮点。第三,在文档最后附一页“局限性与改进方向”,写“当前情感分析模型基于电商语料,对文学作品修辞手法(反讽、隐喻)识别不佳”,这会让老师觉得你的思考深度高于平均水平。
频率更高的是另一种进阶方向:把整个工程封装成命令行工具,支持python main.py --novel 白夜行 --analysis all这样跑。不需要做GUI,命令行足够体现工程的完整性。这会带来一个额外的好处:答辩演示时可以当场改动参数重跑,比如把LDA的主题数从5改成7,展示结果的变化过程,这个“活”演示比什么都加分。
在最后的几次实战里,我惯常把验证环节放在文档写作之前:先重跑一遍从爬虫到LDA的完整流程,把所有中间产物(每章的词频表、关键词表、情感分数)都存成CSV文件,再拿这些CSV去写文档。这样做的好处是,文档里的所有数据都有据可查,答辩时被追问“这个数字怎么来的”能现场演示,而不是支支吾吾说“我忘了”。这个习惯帮我避免了不止一次答辩现场尴尬到空气凝固的场面——数据挖掘作业最怕的就是“代码能跑,但代码为什么对,数据为什么准,一问三不知”。希望帮到你。
本文还有配套的精品资源,点击获取