最近我把“基于Python的网络小说分析系统”这个毕业设计项目从头到尾完整做了一遍。坦白说,标题听起来像是个普通的爬虫项目,实际动手之后才发现,它把数据采集、文本清洗、分词、情感分析、主题聚类到可视化展示全链路都串在了一起,做完之后你对Python数据分析的整个知识树会清晰很多。网络小说这个领域特别适合练手,因为公开数据量大、文本风格鲜明、用户评论里情绪词非常多,分析结果一眼就能看出规律。这篇文章我不打算讲太多虚的,直接按我实际踩过的路,把系统设计思路、技术选型、核心代码、坑点排查和答辩技巧一次性说清楚。如果你正准备选这个题目,或者已经选了但不知道从哪里下手,可以照着这个思路走。
1. 项目整体解读:毕业设计选型背后的逻辑
做毕设第一步不是写代码,而是想清楚“我要做一个什么东西、它要解决什么问题”。很多同学一上来就先写爬虫,抓了一堆数据之后不知道能干什么,最后只能堆一个词云交差。这个思路非常吃亏。
这个系统本质上是一个“文本分析系统”,网络小说只是它的载体。系统需要完成的事情可以拆成四层:从站点页面抓取公开的小说元信息和读者评论,清洗整理成结构化数据;对文本内容做分词、去停用词、关键词抽取;基于文本做情感倾向判断和主题分布计算;最后把结果用图表展示出来,形成可交互的分析看板。
1.1 需求拆解:这不是单纯的爬虫项目
很多参考选题写的是“基于Python的网络小说分析系统”,核心关键词有两个,一个是网络小说,一个是分析系统。网络小说限定数据来源和文本类型,分析系统才是技术主菜。
我把需求拆成四个功能模块:
- 数据采集模块:负责从目标网站抓取小说名称、作者、分类、简介、字数、评分、状态这些元信息,以及读者评论等文本数据。
- 数据存储模块:把抓下来的数据做清洗去重,按照字段存入数据库。
- 分析挖掘模块:对简介和评论进行分词、停用词过滤、TF-IDF关键词提取,再计算情感得分,做分类与主题聚类。
- 可视化展示模块:将分析结果输出为词云、柱状图、折线图、雷达图等,并通过网页的形式集中展示。
如果想要拿高分,还要把模块之间的数据流转做得足够清晰。我当时画不了复杂的架构图没关系,但至少要做到每个模块可以独立替换。比如今天采集某个小说站点,明天想换成漫画评论数据,分析模块和展示模块不用动,只需要改采集器的解析规则。
1.2 整体流程:从网页到看板
系统整体流程我用大白话描述一下:
目标网站页面 -> 爬虫请求页面 -> BeautifulSoup解析HTML -> 提取目标字段 -> 数据清洗 -> 存入SQLite -> 读取数据库 -> jieba分词 -> TF-IDF关键词 -> SnowNLP情感分析 -> LDA主题建模 -> 图表生成 -> Flask展示看板。
这个流程没有特别复杂的算法,但是每一环都需要调通,而且数据在每一步都会变化。很多同学在“存入SQLite”之后就没有下文了,因为分词的输入格式和数据量要重新处理。这个衔接是毕设中最容易卡住的地方。
1.3 数据合规与版权红线
这里必须多说一句。网络小说站点的内容是有版权的,毕业设计做爬虫不能碰全文,重点是抽取公开可见的元数据、排行榜、简介和评论信息,并且控制抓取频率,尽量避开站点明确禁止的路径。我自己的做法是只对前若干页公开列表页建立本地分析数据集,不抓全文,不商用,并在论文中把数据来源和处理方式写清楚。答辩老师很可能会问数据来源合不合规,提前把这块讲明白比现场支支吾吾要好得多。
2. 核心技术选型:从采集到可视化的搭配方案
技术选型决定了开发效率。这里直接给出我最终采用的组合:requests + BeautifulSoup做采集,SQLite做存储,pandas做数据处理,jieba做分词,SnowNLP做情感分析,gensim做主题建模,pyecharts做交互图表,Flask做Web展示。
2.1 requests + BeautifulSoup 还是 Scrapy
我第一版用的是Scrapy,后来放弃了,换成了requests加BeautifulSoup。原因很简单:这个项目的数据体量不需要Scrapy这种重型框架,反而需要快速调试和随时看中间结果。
requests负责拿HTML,BeautifulSoup负责解析。写一个get_page()函数,加入header模拟浏览器、超时设置和随机延时,十行代码就够用了。Scrapy虽然自带并发、去重、中间件,但学习成本高,而且调试相对麻烦。如果你已经熟练使用Scrapy,用它也可以;如果只是刚接触Python爬虫,不要为了炫技给自己挖坑。
2.2 jieba分词和自定义词典
文本分析绕不开分词。jieba是Python中文分词最常用的工具,默认词典能覆盖大多数场景。但网络小说有自己的语言习惯,比如“穿越”“系统流”“金手指”“爽文”“太监”这些词,在通用词典里要么没有,要么被切成单字。我跑了第一次分词后,发现“金手指”被切成“金手”和“指”,非常影响关键词效果。
解决办法是建立自定义词典,格式每行一个词,还可以加词频和词性:
金手指 100 n 爽文 100 n 扮猪吃虎 80 v 系统流 80 n然后在分词前用jieba.load_userdict("userdict.txt")加载。这个小动作让后面的关键词质量上升了一个档次。
2.3 情感分析:SnowNLP和词典方法怎么配合
情感分析我使用了两套方法做交叉验证。第一套是SnowNLP,它对中文短文本的积极性判断比较友好,直接给出一条评论的情感倾向值,在0到1之间,越接近1越积极。但SnowNLP有个明显问题,它对网络流行语和新词不敏感,比如“绝绝子”“YYDS”这种词,默认情感分很容易跑偏。
所以我又补了一个基于情感词典的打分逻辑。简单说就是维护一个自定义的积极词库和消极词库,给每个词打一个权重分数,然后加权求和。两者各跑一遍,如果结果差异很大,就把该条数据标记为“待复核”。在毕业设计答辩里提到这个细节,会显得你有工程意识。
2.4 存储和可视化组合
存储直接用SQLite,理由是单机、免安装、一个文件搞定,适合演示。如果你用的是MySQL,会在环境配置上浪费很多时间。pandas负责把抓下来的列表转成DataFrame,做去重和类型转换后写入SQLite,后续分析时pd.read_sql_query()一条语句就能把数据读回来。
可视化我用的pyecharts,它最大的好处是生成的是HTML图表,可以嵌入Flask页面,图表支持鼠标悬浮、缩放、切换,答辩演示效果明显好于matplotlib的静态图。Flask本身足够轻量,几个路由就够用,没必要上Django。
3. 核心环节实操:手把手走一遍完整流程
这一部分我直接把核心代码和当时的设计思路放出来。不要照抄,关键是理解每一步在干什么。
3.1 数据表设计与建库
先设计两个主要的表:小说信息表和评论表。我用的字段如下:
- 小说信息表:book_id,book_name,author,category,intro,word_count,status,score,update_time。
- 评论表:comment_id,book_id,content,sentiment_score,comment_time。
建表时最需要注意的是字段类型。比如word_count使用int,score使用float,intro使用text。SQLite的建表可以这么做:
CREATE TABLE IF NOT EXISTS book ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, book_name TEXT NOT NULL, author TEXT, category TEXT, intro TEXT, word_count INTEGER, status TEXT, score REAL, update_time TEXT ); CREATE TABLE IF NOT EXISTS comment ( comment_id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, content TEXT NOT NULL, sentiment_score REAL, comment_time TEXT );我当时用pandas.to_sql()直接写入,比手工execute方便得多,但要注意if_exists参数,设成append还是replace要想清楚。如果是重新抓数据,建议先删除旧表再写入,避免主键冲突。
3.2 爬虫模块的核心代码
爬虫模块的核心就两件事:请求HTML和解析字段。我写了一个通用函数:
import requests from bs4 import BeautifulSoup import time import random HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36", "Referer": "https://example.com/" } def get_page(url, retries=3): for i in range(retries): try: resp = requests.get(url, headers=HEADERS, timeout=8) resp.raise_for_status() resp.encoding = "utf-8" return resp.text except Exception as e: print(f"第{i+1}次请求失败: {e}") time.sleep(2) return None解析部分我直接用CSS选择器,比如提取一个书架列表里每本书的信息:
soup = BeautifulSoup(html, "html.parser") items = soup.select(".book-list .book-item") for item in items: name = item.select_one(".book-name").get_text(strip=True) author = item.select_one(".book-author").get_text(strip=True) category = item.select_one(".book-category").get_text(strip=True) intro = item.select_one(".book-intro").get_text(strip=True)这里有两个容易忽略的细节。第一,网页的class名经常带有空格式符号,比如class="book-item hot",用CSS选择器时只写".book-item"就可以匹配,但如果你抄别人的解析代码只看一部分,很容易就漏字段。第二,抓取之间一定要加随机延时,不然请求太快会被封IP或者触发验证码。
3.3 文本清洗与分词的细节
抓下来的简介和评论并不是直接能用的。首先要做清洗,我用正则把网页转义符、邮箱、电话号码、连续标点符号全部去掉,所有英文字母转成小写,数字根据分析需求保留或丢弃。
清洗完之后用jieba做分词:
import jieba import re STOP_WORDS = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: STOP_WORDS.add(line.strip()) def clean_text(text): text = re.sub(r"<.*?>", "", text) text = re.sub(r"[a-zA-Z0-9]+", " ", text) text = re.sub(r"\s+", " ", text) return text.strip() def tokenize(text): text = clean_text(text) words = jieba.lcut(text) result = [] for w in words: w = w.strip() if len(w) > 1 and w not in STOP_WORDS: result.append(w) return result停用词表可以在网上下载一份通用中文停用词,再手动补充一些网络小说领域的常见弱义词,比如“一样”“一个”“没有”“我们”。这一步非常重要,否则词频统计里全是“的”“了”“是”,真正的分析价值会被淹没。
3.4 TF-IDF关键词提取和LDA主题建模
在做关键词提取时,我用jieba内置的analyse.extract_tags直接提取TF-IDF权重最高的词,同时用sklearn做了对照。需要注意,TF-IDF的关键词适合描述单篇文本的重点,但不适合描述整个数据集的整体主题。比如你统计所有玄幻小说简介,排名靠前的一定是“修炼”“功法”“大陆”这类词。
import jieba.analyse def extract_keywords(text, topK=10): return jieba.analyse.extract_tags(text, topK=topK, withWeight=True)主题聚类这里要增加一点深度。我用了gensim的LDA模型,先把所有简介分词后建立词典,过滤掉出现次数过少和过多的词,再训练主题数为5的LDA模型。主题数怎么选?我一开始拍脑袋设成5,后来发现三个主题高度重叠,于是用模型困惑度做了一次对比,按困惑度和可解释性最终选了4个主题。答辩时老师如果问“主题数怎么定”,你就答“我做了不同K值的对比实验,结合困惑度曲线和实际主题词判断,不是随便写的”,这个印象分立刻就不一样。
3.5 可视化看板的实现
可视化部分我用pyecharts生成图表,再通过Flask渲染。核心逻辑是这样:从SQLite读数据,清洗统计,然后生成图表的JSON配置,最后在模板中导入ECharts渲染。
比如做一个各分类小说数量的柱状图:
from pyecharts.charts import Bar from pyecharts import options as opts from pyecharts.globals import ThemeType category_data = df["category"].value_counts() bar = ( Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT)) .add_xaxis(category_data.index.tolist()) .add_yaxis("小说数量", category_data.values.tolist()) .set_global_opts(title_opts=opts.TitleOpts(title="分类数量统计")) )生成图表后,可以用bar.render_embed()拿到HTML代码,再嵌入到Flask模板里。这里有个坑,pyecharts版本不同,API差异会很大,新版用options as opts,旧版可能是pyecharts.charts.Bar直接配置。在写代码之前先固定版本,我用的pyecharts==2.0.0,所有代码都按这个版本写。
Flask部分非常短:
from flask import Flask, render_template app = Flask(__name__) @app.route("/") def dashboard(): return render_template("dashboard.html", book_bar=bar.render_embed())注意render_template的传参方式,图表多的时候不要一次性生成所有图表,先用几个核心图表把框架搭出来,再逐步增加。
4. 常见问题与排查技巧:我踩过的雷
这个段落是真正值钱的部分。每个坑都对应一晚上的调试时间,整理出来大家可以提前避开。
4.1 请求被拦截和失败
最常见的错误是403 Forbidden,原因多半是没有带header或者header不够真实。解决办法是加User-Agent和Referer,并且把浏览器完整的header都带上。另一个问题是如果同一IP短时间请求几十次,会被站点封掉。我的处理方式很笨但有效:每次请求之间time.sleep(random.uniform(1, 3)),同时控制总页数不超过30页。如果确实因为演示需要临时抓取,优先使用抓取频率更低的排行接口,不要连续点击详情页。
4.2 中文乱码和编码问题
爬虫拿到网页之后直接resp.text可能乱码,因为requests默认编码可能不是utf-8。解决方法是resp.encoding = resp.apparent_encoding,或者直接指定resp.encoding = "utf-8"。写入SQLite和前端展示的时候也要注意,数据库连接加charset="utf8",导出JSON时用ensure_ascii=False。如果不处理,最典型的表现是数据库里中文正常,但前端页面显示乱码。
4.3 情感分析的准确率问题
SnowNLP默认模型对“作者断更,气死我了”这种句子会输出比较正的分数,因为句子里的“作者”和“更新”可能被识别成积极词。我尝试用否定词规则优化:在情感词前面如果有“不”“没”“无”这类否定词,就把情感得分反转。同时建立领域情感词典,比如“太监”“烂尾”“弃书”直接给负分,“脑洞”“文笔”“期待”给正分。最终情感分析的正确率比单独用SnowNLP高了大概百分之十几,这个比例没有精算,但答辩时需要的不是精确值,而是“我如何评估和优化”的思考过程。
4.4 图表渲染空白和内存过大
页面图表多的时候,浏览器渲染会变慢,甚至出现空白。我排查后发现是因为Flask调试模式下反复刷新导致资源加载占用,后来把app.run(debug=False)就稳定多了。如果你用的是render_embed(),所有的ECharts库都会打进HTML,页面体积会非常大。更好的做法是使用pyecharts的get_js_links()列出JS链接,或者直接把图表生成JSON配置文件,交给前端ECharts去加载。数据量特别大的时候,先做聚合,再传给图表,不要把所有评论都传给前端。
5. 从“能跑”到“能答辩”:系统优化和加分思路
很多项目的现状是功能都能跑,但外观和工程化程度不够,答辩时很难展示。这里分享一下怎么把一个“能跑的系统”包装成“能答辩的优秀毕设”。
5.1 合理的项目目录结构
不要把所有代码都堆在main.py里。我的项目目录是这样的:
project/ ├── README.md ├── requirements.txt ├── data/ │ └── novel.db ├── crawler/ │ ├── spider.py │ └── cleaner.py ├── analysis/ │ ├── word_process.py │ ├── sentiment.py │ └── topic_model.py ├── web/ │ ├── app.py │ └── templates/ │ └── dashboard.html └── docs/ ├── 需求文档.md └── 设计文档.md模块拆开之后,答辩老师让你“演示一下爬虫模块”时,你可以直接演示crawler/spider.py单独运行,而不是在Flask项目里找半天。这个细节会让人觉得你工程素养不差。
5.2 加入时间维度做趋势分析
基础功能是静态统计,加了时间维度之后,整个系统会高级一截。比如抓取小说列表的更新时间字段,按月份统计不同分类的小说数量变化,可以看出一段时间“玄幻和都市哪个类型更热”。用折线图展示,前端加一个下拉筛选器选择分类,这个交互效果会让老师觉得系统有“分析”的味道。
5.3 文档和演示的准备工作
毕设文档按需求分析、概要设计、详细设计、系统测试、总结几个大块来写。每个模块配一张运行截图,截图要加上边框和标注,不要随手截一个黑窗口。答辩演示时准备两条链路:离线数据展示和现场抓取演示。离线数据确保图表稳定,现场抓取展示爬虫真能跑。我复试时吃了亏,只准备了一条链路,结果现场网络波动,爬虫卡在请求上。后来每次演示都准备了两套方案,慌乱感少了很多。
6. 做完这个项目后的一些真实体会
代码写完、文档写完、答辩结束之后,我再回头看这个项目,最大的感受是“分析系统”的核心不在算法有多复杂,而在于你有没有把数据从无到有、从脏到净、从静到动地跑通。很多同学在选题时想要创新算法,但毕业设计的评价标准更多是看工程和思维:你面对问题怎么拆解、遇到坑怎么排查、结果怎么解释。这也是我写这篇长文的原因。
如果让我给后来者一条最重要的建议,那就是不要怕用现成的库,但一定要弄懂每个库在流程里的位置。jieba和SnowNLP都是工具,真正的设计是你对“网络小说文本如何分析”这个问题的理解。你自己把数据流逻辑搞清楚了,就算不用这些库,也能拿Python基础语法一步步实现。
如果后续想扩展,可以加爬虫调度任务、评论时间序列情感趋势、基于标签的推荐模块,或者把前端换成Vue搭建一个更现代的可视化平台。这些方向每一个都能继续做深。毕业设计只是一个阶段的总结,先做出来,再慢慢变好,是这个项目给我的最大收获。