☰
基于Python的网络小说分析系统:爬虫、文本挖掘与可视化实战
2026/10/20 7:04:22 网站建设 项目流程

最近这几年,“基于Python的网络小说分析系统”这类题目在课程设计和毕业设计里出现的频率非常高。回想我当年带学生选题的时候,每届都有好几个小组选类似的题目,原因很简单:它既有技术深度——爬虫、数据库、文本挖掘、可视化一应俱全;又有天然的趣味性——分析对象是网络小说,同学们调研起来积极性明显比“员工离职预测”这种题目高得多。

这个系统说白了就是一套完整的数据分析流水线:先把网络小说网站的书籍信息、章节内容、评论数据抓下来存进数据库,然后对这些文本做分词、词频统计、情感分析,最后用图表和界面把结论展示出来。它很适合作为Python全链路开发的练手项目,覆盖面广但每一项的难度又不至于让人卡死。

这篇文章我会把这个项目从需求拆解、技术选型、数据库设计、核心代码实现到常见问题排查完完整整过一遍,都是我实际踩过坑、验证过可行性的方案。无论你是正在选毕业设计题目,还是已经开始编码却卡在某一步,按着这个思路走,至少能少走两个星期的弯路。

1. 项目整体设计思路与技术选型

1.1 功能需求怎么拆解才算完整

很多同学拿到这个题目直接就开始写爬虫,两三天抓了几万条数据,然后发现后面不知道干什么了。这就是典型的没做需求拆解。分析系统不是“爬数据+画个图”这么简单,它得回答几个具体问题:这些小说整体有什么特点?不同类型的小说在剧情节奏、用词偏好上有什么区别?某本热门小说的读者对它的评价是正面还是负面?

我建议把功能模块拆成五个层次,每一层都有明确的输入和输出:

  • 数据采集层:从目标网站抓取小说列表、简介、章节标题、章节正文、评论内容。
  • 数据存储层:设计合理的表结构,把异构数据规范化落库,便于后续查询。
  • 文本处理层:对章节正文做清洗、分词、去停用词,提取高频词汇、关键词。
  • 分析计算层:基于处理后的数据做词频统计、情感分析、章节长度分布、字数趋势等指标计算。
  • 可视化展示层:通过网页或图表把分析结果呈现出来,让结论直观可读。

这样拆完,技术点就很清楚了:爬虫、MySQL或SQLite操作、pandas数据分析、jieba分词、SnowNLP情感分析、Flask网页开发、PyECharts或Matplotlib绘图。每个模块单独实现再串联成整体,即使某个环节出问题,也不会影响全局。

1.2 技术栈选择与理由

这个题目的技术选型我特别想多说两句,因为选对了能省掉至少两三天调试时间。核心推荐这套组合:Python 3.9+、requests+BeautifulSoup爬虫、MySQL存储、pandas做数据清洗和聚合分析、jieba分词、SnowNLP情感分析、Flask提供展示界面、PyECharts生成交互图表。

爬虫用requests+BeautifulSoup而不是Scrapy,主要考虑是课程设计规模的数据量通常也就几万章,Scrapy的异步并发优势体现不出来,反而增加了框架学习的认知负担。如果目标网站有JavaScript动态渲染,可以用Selenium做兜底,但尽量优先选择有静态HTML接口的网站,能少踩很多反爬的坑。

数据库选MySQL还是SQLite也是有讲究的。如果你已经会MySQL,那就用MySQL,答辩时可以说“设计了关系型数据库的规范化表结构”,这是一个加分点。如果机器上连数据库都不方便装,SQLite也完全够用,Python内置支持、零配置,唯一的劣势是并发写性能差,但我们这个场景基本是单线程写入,影响可以忽略。

界面层用Flask而不是Django,因为Flask足够轻量,几个路由就能把查询结果渲染到页面上,配合模板引擎就能做出一个简单的可视化后台。数据量大一点还能用Redis缓存,不过课程设计阶段不是必须。

2. 系统架构与数据库设计

2.1 系统分层架构怎么搭

系统的逻辑架构我习惯用分层方式组织,每层只依赖下一层,避免代码写成一块大面团。整个流程的数据链路是:采集模块将网页解析后的数据交给存储层,存储层落库后,分析模块从库里读取数据进行处理,处理结果要么写回单独的结果表,要么直接传给展示层渲染。

这样说可能有点抽象,举个实际流程的例子:爬虫抓取某本小说的Top100书单,解析出书名、作者、分类、简介后写入novel_info表;接着抓取每本书的章节内容写入chapter_content表;分析模块从chapter_content表按小说ID分组读取正文,分词统计后把Top 50高频词写入word_freq表;最后Flask从这些表里取数,在前端页面展示词云、情感分布图和字数趋势图。

这种分层的好处是每一层都能单独测试。你可以先手动往数据库里插几条假数据,把分析模块和展示模块调通,然后再让爬虫跑数据灌进去。互不阻塞,进度好控制。

2.2 数据库表结构设计

表结构设计是答辩时老师最喜欢深挖的部分之一,设计得合理不合理,几句话就能问出来。我这里给出一套经过验证的完整表结构,你直接照着建库就能用。

小说基本信息表 novel_info:

字段名类型说明
idINT 主键自增小说唯一标识
novel_nameVARCHAR(100)书名
authorVARCHAR(50)作者
categoryVARCHAR(30)分类,如玄幻、都市
introTEXT简介
word_countINT总字数
statusVARCHAR(10)连载状态
update_timeDATETIME最近更新时间
crawl_timeDATETIME抓取时间

章节内容表 chapter_content:

字段名类型说明
idINT 主键自增章节ID
novel_idINT 外键关联novel_info的id
chapter_titleVARCHAR(200)章节标题
contentLONGTEXT章节正文
words_numINT本章字数
create_timeDATETIME发布时间

词频统计表 word_freq:

字段名类型说明
idINT 主键自增记录ID
novel_idINT 外键关联小说ID
wordVARCHAR(30)词语
freqINT出现次数
stat_typeVARCHAR(10)统计类型,全书或单章
stat_dateDATE统计日期

情感分析结果表 sentiment_result:

字段名类型说明
idINT 主键自增记录ID
novel_idINT 外键关联小说ID
chapter_idINT对应章节ID
sentiment_scoreDOUBLE情感得分,0到1之间
sentiment_labelVARCHAR(10)正面/中性/负面
analyze_timeDATETIME分析时间

注意几个细节:所有存文本的字段统一用utf8mb4字符集,否则遇到生僻字或者Emoji会插入失败;content用LONGTEXT而不是TEXT,因为一章动辄三五千字,TEXT类型的65535字节上限容易被突破;外键一定要建索引,后面词频统计做多表关联查询时性能差距非常明显。

提示:这一套表结构只需要建5张表左右就够了。不要贪多,课程设计的评分重点在于“表设计合理、字段完整、表间关系清晰”,而不是表数量多。

3. 核心功能实现与代码解析

3.1 数据采集模块的实现要点

爬虫模块是整个系统的数据源头,这个模块如果写挂了,后面全都没有数据可分析。我建议采用“列表页→详情页→章节页”三级抓取策略。第一步解析小说列表页拿到每本书的详情页链接;第二步访问详情页拿到书名、作者、分类、简介这些元信息;第三步循环访问每章链接抓章节正文。

请求时有一个关键参数必须配置好——请求头。很多网站会对没有User-Agent的请求直接拒绝,导致你一脸懵地调半天。伪装成浏览器请求是最基本的操作:

import requests from bs4 import BeautifulSoup import time headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8" } def fetch_page(url): try: resp = requests.get(url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding if resp.status_code == 200: return resp.text except requests.RequestException as e: print(f"请求失败: {url}, 原因: {e}") return None

这里有个容易踩的坑是编码问题。很多网站用的是GBK编码,直接resp.text解码会乱码。较好的方案是用resp.apparent_encoding自动探测,或者根据网页内charset字段手动指定。乱码问题一旦出现,后面分词分析全都会受影响。

抓到HTML文本后,用BeautifulSoup定位目标标签。比如提取所有章节链接,可以这样写:

def parse_chapter_links(html, base_url): soup = BeautifulSoup(html, "html.parser") links = [] for a in soup.select("div.box_con dl dd a"): href = a.get("href") if href and href.startswith("/"): links.append(base_url + href) return links

BeautifulSoup的select方法用的是CSS选择器语法,比find_all链式调用简洁得多。建议提前熟悉一下常用的选择器,比如标签加class用div.box_con,子元素用>,这个技能在解析任何网页时都能复用。

爬虫最容易翻车的地方在请求频率。我曾经有一次没有加延时,一秒钟发了十几个并发请求,IP直接被封了一整天。务必要在循环里加上time.sleep控制节奏:

for link in chapter_links: page_html = fetch_page(link) if page_html: # 解析章节内容 parse_chapter(page_html) time.sleep(0.5)

每抓完一个章节间歇0.5秒,十本书大概几十分钟能跑完,换取的是IP不被封,这笔交易非常划算。

3.2 文本清洗与分词处理

原始抓下来的文本不能直接用来分析。章节里通常夹着大量噪音,比如HTML标签、空格、全角符号、“本章未完”这类作者话术、广告插播等。先用正则把这些噪音清掉:

import re def clean_text(text): # 去掉HTML标签 text = re.sub(r'<[^>]+>', '', text) # 替换全角空格与不间断空格 text = text.replace('\u3000', '').replace('\xa0', '') # 去掉多余换行 text = re.sub(r'\n+', '\n', text) # 去掉前后空白 text = text.strip() return text

清洗完成之后进入分词环节。jieba的分词结果里会夹杂一堆没有实际分析价值的词,像“我们”“但是”“如果”“一个”“什么”,这些词在词频统计里占比极高,却完全反映不出小说风格特征,必须通过停用词表过滤掉。我用的停用词表是常用的中文停用词表扩展来的,加了网络小说里常见的“说道”“知道”“没有”“不是”这类高频口语词:

import jieba import jieba.analyse # 加载停用词表 def load_stopwords(path="stopwords.txt"): with open(path, "r", encoding="utf-8") as f: return {line.strip() for line in f if line.strip()} def cut_words(text, stopwords): words = jieba.lcut(text) # 过滤:停用词、单字、纯符号 filtered = [w for w in words if w not in stopwords and len(w.strip()) > 1 and not re.match(r'^[\W_]+$', w)] return filtered

分词模式我推荐用jieba.lcut精确模式,而不是cut_all全模式。全模式虽然召回率高,但会产生大量冗余词组合,比如“网络小说”会被切出“网络”“小说”“网络小说”,词频统计会变得混乱。

如果你希望统计结果里能突出小说主题词,可以用jieba自带的TF-IDF关键词提取功能,它有内置的IDF语料库,比单纯靠词频更准确:

keywords = jieba.analyse.extract_tags(text, topK=30, withWeight=True)

这个方法会结合词频和逆文档频率综合打分,能自动过滤掉所有文章里都常见的通用词,更适合用来横向对比两本小说的主题差异。

3.3 情感分析的计算方法

网络小说分析系统里情感分析这个功能最出效果,也是答辩时最能引发讨论的亮点模块。这个模块的目标是算出每个章节的情感倾向,然后画出一条“全书情绪起伏曲线”。章节情绪越跌宕,越说明作者擅长制造冲突悬念。

用SnowNLP做中文情感分析非常简单,几行代码就够:

from snownlp import SnowNLP def analyze_sentiment(text): s = SnowNLP(text) return s.sentiments

sentiments返回0到1之间的浮点数,越接近1代表整体情感越正面,越接近0代表越负面。可以设定一个阈值规则把连续值映射成离散标签:大于0.6标为“正面”,小于0.4标为“负面”,中间标为“中性”。

但是用这个库有一个必须知道的前提:SnowNLP默认是基于商品评论语料训练的,对小说这种叙述性文本的敏感度不够高,直接套用会给出许多反直觉的结果。比如一段激烈的战斗描写在情感上明明应该是“愤怒/紧张”,但模型可能因为句子以陈述句为主而判成中性。

我的处理办法是这样的:不做全章节原文的情感分析,而是先按段落切分,再用情感词典给每个段落单独打分,最后汇总成章节得分。这样情绪起伏的细节会被保留下来,比丢一整个章节进去算一个总分合理得多:

def chapter_sentiment(content): # 按句号拆段 paragraphs = re.split(r'[。!?!?]', content) scores = [] for para in paragraphs: if len(para) > 5: # 太短的句子没有分析意义 score = SnowNLP(para).sentiments scores.append(score) if not scores: return 0.5 # 平均情感和各章离散度 avg_score = sum(scores) / len(scores) return round(avg_score, 4)

另外建议同时统计每个章节的“情感方差”,方差大的章节说明情绪波动剧烈——通常是高潮或转折章节,这是一个很有意思的分析维度,很多同学没想到,能做出来会让系统显得更有深度。

3.4 数据统计与可视化展示

分析计算的产出需要用直观的图表呈现。我实验下来,稳定可靠、出图上档次、并且在答辩中演示效果最好的方案是PyECharts加Flask。PyECharts生成的图表是网页交互形式,鼠标悬停有数据提示,也可以缩放拖拽,演示效果比Matplotlib的静态图专业不少。

章节字数分布这类单变量数据,用柱状图最直观;情感得分的变化趋势,用折线图最合适;小说高频词,用词云展示效果拉满。

生成词云时有一个重点,常规wordcloud库默认字体不支持中文,直接生成会出来一堆方块。必须指定一个中文字体文件的路径:

from wordcloud import WordCloud wc = WordCloud( width=800, height=600, background_color="white", font_path="C:/Windows/Fonts/simhei.ttf", # 指定中文字体 max_words=150 ) wordcloud_img = wc.generate_from_frequencies(dict(word_freq_list))

如果你刚好装的是Mac或Linux,中文字体路径不一样(比如Mac上是PingFang.ttc),群里互相拷代码时这个坑常遇到,报错TypeError: update() takes no keyword arguments或生成图片无字,基本都是这里的路径问题。

Flask这边做一个数据看板,把所有图表和统计结果嵌入一个dashboard页面。为了让图表能动态渲染到前端,PyECharts支持直接在前端初始化:

from flask import Flask, render_template from pyecharts import options as opts from pyecharts.charts import Bar app = Flask(__name__) @app.route("/") def index(): # 从数据库读取词频TOP10 data = load_word_freq_top10() bar = ( Bar() .add_xaxis([item["word"] for item in data]) .add_yaxis("出现次数", [item["freq"] for item in data]) .set_global_opts(title_opts=opts.TitleOpts(title="高频词TOP10")) ) return bar.render_embed()

用render_embed()方法把图表内容直接以HTML片段嵌入页面,比一次性生成静态HTML文件灵活得多。只需要保证前端页面引入了pyecharts的JS依赖库。

4. 完整实现流程与坑点速查

4.1 一步步跑通整个系统

我给你的建议是“倒着开发”:先建数据库,再写分析展示,最后写爬虫。

第一步,先把数据库表建好,手动插入两本小说的少量章节数据作为测试样本。第二步,写分词统计和情感分析脚本,确保能从库里读到数据、算出结果、写回统计表。第三步,做Flask页面展示,数据库里查出来的词频、情感曲线能正常渲染成图表。第四步,最后回来写爬虫,让真实数据慢慢灌入库。

为什么倒着开发?因为爬虫是最不确定的模块。网站改版、反爬策略变化、网络波动都会让爬虫罢工,如果一开始就把全部精力押在这里,很可能前两周数据源一崩,整个项目进度全部卡死。反过来先跑通分析流程,哪怕爬虫最后只抓了五本书,也能交出一份功能完整的系统。

真实数据入库的完整流程可以这样组织:

def main(): books = crawl_book_list() for book in books: book_id = save_book_info(book) # 写基础信息 chapters = crawl_chapters(book["url"]) total_content = "" for chapter in chapters: save_chapter(book_id, chapter) # 写章节内容 total_content += chapter["content"] analyze_book(book_id, total_content) # 分词 + 情感分析 time.sleep(1)

这个流程把抓、存、分析的串联方式直接展示清楚了。抓完一本书立刻分析这本书,避免所有数据堆积到最后一口气处理,那样如果中途报错会非常煎熬。

4.2 开发中遇到的高频问题

我把这几年在这个项目上反复出现的问题整理成了表格,按照出现频率从高到低排列:

问题现象根本原因解决方案
网页内容全是乱码页面是GBK编码,直接用response.text解码用response.apparent_encoding探测后再解码
请求被拒,返回403缺请求头,被识别为爬虫添加UA和Accept头,控制抓取频率
数据库插入中文报错表字符集不是utf8mb4建库时指定DEFAULT CHARSET=utf8mb4
词云生成后全是方块wordcloud没指定中文字体显式设置font_path为中文字体文件
情感分析结果全部接近0.5SnowNLP语料不匹配小说文本按段落拆解分析,或换用情感词典法
统计脚本内存溢出一次性把所有章节读入内存用yield生成器逐章读取处理
页面图表不显示pyecharts JS资源未加载检查是否引入echarts.min.js依赖
IP被网站临时封禁请求频率过高每次请求后sleep 1-2秒,并设置重试退避

内存溢出一项我特别再提醒一句。几万章节的正文如果一次性通过Session查询全部加载进List,内存很容易飙到1G以上。正确做法是给查询加上limit分页,或者用fetchmany分批读取:

def process_chunks(cursor, batch_size=500): while True: rows = cursor.fetchmany(batch_size) if not rows: break for row in rows: yield row

这样内存占用始终恒定在一个较低水平,抓多少数据都不怕。

4.3 爬虫被封和异常数据怎么兜底

真实项目中爬虫总会遇到几个情况:单章节请求失败、某个页面结构改版导致解析不到内容、IP半路被封。我在代码里都会加一层容错和断点续传机制。

最简陋但最有效的兜底方案是“失败重试+跳过”。单章节请求失败时最多重试三次,三次都失败就记录日志,跳过本页,不让整个进程崩掉:

def fetch_with_retry(url, max_retries=3): for attempt in range(max_retries): html = fetch_page(url) if html: return html time.sleep(2 * (attempt + 1)) print(f"请求多次失败,放弃: {url}") return None

更稳妥的做法是先把待抓取的章节链接列表存到数据库里,标记一个is_crawled字段。每成功抓取一章就把它标为1,下次脚本重新运行时直接查还没有抓完的章节。这个“待抓取队列+状态标记”的思路简单高效,即使爬虫中途断掉,第二天从断点继续抓就行,不需要从头开始:

UPDATE chapter_queue SET is_crawled = 1 WHERE id = ?; SELECT * FROM chapter_queue WHERE is_crawled = 0 ORDER BY id LIMIT 100;

这一套操作在真实项目里完全够用,也侧面验证了前面表结构设计的价值。

5. 系统测试用例与验收清单

5.1 功能测试的覆盖面

课程设计和毕业设计答辩时,老师通常会现场让系统跑几个数据场景,如果当场报错,印象分会大打折扣。我建议在准备演示前把测试用例做全,至少覆盖以下这些场景:

测试项输入预期结果
数据库连接启动Flask服务页面正常打开,无异常报错
小说列表加载访问首页展示最近爬取的小说列表
词频统计选择一本书正确显示Top20高频词与词云
情感分析选择章节区间折线图展示各章节情感得分变化
字数统计选择分类柱状图对比各类别平均字数
搜索功能按书名关键词搜索返回匹配的小说信息
空数据处理删除测试数据后查询页面提示暂无数据而非报错
爬虫断点续跑中断后重新运行只抓取未完成章节

空数据这个场景特别容易被忽略,也特别容易在演示时翻车。删除数据后如果页面直接抛异常,老师会觉得系统健壮性差。正确处理方式是在查询结果为空时展示友好的空状态页面:

@app.route("/analysis/<int:novel_id>") def analysis(novel_id): stats = get_stats(novel_id) if not stats: return render_template("empty.html", message="暂无数据,请先爬取小说") return render_template("analysis.html", stats=stats)

5.2 性能与并发处理

系统性能在答辩演示时只要不卡顿超过两三秒,一般不会扣分。但有一个细节值得做一下:统计分析接口如果每次动态计算,页面响应会特别慢。解决办法是提前把统计结果算好存到word_freq和sentiment_result表,展示时只做简单的查询聚合,不在请求时重新分词。

我在实际测试中,一万章的小说全书分析脚本大约需要8到10分钟,其中分词消耗约七成时间。这么长的计算如果放在用户请求时执行,页面必然超时崩溃。所以一定要有一个批处理脚本,定时或手动触发执行“爬取→分析→入库”流程,网页端只当数据的呈现者。

如果你希望目录树结构更清晰,也可以在Flask应用内为所有统计分析增加一个简单的内存缓存,比如用字典存上一次结果,配上过期时间:

from datetime import datetime, timedelta cache = {} def cached_query(key, ttl_seconds=120): now = datetime.now() if key in cache and cache[key]["expire"] > now: return cache[key]["data"] data = expensive_query(key) cache[key] = {"data": data, "expire": now + timedelta(seconds=ttl_seconds)} return data

这个方案轻量实用,也不引入额外的Redis依赖。

6. 系统扩展方向与个人体会

这个系统的价值止步于“基础版”就有点可惜了。实际上稍微花点功夫还能延伸出几个很有分量的模块,答辩时往这些方向扩展,绝对能拉开和其他同学的差距。

扩展方向一是加入角色关系网络分析。从章节文本中提取人物名词,计算人物共同出现的频次,构建加权共现网络,用图数据库或者序列化后的邻接表存储,再通过PyECharts的关系图展示。这个模块可以直观呈现主角与配角之间的交互强度。实现上其实不需要很复杂的NLP技术,基于分词结果和“同一段落内人物共现”的规则即可完成。

扩展方向二是做章节热度预测模型。用已经抓取的章节字数、情感波动值、发布时间等特征,尝试用随机森林或XGBoost预测未来章节的读者评论数。虽然预测精度不一定高,但把机器学习引入文本分析系统,能够提升课题的理论高度,尤其在毕业设计里是明显的加分项。

扩展方向三是接入大模型API做自动摘要。每次抓取完小说详情,调用大模型生成一段200字以内的简介和书评,作为分析和展示的补充信息。这一思路既切中了AI热点,又不需要太多额外代码。

我在带着学生做这类项目时,最深的一点体会是:不要总想着“我要做一个功能复杂的大系统”,而要先把“一条完整、闭合的小链路”跑通。从三本书、一百个章节、一个排行榜、一张词云图开始,让用户能操作、能看结果,然后再往里面加内容。课程设计的评分逻辑永远是“完整可用”优先于“功能堆砌”。

代码能跑通只是及格线,真正拉开差距的,是你对每个模块为什么要这样设计的思考深度。比如数据库为什么用三张表而不是一张大宽表、情感分析为什么不能全篇算一个分、爬虫为什么不能一秒并发五十个——这些问题在文档里写清楚,比多写一千行代码有用得多。

最后分享一个我在准备答辩材料时的小技巧:文档里的结果截图不要只截最终图表,把中间过程的统计分析表、数据库表数据截图也放进去,形成一套“数据从哪里来→怎么处理的→最终得出了什么”的完整证据链。评委老师顺着这条证据链走一遍,对你的系统逻辑和数据可信度的印象会好很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询