简介:面向高校计算机相关专业学生,这份基于Python数据挖掘的期末大作业以东野圭吾小说集为分析对象,完成文本挖掘全流程,是毕业设计、课程设计与期末作业的高分参考方案。项目以zip压缩包形式提供,整体约25.78MB,内含可直接运行的Python源码与配套文档说明,代码注释清晰完整,新手也能顺利读懂和二次开发。当前已有519人学习浏览,作者自述为个人手打98分、获导师高度认可的高分项目,实用性与完成度都比较可靠。下载后只需简单部署即可运行,系统功能完善、界面美观、操作简单、功能齐全、管理便捷,且配有完整文档说明,便于快速理解项目结构与设计思路,在此基础上修改扩展也较为方便,能有效满足毕业设计、期末大作业或课程设计的提交要求。
1. 基于python数据挖掘大作业:某推理作家小说集文本挖掘到底在挖什么
数据挖掘大作业最怕的不是算法难,而是找不到一个既有故事性又能支撑起完整分析流程的数据集。某推理作家的小说集恰好满足这个条件:有清晰的人物关系、有推进情节的对话、有反复出现的主题词,而且网上能拿到全套TXT文本。基于python数据挖掘大作业——某推理作家小说集文本挖掘(源码+文档说明)高分项目,本质上就是把这套小说当成一个非结构化语料库,用分词、统计、共现、情感分析、主题模型这些常规手段,把它拆成可量化的数据,再讲出“这部小说集到底在讲什么”的故事。这篇笔记适合正在做课程大作业、需要快速跑通一个完整文本挖掘流程的读者,也适合想拿高分但不打算从零造轮子的人。
2. 从对话到人物关系:把小说拆成可计算的数据
文本挖掘的第一步永远不是建模,而是把文本变成干净的、可逐章处理的结构化数据。小说这种体裁和新闻、评论不一样,它有章节、有对话、有大量环境描写,直接对整个语料做词频统计,得到的结果会被“了”“的”“是”这类虚词淹没。我一般会先把小说按章节切分,再建立人物字典,然后逐章统计出场频次和共现关系。这套流程跑通之后,后面的TF-IDF和LDA才有意义。
2.1 语料准备与清洗:TXT编码、乱码与章节切分
先从最基础的读文件说起。从网上下载的小说TXT,编码格式非常混乱,常见的有UTF-8、GBK、GB2312,甚至同一本书前半部分是GBK后半部分是UTF-8。我习惯用一段兼容读取的代码来统一编码,避免一打开就是乱码。
import chardet def read_novel(path): with open(path, 'rb') as f: raw = f.read() enc = chardet.detect(raw) print('检测到编码:', enc) try: text = raw.decode(enc['encoding']) except UnicodeDecodeError: text = raw.decode('utf-8', errors='ignore') return text这段代码先用二进制模式读出原始字节,再用chardet检测编码,解码失败时降级到utf-8并忽略非法字符。重点是errors='ignore',对小说语料来说,丢失几个坏字节不影响统计结果,但能让程序继续跑下去。如果你明确知道文件是GBK,也可以直接指定encoding='gbk',省去检测时间。
拿到全文后,下一步是按章节切分。常见做法是用正则匹配“第X章”“第X话”“楔子”“尾声”这类标记。小说集的章节格式比较统一,但偶尔会夹杂“序章”“番外”,需要先用一个正则把所有可能的章节标题找出来,再人工确认起始位置。
import re chapter_pat = re.compile(r'第[一二三四五六七八九十百千0-9]+[章话回节]') matches = list(chapter_pat.finditer(text)) print('检测到章节数:', len(matches)) chapters = [] for i, m in enumerate(matches): start = m.start() end = matches[i + 1].start() if i + 1 < len(matches) else len(text) chapters.append({ 'title': m.group(), 'content': text[start:end] })切分之后,建议把每一章单独存成一个字符串,方便后面逐章统计共现。需要注意,有些TXT里“第一章”出现在目录区域,导致前几章内容是空白,这时要把正文真正开始的那个“第一章”作为切分起点。我一般在切分前会先把前1000个字符打印出来看一眼,确认书籍信息和目录区的范围。
2.2 人物出场统计与共现矩阵:用正则和jieba找出主角
人物出场统计是这类大作业里最受欢迎的分析点。做法是先维护一个人物字典,然后遍历每一章,统计每个人名出现的次数。难点在于小说里同一人物可能用全名、简称、代称交替出现,比如某位侦探角色,有时叫全名,有时只叫姓,有时被叫“警官”。如果只靠直接匹配,召回率会很差。
我采用的方案是:先人工整理一份“人物别名表”,把同一个角色的不同称呼映射到一起,再用jieba分词配合自定义词典来增强识别。
import jieba character_map = { '主角': ['主角', '某侦探'], '助手': ['助手', '搭档'], '凶手': ['凶手', '真凶'], } alias_to_main = {} for main_name, aliases in character_map.items(): for alias in aliases: alias_to_main[alias] = main_name jieba.add_word(main_name) for alias in aliases: jieba.add_word(alias)这里为每个主要角色定义了一个标准名和若干别名,并把所有名字都加入jieba词典。jieba.add_word的作用是让分词器知道这些词不能被切开。如果缺少这一步,像“某侦探”这种四个字的词很可能被切成“某/侦探”,导致统计不到。对于日本推理小说中的人名,自定义词典尤其重要。
统计出场频次时,我直接遍历每章文本,用词典key做精确匹配。不依赖分词也可以,因为人物的别名是有限的,精确字符串匹配速度更快,而且不会引入分词噪声。
from collections import Counter def count_characters(text): counter = Counter() for alias, main_name in alias_to_main.items(): cnt = text.count(alias) counter[main_name] += cnt return counter chapter_counts = [] for ch in chapters: chapter_counts.append(count_characters(ch['content'])) total_counts = sum(chapter_counts, Counter()) print('出场次数最多的人物:', total_counts.most_common(5))这里的参数是“按人物别名逐个count”。文本较短时,text.count(alias)是可行且高效的做法。你不要为了炫技把文本切成词再统计,那样反而会被分词错误影响。统计结果出来后,可以画一个简单的柱状图展示前10人物频次,这是报告中第一张插图。
共现矩阵的定义可以有很多种。我采用的是“同一章节内共同出现即算共现”,因为小说章节是叙事单元,人物在同一个章节出现通常意味着他们在同一场景或同一条线索中。计算方法很直接:先统计每章的人物出现布尔向量,再对向量做外积累加。
import itertools characters = list(character_map.keys()) co_matrix = pd.DataFrame(0, columns=characters, index=characters) for ch in chapters: presence = {c: False for c in characters} for c in characters: presence[c] = any(alias in ch['content'] for alias in character_map[c]) present_list = [c for c in characters if presence[c]] for a, b in itertools.combinations(present_list, 2): co_matrix.loc[a, b] += 1 co_matrix.loc[b, a] += 1构建出来的共现矩阵可以直接喂给networkx画人物关系图。这一步能直观展示主要人物之间的关联强度,是报告中很有说服力的图。需要注意,共现矩阵的对角线没有意义,画图时要把对角线置零,否则自环会干扰布局。
2.3 对话归属与情感倾向:SnowNLP的局限与调参
推理小说里对话比叙述更能反映人物情绪,所以很多大作业要求做对话级的情感分析。我的做法是先提取引号内的内容,然后逐句做情感判断。提取对话的正则很简单,但要注意中文引号的有弯引号和直角引号两种。
dialogue_pat = re.compile(r'[“"]([^”"]+)[”"]') all_dialogues = [] for ch in chapters: dialogues = dialogue_pat.findall(ch['content']) all_dialogues.extend(dialogues)提取出的对话是一句一句的,接下来用SnowNLP打分。SnowNLP默认训练语料是电商评论,对小说对话的适用性较差,它的得分会倾向于中性或正向。但作为大作业来说,使用它作为baseline,再对比自己的人工标注,说明验证集上的准确率,这样反而能体现出你了解工具的局限。
from snownlp import SnowNLP scores = [] for dlg in all_dialogues: s = SnowNLP(dlg) scores.append(s.sentiments)这里的关键参数是sentiments的阈值。默认0.5以上算正向,但对小说文本,很多愤怒、讽刺的句子得分也在0.5附近。我一般会先人工标注50条对话,观察得分分布,再决定阈值。比如标注20条明显负面对话,如果其中15条得分低于0.4,就把阈值降到0.4。这个“调阈值”的过程一定要写进报告,它是体现你做了真实数据分析的证据。
如果时间允许,还可以按人物统计平均情感得分,比如主角和助手的对话情感波动。不过要注意,对话归属需要更复杂的引号配对和说话人识别,我建议只抽取对话内容做整体情感分布,不要轻易做自动归属,否则错误率会很高。
3. 高频词与主题模型:用TF-IDF和LDA给小说做标签
人物统计解决的是“谁出现了”,这章解决的是“文本在说什么”。推理小说里反复出现的场景词,比如“房间”“尸体”“电话”“证据”,会构成一个明显的语义场。用TF-IDF可以把这些词从常用词中突显出来,再用LDA找到几类隐含主题,就能给整套小说集贴上几个标签,比如“家庭悲剧”“暴力事件”“追查真相”。
3.1 TF-IDF提取关键词:区分叙述词与人物词
直接用词频会得到一大堆“的”“了”“说”。TF-IDF通过逆文档频率惩罚那些在每一章都出现的词,让只在部分章节密集出现的词排到前面。我建议对每一章分别提取关键词,而不是对整个语料提一次,因为这样能看到剧情推进的差异。
import jieba.analyse stop_words = set(open('stopwords.txt', encoding='utf-8').read().split()) for ch in chapters: keywords = jieba.analyse.extract_tags( ch['content'], topK=10, withWeight=True, allowPOS=('n', 'v', 'vn') ) print(ch['title'], keywords)参数allowPOS限定词性为名词和动词,这对小说很有效,因为形容词和副词大多是修饰性的,不能代表主题。topK取10表示每章提取10个关键词。withWeight=True会返回权重,可以用于展示。你平时会用到的停用词表要自己整理,网上通用的中文停用词表对小说适配度一般,需要额外加入“他说”“她说”“然后”“接着”这类小说常见词。
一个容易被忽略的点:jieba.analyse在内部会自动加载IDF语料,这个语料来自通用领域,对小说文本不友好。比如“电话”这种日常词汇在通用语料里IDF较高,在小说里却很重要。如果你要更准,可以自己统计每章文档频率来计算TF-IDF。不过在大作业中,用jieba默认值作为baseline,然后指出这个局限,已经足够体现思考深度。
3.2 LDA主题建模:困惑度与主题数的选择
LDA是这些大作业里最有“算法含量”的部分。它的输入是文档-词频矩阵,输出是每个主题的词分布和每篇文档的主题分布。对小说集来说,我先把每一章当作一篇文档,合并同义词和停用词之后,再用gensim训练。
from gensim.corpora.dictionary import Dictionary from gensim.models import LdaModel tokenized = [] for ch in chapters: words = [w for w in jieba.lcut(ch['content']) if w not in stop_words and len(w) > 1] tokenized.append(words) dictionary = Dictionary(tokenized) corpus = [dictionary.doc2bow(doc) for doc in tokenized] lda = LdaModel( corpus=corpus, num_topics=5, id2word=dictionary, passes=15, random_state=42 ) for idx, topic in lda.print_topics(-1, topn=10): print('主题', idx, ':', topic)这里最关键的参数是num_topics。passes控制迭代次数,15次足够收敛;random_state固定可以让结果可复现,报告里一定要写固定种子。
那么主题数选多少?我不建议拍脑袋定5或8。用困惑度来辅助判断是常见做法,虽然困惑度不是万能的,但能让你有一个相对客观的参考。我一般会尝试2到12个主题,计算每个模型的困惑度,然后选困惑度开始放缓或出现拐点的值。
from gensim.models import CoherenceModel coherence_scores = [] for k in range(2, 13): lda_k = LdaModel(corpus=corpus, num_topics=k, id2word=dictionary, passes=15, random_state=42) cm = CoherenceModel(model=lda_k, texts=tokenized, dictionary=dictionary, coherence='c_v') coherence_scores.append((k, cm.get_coherence())) print(coherence_scores)除了困惑度,还可以看主题连贯度coherence。两个指标都算出来后,选一个主题数让连贯度较高的同时困惑度又不过大。在大作业报告里,把这张折线图放进去,比直接说“我选了5个主题”要可信得多。
3.3 可视化:词云与主题分布图
词云是文本挖掘大作业的标配,但它也是最容易被扣分的地方——默认字体不支持中文,或者词云里全是停用词。我建议用wordcloud库,并且手动指定一个中文字体文件路径,比如Windows下的simhei.ttf或用matplotlib自带的字体。
from wordcloud import WordCloud import matplotlib.pyplot as plt font_path = 'C:/Windows/Fonts/simhei.ttf' wc = WordCloud( font_path=font_path, width=800, height=600, max_words=100, background_color='white' ).generate(' '.join(jieba.lcut(text))) plt.imshow(wc, interpolation='bilinear') plt.axis('off') plt.savefig('wordcloud.png', dpi=150)参数max_words控制词云中显示的词数量,我一般设为100,词太小会看不清。background_color设为白色更适合打印。如果代码在Linux服务器上跑,simhei.ttf路径不存在,需要先从系统字体里找一个中文字体文件。这个坑在第4章细说。
主题分布图可以用pyLDAvis库生成交互式HTML,在报告里截一张图即可。不过pyLDAvis依赖较复杂,在Jupyter里偶尔因版本冲突跑不出来。如果不想折腾,可以退而求其次,把每章的主题分布画成热力图:横轴是主题,纵轴是章节,值是该章节在某个主题上的权重。这个图用matplotlib就能画,代码量不多,效果也不错。
4. 文本挖掘避坑指南:编码、分词与人名识别的四个大坑
每次大作业季,总能看到有人发帖求助:“文本读进来是乱码”“LDA结果全是废话”“词云图中文变方块”。这些坑我几乎全踩过。这个章节列出我遇到的5个最典型问题,每一条都是现象、原因、解决三步走。
4.1 现象:read()出来全是乱码
有一次我读一本下载好的TXT,print前500个字符全是“锟斤拷”。原因很简单,文件是GBK编码,我用默认的UTF-8去读。解决方法是先用chardet检测字节编码,或者直接指定encoding='gbk'。
更隐蔽的问题是文件混合编码。小说文本里偶尔夹杂几个繁体字或日文汉字,导致检测结果不准。我的建议是读取时同时尝试两种编码,并比较“可读性”分数,但为了简单,errors='ignore'最省事。在大作业里不要用“手动另存为UTF-8”这种方案,因为你要证明你的程序能自动处理任意编码的文件。
4.2 现象:jieba把“主角侦探”切成“主角/侦探”
人名识别是中文分词的老大难。默认词库里没有小说里的专有人名,jieba会把它们切成不相干片段。比如某位角色名有四个字,结果被切成“某/侦探”,这样人物统计彻底报废。解决方案有三个:一是提前在代码里调用jieba.add_word把人物全名加入词典;二是准备一个userdict.txt,内容每行一个词,让jieba在初始化时加载;三是用正则先做人名替换,把“某侦探”暂时替换成“主角”再统计。第三种方案最适合人物统计场景,因为它不依赖分词,直接做字符串替换后再用人物的标准名匹配。
text_fixed = text for alias, main_name in alias_to_main.items(): text_fixed = text_fixed.replace(alias, main_name)这里要注意替换顺序,先替换长别名再替换短别名,否则“某侦探”先被“某”替换,导致后面的替换失效。
4.3 现象:LDA主题数选10结果全是一堆“的”“了”
我第一版LDA训练时,每个主题的前几个词都是“的”“了”“在”“是”。原因是密码本里包含了大量虚词,且停用词表太弱。解决方法是扩充停用词表,至少加入“没有”“什么”“一个”“我们”“他们”这类高频词。另一个原因是单字词也混进去了,比如“手”“门”“人”,它们本身有语义,但对主题区分度低。我一般会把长度小于2的词全部过滤,只保留二字以上词。
words = [w for w in jieba.lcut(ch['content']) if w not in stop_words and len(w) > 1 and not w.isdigit()]过滤单字词之后,LDA的结果会立刻变干净。但要注意,“人”这种单字词在推理小说里其实是重要主题词,如果完全过滤会损失信息。折中方案是只过滤在文档频率超过80%的实词,这类词对所有文档都出现,区分度太差。这个细节可以在报告中写出来,体现你的调参过程。
4.4 现象:可视化图中文变方块
matplotlib画图时,中文字体缺失会出现方框。这个坑通常在代码迁移到新环境或者服务器时出现。解决方法是显式设置中文字体:
import matplotlib matplotlib.rcParams['font.family'] = 'SimHei' matplotlib.rcParams['axes.unicode_minus'] = FalseSimHei是Windows常见黑体,如果没这个字体,改用plt.rcParams['font.sans-serif'] = ['Noto Sans CJK SC']在Linux上比较通用。注意axes.unicode_minus=False一定要设,否则坐标轴负号也会变成方块。最保险的方式是动态搜索系统所有字体,挑一个支持中文的来用。
4.5 现象:运行速度慢到怀疑人生
全套小说集加起来可能有几百万字,逐章分词加LDA训练,单线程通常要跑半小时以上。我一开始以为死机了,其实它还在跑。这个坑的解决思路是分层加速。第一,只对正文部分做分词,目录、前言、附录丢去除。第二,用jieba.enable_parallel()开启并行分词,但只在有多个CPU核心时有效,而且需要在程序入口处调用。第三,LDA改成在线训练,或者降低passes到5,虽然结果略差但能大幅缩短时间。如果大作业只要求演示流程,可以用前两三本书作为子集跑通,结果一样能说明问题。
jieba.enable_parallel(4)并行分词结束后,记得调用jieba.disable_parallel()恢复单线程,否则再次调用分词时可能出问题。并行分词不支持Windows系统,这一点要特别注意。如果你的本机是Windows,就不要开并行,老老实实等它跑完,或者把语料切小。
5. 把大作业从“能跑”提升到“高分”:报告结构与验证技巧
高分项目不只是“代码能跑”,更重要的是报告里有验证、有对比、有反思。我从自己的经验里挑出三个最有效的技巧:用留出集验证结果、把参数选择过程写清楚、克制可视化数量。
5.1 用留出集验证人物出场统计的召回率
人物出场统计如果没验证,你没法说服老师“统计结果是真的”。我的做法是随机抽3章,人工阅读并记录主要人物实际出场次数,再和程序统计结果对比,计算召回率和精确率。
def evaluate_character_count(chapter_text, manual_count): auto_count = count_characters(chapter_text) tp = 0 total_manual = 0 for char, cnt in manual_count.items(): total_manual += cnt predicted = auto_count.get(char, 0) tp += min(cnt, predicted) recall = tp / total_manual precision = tp / (sum(auto_count.values()) + 1e-9) return recall, precision这里recall评估的是“人工标注出现的人物,程序找回来多少”,precision评估的是“程序报出来的人物,有多少是真正的”。对小说人物统计来说,精确率容易高因为别名表是精确的,召回率容易低因为某些章节里角色只以“他”或“她”出现。把这个短板写进报告,再提出用指代消解做改进的方向,老师会认为你真的理解了问题。
5.2 三个能写进报告的细节:停用词表、参数表、错误样本
我最注重报告里这些看似不起眼的部分:
第一,停用词表不要直接放一个几百词的文件就算完,而要挑选几个对小说影响最大的词说明,比如“警官”“转身”这类词如果出现在LDA主题里,会误导读者。解释为什么把它们加入停用词表,比词表本身更有价值。
第二,参数表要记录每一次关键实验的设置。比如LDA的主题数,要写“num_topics=5,因为困惑度曲线在5处放缓,且5个主题的可解释性最好”,不要只写一个最终参数。你需要把2到12个主题的困惑度、连贯度都列出来。
第三,错误样本分析。在情感分析部分,至少挑出3条被判定错误的对话,分析错误原因。例如某条讽刺性对话“你可真厉害啊”被判定为正向,因为SnowNLP无法理解反讽。这种错误样本会让报告显得非常扎实。
5.3 我的教训:不要为了图表数量堆可视化
我第一次做大作业时,觉得图表越多越高分,结果画了人物关系图、词云、情感分布折线、LDA气泡图、热力图,共10张图。但报告里没有一张图能回答一个问题。老师评语是“图表很多,但看不到分析主线”。
后来的经验是:每张图必须对应一个问题。人物关系图回答“谁和谁经常一起出现”,TF-IDF关键词表回答“每章在讲什么”,LDA热力图回答“整个小说集有几类故事模式”。如果图不能直接回答某个问题,即使再好看也不放。我最后只留了5张图,但每一张都能在文字部分找到对应的解读段落。
还有一个教训是代码可复现性。我习惯把所有实验的随机种子固定写在最前面,并在报告开头说明运行的Python版本和依赖库版本。这个习惯帮我避免了很多次“你在我电脑上跑不出来”的尴尬。你的大作业代码也应该用一个requirements.txt列出依赖,并且保证在别人的电脑上从头跑到尾只需要改数据路径。
如果你需要快速起步,我建议用“先跑通最小流程再加分析点”的策略:第一版只做整本词频和情感分布,第二版加入章节切分和人物统计,第三版再加入LDA。每一版都对应报告的一个小节,迭代起来不会乱。希望这篇笔记能帮你在文本挖掘大作业上少踩几个坑,把数据玩明白,而不是把时间花在折腾编码和字体上。
本文还有配套的精品资源,点击获取