基于Python爬虫与MySQL的网络小说数据分析系统

发布时间:2026/10/13 19:25:54
基于Python爬虫与MySQL的网络小说数据分析系统 简介一份基于Python爬虫的网络小说数据分析系统毕业设计完整方案涵盖前台展示与管理后台两大部分前台可查看作者作品、分类占比、小说名称与分类统计后台支持用户管理、公告发布等操作适合计算机相关专业学生用于课程设计或毕业设计参考也适合想学习爬虫、数据分析与可视化技术链路的开发者。压缩包共415个文件总大小约16.91MB核心文件类型包括Python源码、Vue页面、JavaScript脚本、MySQL数据库脚本以及开发文档与论文等并附带一键安装、初始化数据库和运行等批处理脚本部署门槛较低。目前已有122人浏览学习可快速上手。内容提供完整源码、数据库备份、说明文档与论文系统已测试可运行。通过该项目可以掌握爬虫抓取、数据处理与可视化展示的完整技术链路是一套实用且完整的毕业设计参考资料。1. 这个毕设题在做什么爬虫、MySQL 与数据分析的一次完整闭环基于Python爬虫的网络小说数据分析系统真正要交付的不是一堆抓下来的页面而是一条「采集—存储—分析—展示」都能讲清楚的数据链路。用 Python 爬虫把小说网站的公开列表、简介和点击量抓下来清洗后写进 MySQL再通过自带的前后端页面把热门分类、更新趋势、作者产出呈现出来。这个方案能解决「论文里的数据没有来源、没有说服力」的问题适合毕业设计、课程设计也适合想把 web 开发、数据库和采集串起来练一遍的入门者。反直觉的地方在于最容易卡进度的往往不是爬虫而是数据口径和前后端联调这类黑匣子。2. 网络小说数据采集用 requests lxml 从目录页取数入库2.1 为什么用 requests lxml 而不是 ScrapyScrapy 功能很强但一个毕设题通常只面向一两个小说站要抓的量也就是几千本书用 requests 串行请求加随机延时跑一轮只要几分钟到十几分钟。Scrapy 的爬虫类、管道、中间件这些概念答辩时如果没真正用到反而会增加解释成本。常见做法是 requests 负责网络下载lxml 负责 xpath 解析pymysql 负责写库三个库就能跑通最小闭环。pip install requests lxml pymysql flask这四个依赖分别承担下载、解析、入库和接口层。后面所有代码都基于这套组合不需要再引入额外框架。如果以后要抓的站点增加到几十个再考虑把解析逻辑拆成插件式也不迟。2.2 跑通一个最小可用的目录页解析脚本小说站最典型的结构是目录页放一批书卡片每张卡片里有书名、作者、简介、点击量点进详情页才有完整章节目录。先抓目录页把每本书的基础信息和详情页链接拿到再决定要不要深入章节页。下面这段代码只做「目录页 → 结构化数据」这一件事。import time import random import requests from lxml import etree HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def fetch(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding resp.apparent_encoding return resp.text def parse_list(html): tree etree.HTML(html) cards tree.xpath(//div[contains(class, book-card)]) books [] for card in cards: title card.xpath(.//h3/a/text()) author card.xpath(.//p[classauthor]/text()) intro card.xpath(.//p[classintro]/text()) click card.xpath(.//span[classclick-count]/text()) if not title or not author: continue books.append({ title: title[0].strip(), author: author[0].strip(), intro: intro[0].strip() if intro else , click_text: click[0].strip() if click else 0, url: card.xpath(.//h3/a/href)[0] }) return books def main(): base_url https://example.com/list?page{} for page in range(1, 3): html fetch(base_url.format(page)) books parse_list(html) print(fpage {page}: got {len(books)} books) time.sleep(random.uniform(1, 3)) if __name__ __main__: main()这里有两个关键点。一是 xpath 中用text()取的是元素直接子文本返回结果是列表所以用title[0]取值之前必须先判空否则某本书缺简介时整个脚本会抛 IndexError。二是resp.apparent_encoding会根据页面字节内容推断编码很多小说站还在用 GBK这行能省掉手动指定编码的麻烦。random.uniform(1, 3)是串行请求之间 1 到 3 秒的随机延时目的是把请求节奏打散避免固定间隔被服务端识别。页面 URL 里的example.com是占位实际抓取时换成目标站点目录页的地址即可。2.3 清洗「12.5万」与入库事务边界目录页拿到的点击量是文本可能是3456也可能是12.5万。如果直接写进 MySQL排序时会按字符串字典序排结果完全不是那么回事。清洗必须在入库前完成常见做法是用正则把文本转成整数。import re import pymysql def clean_click(text: str) - int: text text.strip() if not text: return 0 m re.search(r([\d.])\s*(万|亿)?, text) if not m: return 0 num float(m.group(1)) if m.group(2) 万: num * 10000 elif m.group(2) 亿: num * 100000000 return int(num) def insert_books(conn, books): sql ( INSERT INTO books (source_url, title, author, category, intro, click_count, word_count, created_at, updated_at) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) ) data [ (b[url], b[title], b[author], b.get(category, ), b[intro], b[click_count], b.get(word_count, 0)) for b in books ] conn.ping(reconnectTrue) try: with conn.cursor() as cur: cur.executemany(sql, data) conn.commit() except Exception: conn.rollback() raiseexecutemany把多条记录一次提交效率比逐条 execute 高一个量级。这段代码把整个批量插入包在事务里commit之后再算成功中途任何一个坏数据都触发rollback不会出现插了一半的脏库。conn.ping(reconnectTrue)是防止 MySQL 服务器超过 wait_timeout 后连接被断开重跑长任务时最稳。清洗函数同时考虑了「万」和「亿」两种单位翻页抓下来的数据直接进库后面所有 SQL 都不用再做字符串转换。3. MySQL 数据模型与查询口径三张表支撑排行榜分析数据抓下来之后表结构决定后面所有接口的写法。第二个最容易翻车的地方就是「字段类型和口径没设计好」等前端画图时再回头改库成本很高。先把三张表建清楚。3.1 三张核心表的结构与唯一索引这张设计对单个毕设量级足够books放书籍基础信息和统计指标chapters放章节级数据analysis_record放历史统计结果便于画时间趋势图。CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, source_url VARCHAR(255) NOT NULL, title VARCHAR(128) NOT NULL, author VARCHAR(64) NOT NULL, category VARCHAR(32) NOT NULL, intro TEXT, click_count INT NOT NULL DEFAULT 0, word_count INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE KEY uk_source_url (source_url) ) DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; CREATE TABLE chapters ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, chapter_title VARCHAR(255) NOT NULL, content_length INT NOT NULL DEFAULT 0, crawl_time DATETIME NOT NULL, KEY idx_book_id (book_id), CONSTRAINT fk_chapters_book FOREIGN KEY (book_id) REFERENCES books(id) ) DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; CREATE TABLE analysis_record ( id INT AUTO_INCREMENT PRIMARY KEY, metric_type VARCHAR(64) NOT NULL, metric_value DECIMAL(12,2) NOT NULL, stat_date DATE NOT NULL, KEY idx_metric_date (metric_type, stat_date) ) DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;三张表里最值得说的是source_url的唯一索引。爬虫脚本会反复执行每次全量抓取时URL 是内容的天然指纹唯一索引能挡住同一本书被插入两次。click_count和word_count用 INT 而不是 VARCHAR是排行榜能正常ORDER BY的前提。analysis_record的metric_value用 DECIMAL因为增长率和占比这类指标有小数FLOAT 在精度上容易出问题。建表语句末尾统一指定utf8mb4和utf8mb4_unicode_ci这是中文内容站的基本配置。utf8mb4 比 utf8 多支持四字节字符小说里偶尔会出现特殊符号或 emoji用 utf8 建表在写入时可能直接报错。3.2 用 SQL 算分类热度榜与更新趋势数据入库后最直接的分析需求是「哪个分类的书最火」「最近一周收录了多少新书」。这两个查询都简单但口径要先定清楚。SELECT category, COUNT(*) AS book_count, ROUND(AVG(click_count)) AS avg_click FROM books GROUP BY category ORDER BY avg_click DESC, book_count DESC LIMIT 10;这个语句按分类聚合算每类书的数量和平均点击量。ORDER BY avg_click DESC, book_count DESC的意思是先按平均点击排序点击接近时再看书多的分类排前面。这里藏着一个常见的口径坑如果建表时把分类存成逗号分隔的字符串比如「玄幻,仙侠」GROUP BY 会按整串分组同一个分类被拆得七零八落排行榜就废了。所以建表时 category 一定存单一值一本书属于多分类的场景应该用关联表而不是逗号拼接。再看更新趋势created_at是爬虫写入时间可以直接当入库时间用。SELECT DATE(created_at) AS d, COUNT(*) AS cnt FROM books WHERE created_at NOW() - INTERVAL 7 DAY GROUP BY DATE(created_at) ORDER BY d;窗口时间用NOW() - INTERVAL 7 DAY写在 SQL 里前端不用算日期。MySQL 排序在数字字段上完全可靠但如果 click_count 是 VARCHAR这个查询的排序结果会按字典序出来比如 9999 排在 100000 后面这就是为什么第 2 章的清洗环节不能省。3.3 接口返回结构与分页约定前后端分离的第一步前后端分离项目实战里最怕各写各的。后端返回什么结构前端就按什么结构取两边没有约定联调时全是破事。接口统一用「code message data」的外壳code 为 0 表示成功非 0 时 message 放失败原因。{ code: 0, message: ok, data: { total: 1200, page: 1, page_size: 20, items: [ {title: 书甲, author: 某作者, click_count: 12000} ] } }total和分页参数方便前端做分页器。列表数据永远放在data.items里字段名用下划线风格避免驼峰和蛇形混用。日期统一返回YYYY-MM-DD字符串前端 new Date 解析时不会踩时区坑。这套约定在后面的 Flask 接口里直接落地前端页面只认这个结构。4. 前后端分离落地Flask 接口、ECharts 图表与中文乱码排查4.1 Flask 接口最小实现与数据库连接管理接口层选 Flask 而不是 Django理由和选 requests 不选 Scrapy 类似单个分析系统只有几个页面Flask 几十行就能起服务Django 的 ORM、Admin、中间件在这个规模下都用不满。数据库操作用 PyMySQL 原生 SQL表结构就那么几张SQL 反而比 ORM 直观。from flask import Flask, jsonify import pymysql app Flask(__name__) app.config[JSON_AS_ASCII] False DB { host: 127.0.0.1, user: root, password: your_password, database: novel_analysis, charset: utf8mb4, cursorclass: pymysql.cursors.DictCursor, } def query_all(sql, argsNone): conn pymysql.connect(**DB) try: with conn.cursor() as cur: cur.execute(sql, args or ()) return cur.fetchall() finally: conn.close() app.route(/api/category-stats) def category_stats(): sql SELECT category, COUNT(*) AS book_count, ROUND(AVG(click_count)) AS avg_click FROM books GROUP BY category ORDER BY avg_click DESC, book_count DESC rows query_all(sql) return jsonify(code0, messageok, datarows) if __name__ __main__: app.run(host127.0.0.1, port5000, debugTrue)query_all把「连接—查询—关闭」收在一个函数里每个请求开一次连接对毕业设计量级足够不用引入连接池。用finally保证异常时连接也一定关闭这是血泪经验Windows 下连接不关跑几个小时 MySQL 会报 Too many connections。DictCursor让查询结果直接是字典列表前端拿到就是干净的 JSON。注意JSON_AS_ASCII False这个配置让 Flask 返回中文时不做 \u 转义接口里直接能看到「玄幻」而不是\u7384\u5e7b。新版 Flask 里这个配置挪到了app.json.ensure_ascii如果浏览器里看到中文正常、终端里看到转义串就用新写法。4.2 ECharts 图表与空数据显示兜底前端部分用 ECharts 画柱状图和趋势线最省事一张 CDN 引用的 JS 文件就够。页面逻辑很简单fetch 接口拿数据映射成 x 轴分类和 y 轴数值再 setOption 绘出图表。!DOCTYPE html html langzh head meta charsetUTF-8 script src/static/js/echarts.min.js/script /head body div idchart stylewidth:900px;height:480px;margin:40px auto;/div script fetch(/api/category-stats) .then(res res.json()) .then(json { if (json.code ! 0) throw new Error(json.message); const data json.data || []; const cats data.map(d d.category); const values data.map(d Number(d.avg_click)); if (values.length 0) { document.getElementById(chart).innerHTML 暂无数据请先运行爬虫入库; return; } const chart echarts.init(document.getElementById(chart)); chart.setOption({ tooltip: {}, xAxis: { type: category, data: cats }, yAxis: { type: value }, series: [{ type: bar, data: values, name: 平均点击量 }] }); }) .catch(err { document.getElementById(chart).innerHTML 加载失败 err.message; }); /script /body /htmlECharts 的坑集中在数据格式分类轴 data 必须是数组系列值也必须是数组。Number(d.avg_click)这步很关键数据库返回的 DECIMAL 或字符串如果不转数字ECharts 有时能画、有时直接空白属于典型的「接口有数据但页面画不出来」。空数据显示兜底文案演示时先跑爬虫再刷新页面不会面对一块空白尴尬。4.3 中文不乱码三个位置必须统一中文乱码是前后端分离项目里出现频率最高的联调问题。乱码不是玄学是编码链路断了一处。按「建表—连接—返回」三个位置逐一检查通常五分钟内定位。位置推荐配置乱码时的典型现象MySQL 建表DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci特殊字符写入失败或被替换PyMySQL 连接charsetutf8mb4中文能看但特殊字符报错Flask 返回JSON_AS_ASCIIFalse接口返回 \uXXXX排查困难前端页面meta charsetUTF-8页面直接显示乱码方块排查顺序也有讲究先看 MySQL 里存的数据是不是正常中文再用命令行工具查接口返回最后看页面。数据源头就是乱的前端怎么改都没用。通常是 PyMySQL 连接时漏了charsetutf8mb4或者建表时用了默认的 latin1这两处最常踩。5. 毕业设计避坑指南反爬、重复入库与联调常见的 5 个坑5.1 反爬拦截浏览器正常而 requests 拿到 403现象脚本第一次跑能抓几十页多跑几次后 requests 返回 403页面内容变成验证提示浏览器却一切正常。原因请求头不完整只是一小部分更多是请求频率太高服务端按 IP 或会话维度做限流。固定间隔、并发请求都会加速触发拦截。解决requests 里带上完整请求头至少包含 User-Agent、Referer、Accept每次请求之间用 random 延时打散节奏循环里加失败退避连续三次失败就停止 60 秒再继续。抓小说站这类内容站频率控制在每秒一个请求以内通常能稳定跑完全量。5.2 重复入库重跑脚本后统计数量直接翻倍现象第一次跑完 books 表 800 行第二次跑完变 1600 行排行榜接口的 COUNT 也跟着翻倍。原因表里只有自增 id没有业务唯一键。爬虫又是全量抓取脚本每执行一次就插入一份新数据。解决给 source_url 建唯一索引插入语句用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE。执行完后 COUNT(*) 验证数量没有翻倍。用 ON DUPLICATE KEY UPDATE 时顺手更新 updated_at重复跑也能保留最后的采集时间。5.3 图表空白接口有数据但页面画不出来现象浏览器直接访问 /api/category-stats 能看到 JSON页面里 ECharts 区域却是空白控制台报错。原因JS 报错通常是字段名不一致比如接口返回 avg_click前端写成 click_avg或者值不是数字字符串参加了数值运算再或者 data 里混了 null。解决先打开浏览器开发者工具看 Network 面板里接口响应再 Console 打印 json.data 第一条记录确认字段名。x 轴数据里的空值先过滤再做 map 映射。这类问题不是库设计的问题是前后端字段口径没对齐。5.4 MySQL 8.0 认证插件报错caching_sha2_password 连不上现象程序连接数据库时抛 Authentication plugin caching_sha2_password cannot be loaded代码一行没改换台电脑跑就报。原因MySQL 8.0 默认认证插件是 caching_sha2_password某些较旧版本的 PyMySQL 不支持这个插件。解决升级 PyMySQL 到 1.0 以上新装环境直接用新版本库最省事。如果确实不能升级可以在 MySQL 侧把该用户的插件改成 mysql_native_password。注意数据库密码不要硬编码在前端 JS 或公开仓库里给后端单独建一个最小权限账号。5.5 CSV 导出乱码Excel 打开全是问号现象把 books 表导出成 CSV用记事本打开正常用 Excel 打开中文全变成问号。原因导出文件是 UTF-8 无 BOM 编码Excel 在 Windows 下默认用本地代码页去猜编码中文被读错。解决用 pandas 导出时指定编码encodingutf-8-sig文件会带 BOM 头Excel 打开不乱码。用数据库工具导出时选 UTF-8 或 GBK 都行关键是导出编码和打开工具的默认编码一致别让它们打架。6. 从「能跑」到「能答辩」衍生指标、定时更新与正确性验证6.1 增加三个真正体现「分析」的指标只有一张图书列表的展示答辩时容易被问「分析在哪」。加三个算得上分析的指标更新频率、点击增长率、作者活跃度。更新频率用 chapters 表按周统计章节量增长率对比前后两天的平均点击作者活跃度按 author 聚合出书数量和平均点击。增长率这类窗口计算MySQL 8 支持 LAG 窗口函数直接算5.7 就在 Python 里取两天数据自己算更省事。6.2 验证正确性并与定时任务联动数据正确性要靠验证否则演示时翻车很难救场。每类随机抽 5 本书浏览器打开原始页面和数据库记录逐字段核对再对比 books 总数和目录页总数数量对不上说明解析漏了卡片。最后把爬虫脚本挂到定时任务上Linux 用 crontab 每周一凌晨跑一次Windows 用任务计划程序增量逻辑靠 source_url 唯一索引挡住重复写入。我当年做类似题目时第一个版本在凌晨一次性抓完全部列表页第二天 IP 就被临时限制。改成串行请求加随机延时、失败退避之后才稳定下来。这套系统真正锻炼人的地方是把黑匣子打开数据从哪来、怎么存、怎么算、怎么展示每段都能讲出理由而不是只把别人的成品跑起来。宁可把脚本拆小也不要让一个任务一次干太多事这是踩坑换来的习惯。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询