Hadoop+Spark+Python起点小说数据可视化分析系统毕设全攻略
2026/9/9 20:42:26 网站建设 项目流程

做毕设选这个题目的人,我见的可太多了。每年都有大批计算机专业的学生,拿到“XX数据可视化分析系统”这种题目就头疼,要么不知道从哪里下手,要么做成了简单的管理系统,技术含量不够,答辩的时候被老师追问几句就卡壳。

这篇就专门拆解一下基于Hadoop+Spark+Python的起点小说网数据可视化分析系统这种经典毕设项目,从数据爬取到数据分析再到可视化展示,整个链路每一步怎么落地、怎么避开那些坑,全部讲清楚。无论你是刚接触大数据的小白,还是已经折腾过几天环境的老手,这篇都能让你少走不少弯路。

先说一下这个项目到底解决了什么问题。很多同学一看到“起点小说网”就想着把整个网站全部爬下来,这实际上是一个典型的错误开题姿势。核心在于毕设的目标不是做一个完整的搜索引擎,也不是替代起点本身,而是要做一套能体现大数据处理全流程的Demo系统。爬虫负责采集一部分有代表性的数据,Hadoop负责存储,Spark负责分析,前端页面负责把分析结果展示出来,整个过程演示了完整的大数据离线处理流程,这才是老师在毕业设计里想看到的东西。

文章后面会分成几大块来讲:第一块是整个项目的技术选型和架构设计思路;第二块是数据爬取环节怎么实现、怎么应对反爬;第三块是数据落到HDFS之后,怎么用Hive做清洗,怎么设计表结构;第四块是Spark核心分析逻辑怎么写,包括分词、统计、TopN这些常见需求;最后讲可视化端怎么对接,以及实际开发中会踩到的高频坑。每一块我都会把实践细节和参数选择讲透,而不是只给个概念。

1. 项目设计与技术选型:为什么要用这套组合

1.1 核心需求解构

先说清楚这个题目背后真正考察的是什么。起点小说网数据可视化分析系统,关键词拆开是三个:起点小说网(数据源)、数据可视化(展示目标)、分析系统(核心计算)。

所以整套系统要回答的问题其实是:起点的小说数据长什么样?哪些作品热度高?哪些作者高产?小说分类分布如何?读者对哪些类型的书最感兴趣?这些问题的答案,最终都要落到图表上。

从老师的评分角度来说,最看重的是三个维度:

  • 技术栈是否完整:是否覆盖了数据采集、存储、计算、展示的完整链路
  • 工程实现是否可靠:代码能不能跑通,结果对不对,系统稳不稳定
  • 分析结果是否有意义:做出来的图表能不能说明问题,而不是为了有图而有图

这套技术栈的选择,就是为了在这三个维度上都拿到高分。

1.2 为什么是Hadoop+Spark+Python

很多同学纠结技术选型,其实这个组合是最稳的答案,原因很简单:

Hadoop承担的是分布式存储底座。爬虫爬下来的原始数据,直接写本地文件,那就体现不出大数据的含义。数据放到HDFS上,伪分布式集群在你自己的电脑上就能跑,这就能完整演示Hadoop的存储能力。而且HDFS对批量读写的支持非常好,尤其适合这种“一次写入、多次读取”的分析场景。

Spark承担的是核心计算引擎。HDFS存数据没问题,但它不能高效做复杂的分析计算。Spark基于内存计算,在处理迭代式算法和交互式查询时,性能明显优于MapReduce。而且Spark提供Python API(PySpark),这对用Python写毕设的学生来说非常友好,不用去写那一大堆Java代码。

Python负责串联整个链路。爬虫用Python写,调用Spark用Python写,后端Web框架用Flask或Django,可视化参数用Python处理后传前端。整个项目只用一种语言就把全链路串通了,代码量少,调试方便,答辩时解释起来也流畅。

有一个常见的误解是“Hadoop和Spark必须分开部署在不同的机器上”,其实完全不是这样。毕设级别的伪分布式环境,所有组件都在一台机器上跑,完全没问题。Hadoop的NameNode、DataNode、Spark的Master、Worker,这些进程在单机模式下都是可以共存的。

1.3 整体架构分层设计

我建议把整个系统按照如下分层来设计,这也是大数据项目的标准分层方式:

层级组件职责说明
数据采集层Python爬虫(Requests + BeautifulSoup / Scrapy)采集起点小说网的小说基础信息、作者信息、推荐数据等
数据存储层HDFS + HiveHDFS存原始数据,Hive建外表做结构化映射和SQL清洗
数据分析层Spark(PySpark)基于清洗后的数据做统计分析、文本分词计算
数据展示层Flask + ECharts后端接口返回JSON数据,前端ECharts渲染图表

这个分层有一个好处,每一层之间有清晰的边界,出问题的时候定位方便,写论文的时候也能分章节讲清楚。而且每一层都有独立的技术点可以展开,论文的篇幅天然就撑起来了。

1.4 开发环境选型与安装要点

环境搭建是很多学生最先卡住的地方,这里直接给一套经过实践的方案。

基础环境信息:

  • 操作系统:Ubuntu 20.04(Windows也可以搭,但Linux下遇到问题的概率小很多)
  • JDK版本:1.8(Hadoop 3.x和Spark 3.x对JDK8支持最稳定)
  • Hadoop版本:3.3.x
  • Spark版本:3.3.x(配套选择Scala 2.12版本包)
  • Python版本:3.8或3.9
  • Hive版本:3.1.x

具体安装步骤不展开细说了,网上教程一大堆,但说三个关键注意点:

第一,Hadoop的etc/hadoop/目录下core-site.xml、hdfs-site.xml、yarn-site.xml这几个配置文件的参数别乱改。很多教程教你把副本数设成3,伪分布式模式下只有一个DataNode,副本数必须改成1,否则会一直报副本缺失的告警。

第二,Spark和Hadoop的版本必须兼容。Spark 3.x对应Hadoop 3.x的构建版本,下载的时候选spark-3.3.x-bin-hadoop3版本,如果选错了后面提交任务会报各种奇怪的兼容性错误。

第三,环境变量要配置全。JAVA_HOME、HADOOP_HOME、SPARK_HOME、PYTHON_HOME都要配好,PATH里把bin目录都加进去。我见过太多人卡在环境变量上,明明安装都是对的,就是在启动的时候报“command not found”。

2. 数据爬取环节:采集小说数据与反爬应对

2.1 爬虫需求分析与字段设计

在动手写爬虫之前,先想清楚一个问题:到底需要爬哪些数据?

这两类页面是数据采集的核心,按以下字段抓取:

  • 小说列表页/搜索页:书名、作者、分类、简介、总字数、状态(连载/完结)、收藏数、推荐票数、最近更新时间
  • 小说详情页:章节数、评分、月票数、粉丝数、最新章节信息

字段不要贪多,够用就行。有些同学想着把所有能看到的字段都爬下来,结果数据结构臃肿,Hive表建起来麻烦,Spark分析也用不到几个字段,反而增加工作量。

起点网比较核心的指标数据,比如推荐票、月票、收藏量,这些字段在详情页是能直接拿到的。为了分析的便利性,建议把数值型字段统一转成int或float,比如“12.3万”这种格式,存的时候就要转成123000,不然后续做排序、聚合的时候会非常痛苦。

2.2 爬虫框架选择:Requests还是Scrapy

两种方案各有利弊,我分别说。

Requests + BeautifulSoup方案更轻量。对于毕设来说,数据量并不大,抓个几千本小说就足够了,用Requests写同步请求就够了。代码逻辑直观,调试方便,遇到反爬时可以快速加请求头、加延时。代码结构就是经典的:构造请求、获取响应、解析HTML、提取字段、存储结果。

Scrapy方案是分布式爬虫的首选,自带异步处理、中间件机制、Item Pipeline,扩展性好。但Scrapy的学习曲线相对陡峭,而且对HTML解析没那么直接,还需要配合Selector语法。如果之前的项目里没用过Scrapy,不建议在毕设阶段临时换新框架。

我自己做这个毕设项目时,用的是Requests + BeautifulSoup,爬约2000本热门小说数据,每分钟能爬到几十本小说信息,总耗时约半小时。这个速度对于毕设演示完全够用。核心代码如下:

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/91.0.4472.124 Safari/537.36', 'Referer': 'https://www.qidian.com/', 'Accept-Language': 'zh-CN,zh;q=0.9' } def fetch_book_info(session, book_id): """抓取单本小说详情页的基础信息""" url = f"https://book.qidian.com/info/{book_id}/" try: resp = session.get(url, headers=headers, timeout=10) resp.encoding = 'utf-8' if resp.status_code != 200: return None soup = BeautifulSoup(resp.text, 'html.parser') # 书名、作者、分类等字段提取逻辑 book_info = { 'book_id': book_id, 'book_name': soup.select_one('.book-info h1 em').text.strip(), 'author': soup.select_one('.book-info h1 a').text.strip(), 'category': soup.select_one('.book-info .tag a').text.strip(), 'intro': soup.select_one('.book-info .intro').text.strip(), 'status': soup.select_one('.book-info .status').text.strip(), 'word_count': parse_word_count(soup.select_one('.book-info .total-word').text.strip()) } return book_info except Exception as e: print(f"抓取失败: {book_id}, 错误: {e}") return None def parse_word_count(text): """把'123.45万字'转成整数1234500""" if '万' in text: num = float(text.replace('万字', '')) return int(num * 10000) return int(text.replace('字', ''))

2.3 反爬策略应对方案

起点小说网站的反爬策略,我实测下来主要有这几个层面:

UA检测。最简单的反爬,如果不设置User-Agent,请求会被直接拒绝。解决方法就是伪装浏览器,上面代码里的headers就是干这个用的。

IP频率限制。短时间大流量访问会被封IP。毕设阶段不需要并发拉高,老老实实加延时。建议每两个请求之间延迟3到5秒,偶尔随机休眠。这样虽然慢一点,但胜在稳定。

详情页URL规律。热门小说详情页的URL是有规律的,格式是book.qidian.com/info/数字ID。但这里的ID是起点站的内部编号,不是连续递增的,需要先去排行榜页面把热门书的ID抓下来,再去拼详情页URL。所以完整的爬虫流程是:先爬排行榜页拿ID列表,再逐本爬详情页。

动态渲染的内容。部分字段(比如月票数、打赏数)可能由前端JavaScript异步加载生成。如果发现BeautifulSoup解析不到对应字段的值,可以先用浏览器开发者工具(F12)查看网络请求,找到真实的JSON数据接口,直接请求接口拿数据,比解析HTML更稳定。

下面是一段从排行榜页提取小说ID的代码,核心思路是用正则表达式从HTML里抽取URL中的数字ID:

import re def fetch_book_ids(session, rank_url): """从排行榜页面提取小说ID列表""" resp = session.get(rank_url, headers=headers, timeout=10) resp.encoding = 'utf-8' # 在HTML中查找 /info/后面的数字ID pattern = re.compile(r'/info/(\d+)/?') book_ids = list(set(pattern.findall(resp.text))) return book_ids

2.4 数据落盘方案与备份策略

爬下来的数据落盘,我推荐直接写JSON Lines格式,每一行一个JSON对象。这种格式的好处很多:每行独立,可以流式读取,不需要加载整个文件到内存;字段结构清晰,Hive建表时直接映射JSON;Python和Spark都原生支持。

import json def save_books_to_file(book_list, filepath): """将小说信息写入JSON Lines文件""" with open(filepath, 'w', encoding='utf-8') as f: for book in book_list: f.write(json.dumps(book, ensure_ascii=False) + '\n')

存文件的时候建议按分类分成不同的文件,比如novel_fantasy.jsonnovel_science.json,后面把数据上传到HDFS的时候,可以按目录整体上传,Hive建表时也可以按分类partition,分析的时候可以用分区裁剪避免全表扫描,对这个体量的项目来说不是必须的,但对理解分区的概念有帮助。

注意:爬虫采集数据时要遵守目标网站的robots协议,仅用于学习研究目的。项目演示时用到的数据量控制在小规模即可,不要大规模、高频次抓取。

3. 数据存储与预处理:HDFS建目录与Hive清洗

3.1 HDFS目录规划与数据上传

HDFS本身就是一个文件系统,存储结构以目录树形式组织。对于这个项目,建议在HDFS上按时间建立目录层次,这样后续如果想做增量更新,目录结构不用改。

推荐的HDFS目录结构如下:

/user/hadoop/qidian/ ├── rawdata/ # 原始采集数据 │ ├── book_info.json # 小说基础信息 │ └── book_rank.json # 排行榜数据 ├── cleaned/ # 清洗后的数据 │ ├── book_clean.parquet # 清洗后的小说数据 │ └── ... ├── analysis/ # 分析结果输出 │ ├── category_stats.csv │ ├── author_rank.csv │ └── word_freq.csv └── tmp/ # 临时文件目录

上传文件用hdfs dfs -put命令即可:

# 创建目录 hdfs dfs -mkdir -p /user/hadoop/qidian/rawdata # 上传本地文件 hdfs dfs -put ./data/novel_fantasy.json /user/hadoop/qidian/rawdata/ # 查看上传结果 hdfs dfs -ls /user/hadoop/qidian/rawdata/

提示:如果你用的是root用户,HDFS的根目录权限可能受限。建议在创建Hadoop用户后用该用户操作,或者先对目录执行hdfs dfs -chmod -R 777 /user/hadoop,否则后续写数据时会报Permission denied

3.2 Hive外部表设计:为什么选外部表

Hive本身是一个数据仓库工具。这里要重点说一个概念:Hive表分为内部表和外部表,外部表和内部表的核心区别在于,删除外部表时只删除Hive中的元数据,HDFS上的实际数据文件不会被删除。

在这个项目里,我强烈建议用外部表。原因很简单:原始数据是爬虫程序生成的,如果Hive用内部表管理,后续重新爬数据时不小心把表drop掉了,HDFS上的数据也就没了,等于前功尽弃。外部表则允许你随时重建表结构,数据文件始终在HDFS上,不会因为误操作丢失。

建表语句设计如下,注意JSON数据需要用json_tuple或者get_json_object进行解析:

CREATE EXTERNAL TABLE IF NOT EXISTS qidian_book_ods ( book_id STRING, book_name STRING, author STRING, category STRING, intro STRING, status STRING, word_count BIGINT, recommend_count BIGINT, collect_count BIGINT ) ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe' LOCATION '/user/hadoop/qidian/rawdata/';

如果JSON字段比较多、嵌套层级较深,直接映射SerDe可能会出问题。这时候可以先建一个仅包含原始JSON字符串的外部表,然后用get_json_object函数提取字段:

CREATE EXTERNAL TABLE IF NOT EXISTS qidian_book_raw ( json_data STRING ) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\n' STORED AS TEXTFILE LOCATION '/user/hadoop/qidian/rawdata/'; -- 查询解析 SELECT get_json_object(json_data, '$.book_id') AS book_id, get_json_object(json_data, '$.book_name') AS book_name, get_json_object(json_data, '$.word_count') AS word_count FROM qidian_book_raw LIMIT 10;

这个方案调试起来很容易,解析出问题的时候,先看原始JSON字符串,再排查解析函数。

3.3 数据清洗细节:空值、重复值和异常值

爬虫爬回来的数据非常“脏”,直接拿去分析一定会出问题。数据清洗这个环节,是你论文中“数据预处理”章节的核心内容,也是答辩时体现工程能力的地方。

我总结了三个类型的清洗需求:

空值和缺失值。部分小说简介可能为空,部分字段可能缺失。处理策略是:核心分析的字段(书名、作者)如果为空,直接丢弃该条记录;非核心字段(简介、评分)为空,填默认值。例如:COALESCE(intro, '暂无简介')

去重。爬数据的时候,同一本书可能被爬了多次。去重的依据是book_id,保留最新一条即可。Hive里可以用ROW_NUMBER()窗口函数实现:

CREATE TABLE qidian_book_cleaned AS SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY book_id ORDER BY collect_count DESC) AS rn FROM qidian_book_ods ) t WHERE rn = 1;

异常值。有些字段的值不合理,比如字数为0、收藏数为负数等。这类记录要么删除,要么修正。建议在清洗SQL中加上过滤条件:

WHERE word_count > 0 AND collect_count >= 0 AND category != ''

3.4 清洗结果写入Parquet格式

清洗完的数据,写入时建议用Parquet格式,而不是继续存文本文件。Parquet是列式存储格式,Spark读Parquet的查询性能远高于读文本文件,而且自带压缩,存储空间能节省不少。

Hive里创建Parquet格式的表,用STORED AS PARQUET即可:

CREATE TABLE qidian_book_cleaned_parquet ( book_id STRING, book_name STRING, author STRING, category STRING, intro STRING, status STRING, word_count BIGINT, recommend_count BIGINT, collect_count BIGINT ) STORED AS PARQUET LOCATION '/user/hadoop/qidian/cleaned/';

把清洗结果写入这张表:

INSERT OVERWRITE TABLE qidian_book_cleaned_parquet SELECT book_id, book_name, author, category, intro, status, word_count, recommend_count, collect_count FROM qidian_book_cleaned;

到这一步,存储层和预处理层的活就算干完了。接下来进入整个项目最核心的部分——用Spark做数据分析。

4. 数据分析:Spark核心计算逻辑实现

4.1 Spark在项目中的角色定位

Spark在这个项目里干的是最核心的活,也可以说是整个系统中最有技术含量的部分。

很多人对Spark的理解停留在“大数据计算框架”这个层面,但实际用起来会困惑:明明Hive SQL也能做统计,为什么还要用Spark?这个问题想明白了,你对这个项目的答辩也就稳了。

这个项目里Spark承担两类任务:

批处理ETL。把Hive里的清洗结果读出来,转为DataFrame,做更复杂的转换、聚合操作。Hive SQL能做的,Spark SQL都能做,而且Spark基于内存计算,速度更快。

复杂分析算法。比如对小说简介做中文分词,统计词频,提取热词。这类文本处理任务,如果用Hive SQL写会非常痛苦,而Spark的RDD/DataFrame API配合Python的jieba分词库,几行代码就能搞定。

4.2 PySpark环境配置与基础代码框架

写PySpark代码之前,先确保pyspark能正常导入运行。在Linux环境下,安装好Spark之后,可以用pip安装pyspark:

pip install pyspark==3.3.0

然后写一段测试代码,确认连接没问题。以下是我在这个项目里使用的PySpark基础框架:

from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("QidianNovelAnalysis") \ .master("local[*]") \ .config("spark.sql.shuffle.partitions", "4") \ .config("hive.metastore.uris", "thrift://localhost:9083") \ .enableHiveSupport() \ .getOrCreate() # 读取Hive表数据,此时拿到的是DataFrame df = spark.sql("SELECT * FROM qidian_book_cleaned_parquet") df.show(5) df.printSchema()

几个配置参数的说明:

  • master("local[*]"):本地模式运行,*表示使用所有可用CPU核心。毕设阶段不需要提交到YARN集群,本地跑最省事。
  • spark.sql.shuffle.partitions:shuffle分区数。默认200,对于小数据集反而带来不必要的开销,改成4能明显提升小数据集的运行速度。
  • enableHiveSupport():让Spark能访问Hive表。这需要Spark能连接到Hive Metastore,确保Hive服务已启动。

4.3 热门小说Top榜单统计计算

先做一个最简单的分析需求:统计收藏数最高的Top10小说。

用PySpark实现如下:

from pyspark.sql.functions import col, desc # 收藏数最高的Top10小说 top10_collect = df.orderBy(col("collect_count").desc()).select( "book_name", "author", "category", "collect_count", "word_count" ).limit(10) top10_collect.show(truncate=False) # 推荐票数最高的Top10小说 top10_recommend = df.orderBy(col("recommend_count").desc()).select( "book_name", "author", "category", "recommend_count" ).limit(10) top10_recommend.show(truncate=False)

这里有一个非常实用的小技巧:把所有分析结果都写入CSV文件,保存到HDFS的分析结果目录,供后续可视化系统读取。为什么不直接从Spark结果到前端?因为Spark的计算是一次性完成,可视化的展示可以复用计算结果,没必要每次都重新跑一遍Spark任务。

# 将结果导出为CSV,供后续可视化使用 top10_collect.write.csv("/user/hadoop/qidian/analysis/top10_collect", header=True, mode="overwrite") top10_recommend.write.csv("/user/hadoop/qidian/analysis/top10_recommend", header=True, mode="overwrite")

注意:CSV写入后,Spark会生成一个目录而不是单个文件,里面是part-00000这样的分区文件。前端读取的时候,直接用spark.read.csv读取整个目录即可。

4.4 分类维度聚合统计

分析小说在各分类下的数量分布、平均字数、收藏总量,是非常经典的多维统计分析。用Spark的groupBy和聚合函数可以实现:

from pyspark.sql.functions import count, avg, sum, round as spark_round category_stats = df.groupBy("category").agg( count("book_id").alias("book_count"), spark_round(avg("word_count"), 2).alias("avg_word_count"), sum("collect_count").alias("total_collect_count"), sum("recommend_count").alias("total_recommend_count"), spark_round(avg("collect_count"), 2).alias("avg_collect_count") ).orderBy(col("book_count").desc()) category_stats.show(truncate=False)

这个结果表可以做出一张非常漂亮的分组柱状图,展示各类别的小说数量和热度。答辩的时候,老师看到这个分析维度,会认为你对业务场景有理解。

4.5 文本分析与热词提取

这部分是Spark能力的最佳展示。小说简介文本可以拿来提取关键词,看看这些热门小说都在讲些什么主题。

流程分成三步:

第一步:用jieba对简介做分词。把所有简介字段合并成长文本,然后逐条分词。jieba在Python单机环境下好用,但在Spark集群上要在每个executor上安装jieba库,这里用udf(用户自定义函数)的方式把分词逻辑封装起来。

import jieba from pyspark.sql.functions import udf from pyspark.sql.types import ArrayType, StringType def segment(text): if not text or text == '暂无简介': return [] # 去掉无意义字符 text = text.replace('\n', '').replace(' ', '') # 精确模式分词 return [word for word in jieba.cut(text) if len(word) > 1] segment_udf = udf(segment, ArrayType(StringType())) df_with_words = df.withColumn("words", segment_udf(df["intro"])) df_with_words.show(5, truncate=True)

第二步:加载停用词表,过滤无意义词汇。分词后会有大量“如果、但是、一个、没有”这类停用词,不去掉的话统计出来的词频就是一片噪声。网上可以下载中文停用词表,然后加载进程序里过滤。

stopwords = set() with open("data/stopwords.txt", "r", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) def filter_words(words): return [w for w in words if w not in stopwords and len(w) >= 2] filter_udf = udf(filter_words, ArrayType(StringType())) df_filtered = df_with_words.withColumn("filtered_words", filter_udf(df_with_words["words"]))

第三步:用explode把词数组展开成多行,然后做词频统计

from pyspark.sql.functions import explode word_counts = df_filtered.select(explode("filtered_words").alias("word")).groupBy("word").agg(count("*").alias("count")).orderBy(col("count").desc()) # 取Top50热词 top50_words = word_counts.limit(50) top50_words.show(truncate=False) # 写结果到HDFS,后续给词云展示用 top50_words.write.csv("/user/hadoop/qidian/analysis/top50_words", header=True, mode="overwrite")

4.6 作者维度分析与情感分析扩展

再补充一个常见的分析维度:作者作品量与作品热度。哪些作者写了最多本书?哪些作者的作品收藏量最高?

author_stats = df.groupBy("author").agg( count("book_id").alias("book_count"), sum("collect_count").alias("total_collect_count") ).orderBy(col("total_collect_count").desc()) author_stats.show(truncate=False)

如果还想增加内容深度,可以尝试对小说简介做简单的情感分析。我这里用SnowNLP库,给每条简介打一个情感分数(0到1之间,越接近1越正面),然后统计各分类的情感均值。这个分析维度属于“锦上添花”,如果时间来得及可以做,做完了论文里能多一章内容。

from snownlp import SnowNLP def sentiment_score(text): if not text or len(text) < 10: return 0.5 try: return SnowNLP(text).sentiments except: return 0.5 sentiment_udf = udf(sentiment_score, DoubleType()) df_sentiment = df.withColumn("sentiment", sentiment_udf(df["intro"])) category_sentiment = df_sentiment.groupBy("category").agg(avg("sentiment").alias("avg_sentiment")).orderBy(col("avg_sentiment").desc()) category_sentiment.show(truncate=False)

关于spark任务调优,毕设数据集比较小,基本不需要太复杂的调优。但有两点可以注意:

一是Shuffle分区设置为CPU核心数的2-3倍比较合适。比如4核CPU就设8,数据量大一点也没关系。

二是常见的数据倾斜问题在这个量级下基本遇不到,如果数据量比较少就不必过分担心。

5. 数据可视化:Excel还是Web前端展示

5.1 可视化技术选型对比

可视化是系统最终的呈现窗口,这部分直接决定了答辩时老师的第一印象。很多同学在这里纠结要不要学Vue、学React,其实完全没有必要。

我对比一下常见的几种可视化方案:

方案优点缺点适用场景
Flask + ECharts轻量快速,图表丰富,交互性好需要写前后端代码推荐方案
Django + Highcharts全家桶方案,自带后台项目偏重,比Flask重有一定Django基础时可用
Pyecharts直接生成HTML最省事,几行代码出图交互性弱,图表不够灵活时间紧张时的备选方案
纯Jupyter Notebook写分析报告方便不是Web系统,展示效果差不适合做毕设系统

推荐方案是Flask + ECharts。原因有三点:Flask是Python后端中最轻量的Web框架,几十行代码就能起一个服务;ECharts是百度开源的JavaScript图表库,图表类型丰富、交互效果好,是数据可视化的事实标准;前后端分离清晰,前端负责渲染,后端提供JSON数据接口,架构上说出来有说服力。

5.2 Flask后端接口设计

后端接口的设计逻辑很简单:Spark分析结果已经落盘了,后端读这些结果文件,转成JSON返回给前端即可。

from flask import Flask, jsonify import pandas as pd app = Flask(__name__) def load_analysis_result(path): """读取Spark导出的CSV结果文件""" df = pd.read_csv(path) return df.to_dict(orient='records') @app.route('/api/top10/collect') def top10_collect(): data = load_analysis_result('/data/analysis/top10_collect.csv') return jsonify({'code': 0, 'data': data}) @app.route('/api/category/stats') def category_stats(): data = load_analysis_result('/data/analysis/category_stats.csv') return jsonify({'code': 0, 'data': data}) @app.route('/api/word/freq') def word_freq(): data = load_analysis_result('/data/analysis/top50_words.csv') return jsonify({'code': 0, 'data': data}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)

接口设计有几个要点要说清楚:

返回格式统一。无论什么接口,都返回{code: 0, data: [...]}这种格式,前端统一处理。这样前端代码简洁,不那么乱。

数据分析结果和可视化前端解耦。Spark分析只需跑一次,结果落盘后,不管前端怎么改、怎么刷新,都不用重新触发Spark计算。这个设计的好处非常多:开发期调试前端时不用反复跑Spark任务;演示效果更流畅;架构上的“离线计算+在线展示”分层也更清晰。

5.3 ECharts页面设计与图表类型选择

ECharts的使用不复杂,引入JS文件,写一个容器div,初始化图表,设置option就能渲染。核心在于选择什么图表来表达什么数据。

针对这个项目的分析结果,我推荐以下图表组合,这也是一个信息丰富的可视化Dashboard的标准配置:

分类分布柱状图:小说数量最多的Top10分类,X轴为分类名称,Y轴为小说数量,直观体现热门分类。

收藏量Top10排行榜:横向条形图,按收藏量排序展示,方便评委一眼看出头部作品。

字数分布箱线图或直方图:展示小说总字数的分布情况,体现数据分布特征。

词云图:基于Top50热词渲染词云,视觉冲击力强,是整个Dashboard的视觉焦点。

分类与收藏量散点图或热力图:展示不同分类下作品的数量、平均收藏关系。

一个页面的骨架代码示意:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>起点小说网数据可视化分析系统</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts-wordcloud@2.1.0/dist/echarts-wordcloud.min.js"></script> <style> .chart-container { width: 45%; height: 400px; display: inline-block; margin: 1%; } </style> </head> <body> <h1 style="text-align:center;">起点小说网数据可视化分析系统</h1> <div id="chartCategory" class="chart-container"></div> <div id="chartTop10" class="chart-container"></div> <div id="chartWordCloud" class="chart-container"></div> <div id="chartWordCount" class="chart-container"></div> <script> // 用fetch请求后端接口,渲染图表 fetch('/api/category/stats') .then(response => response.json()) .then(res => { var data = res.data; var chart = echarts.init(document.getElementById('chartCategory')); chart.setOption({ title: { text: '小说分类分布Top10' }, xAxis: { type: 'category', data: data.map(item => item.category) }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.map(item => item.book_count) }] }); }); </script> </body> </html>

5.4 页面布局与交互设计细节

视觉布局上,不要直接把所有图表堆在一个页面上。推荐的分区方式是:

  • 顶部:系统标题和数据概览卡片(总小说数、总作者数、总字数、更新状态分布)
  • 中间主区域:大尺寸图表区,放分类分布和Top10排行
  • 底部辅助区域:词云展示和作者分析

ECharts本身自带了一些交互能力,比如鼠标悬停显示数值、图例筛选、数据缩放等。这些默认交互已经能满足毕设展示需求。

有一个加分项要特别提醒:让图表支持点击联动。比如点击分类柱状图的某个分类,下面的排行榜图表自动切换到该分类下的Top10小说。这个效果在ECharts里可以用click事件实现,代码量不大,但答辩时演示效果会非常加分。

6. 常见问题与排查技巧实录

6.1 环境配置类问题的排查方法

这一类问题是占比最高的,大多数是Hadoop和Spark环境没配好。这里整理了高频问题的症状和解决思路:

症状可能原因排查和处理方法
NameNode is not startedcore-site.xml配置错误或未格式化执行hdfs namenode -format后重启
Connection refusedHDFS服务未启动start-dfs.sh启动,jps命令检查Java进程
Spark提交任务时ClassNotFoundSpark和Hadoop版本不兼容换用对应的hadoop3构建版本
Hive表查不到数据表目录位置错误DESCRIBE FORMATTED table_name查看Location
Permission deniedHDFS目录无权限切到hadoop用户或对目录执行chmod
Python导入pyspark失败环境变量未配置或未安装pyspark配置SPARK_HOME,pip install pyspark

6.2 Spark分析过程中的常见报错

这类报错集中在PySpark代码运行时的RDD操作、DataFrame转换和资源分配。

第一个坑:java.lang.OutOfMemoryError堆内存溢出

这个报错在毕设阶段出现的原因是Spark默认分配的executor内存不足。解决方式是在启动SparkSession时显式指定内存参数:

from pyspark import SparkConf from pyspark.sql import SparkSession conf = SparkConf() \ .set("spark.executor.memory", "2g") \ .set("spark.driver.memory", "2g") \ .set("spark.sql.shuffle.partitions", "4") spark = SparkSession.builder \ .appName("QidianNovelAnalysis") \ .config(conf=conf) \ .getOrCreate()

如果你的电脑内存本身就小,比如只有8G,建议爬取数据量控制在1万条以内,否则内存压力确实大。

第二个坑:CSV输出时生成了一堆part文件

这是正常现象。Spark的写操作是按分区写的,每个分区生成一个文件。解决方式有两种:一是读取时使用spark.read.csv("/path/to/dir")读取整个目录;二是写之前用coalesce(1)强制合并成一个分区,但要注意数据量过大的时候反而会造成单节点内存压力。

df_top10.coalesce(1).write.csv("/user/hadoop/qidian/analysis/top10_collect", header=True, mode="overwrite")

第三个坑:AnalysisException: Table not found

Spark默认没有连接到Hive Metastore。要么在SparkSession里配置enableHiveSupport(),要么直接用spark.read.parquet("hdfs:///path/to/parquet")读取Parquet文件而不是通过表名访问。

6.3 中文乱码的处理

中文乱码几乎是所有中文数据处理项目都会碰到的问题,这个项目里可能出现在三个环节:

爬虫阶段:Requests请求网页时,如果网站的编码是UTF-8,但Response对象没有正确识别编码,解析出来的中文就会乱码。解决方式是在拿到Response对象后立刻指定编码:

resp = session.get(url, headers=headers, timeout=10) resp.encoding = 'utf-8' # 强制指定编码

CSV文件读写阶段:Spark写CSV到HDFS时,默认编码是UTF-8,如果你在本地用Excel打开CSV,看到的就是乱码。因为Excel默认按GBK解析CSV文件。解决方式是用记事本或VSCode打开,或者让Spark写CSV时加上UTF-8 BOM头:

df.write.option("encoding", "UTF-8").csv("path", header=True)

前端展示阶段:浏览器默认按UTF-8解析HTML,只要HTML文件本身是UTF-8编码,一般不会乱码。如果仍然乱码,检查一下Flask的响应头,确保Content-Type: application/json; charset=utf-8

6.4 爬虫采集不到数据

有几个真实场景需要注意:

一个是请求头伪装不够。部分反爬策略会检查User-Agent、Accept-Encoding、Referer等头信息。解决方式是把Headers补充完整,模拟真实浏览器的请求。

另一个是登录限制。起点网的部分数据接口需要登录Cookie才能获取。如果只爬公开的排行榜和详情页普通字段,不登录也能完成,涉及登录态的数据就量力而行,没必要强行突破。

还有一个容易被忽略的点是网络重试机制。爬虫跑的时候,网络抖动可能导致单个请求失败,如果在代码里不做重试,就会漏掉一批数据。建议写一个简单的装饰器,失败后等2秒再重试,最多重试3次。

6.5 系统演示时的注意事项

最后补充几个关于最终演示和答辩的小建议,这些细节分享可能比技术本身还关键:

第一,确认Hadoop和Spark服务是否已启动。很多同学写好了系统,答辩现场演示时发现服务没启动,或者被之前关机后忘了重新开启。建议答辩前专门列一个启动清单:启动HDFS、启动YARN、启动Hive Metastore、启动Flask服务,一步步确认。

第二,预跑一次完整的Spark任务。答辩现场如果临时跑Spark,可能因为数据量或内存原因卡顿好几秒甚至几十秒,场面会比较尴尬。建议先把分析结果导出到本地静态JSON文件,并让Flask优先读取静态JSON;把Spark计算流程单独录制或准备好截图,用于讲解分析逻辑时展示,现场的主流程演示更稳。

第三,图表数据要多做几组。如果只有一张柱状图、一个词云,视觉效果会比较单薄。把分类分布、排行、字数分布、作者分析、情感分析、词云全部放上去,页面信息量丰富,老师的观感会好很多。

7. 项目扩展与答辩建议

7.1 数据增量更新机制

毕设做完之后,很多同学会想:这个系统能不能扩展成真正实用的系统?这里我给一个切实可行的扩展思路——数据增量更新。

当前的设计是一次性爬取全量数据,然后做分析。实际场景中,小说数据是实时变化的:新书发布、收藏量增加、作者更新章节。做一个定时增量更新的机制,系统的实用性会大大提升。

实现思路:爬虫定期(比如每天凌晨2点)运行,拉取增量数据;增量数据追加到HDFS的当日目录;Hive外部表新增分区;Spark任务重新执行分析并更新结果。

这套机制涉及的技术点包括调度系统(Airflow或Crontab)、Hive分区表、HDFS目录设计等,对毕设来说是高质量的扩展点。

7.2 实时计算扩展方向

另外一个扩展方向是引入Kafka + Spark Streaming做实时分析。

小说网站的某些数据(如实时热搜、实时推荐票变化)是有实时分析价值的。当前系统是离线批处理,可以扩展到实时计算。架构变为:爬虫或日志采集模块把数据发送到Kafka,Spark Streaming消费Kafka消息,以微批次方式做实时统计,结果写入Redis或数据库,前端通过WebSocket展示实时变化。

这个扩展方向的实现难度比增量更新大,需要引入Kafka和消息队列的概念,如果基础一般,不建议放进毕设正题,可以作为系统展望来讨论。

7.3 答辩常见问题与应答思路

答辩环节,老师大概率会围绕技术选型和数据结果来提问。以下是我总结的几个高频问题,提前准备好应答思路。

问题一:为什么选Spark而不是MapReduce?

回答思路:Spark基于内存计算,性能优于MapReduce的磁盘迭代;Spark提供DataFrame高级API和Python接口,开发效率远高于MapReduce的Java代码;本项目涉及文本分词等复杂计算,Spark的表达能力更强。

问题二:数据量这么小,用什么Hadoop?

回答思路:项目模拟的是大数据处理全流程。从技术架构来看,系统设计可扩展,当数据量增长时,只需增加节点和调整资源配置即可;从学习角度来看,掌握Hadoop生态是了解大数据处理的核心基础。同时可以指出,在实际生产环境中,数据量是不断增长的,当前的小量数据是功能验证阶段。

问题三:爬虫过程中怎么处理网站反爬?

回答思路:从请求头伪装、请求频率控制和多源数据获取三个维度回答,并强调遵守robots协议,仅做学习研究使用。

问题四:整个系统的性能瓶颈在哪?

回答思路:当前瓶颈在爬虫采集速度和单机资源限制,包括内存和CPU。如果数据量增长,可以考虑分布式爬虫(Scrapy+Redis)、部署到集群环境、引入消息队列削峰填谷。

问题五:分析结果有什么实际价值?

回答思路:从读者、作者、平台三个角度看:读者可以了解热门小说趋势;作者可以发现热门题材和竞争格局;平台可以了解用户偏好和内容生态结构,支持内容运营决策。

7.4 论文结构建议与工作量分配

搞定了开发之后,论文的写作也直接关系到最终成绩。建议论文的主体结构按以下来组织:

  • 绪论(研究背景、选题意义、国内外研究现状)
  • 相关技术介绍(Hadoop、Spark、Python爬虫、ECharts)
  • 系统需求分析(功能性需求、非功能性需求、可行性分析)
  • 系统总体设计(架构设计、模块划分、数据库设计)
  • 系统详细设计与实现(分章讲爬虫模块、存储模块、分析模块、可视化模块)
  • 系统测试(功能测试、性能测试)
  • 总结与展望

论文中技术章节的工作量分配,建议遵循“分析模块 > 爬虫模块 > 可视化模块 > 存储模块”的优先级。Spark分析模块最能体现技术含量,要写深写透;爬虫模块的细节也可以写得很充实;可视化和存储侧重展示实现效果和遇到的坑。

图表方面建议多画系统架构图、流程图、时序图和界面截图,这些图在答辩PPT里也直接用得上,比大段文字好讲很多。

最后再分享一个我在指导学生做类似项目时的体会:这个题目的完成度,很大程度上取决于你是否真正跑通了整体流程,而不是某一个环节做得多花哨。爬虫、Hadoop、Spark、可视化,任何一环断裂都会让系统显得不完整。建议严格按照上面的步骤一步步推进,每完成一个环节就做一次完整验证,不要攒到最后才联调。整个链路跑通的那一刻,你就已经超过大多数只写了半截代码的同学了。

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

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

立即咨询