ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

网络小说数据分析系统毕设:Python爬虫+MySQL+Flask+ECharts全链路实现

网络小说数据分析系统毕设:Python爬虫+MySQL+Flask+ECharts全链路实现

简介:这套毕业设计是一套基于Python爬虫的网络小说数据分析系统,面向计算机相关专业毕业设计选题、Python爬虫数据分析学习以及可视化项目实践,提供从数据采集、清洗、存储到前端展示的完整闭环。系统前台围绕作者作品、小说名称、分类占比、网络小说分类统计等模块组织内容,可直观呈现不同作者和类别的分布情况;后台管理员端包括系统首页、个人中心、用户管理、网络小说、系统公告等管理功能,结构清晰,适合作为全栈项目的改造底子。资源压缩包共415个文件,占用16.91MB,以Python源码、Vue前端页面、JavaScript脚本、SVG图标素材以及SQL数据库文件和Word文档为主体,另外还配有多个bat一键安装与运行脚本,从环境准备、数据库初始化到启动程序均有对应入口,降低上手门槛。项目内还包含开发文档和论文稿(LW),方便参考开题、答辩或直接复用部分章节。目前已有122人学习下载,项目结构清晰、注释较完整,适合在此基础上做二次开发或作为毕设演示系统使用。

1. 这个毕业设计为什么值得你打开看一眼

如果你正在找网络小说相关的毕设题目,或者已经拿到了这个 zip 但不知道从哪下手,这篇笔记就是给你准备的。标题里其实已经把整个系统的技术栈写透了——Python 爬虫做数据采集,MySQL 做存储,前端页面做展示,再加上一份说明文档和 LW,合起来就是一个完整的「数据采集→清洗→存储→分析→可视化」全链路项目。这类项目最大的价值在于它不只是一个爬虫脚本,而是一整套可以演示、可以答辩、可以扩展的闭环系统。

说实话,毕设选题最怕的就是「看起来简单做起来翻车」。爬虫这块踩坑多,反爬策略、编码乱码、字段缺失都是老熟人;数据分析部分看似容易,但真正决定分数的是分析维度有没有说服力;前后端联调又是一个独立的坑。这篇笔记我会从打开这个 zip 之后你要做什么开始讲,把每个模块的选型理由、核心代码、参数设置和常见问题一次说清,让你照着走就能跑通,不跑偏。

2. 系统架构与整体模块拆解:先别急着写代码,搞清楚数据怎么流

拿到项目第一步不是跑起来,而是把「数据从哪来、存到哪、怎么展示」这条链路画出来。网络小说数据分析系统,按功能划分就是四个模块:爬虫采集模块负责从小说网站抓取书名、作者、分类、字数、连载状态、评分等元数据和章节内容;数据清洗与存储模块负责格式化、去重、入库;后端服务模块负责向数据库查询数据并提供 JSON 接口给前端;前端展示模块负责用表格、图表把这些数据可视化呈现出来。

2.1 为什么这种「爬虫+存储+展示」结构最适合毕设

毕设评审看的是完整度和工程规范性,而不是算法复杂度。你抓的是网络小说的元数据,这类数据结构化程度高,天然适合关系型数据库存储,整个项目不需要引入 Hadoop、Spark 这类重型组件,一台普通电脑就能跑完所有环节。数据量控制在几万条以内时,MySQL 的查询性能绰绰有余。

另一个关键点是「演示效果好」。网络小说数据天然带有几个适合可视化的维度:各大分类的书籍数量、不同平台的更新频率、作者的作品数量分布、小说字数与评分的关系。这些维度用 ECharts 画出柱状图、饼图、散点图,答辩现场的视觉效果非常直观,评审老师一眼就能看懂你想表达什么。

我一般的习惯是先画出一张数据流向图,大致是「目标网站 HTML → 解析提取 → 结构化数据 → MySQL 表 → Flask 后端接口 → 前端 Vue/原生 JS 图表」。这张图心里有数之后,再动手写代码,每一步都知道自己在干什么,不会写到一半迷失方向。

2.2 技术选型与版本搭配的常见方案

爬虫部分的选型,requests 负责发 HTTP 请求,BeautifulSoup4 负责解析 HTML,这两个库是组合用法的经典搭配,学起来快,出问题也好排查。如果你的目标是动态渲染的网站,比如内容靠 JavaScript 异步加载的,那需要 selenium 或 playwright,但毕设场景下你完全可以选一个静态渲染的小说网站来抓,既可以避免跟高难度反爬死磕,又能把核心功能讲清楚。

数据存储用 MySQL,后端接口层我推荐 Flask,原因很直接——Flask 的轻量级特性让开发者能完全掌控路由和返回格式,对毕设体量来说绰绰有余,而且写起来比 Django 更直白,调试成本低很多。做接口时注意设置统一的返回格式:{"code": 0, "data": [...]}这种结构,前端处理起来统一,排查问题也方便。

前端部分如果不是专业前端方向,不建议硬上复杂框架,原生 HTML + CSS + JavaScript + ECharts 是最稳妥的选择。骨架用 Bootstrap 或者直接手写一个简单的管理后台风格布局,图表全部由 ECharts 承担。把爬到的数据算清楚,把可视化图表配置写明白,比折腾一个半吊子的 Vue 全家桶更有展示价值。

2.3 目录结构与关键文件的功能定位

如果你已经拿到项目包,打开后会看到类似这样的核心结构:爬虫模块文件夹存放站点爬虫和解析脚本;后端模块存放 Flask 主入口程序和 API 路由定义;前端模块存放页面模板、样式文件和图表初始化脚本;SQL 文件夹存放建库建表语句;说明文档与 LW 则是你的论文和设计说明。

我的建议是不要打乱目录结构,因为 Flask 的模板和静态文件路径是有默认约定的,动了一个路径之后经常牵出一串 404。先按原有结构跑通一遍,想改再改。跑通之后,你自然就知道哪些文件是核心、哪些只是辅助,改起来就有把握得多。

3. 用 Python 爬虫采集小说数据:从请求到解析的最小闭环

爬虫是整个系统的数据入口,这部分做不好,后面全是无米之炊。日常的爬虫目标站点都有反爬措施,常见的包括 User-Agent 检测、请求频率限制、IP 封禁、数据加密等,我们逐个击破。下面是你拿到项目后建议先跑通的一段代码,它完成了从请求发出到解析结果落 CSV 文件的完整闭环。

3.1 核心爬虫代码:requests + BeautifulSoup 的经典组合

import requests from bs4 import BeautifulSoup import csv import time from random import randint 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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9" } def fetch_page(url): """请求页面并返回 HTML 文本,失败时返回空字符串""" try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = resp.apparent_encoding # 使用网页实际编码,防止乱码 if resp.status_code == 200: return resp.text else: print(f"[错误] 状态码 {resp.status_code}: {url}") return "" except requests.RequestException as e: print(f"[异常] 请求失败 {url}: {e}") return "" def parse_novel_list(html): """解析小说列表页,提取书名、作者、分类和链接""" soup = BeautifulSoup(html, "html.parser") items = [] # 根据目标站点的实际结构选择合适的选择器,这里是通用的布局写法 for block in soup.select(".novel-list .item"): title_tag = block.select_one(".title a") author_tag = block.select_one(".author") category_tag = block.select_one(".category") if not title_tag: continue items.append({ "title": title_tag.get_text(strip=True), "author": author_tag.get_text(strip=True) if author_tag else "未知", "category": category_tag.get_text(strip=True) if category_tag else "未知", "url": title_tag.get("href") }) return items def main(): base_url = "https://example-novel-site.com/list/1" all_data = [] for page in range(1, 4): # 抓前 3 页作为演示 url = f"{base_url}?page={page}" html = fetch_page(url) if html: data = parse_novel_list(html) all_data.extend(data) print(f"[完成] 第 {page} 页,共 {len(data)} 条") time.sleep(randint(2, 5)) # 随机休眠,降低请求频率特征 with open("novels_raw.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "author", "category", "url"]) writer.writeheader() writer.writerows(all_data) print(f"[汇总] 共采集 {len(all_data)} 条数据") if __name__ == "__main__": main()

这段代码的逻辑并不复杂:fetch_page 负责获取网页源码并把编码调整为网页实际编码,避免中文乱码——这里用的是apparent_encoding,比手动指定utf-8更稳,因为不少网站实际输出是 GBK 编码。parse_novel_list 接收 HTML 后用 BeautifulSoup 提取目标信息,.novel-list .item这类选择器要根据你实际抓取的站点结构调整,这是最容易翻车的地方。

关于 HEADERS 和 sleep 的参数其实有讲究。User-Agent 直接用了 Chrome 的标准 UA 字符串,这是最低限度的反爬绕过措施;不加这个,很多服务器会直接拒绝请求。sleep 的随机范围设在 2 到 5 秒之间,既有间隔又不至于太慢,是在效率和安全之间的平衡点。如果你是正式跑一次全量数据,建议把间隔加长到 4~7 秒,页面数量控制在 50 页以内,单个 IP 就不会被盯上。

3.2 数据清洗:把「脏数据」变成「可用数据」

爬出来的数据不能直接入库,这一步被很多人忽略但恰恰是答辩时的加分项。清洗的主要工作是去重、补全缺失字段、格式化异常值。比如某本书的作者字段为空,你可以标记为「佚名」,而不是直接丢弃;有的书分类是「其他/未知」,可以统一归为「未分类」。以下是一个清洗与去重的处理片段:

import pandas as pd df = pd.read_csv("novels_raw.csv") # 去除完全重复的记录 df.drop_duplicates(subset=["title", "author"], inplace=True) # 缺失值统一处理 df["author"] = df["author"].fillna("佚名") df["category"] = df["category"].replace({"其他/未知": "未分类", "未知": "未分类"}) # 删除无效链接(空值或非法前缀) df = df[df["url"].notna() & df["url"].str.startswith("http")] # 输出清洗结果 df.to_csv("novels_clean.csv", index=False, encoding="utf-8-sig") print(f"清洗后剩余 {len(df)} 条有效数据")

pandas 做数据清洗的优势是符号化操作,代码可读性强,你不需要写一堆循环就能完成整个流程。清洗逻辑的核心是「宁可保留原值,也不要随意推断」,比如作者缺失填「佚名」就是对原始数据的忠实处理,不会引入错误信息。

清洗后的数据要更进一步,给表里增加一个采集日期字段,标明这份数据是某天抓取的。这个看似多余的字段在后续写数据分析报告的时候非常有用,你可以说出数据的时效性和覆盖范围,回答答辩老师对数据源的质疑。

3.3 编码、乱码与落地文件的坑

爬虫踩坑指数最高的就是编码问题。很多直接用response.encoding = "utf-8"的写法是给自己挖坑,因为网站返回的可能根本不是 UTF-8。平时权威的做法是先看响应头里的Content-Type字段有没有charset声明,有就用声明的编码;没有就交给apparent_encoding去探测。

另一个坑是 CSV 文件的编码。Windows 上用 Excel 打开 CSV 经常乱码,解决方法是写入时指定utf-8-sig而不是utf-8。这是不少开发者没注意的细节,但答辩论证时你在 Windows 电脑上打开 CSV 给老师看,数据干干净净,细节分就是从这里拉开的。

还有一个容易忽略的是「限速策略」。如果你连续发请求的速度固定,比如每隔固定 3 秒一次,很容易被识别为机器行为。随机化间隔 2 到 5 秒,或者 4 到 7 秒,请求到达的时间模式接近人类行为,反而比固定间隔更不容易被触发反爬。

4. 数据建模与 MySQL 存储:从 CSV 到关系型表的设计思路

数据清洗完成之后,下一步是设计 MySQL 的表结构。表结构设计直接决定了后续分析查询好不好写。网络小说数据常见的实体包括小说本身、作者、分类,分析需求包括按分类统计数量、按作者统计作品量、按字数范围分布、按评分排序等。基于这些需求,我的推荐做法是建立两张表:小说信息表和分析统计结果表。

4.1 核心建表语句:字段类型与索引的取舍

CREATE DATABASE IF NOT EXISTS novel_system DEFAULT CHARSET utf8mb4; USE novel_system; -- 小说信息主表 CREATE TABLE IF NOT EXISTS t_novel ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键', title VARCHAR(255) NOT NULL COMMENT '小说书名', author VARCHAR(100) DEFAULT '佚名' COMMENT '作者', category VARCHAR(50) DEFAULT '未分类' COMMENT '分类', status TINYINT DEFAULT 1 COMMENT '连载状态:1=连载中 2=已完结', word_count INT DEFAULT 0 COMMENT '字数', score DECIMAL(3,2) DEFAULT 0 COMMENT '评分,保留两位小数', url VARCHAR(500) DEFAULT '' COMMENT '来源链接', crawl_date DATE COMMENT '采集日期', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', UNIQUE KEY uk_title_author (title, author), KEY idx_category (category), KEY idx_score (score) ) ENGINE=InnoDB COMMENT='网络小说信息表';

这个表结构的设计有几个需要考虑的决策点。title和author加联合唯一索引,目的是从源头消灭重复数据,这比先入库再查重要节省不少开销;category和score加普通索引,是因为后续统计查询大概率会按分类分组、按评分排序。word_count的类型用 INT 而不是 VARCHAR,是为了后续可以做数值区间分析,比如筛选 100 万字以上的长篇。score用 DECIMAL(3,2) 可以精确存到两位小数,避免浮点误差。

crawl_date这个字段是我强烈建议保留的,它把「当前这批数据的抓取日期」显式存下来了。你在后面写分析报告时,可以理直气壮地写「数据截至某年某月某日采集」,这比笼统说一句「数据来自某某网站」有说服力得多。

4.2 数据入库:批量写入比逐条插入靠谱

import pymysql import pandas as pd df = pd.read_csv("novels_clean.csv") conn = pymysql.connect( host="localhost", user="root", password="your_password", database="novel_system", charset="utf8mb4" ) cursor = conn.cursor() # 批量插入,用 executemany 减少与数据库的交互次数 sql = """ INSERT INTO t_novel (title, author, category, word_count, score, url, crawl_date) VALUES (%s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE score = VALUES(score), word_count = VALUES(word_count), crawl_date = VALUES(crawl_date) """ data_tuples = [ (row["title"], row["author"], row["category"], int(row.get("word_count", 0) or 0), float(row.get("score", 0) or 0), row["url"], row["crawl_date"]) for _, row in df.iterrows() ] try: cursor.executemany(sql, data_tuples) conn.commit() print(f"入库成功,影响 {cursor.rowcount} 行") except Exception as e: conn.rollback() print(f"入库失败,已回滚: {e}") finally: cursor.close() conn.close()

这段代码里最关键的是ON DUPLICATE KEY UPDATE子句。它配合表上的唯一索引使用,一旦检测到相同书名和作者的小说不存在就插入,存在就更新评分、字数和采集日期。这个「到了再说」的 upsert 策略比先查一遍再决定插入省一次数据库往返,大批量入库时性能差异明显。

executemany的批量方式比逐条 execute 快一个量级,原因是减少了网络往返次数。如果你的数据量有几万条,这个差别就非常明显。另外charset参数务必确认是utf8mb4,utf8 在 MySQL 中存不了完整的 emoji 字符,小说标题里偶尔会有特殊符号,保证存储容量没有缺口总归更从容。

4.3 验证入库结果:三个查询帮你确认数据可信

数据入完库之后,先不要急着跑接口。先用几条 SQL 确认基础数据是正常的:

-- 验证总量 SELECT COUNT(*) FROM t_novel; -- 验证字段完整性:不可能有空的标题 SELECT COUNT(*) FROM t_novel WHERE title IS NULL OR title = ''; -- 验证分类分布是否合理 SELECT category, COUNT(*) AS cnt FROM t_novel GROUP BY category ORDER BY cnt DESC LIMIT 10;

这三个查询分别回答了「总共抓了多少」「字段有没有脏数据」「分类是否均匀」三个问题。尤其是第二个查询,如果返回结果不为 0,说明清洗环节有漏洞,需要回到 pandas 清洗那段代码,把标题为空的行提前剔除。把「验证」变成习惯,而不是跑完入库就觉得自己已经大功告成了,这也是跟纯新手拉开差距的地方。

5. 数据分析维度与前后端可视化展示:把数据变成「能讲的故事」

数据有了,存储也做完了,接下来这部分是整个系统最出彩的地方,也是答辩时评委最爱提问的环节。你要让评审老师看到的不只是「爬虫+数据库」,而是「数据分析系统」这几个字的实际重量。

5.1 数据分析有哪些「既有价值又好实现」的维度

数据分析不能为了分析而分析,每个分析维度都要能回答一个具体问题。以网络小说数据为例,天然存在的分析维度包括但不限于:

  • 分类分布分析:全网小说的分类数量占比如何,反映当前网文市场的偏好结构
  • 作品字数分布分析:大部分小说集中在哪个字数区间,长篇小说平均字数是多少
  • 作者产出分析:头部作者的作品数量,作者群体是二八分布还是均匀分布
  • 评分与字数/分类交叉分析:高评分作品主要出现在哪个分类和哪个字数区间
  • 连载状态分析:连载中与已完结作品的比例,按分类拆分有什么差异

这些分析的实现方式分两类:一类直接用 SQL 的 GROUP BY 和聚合函数就能算出来,另一类需要把数据拉回 Python 用 pandas 计算。以「评分与字数关系」为例,你可以把字数量化为区间段,比如「50 万字以下」「50-100 万」「100-300 万」「300 万以上」四档,然后计算每档的平均评分,验证「是不是越长的书评分越高」这个直觉判断。这种假设验证型的分析,在毕设答辩中很容易展开叙述。

实现这种频次分箱分析的代码用 pandas 简洁直观:

import pandas as pd import pymysql conn = pymysql.connect(host="localhost", user="root", password="your_password", database="novel_system", charset="utf8mb4") df = pd.read_sql("SELECT word_count, score FROM t_novel WHERE score > 0", conn) bins = [0, 50*10000, 100*10000, 300*10000, float('inf')] labels = ["50万以下", "50-100万", "100-300万", "300万以上"] df["区间"] = pd.cut(df["word_count"], bins=bins, labels=labels, right=False) result = df.groupby("区间", observed=True)["score"].agg(["count", "mean"]) print(result)

pd.cut是分箱操作的核心函数,right=False表示区间左闭右开,比如「50-100万」包含 50 万但不包含 100 万,这样能保证每个数据只落在一个箱子里,不会出现边界值归属模糊问题。这种分析结果放到答辩 PPT 里,你的研究深度就立住了——你的结论是「基于实测数据的观察」而不是空泛列举。

5.2 后端接口层:用 Flask 把查询结果暴露给前端

后端接口层是整个系统前后端之间的桥梁。Flask 的设计理念是把每条分析维度映射到一个 API 路由,前端通过 fetch 或者 axios 请求这些接口拿到 JSON 数据并渲染图表。以下是一个后端接口的示例代码:

from flask import Flask, jsonify, request import pymysql app = Flask(__name__) def get_conn(): return pymysql.connect( host="localhost", user="root", password="your_password", database="novel_system", charset="utf8mb4" ) @app.route("/api/category_distribution", methods=["GET"]) def category_distribution(): """分类分布统计,返回每类的数量与占比""" conn = get_conn() cursor = conn.cursor() sql = """ SELECT category, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS pct FROM t_novel GROUP BY category ORDER BY cnt DESC """ cursor.execute(sql) columns = [desc[0] for desc in cursor.description] rows = [dict(zip(columns, row)) for row in cursor.fetchall()] cursor.close() conn.close() return jsonify({"code": 0, "data": rows}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)

这个接口用到了窗口函数SUM(COUNT(*)) OVER(),在同一个查询里同时完成了分类统计和占总比计算,省掉了先查总量再算占比的二次查询,SQL 层面就把问题解决了。返回的{"code": 0, "data": rows}是前后端约定好的数据结构,前端拿到后统一处理,清晰不易出错。

app.run的debug=True是为开发调试准备的,正式演示的时候应该把debug=False打开,否则报错堆栈会直接在前端展示,观感很不专业。接口路径统一加/api前缀,这是常见的后端接口设计规范,也让前端开发者一眼分清哪些是页面渲染路由,哪些是数据接口。

5.3 前端可视化:ECharts 配置的核心逻辑

前端部分的核心工作量集中在 ECharts 图表的初始化和配置上。以分类分布饼图和评分字数散点图为例,框架性的配置方式是想通的。以下是饼图初始化的一个典型写法:

fetch("/api/category_distribution") .then(response => response.json()) .then(res => { if (res.code !== 0) { alert("接口返回异常:" + res.message); return; } const chart = echarts.init(document.getElementById("categoryChart")); const data = res.data.map(item => ({ name: item.category, value: item.cnt })); chart.setOption({ title: { text: "网络小说分类分布", left: "center" }, tooltip: { trigger: "item" }, legend: { orient: "vertical", left: "left" }, series: [{ type: "pie", radius: "60%", data: data }] }); window.addEventListener("resize", () => chart.resize()); }) .catch(err => console.error("请求失败:", err));

这段代码的关键在于map转换数据格式。后端返回的字段是cnt,但 ECharts 的饼图数据结构要求value字段,所以前端要做一层适配。不少新手在这里犯迷糊,以为后端返回什么前端就能直接用,其实图表库每个系列都有自己的数据规范,前端适配这一步省不掉。

window.addEventListener("resize", () => chart.resize())这一行看似不起眼,但如果没有它,浏览器窗口大小变化时图表不会自动适应,整个页面看起来就很业余。这类细节处理多了,整体观感才会有质的变化。导航栏、卡片布局、统计卡片数字滚动效果这些如果时间充足可以去打磨,但先把图表功能跑通才是第一优先级。

6. 必踩的坑与排查方法:我从这套系统里捡回来的经验

做这类全栈系统,调试时间占整个项目的一半以上是常态,下面几条踩坑记录是我希望你提前有个心理准备的,给出的都是「现象→原因→解决」的完整链条,节省你在线排查的时间。

6.1 爬虫请求返回 403 或空页面

现象是代码明明没问题,但请求回来的 HTML 是空的或者是一个验证页面,status_code 显示 403。

现象的根源通常是服务器检测到了非浏览器的请求特征。只设置了 User-Agent 但缺少 Accept-Language 和 Accept 头,或者请求频率固定不变,都容易触发站点的基础风控。另一个容易忽略的是没有携带 Cookie——部分站点首次访问会种下 Cookie,后续请求都要带上才能正常返回内容。

解决办法是把请求头补全,模仿正常浏览器的标准请求头组;同时把请求间隔随机化;如果目标页面要求 Cookie,先在浏览器里登录一次,再手动把 Cookie 字符串复制到爬虫的请求头里。这是成本最低的绕过方式,不需要维护 Session。

6.2 MySQL 中文乱码

现象是数据库里存进去的中文显示为问号或者方框乱码,但在 CSV 里看是完全正常的。

原因的根源在编码不一致,极大概率是连接字符集设置错了:pymysql 的charset参数没有指定为utf8mb4,或者数据库本身建的就不是 utf8mb4。有时两者的字符集不一致,Python 端传的是 UTF-8 字节,数据库按 latin1 解开,就出现乱码。

排查方法是先查数据库和表的字符集:SHOW CREATE TABLE t_novel;,确认表字符集是 utf8mb4,再确认建库语句里显式声明DEFAULT CHARSET utf8mb4。不要直接改 MySQL 默认的配置文件,因为有些环境里字符集设置没有生效,逐层排查数字段、库、连接参数才是靠谱路径。

6.3 CSV 文件 Excel 打开乱码

现象是 CSV 文件在 VSCode、Notepad 里看是正常中文,一用 Excel 打开就乱成一团。

原因主要出在 BOM 标识上。Excel 默认用系统 ANSI 编码来解释无 BOM 的 CSV 文件,而系统 ANSI 在中文 Windows 上是 GBK,用 GBK 去解码 UTF-8 的中文内容必然乱码。解决办法是在写入 CSV 时指定encoding="utf-8-sig",这个编码会在文件开头写入 BOM 头,Excel 认出 BOM 后就能正确按 UTF-8 解析。

6.4 图表不显示或数据为空

现象是前端页面打开后图表区域是空白的,浏览器控制台能看到一个请求返回了 200,但 data 数组为空。

查这个问题时有个固定的排查顺序:先确认 MySQL 里表有数据,再直接用浏览器访问后端接口地址看返回的 JSON 是否正常,然后检查前端 JavaScript 里到底是哪个字段出了错。很多情况下后端返回{"code": 0, "data": []}的前端逻辑没有做空数据兜底,data.map直接调用没问题,但图表初始化时拿到的数据为空,自然什么也不显示。

在前端代码里增加空判逻辑是最直接的解决手段,比如if (res.data.length === 0) { alert("当前时间段暂无数据"); return; },这个处理能在演示时至少优雅地提示问题,不至于让页面白屏得很突兀。

7. 验收清单与两个「让评分往上走」的加分细节

系统跑通不是终点,能演示、能解释、能回答追问才是答辩的真正考验。我建议把下面的验收清单当成「上线前检查表」,每一项确认无问题后再去接近终版交付。

功能完整性的验收关键点包括:爬虫模块能够独立运行,并把数据写入 MySQL;后端接口返回数据正常且没有 SQL 报错;前端图表能正确渲染所有分析维度;前端与后端联调过程不存在跨域报错问题;整体系统能在你演示用的电脑上从零启动起来。数据层面的验收关注点包括:MySQL 数据量与你爬虫抓取的数量一致,分类聚合查询结果条数与实际相符,评分字段不存在异常值导致图表失败。

有一个容易被忽略的加分细节是「给接口增加一个缓存机制」。当你的查询涉及全量数据的占比运算时,每次请求都实时扫描全表,数据量几万条时延迟还能接受,但如果爬到十万级就会明显变慢。你可以用 Python 内置的字典做简单 TTL 缓存,把分类统计结果缓存 5 分钟,前端请求直接命中缓存返回,速度立刻提上来。答辩时你可以顺手提一句「这里做了接口层的定时缓存,避免了统计接口的高频访问压力」,这个细节比多写一个图表更能体现工程思维。

第二个习惯是你从第一天就该养成的:「从需求反推表结构」。每次新增一个分析维度时问问自己,这个指标需要哪些字段、通过什么查询才能得出,如果现有表结构支持不了,优先考虑调整表设计而不是硬凑查询。

这轮做完以后你会多一个经验:爬虫系统不是「爬完就结束」的,真正花时间的在数据清洗之后,在接口设计之间,在图表渲染的细节里。希望这篇笔记能帮你省下几个晚上的排错时间,把精力放在真正决定成绩的事情上。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进