1. 项目概述与设计思路
1.1 这个毕设项目到底解决什么问题
先说说这个题目的来头。每年到毕业季,计算机相关专业的学生都会被同一个问题折磨:毕设选什么题。选得太偏怕做不完,选得太大众怕撞车,选得太简单又过不了答辩。而“基于python爬取番茄小说数据及可视化系统的设计与实现”这个题目,恰好踩在了几个非常稳的位置上——有明确的数据来源、有清晰的技术栈要求、有可视化的呈现结果,整个链路是完整且闭环的。
从题目本身来看,它要求做的事情可以拆成三块:一是用Python爬虫去采集番茄小说平台上的数据,二是把这些数据存下来并做清洗和处理,三是在前端页面上用可视化图表把数据呈现出来。听起来很简单,但真正动手做的时候你会发现,每一块都有不少可以深挖的地方,也是答辩时老师最容易追问的环节。
我去年陪一个学弟捋过类似的毕设题目,当时他特别担心一个问题:爬虫部分会不会被认定成“违法”或者“违规”。这里说实话,任何数据采集类项目在公开分享时都要注意边界。毕设场景下,爬取公开的、非隐私的小说元数据(书名、作者、分类、字数、评分这些公开页面信息)用于个人学习和学术研究,是常见的毕设做法。但要注意控制请求频率、不爬取用户个人信息、不涉及付费内容,并且最终呈现时要说明“该数据仅用于学术研究”。论文和答辩PPT里一定要有这一段的合规性说明,很多学生栽在这一句话上。
1.2 技术选型:为什么是这些组件
先看一套比较稳妥的技术组合:
| 模块 | 选型 | 说明 |
|---|---|---|
| 爬虫框架 | Requests + BeautifulSoup,后续可扩展Scrapy | 轻量、灵活、代码量可控 |
| 动态渲染处理 | Selenium 或 Playwright | 处理番茄小说页面的Ajax加载场景 |
| 数据存储 | MySQL 或 SQLite | 结构化小说数据,兼顾毕业论文所需数据库设计 |
| 数据处理 | Pandas 用于清洗和聚合 | 最直观的表格操作方式 |
| 后端框架 | Flask 或 Django | 提供数据查询接口并渲染页面 |
| 可视化 | ECharts 前端图表 | 交互丰富、中文文档友好 |
| 缓存加速 | Redis | 非必需,但可以作为加分项 |
先解释几个关键的选型理由。Requests + BeautifulSoup是爬虫入门的黄金组合,代码写起来直白,调试成本低。番茄小说虽然是动态渲染的站点,但它的很多数据接口是JSON返回的,通过抓包分析直接请求接口拿到结构化数据,比用Selenium等渲染快一个数量级。所以这个项目里真正的核心技能是“抓包分析能力”,而不是框架本身。
可视化部分选中ECharts而不是其他图表库,主要是看中它的社区活跃度和示例丰富度。毕设答辩现场,老师通常会打开页面看看图表效果,ECharts的视觉效果比较“能打”,动效也流畅,能在三分钟内给老师留下好印象。
Flask作为后端是因为它足够轻。这个项目本身就是一个展示型系统,不需要复杂的状态管理和权限体系,Flask几十行代码就能把数据接口和页面渲染串起来。Django虽然功能全,但引入了太多不必要的概念,对答辩来说Flask更好解释。
1.3 系统整体架构
这个项目的系统架构可以分成四层来理解:
数据采集层负责从番茄小说获取原始数据,包括列表页的书籍信息。业务层做数据清洗、去重、入库,把非结构化的网页数据转成规范化的数据库记录。接口层通过Flask提供统计数据查询,返回JSON格式数据。展示层则是浏览器端的可视化页面,使用ECharts渲染各类图表。
这四层每一层都可以在毕业论文里单独写一章,内容非常充实,也是后期写论文时逻辑清晰的整体骨架。数据流在系统里是单向流动的:爬虫采集完写入数据库,后端从数据库读取并聚合,前端通过接口拿到数据后渲染图表。串起来的时候不复杂,但每层都需要你亲手写,这正好对应毕业设计“工作量”的硬性要求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 爬虫采集环节设计与实操
2.1 番茄小说数据的抓包分析流程
番茄小说的数据抓取是整个项目里最有技术含量、也是答辩时最容易被追问的部分,咱们把这块展开讲透,争取让你拿到代码之后不仅会跑,更能向老师讲清楚原理。
先说抓包。你在PC浏览器里打开番茄小说的网页版,按F12进入开发者工具,切到Network面板,然后滚动页面触发加载更多,会看到一批XHR请求。番茄小说的接口路径通常会带着类似 v2 或 api 这样的关键字。点开响应体看返回的JSON,里面就是书籍列表数据,包含书籍ID、书名、作者、类型、字数、评分、简介等字段。
很多学生在第一次看到这种JSON返回时有一个误区——以为必须从头解析网页源码。其实这类平台的数据几乎都是前后端分离的,页面只是通过JavaScript把JSON渲染出来。所以我们的爬虫核心其实就是在模拟客户端去请求这些JSON接口,然后做解析和入库。
抓包时会遇到两个门槛,需要明确应对方案:
第一个门槛是签名参数。番茄小说的部分接口会带 sign 或者 token 之类的参数,这个参数通常是把请求参数按一定规则拼接后用MD5或者SHA加密得到的。处理方式有一种比较简单——直接梳理反爬要求,通常需要带上必须的请求头,构建合理请求参数即可。对于前台反爬比较复杂的接口,可以用Selenium直接驱动浏览器来获取Cookie,然后把Cookie带到Requests的请求头里,也可以选用Playwright接管的真实浏览器上下文。说白了,前端校验传参,我们就把浏览器当合法调用方去拿数据。
第二个门槛是页面动态加载。番茄小说的列表页在往下滚动的过程中,会通过XHR加载后续页面的数据。爬虫层面的思路是直接循环请求下一页的接口URL,只要找到分页参数(通常是 page 或 offset),用循环就能一次性拉取多页数据。如果你只是用Requests请求第一个HTML页面,你永远只能拿到第一屏的内容。
2.2 爬虫代码的完整实现
这里给出一套可以稳定运行的爬虫核心代码,咱们以“分类-书籍列表”这种最简单的链路为例子,抓取某分类下的一批小说基本信息:
python复制import requests
import pandas as pd
from bs4 import BeautifulSoup
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/122.0.0.0 Safari/537.36",
"Referer": "https://fanqienovel.com/"
}
def fetch_book_list(category_url, pages=5):
book_items = []
for page in range(1, pages + 1):
params = {"page": page, "page_size": 20}
try:
resp = requests.get(
category_url,
headers=HEADERS,
params=params,
timeout=10
)
resp.raise_for_status()
json_data = resp.json()
book_list = json_data.get("data", {}).get("book_list", [])
for book in book_list:
book_items.append({
"book_id": book.get("book_id"),
"book_name": book.get("book_name"),
"author": book.get("author_name"),
"category": book.get("category"),
"word_count": book.get("word_count"),
"score": book.get("score"),
"status": book.get("book_status"),
"description": book.get("description")
})
except Exception as e:
print(f"第{page}页请求失败: {e}")
continue
# 控制请求节奏,避免对目标网站造成压力,同时也是反爬的基础策略
time.sleep(1.5)
return book_items
def save_to_csv(data, filename="books.csv"):
df = pd.DataFrame(data)
df.to_csv(filename, index=False, encoding="utf-8-sig")
print(f"已保存 {len(data)} 条记录到 {filename}")
if __name__ == "__main__":
url = "https://fanqienovel.com/api/category/list"
result = fetch_book_list(url, pages=10)
save_to_csv(result)
这套代码核心就做了三件事:拼接请求、解析JSON、落盘CSV。其中 time.sleep(1.5) 这个延时非常关键,既能防止请求频率过快导致IP被限流,也是合规操作的体现。如果你需要爬几千条数据,建议把延时控制在1到3秒之间,并对每页的响应做日志记录。
细心的话你已经发现,这里没有用BeautifulSoup去解析网页,因为接口直接返回了JSON。这种方式的鲁棒性远高于解析HTML——番茄小说如果调整了页面布局,你的选择器就会失效,但接口返回的JSON字段通常更稳定。这也是毕设里可以写进“系统设计”章节的关键判断。
2.3 数据入库设计
CSV只适合临时存储,毕设论文的数据库设计部分还是需要用MySQL来体现。下面是建表语句:
sql复制CREATE TABLE `book_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`book_id` varchar(32) NOT NULL COMMENT '小说唯一ID',
`book_name` varchar(128) NOT NULL COMMENT '书名',
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`category` varchar(32) DEFAULT NULL COMMENT '分类',
`word_count` int(11) DEFAULT 0 COMMENT '字数',
`score` decimal(3,1) DEFAULT 0.0 COMMENT '评分',
`status` varchar(16) DEFAULT '连载中' COMMENT '连载状态',
`description` text COMMENT '简介',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '首次采集时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_book_id` (`book_id`),
KEY `idx_category` (`category`),
KEY `idx_score` (`score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='番茄小说书籍信息表';
这里有个细节值得留意:book_id 加了唯一索引。因为爬虫可能会重复运行,同一本书会被抓取多次,如果只是执行INSERT,会产生大量重复数据。正确的做法是先判断 book_id 是否已存在,存在就跳过或更新,不存在才插入。用 INSERT ... ON DUPLICATE KEY UPDATE 可以一次性解决:
python复制insert_sql = """
INSERT INTO book_info
(book_id, book_name, author, category, word_count, score, status, description)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
book_name = VALUES(book_name),
score = VALUES(score),
word_count = VALUES(word_count),
updated_at = CURRENT_TIMESTAMP
"""
这行SQL在论文里很有价值,因为它体现了你对“数据一致性”的理解。答辩时老师很可能会问“你重复运行爬虫,数据不会出现重复吗”,你可以直接拿这段代码来回答,比口头解释半天都有说服力。
3. 数据清洗与预处理环节
3.1 原始数据有哪些“脏”问题
从接口拿到的数据并不是直接就能做可视化的,大概率会遇到几类典型问题:
- 字段缺失:有些书籍没有评分、没有简介
- 格式不统一:字数是数字和“万字”混合,比如
12.3万字和123000字同时出现 - 分类冗余:同名分类在不同页面叫法不一致,比如“都市”和“都市生活”
- 特殊字符:简介里有换行符、HTML标签残留
这些问题在毕设论文里可以写成“数据清洗模块”,对应数据处理的一般流程。实际处理时用pandas做非常方便,下面的代码片段可以解决绝大多数情况:
python复制import pandas as pd
df = pd.read_csv("books.csv")
# 1. 统一字数 -> 转为数字,以"万字"为单位
def parse_word_count(text):
if pd.isna(text):
return 0
text = str(text).replace("万字", "").strip()
try:
if "万" in str(text):
return int(float(text.replace("万", "")) * 10000)
return int(float(text))
except ValueError:
return 0
df["word_count"] = df["word_count"].apply(parse_word_count)
# 2. 补全缺失评分,用分类均值填充
df["score"] = pd.to_numeric(df["score"], errors="coerce")
df["score"] = df["score"].fillna(df.groupby("category")["score"].transform("mean"))
# 3. 清洗简介换行与HTML标签
df["description"] = df["description"].astype(str).str.replace(r"<[^>]+>", "", regex=True)
df["description"] = df["description"].str.replace("\n", "").str.strip()
# 4. 去除完全重复的行
df = df.drop_duplicates(subset=["book_id"])
df.to_csv("books_clean.csv", index=False, encoding="utf-8-sig")
3.2 清洗之后的分析指标设计
数据清洗干净后,要根据毕业论文的需求确定分析指标。结合番茄小说平台的业务场景,这几个指标比较合适,后续可视化页面上也能用得上:
| 分析维度 | 统计指标 | 数据价值 |
|---|---|---|
| 分类分布 | 各分类的书籍数量占比 | 看出平台哪些类目是内容供给主力 |
| 阅读热度 | 各分类的平均评分(近似热度) | 判断不同类别的内容质量水平 |
| 字数分布 | 小说字数区间分布 | 分析平台内容的长短趋势 |
| 头部作者 | 作品数量最多的作者TOP10 | 反映内容生态的头部集中度 |
| 连载状态 | 连载中与已完结占比 | 反映平台完成率生态 |
这些指标不需要用到复杂的算法,就是简单的分组聚合,但足够支撑一个内容充实的可视化系统。答辩时你也能清楚地告诉老师:每一个指标背后对应的是什么业务含义,而不是随便画几张图。
4. 可视化系统设计与实现
4.1 系统功能模块与页面规划
可视化系统从使用逻辑上分为两大模块:数据概览模块和分类分析模块。
数据概览模块放在首页,用四张卡片展示核心KPI:书籍总数、作者总数、分类总数、平均评分。下面配两个大图:一个是分类分布饼图,一个是字数分布柱状图。这四卡两图的设计在视觉上非常饱满,一下子就能让页面看起来内容丰富。
分类分析模块提供一个下拉框,可以选择不同的分类,然后展示该分类下的评分分布散点图、连载状态占比环形图、以及这个分类下头部作者的横向柱状图。这个模块要动态请求后端接口,体现的是“前后端联调”的能力,在答辩中属于加分项。
页面风格建议采用深色主题,背景为深蓝或墨色,图表用高饱和度的亮色。参考现在很多大屏可视化项目的风格,会让你的系统看起来很“专业”。ECharts自带的 dark 主题可以一键启用:
javascript复制echarts.registerTheme('myTheme', {
backgroundColor: '#0f1c2e',
// ...自定义主题配色
});
var chart = echarts.init(document.getElementById('chart'), 'myTheme');
4.2 后端接口设计与Flask实现
整个项目采用前后端分离的架构,Flask只负责提供JSON数据接口和渲染静态页面。下面给出后端接口的骨架代码:
python复制from flask import Flask, jsonify, render_template
import mysql.connector
import pandas as pd
app = Flask(__name__)
def get_data():
conn = mysql.connector.connect(
host="localhost",
user="root",
password="your_password",
database="fanqie_novel"
)
sql = "SELECT book_name, author, category, word_count, score, status FROM book_info"
df = pd.read_sql(sql, conn)
conn.close()
return df
@app.route("/")
def index():
return render_template("index.html")
@app.route("/api/overview")
def api_overview():
df = get_data()
result = {
"total_books": int(df.shape[0]),
"total_authors": int(df["author"].nunique()),
"total_categories": int(df["category"].nunique()),
"avg_score": round(float(df["score"].mean()), 2)
}
return jsonify(result)
@app.route("/api/category_distribution")
def api_category_distribution():
df = get_data()
data = df["category"].value_counts().to_dict()
return jsonify(data)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
这个接口用到了 pandas.read_sql() 直接读数据库,然后转成字典返回JSON,省掉了很多手写SQL的麻烦。虽然理论上有SQL注入风险,但因为字段是自己拼的复杂聚合语句,内部项目问题不大。如果你担心,可以使用参数化查询替代字符串拼接方式。
前端方面用原生JavaScript的 fetch 获取数据,再传给ECharts实例:
javascript复制fetch('/api/category_distribution')
.then(response => response.json())
.then(data => {
const chart = echarts.init(document.getElementById('pieChart'));
chart.setOption({
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: Object.entries(data).map(([name, value]) => ({
name, value
}))
}]
});
});
这里用到了ECharts的环形饼图语法——radius: ['40%', '70%'] 表示内半径和外半径。这个写法在论文里可以展开写:为什么用环形而不是实心饼图?因为环形中间的区域可以放分类总数,视觉信息密度更高。这种细节答辩时提出任何一个,都能展示你对系统的深度理解。
5. 常见问题与排查技巧实录
5.1 爬虫运行过程中最常踩的坑
先说一个几乎每个人都会遇到的问题:爬了两页之后请求就报403或者被要求验证。原因大概率是缺少Cookie或者请求头不完整。解决方案很简单,先手动用浏览器访问一次,把Network面板里的完整请求头拷贝出来,尤其注意 User-Agent 和 Cookie 两个字段,把它们更新到代码的HEADERS里。另外番茄小说对同一IP的请求频率有隐式限制,所以延时不能去掉,实测下来1.5 ~ 2秒比较稳妥。
第二个问题是编码问题。爬出来的数据写进MySQL以后是乱码。这个多半是建库时字符集没指定。创建数据库时执行:
sql复制CREATE DATABASE fanqie_novel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
utf8mb4 在MySQL中用于支持完整的Unicode字符,且个别特殊符号(比如某些空格)也能正常存储。写CSV时也要注意,Excel打开乱码时可以改用 utf-8-sig 编码,也就是加BOM(Byte Order Mark,文件开头标记字节序,Excel才会识别UTF-8)。
第三个问题是字段类型转换报错。前端图表显示NaN或者数据错乱,通常是数据里有非数字类型。处理方法在数据清洗章节已经说过,核心就一句话:做可视化之前,先对数据进行类型转换和空值填充,让前端拿到的JSON里全部都是 number 或 string。
5.2 可视化页面常见报错与排查
做前端图表时,最经典的问题是 Cannot read property 'xxx' of undefined。这个错误90%的情况都是接口返回的数据格式和前端预期不一致。排查步骤很简单:先打开浏览器的开发者工具,切到Network面板,找到对应的接口请求,查看 Response 里的JSON结构,再对照前端代码里的 data.xxx 是否对应。屡试不爽。
另一个常见问题是图表加载出来是空白。遇到这种情况,先检查容器 div 是否设置了宽高。ECharts在初始化时要求容器有明确的宽度和高度,否则会渲染成0像素。解决办法是给容器加上 style="width:100%;height:400px"。这个问题很基础,但这正是很多新手卡壳半天的原因。
再分享一个提升过程体验的技巧:开发阶段把 debug=True 开着,后端代码改完自动重启。前端页面建议不要用整体刷新,而是单独写一个测试页面,把图表数据和HTML解耦,先用假数据调试图表效果,最后再对接真实接口。这样能大幅减少联调时的排查时间。
6. 论文与答辩准备参考
6.1 毕业论文的章节架构建议
如果要用这个项目写毕业论文,一个清晰且能凑足篇幅的章节架构供你参考:
第一章 绪论,讲课题背景和研究意义,重点说“数字阅读时代下,对小说平台数据进行采集与分析对内容生态理解的参考价值”,这里要引用几篇文献。第二章 相关技术介绍,分别写Python语言特性、爬虫技术原理、Flask框架、MySQL、ECharts。第三章 系统需求分析,写功能需求和非功能需求,画出用例图。第四章 系统设计,包含架构设计、数据库设计、接口设计。第五章 系统实现,按爬虫模块、数据处理模块、可视化模块逐一贴核心代码并讲解。第六章 系统测试,写功能测试用例表格和结果。第七章 总结与展望。
每章控制在6000到10000字,总体论文轻松超过3万字。最核心的其实是第四章和第五章,这两章一定不要只贴代码,每一个模块都要配“设计思路”和“核心难点”的说明文字。老师看论文最反感的就是纯代码堆砌没有任何解释。
6.2 答辩现场的加分表达
答辩时老师最常问的几个问题,提前准备好答案就稳了:
第一个问题:“你的数据是怎么来的,合法性怎么保证?”这时候你回答说采集的是书籍公开元数据,请求频率做了限制,解析逻辑只针对公开接口,数据用途仅限学术研究。回答时态度要坦诚,逻辑清楚就好。
第二个问题:“为什么选Python而不是Java?”这个问题的背后是考察技术选型意识。你可以说Python的requests和pandas库把网络请求和数据处理封装得很简洁,迭代效率高,开发周期短,非常适合数据采集类项目。
第三个问题:“整个系统里你觉得哪个部分最复杂?”建议回答“数据采集过程中的反爬对抗”。把前面讲的抓包分析、签名参数、动态加载的处理过程讲一遍,老师会觉得你有真正的工程经验。
6.3 一条龙定制时的源码交付标准
如果你是帮别人做这个毕设,交付的东西不只是代码压缩包,而是应该包含完整的一套:项目源码、数据库脚本SQL文件、环境配置说明文档(Python版本、依赖库安装命令)、操作手册(怎么启动爬虫、怎么启动后端、怎么打开页面)、答辩PPT模板、论文初稿框架。依赖库用 requirements.txt 锁定版本。
txt复制requests==2.31.0
pandas==2.0.3
flask==2.3.3
mysql-connector-python==8.1.0
这套交付标准里最容易被忽略的是“环境配置说明”。看过太多人下载了源码却起不来项目,原因十有八九是依赖版本冲突或者Python版本不对。Python3.8以上基本都能跑,但请把环境要求写明。能够真正解决问题、能够跑通、论文能自圆其说,这才是“一条龙”的意义所在。
最后再给一个人建议:拿到源码后先通读爬虫模块和接口模块,把每一段代码的用途写注释,然后手动跑一遍流程,把数据表结构记熟。这个项目真正难的从来不是技术本身,而是你是否能对着屏幕前的代码,说清楚“我为什么这么做”。把这条做到了,答辩就没问题了。
