又到一年毕业设计选题季的时候,CSDN 上“基于 Hadoop 的 XX 数据分析系统”这类题目的热度一直很高。相比传统的图书管理、课堂考勤、电商后台,网络小说数据分析系统天然具备“数据量可扩展、分析维度多、可视化效果好”三个优势:平台上有分类、字数、点击、收藏、评分、作者等结构化字段,适合做爬虫采集;这些数据放到 HDFS 上做离线统计,能体现大数据技术栈;再配合协同过滤推荐算法和可视化大屏,整个项目的技术深度和演示效果都能兼顾。如果你正在做 Python 或大数据方向的毕业设计,这是一个性价比很高的选题方向。
但很多同学在拿到题目后容易陷入一个误区:把全部精力放在“爬虫”上,以为爬到几万条小说数据就是项目成功了一大半。实际上,爬虫只是整个系统的第一公里。这个项目的真正难点在于:采集到的数据怎样从 CSV 落到 HDFS,怎样通过 Hive 或 MapReduce 统计成业务指标,统计结果怎样转移到 MySQL 并提供给后端接口,推荐算法又怎么和这条数据链路串成闭环。如果缺少对这一层的设计,最后做出来的效果往往停留在“爬虫 + Excel + 一个饼图”的层面,技术深度不够,答辩时也容易露怯。
这篇文章从工程视角拆解一个典型的基于 Hadoop 的网络小说数据分析系统,覆盖数据采集、Hadoop 存储、离线统计分析、协同过滤推荐算法、可视化大屏展示五大部分,并给出可运行的代码示例、环境搭建步骤、常见 Bug 排查表和答辩准备建议。全文不保证能让你“一键复制跑通”,但会帮你把项目的骨架和关键模块讲清楚,让毕设作品从“能跑”提升到“能讲、能答、有亮点”。
1. 这个选题真正要解决的问题
毕设选题最重要的不是代码量,而是在有限的几个月里能做完整、能自圆其说。网络小说数据分析系统的巧妙之处在于,它把大数据开发中最常用的几项能力串成了一条完整链路:
- 数据采集能力,用 Python 爬虫抓取公开页面数据;
- 分布式存储能力,把采集结果落地到 HDFS;
- 离线计算能力,用 Hive SQL 或 MapReduce 做分类统计、排行榜、数量趋势分析;
- 算法应用能力,基于用户行为数据实现协同过滤推荐;
- 前后端与可视化能力,通过后端接口把统计结果渲染成大屏图表。
这套题目之所以在毕业设计里受欢迎,是因为它不像“基于 SSM 的 XX 管理系统”那样停留在增删改查,而是让读者真实地接触一次“数据从无到有、从原始数据到分析结果”的完整过程。对于准备找大数据开发、Python 开发相关岗位的同学来说,这个项目里的每一个模块都可以写进简历,面试时被问到的概率也很高。
同时也要泼一盆冷水:如果你的 Hadoop 环境只是摆设,数据量只有几百条,统计逻辑全靠 MySQL 查询,那这个项目也就失去了“基于 Hadoop”的意义。老师最常追问的一个问题就是:“你的项目中 Hadoop 到底解决了什么问题?不用 Hadoop 行不行?”所以这篇文章有一个基本判断:选题本身并不新鲜,拉开差距的,是你有没有把数据链路讲透。
2. 系统整体架构与技术选型
在写代码之前,先确定整体架构。一个合格的毕业设计系统不要求技术栈有多复杂,但分层必须清晰。典型的项目结构可以按下面的方式来划分。
| 层次 | 技术选型 | 主要职责 |
|---|---|---|
| 数据采集层 | Python 3、requests、BeautifulSoup | 抓取小说列表、详情页公开信息,生成清洗后的 CSV 数据 |
| 数据存储层 | HDFS、Hive、MySQL | HDFS 存原始数据文件,Hive 建外部表做分析,MySQL 存统计结果和用户行为数据 |
| 计算分析层 | Hive SQL、MapReduce | 完成分类统计、字数分布、点击排行等离线指标计算 |
| 推荐算法层 | Python、pandas、scikit-learn | 基于物品的协同过滤,生成相似小说与 TopN 推荐 |
| 应用展示层 | Flask、ECharts、HTML/CSS/JS | 提供数据接口,渲染可视化大屏 |
这个选型对 Python 方向的毕设项目比较友好,因为爬虫、推荐算法、后端接口都可以用 Python 完成,不必在 Java 和 Python 之间频繁切换语言。当然,如果学校要求后端必须用 Java,也可以把 Flask 替换成 Spring Boot,整体架构仍然成立,只是推荐算法和爬虫部分可以根据团队情况拆分开。
数据流大致是:爬虫把数据写入本地 CSV → 将 CSV 上传到 HDFS → 在 Hive 中建立外部表并执行统计分析 → 将 Hive 统计结果导出并导入 MySQL → Flask 从 MySQL 读取数据并提供 JSON 接口 → 前端页面调用接口渲染 ECharts 大屏。推荐算法依赖用户行为数据,这套数据既可以是系统通过埋点收集的模拟行为,也可以由公开数据或实验数据生成,具体在第五章展开说明。
3. Python 爬虫:网络小说数据采集与清洗实现
3.1 采集哪些数据
网络小说领域的数据字段很有规律,爬虫只需要抓取公开列表页或详情页即可,一般不建议绕登录去采集用户隐私数据。采集的重点字段可以这样规划。
| 字段 | 示例 | 用途 |
|---|---|---|
| title | 某本玄幻小说 | 书名,是推荐的唯一标识 |
| author | 作者名 | 作者维度统计 |
| category | 玄幻/仙侠/都市 | 分类占比分析 |
| words | 356万字 | 字数区间分布分析 |
| clicks | 120万 | 点击排行 |
| recommend | 8.9万 | 推荐票/热度分析 |
| update_time | 2024-05-12 | 更新频率分析 |
从网页上抓到的字段通常是字符串,不能直接用于统计。尤其要处理“万字”“万”这类中文单位,建议在清洗阶段统一转成 int 类型。
3.2 一个可运行的爬虫代码示例
下面是一个用 requests 和 BeautifulSoup 实现的简化示例,抓取小说列表页并保存为 CSV。代码中的页面地址和 CSS 选择器是示意写法,真实项目中请替换为目标站点在浏览器中看到的实际选择器。
# -*- coding: utf-8 -*- # 文件路径:spider/novel_spider.py import time import pandas as pd import requests 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 Safari/537.36" } def fetch_novel_list(page=1): # 示意地址,实际使用时替换为可公开访问的目标页面 url = f"https://www.example-novel.com/list?page={page}" resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") books = [] for item in soup.select(".book-item"): books.append({ "title": item.select_one(".title").get_text(strip=True), "author": item.select_one(".author").get_text(strip=True), "category": item.select_one(".category").get_text(strip=True), "words": item.select_one(".words").get_text(strip=True), "clicks": item.select_one(".clicks").get_text(strip=True), "recommend": item.select_one(".recommend").get_text(strip=True), "update_time": item.select_one(".update-time").get_text(strip=True) }) return books def clean_words(value): """把 '356万字' 清洗成整数 3560000,便于后续统计""" value = str(value).replace("字", "").strip() if "万" in value: return int(float(value.replace("万", "")) * 10000) return int(value) def main(): all_books = [] for page in range(1, 11): page_books = fetch_novel_list(page) print(f"第 {page} 页抓取到 {len(page_books)} 本小说") all_books.extend(page_books) time.sleep(1) # 控制抓取频率,避免给目标站点造成压力 if not all_books: print("未采集到数据,请检查页面结构和选择器") return df = pd.DataFrame(all_books) df["words"] = df["words"].apply(clean_words) # 使用 utf-8-sig 编码,方便 Excel 打开,也避免表头乱码 df.to_csv("novel_data.csv", index=False, encoding="utf-8-sig") print(f"清洗完成,共保存 {len(df)} 条数据") if __name__ == "__main__": main()这段代码看似简单,实际运行中会有几个容易出问题的点。第一,很多小说的点击量、推荐量也带有“万”单位,光清洗words字段不够,clicks和recommend也要做同样的转换。第二,to_csv时如果使用默认的utf-8,用 Excel 打开 CSV 会出现中文乱码,改为utf-8-sig基本可以解决。第三,time.sleep(1)是必要的,没有停顿的连续请求很容易触发站点访问频率限制,导致后续请求返回验证页或空数据。
3.3 清洗与去重策略
页面数据抓下来之后还有一道工序,就是按业务分析需求做清洗。除了上面对齐“万”单位,通常还要做以下处理:
- 去重:小说可能出现在多个榜单中,要按
title + author或title去重; - 去空:删除标题、分类为空的记录;
- 统一分类:不同页面可能把“玄幻/奇幻”“东方玄幻”混用,建议先映射到一级分类;
- 时间格式:
update_time尽量统一成YYYY-MM-DD,便于做按周、按月趋势统计。
清洗逻辑在落盘之前完成,输出成一份规范化的novel_data.csv。这份文件既是 HDFS 的输入,也是后续所有统计和推荐的数据基础,所以清洗质量会直接影响最终图表的可信度。在实际毕设项目中,强烈建议把清洗逻辑单独封装成一个函数或脚本,不要和爬虫入口混在一起。
3.4 爬虫运行结束却没有数据的排查方向
很多 Python 新手遇到的第一个困惑是:在 PyCharm 里运行爬虫,控制台只显示“Process finished with exit code 0”,完全没有输出,也不知道爬到了什么。遇到这种情况,不要怀疑电脑坏了,按下面的顺序排查。
- 检查
main()是否真的被调用。如果文件底部没有if __name__ == "__main__":,或者忘记调用main(),代码会安静地执行完就退出。 - 打印响应状态码和响应内容前 500 个字符。如果
resp.status_code不是 200,或者返回内容里没有小说列表节点,大概率是访问被限制或页面结构不同。 - 确认 CSS 选择器是否正确。可以在浏览器开发者工具里复制实际元素的选择器,再和代码中的选择器对比。
- 确认是否设置了编码。中文字符站点如果编码判断错误,解析出来也往往是空列表。
把这些检查做成一个print日志输出,能节省大量调试时间。
4. Hadoop 数据落地:HDFS 存储与 Hive 离线统计
4.1 为什么要把数据放到 HDFS 上
用 MySQL 存小说数据当然也可以跑通,但“基于 Hadoop”的项目不能只在标题里体现。让数据真正进入 HDFS,你才有机会讲清楚大数据组件怎么用、为什么这样设计。HDFS 解决的并不是“单机存不下”这一个问题,而是通过把文件切成块并分布到多台节点上,让系统具备横向扩展能力。在单机伪分布式环境下,它模拟的正是这种分布式存储机制。
对毕设项目来说,不需要追求几十 GB 的数据量。几百 MB 的 CSV 文件放在 HDFS 上完全足够演示,只要你能解释清楚:如果数据继续增长到 GB 甚至 TB 级别,HDFS 如何通过增加 DataNode 来扩展容量,Hive 如何通过 MapReduce 把计算任务分散到多台机器上。
4.2 将本地 CSV 上传到 HDFS
Hadoop 环境启动好之后,第一步是在 HDFS 上创建项目目录,然后把爬虫生成的 CSV 文件上传上去。
# 在 HDFS 上创建原始数据目录 hdfs dfs -mkdir -p /novel/raw # 上传本地清洗后的数据 hdfs dfs -put novel_data.csv /novel/raw/ # 确认上传结果 hdfs dfs -ls /novel/raw/这里有一个容易踩的坑:不要在目录没有创建的情况下直接执行-put;另外,如果后续还要重复覆盖数据,建议把-put换成-put -f,否则文件已存在时会报错。开发阶段的伪分布式环境数据量不大,覆盖式上传问题不大。
4.3 在 Hive 中创建外部表
上传完成后,在 Hive 中创建一个外部表来映射这份 CSV。使用EXTERNAL TABLE的好处是:删除表不会删除 HDFS 上的原始文件,数据更容易管理和恢复。因为爬虫生成的 CSV 第一行是表头,所以建表时要用skip.header.line.count属性跳过第一行。
CREATE EXTERNAL TABLE IF NOT EXISTS novel_db.novel_info( title STRING, author STRING, category STRING, words BIGINT, clicks BIGINT, recommend BIGINT, update_time STRING ) ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE LOCATION '/novel/raw/' TBLPROPERTIES ('skip.header.line.count'='1');建表成功后,先执行几条简单的查询确认数据可以正常读取。
-- 查看表有多少条数据 SELECT COUNT(*) FROM novel_db.novel_info; -- 按分类统计小说数量 SELECT category, COUNT(*) AS cnt FROM novel_db.novel_info GROUP BY category ORDER BY cnt DESC LIMIT 10;发现查询结果都是 NULL 时,优先检查 CSV 的分隔符是否真的是英文逗号,以及字段顺序与建表语句是否一致。小说简介或书名中如果包含中文逗号,一般不会造成影响;但如果数据中出现了字段值本身带英文逗号的情况,CSV 格式就不稳健了。工程上更稳妥的做法是爬虫导出时使用\x01作为分隔符,比如sep="\x01",再在 Hive 建表时写成FIELDS TERMINATED BY '\001',这样能很大程度规避逗号转义问题。
4.4 Hive 与 MapReduce 的分工
在一些学院派项目中,老师希望看到你实现了自定义 MapReduce,而不是只会写 Hive SQL。这里有个平衡建议:核心的统计分析,比如分类占比、点击 Top N,用 Hive SQL 完成,开发效率高,也好解释;另外挑选一个适合体现 Hadoop 编程能力的指标,用 Java 写一个 MapReduce Job,比如统计“每天上架小说数量”或“各作者入库作品数”。逻辑本身不复杂,Mapper 解析每一行并输出(author, 1),Reducer 对相同作者累加计数,最终把结果输出到 HDFS 指定目录。
MapReduce 代码在原理上其实等价于GROUP BY author,但亲手实现一遍能帮助你理解 Hive 底层是怎么翻译成分布式任务的。这一步对答辩极其有帮助,因为老师经常会顺着问你“Hive 和 MapReduce 是什么关系”。能答出“Hive 会把 SQL 编译成 MapReduce 作业在集群上执行”,在大多数答辩里已经是加分效果。
4.5 统计结果如何导出
Hive 统计结果通常需要从 HDFS 拿到本地,再导入 MySQL,因为可视化后端依然基于关系型数据库查询。常用的方法是用INSERT OVERWRITE DIRECTORY把结果写到指定目录,然后用hdfs dfs -getmerge合并成单个 CSV 文件。
hive -e "INSERT OVERWRITE DIRECTORY '/novel/result/category_stat' ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' SELECT category, COUNT(*) AS cnt FROM novel_db.novel_info GROUP BY category;" hdfs dfs -getmerge /novel/result/category_stat /tmp/category_stat.csv这里的思路是:统计分析在 Hadoop 侧完成,减少 MySQL 的计算压力;MySQL 只存放最终结果表,供后端页面查询。对一个小型毕设系统来说,这种分层已经够清晰了。
5. 协同过滤推荐算法:让系统具备个性化能力
5.1 为什么选择基于物品的协同过滤
网络小说推荐本质上是一个“用户-物品”场景。推荐算法有很多种:基于内容的推荐、基于用户的协同过滤、基于物品的协同过滤、矩阵分解