ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python文本挖掘实战:从爬虫到LDA的东野圭吾小说分析全流程

Python文本挖掘实战:从爬虫到LDA的东野圭吾小说分析全流程 简介围绕东野圭吾小说集展开的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 idcontent ... 段落 ... /div try: resp requests.get(url, headersHEADERS, timeout10) resp.encoding resp.apparent_encoding # 关键: 多数站点是gbk或utf-8apparent_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, idcontent) 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, stripTrue) 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实体如nbsp;。这些都要清洗否则词频统计时会跑出一堆莫名其妙的高频词。另一个重点是全半角统一和标点归一中文引号、英文引号、空格混在一起时分句正则很难写。我用的清洗策略分四步去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, freq9999) # 高频强制成词 # 停用词表: 直接用文本列表, 不需要额外文件 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参数说明freq9999是强制手段让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, topKtop_k, withWeightTrue) return tags # 对《白夜行》全书文本提取关键词 with open(data/白夜行_all.txt, r, encodingutf-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_below2, no_above0.5) corpus [dictionary.doc2bow(doc) for doc in tokenized_docs] lda_model models.LdaModel( corpuscorpus, id2worddictionary, num_topicsnum_topics, random_state42, # 固定随机种子, 保证结果可复现 passes10, # 迭代轮数, 语料不大时10轮足够 alphaauto, # 自动学习主题稀疏度 per_word_topicsTrue ) return lda_model, dictionary, corpus # 书级别粒度: 每本书当一个文档 novel_tokens [] # 在预处理阶段构建 model, dict_, corpus train_lda(novel_tokens, num_topics5) for idx, topic in model.print_topics(num_words8): print(f主题{idx}: {topic})关键参数说明random_state必须固定否则每次运行主题词都在变答辩时如果前一次跑出来的结果和后一次对不上老师会质疑代码稳定性filter_extremes的no_above0.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, encodingutf-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_pathC:/Windows/Fonts/simhei.ttf, # 黑体 width1200, height800, background_colorwhite, max_words300, collocationsFalse # 不显示二元词组 ).generate(text_for_cloud) plt.figure(figsize(10, 6)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(save_path, dpi150, bbox_inchestight)一个小细节collocationsFalse在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, errorsignore)手动指定。注意errorsignore会静默丢弃无法解码的字节适合小说正文不适合需要严格保真的场景。6.2 jieba分词把主角名切得稀碎现象Person名字“汤川学”被分成“汤川”和“学”或者“雪穗”和“亮司”根本不出现。原因自定义词典没有正确加载或者词典里的词频太低竞争不过jieba的HMM识别。解决先把jieba.add_word改成jieba.add_word(word, freq10000)把词频顶到极高再用jieba.suggest_freq手动调整。如果还不行检查词典文件是不是保存成了带BOM的UTF-8格式Python读取时会读入\ufeff导致匹配失败。6.3 LDA每跑一次结果都不一样现象两次运行主题词排序完全变了甚至主题数都好像不一样。原因LDA采样的初始状态是随机的不固定种子的情况下结果天然不稳定。解决在LdaModel初始化时传入random_state42同时把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去写文档。这样做的好处是文档里的所有数据都有据可查答辩时被追问“这个数字怎么来的”能现场演示而不是支支吾吾说“我忘了”。这个习惯帮我避免了不止一次答辩现场尴尬到空气凝固的场面——数据挖掘作业最怕的就是“代码能跑但代码为什么对数据为什么准一问三不知”。希望帮到你。本文还有配套的精品资源点击获取
返回列表