2. 项目概述
2.1 这套系统到底是什么
先别被那一长串名字唬住。hadoop+Spark+django+网络爬虫+数据分析可视化,听起来像是一堆技术名词的堆砌,但拆开来看,它其实是一条非常清晰的数据流水线:用爬虫从网上抓数据,存到分布式环境里,用Spark做清洗和计算,再用Django把结果呈现在可视化大屏上。说白了,就是“采集—存储—计算—展示”四个环节的完整闭环。
我做这套系统的初衷很实际。当时手头有个课程设计的需求,要求做一个“基于大数据技术的网络爬虫及数据分析可视化”项目,不能只是随便爬个网页然后用matplotlib画两张图交差,得有大数据技术栈的参与感。于是我从零开始搭了这套架构,全程踩坑无数,最后把完整的源码、文档、调试过程和大屏可视化都整理了出来。这篇文章就是把整个项目的设计思路、技术选型、实操过程、踩坑记录完整地交代一遍,适合正在做大数据课程设计、毕业设计,或者想入门大数据全链路开发的同学参考。
2.2 这套系统能解决什么问题
很多人在做类似项目时会陷入两个极端:要么技术栈太浅,爬完数据用pandas一算就完事,答辩时被老师一问“你的数据量多大?用什么分布式框架处理的?”就哑火;要么过度设计,明明只有几万条数据,非要用Flink实时流处理,把简单问题复杂化。
这套系统的设计目标是:在“够用”和“有亮点”之间找平衡。用Hadoop做分布式存储,用Spark做离线批处理,用Django提供Web服务和可视化大屏接口,爬虫负责数据采集。整条链路是完整的大数据离线处理架构,但每个环节的复杂度都控制在一个人能够独立完成的范围之内。换句话说,这套系统既能满足课程设计对“大数据技术栈”的要求,又不会让你在答辩前因为搞不定高可用集群而崩溃。
我实测下来,单机伪分布式模式下,处理几万到几十万条数据完全没有压力,Spark的分布式计算能力在这种数据量下虽然属于“杀鸡用牛刀”,但作为学习演示和架构展示,价值是实实在在的。
3. 技术选型与整体架构设计
3.1 为什么是Hadoop和Spark这对组合
先聊一个很多新手会问的问题:有了Hadoop的MapReduce,为什么还要Spark?这两个东西不都是大数据计算框架吗?
确实,MapReduce是Hadoop的核心计算模型,但它的设计哲学是“磁盘迭代”——每一步计算的结果都要写回磁盘,下一阶段再从磁盘读出来。这种设计在处理复杂迭代计算时效率很低。Spark则完全不同,它基于内存计算,能直接把中间结果缓存在内存里,对于需要多轮迭代的任务(比如机器学习算法、图计算、多次join操作),性能可以比MapReduce快上几十倍。
在我的实际项目里,数据清洗和指标聚合的过程涉及多个阶段的转换操作。如果全部用MapReduce写,代码量会非常庞大,而且每个Job之间都要通过HDFS中转数据,调试效率很低。用Spark的RDD和DataFrame API,几十行代码就能完成相同的工作,开发和调试成本降低了一个量级。
但这并不意味着Hadoop就没用了。Spark虽然算得快,它本身并不负责存储,数据还是得放在HDFS上。所以合理的组合是:HDFS提供分布式存储,Spark负责计算,YARN负责资源调度。三者的关系就像一个餐厅——HDFS是仓库,Spark是厨师,YARN是前厅经理。仓库囤原料,厨师做菜,经理分配人力。
3.2 Django在这个项目里承担什么角色
Django作为Python最成熟的Web框架之一,在这个项目中负责两件事:提供HTTP接口给前端可视化大屏调用,以及作为整个项目的“门面”。
不少人会有疑问:数据分析和可视化不是有ECharts、Tableau这些工具吗?为什么还需要Django?答案很简单——可视化大屏的页面和数据是动态的,不可能每次都用静态HTML写死数值。我需要一个后端服务来读取Spark处理好的结果数据,通过接口返回给前端页面,再由ECharts渲染成图表。Django的ORM可以直接读数据库,Django REST Framework可以快速封装API接口,用起来非常顺手。
另外,Django自带的后台管理功能在调试阶段也帮了大忙。爬虫抓下来的原始数据存在MySQL里,我直接用Django Admin界面就能浏览和检查数据质量,不用额外写查询工具,省了很多事。
3.3 系统架构分层
从整体上看,系统的架构可以分成四层:
- 数据采集层:Scrapy框架写的爬虫,负责从目标网站抓取数据,经过清洗后存入MySQL数据库。
- 数据存储层:MySQL存原始业务数据,HDFS存放从MySQL导入的待处理数据以及Spark的计算结果。
- 数据处理层:Spark读取HDFS上的数据,完成清洗、聚合、指标计算,输出结果写回HDFS或MySQL。
- 应用展示层:Django读取计算结果,通过REST API返回给前端,前端用ECharts绘制可视化大屏。
这个架构的好处是每一层都能独立替换。比如你想把爬虫换成Flume采集日志,只需要改采集层;想用Flink替代Spark做实时计算,只需要改处理层。这为后续扩展留下了空间,也方便在答辩时向评委展示你的架构设计能力。
4. 环境搭建与集群配置
4.1 Hadoop伪分布式环境搭建
真正的生产环境需要多台机器组成集群,但对于个人学习和课程设计,伪分布式模式是最务实的选择。所谓伪分布式,就是在单台机器上,用多个Java进程模拟出集群的效果——NameNode、DataNode、SecondaryNameNode都跑在同一台机器的不同进程上。
我用的环境是CentOS 7虚拟机,Hadoop版本是2.10.2。跟Hadoop 3.x相比,2.x的兼容性更好,资料也多,遇到问题容易搜到解决方案。安装步骤大致如下:
这里有一个新手最容易踩的坑:配置了免密登录之后,一定要先执行ssh localhost测试一下能不能免密登录成功,再继续下一步。我一开始没注意这个细节,结果启动HDFS的时候一直报错,排查了半天才发现是SSH的问题。
另外提醒一点,格式化NameNode这个操作要谨慎。每次重新格式化之前,必须把hdfs-site.xml中配置的NameNode和DataNode的数据目录清空,否则会出现集群元数据不一致的问题。我见过不少人因为反复格式化导致集群起不来,然后去百度搜“NameNode启动失败”,其实根因就是旧数据没清干净。
4.2 Spark的部署与配置
Spark本身不一定要部署在Hadoop集群上,它可以独立运行,但为了跟HDFS配合使用,最好在YARN模式下运行。我这里选择的是Spark Standalone模式,理由很简单:单机环境下,Standalone模式比YARN模式配置简单,资源调度也更直接,更适合学习和调试。
安装Spark的步骤相对简单:去官网下载预编译的二进制包,解压后配置spark-env.sh文件,指定JAVA_HOME和SPARK_MASTER_HOST即可。启动后访问8080端口,能看到Spark的Web UI,master和worker的状态一目了然。
在编写Spark作业时,我选择的是Scala语言。原因是Spark本身是Scala写的,用Scala API功能最全,而且Spark SQL在Scala中的表现力远强于Python。但对于不熟悉Scala的同学,使用PySpark也完全可以,API设计几乎一致,只是性能略有差异。
我的Spark作业主要完成以下几件事情:
- 读取HDFS上的原始数据文件(从MySQL导出的CSV或Parquet格式)。
- 使用DataFrame API进行数据清洗,包括缺失值处理、格式统一、异常值过滤。
- 根据业务需求进行聚合统计,比如按品类统计商品数量、按时间统计销量趋势、按区域统计用户分布。
- 计算结果以Parquet格式写回HDFS,方便后续Django读取。
这里要特别强调一个实践经验:Spark作业千万不要直接在本地运行spark-submit,然后把数据输出打印到控制台看结果。正确的做法是,先在小数据集上做逻辑验证,确认无误后再提交到集群跑全量数据,结果写回HDFS。否则你会被海量的日志信息淹没,调试效率极低。
4.3 Django环境的准备
Django部分相对简单,用虚拟环境安装Python 3.8和Django 3.2即可。我额外使用了django-rest-framework来封装API接口,配合CORS头解决跨域请求问题。
因为Spark和Hadoop是用CentOS跑的,而Django开发环境在Windows上,所以数据交换通过HDFS的API进行。具体来说,我在Windows上开发Django应用时,通过pyhdfs库读取HDFS上的Parquet文件,转换为pandas DataFrame后再存入MySQL。这样既发挥了HDFS的存储优势,又不影响Web开发的效率。
5. 网络爬虫的设计与实现
5.1 爬虫目标与数据建模
在动手写爬虫之前,先想清楚一个问题:你究竟要爬什么数据?这个问题的答案直接决定了后续全部的技术方案。在我的项目中,我选择爬取电商平台的商品信息,包括商品标题、价格、销量、店铺、评论数等字段。选择电商数据的原因很简单:
- 数据特征典型。电商数据包含结构化字段(价格、销量)和非结构化字段(商品描述),非常贴近真实业务场景。
- 容易验证数据质量。我自己就能判断爬到的价格是否合理、销量是否符合常识,不用依赖外部知识。
- 可视化效果好。价格区间分布、销量Top10、品牌占比、地域分布,这些指标做成的图表非常直观,适合可视化大屏展示。
数据建模阶段,我在MySQL中建了一张product表,字段包括id、title、price、sales、shop_name、comment_count、category、crawl_time等。设计表结构时要注意一个原则:爬虫的数据不可信,不能完全按照爬虫的输出设计表结构。比如价格字段,爬下来的可能是“¥399”这种带符号的字符串,必须清洗后才能存入数值字段。所以在建表时就要考虑好数据清洗的边界。
5.2 Scrapy框架的核心组件
爬虫我用的是Scrapy框架,很多人觉得Scrapy难上手,其实它的核心概念并不多,搞懂四个组件就能起飞:
- Spider:你的爬虫主体,定义了初始URL和解析规则。
- Item:数据容器,类似Python中的字典,定义了你要采集的字段。
- Pipeline:管道组件,对采集到的Item进行后续处理,如去重、清洗、存入数据库。
- Middleware:中间件,处理请求和响应的预处理,比如设置代理、修改User-Agent、处理重试。
我爬取目标网站时,遇到了反爬机制,所以写了一个Downloader Middleware来轮换User-Agent和代理IP。这个中间件的逻辑并不复杂:维护一个UA池,每次请求随机选取一个;配合随机延时,模拟人类浏览行为。
有些人喜欢在Spider里写各种print来调试,但Scrapy提供了一套很完善的日志机制,推荐用logging模块记录关键信息,方便追踪爬虫的运行状态。在ROBOTSTXT_OBEY设置上,我关闭了robots协议限制,因为很多课程设计目标网站并没有robots.txt文件或者限制比较严格,但这里要提醒大家:如果是做商业用途的爬虫,必须遵守目标网站的robots协议和相关法律法规,课程设计内网环境下问题不大。
5.3 抓取、清洗与入库
爬虫的关键不只在于“抓得到”,更在于“清洗得干净”。我遇到最多的问题是编码问题。不少网站返回的响应是GBK编码,如果直接用UTF-8解析,拿到的就是一堆乱码。Subcategory的处理方式是:在Spider的parse过程中,检测响应的encoding属性,如果不是UTF-8,就用response.encoding进行解码。
另一个常见问题是价格字段的清洗。爬下来的“¥399.00”需要去掉货币符号,转成float类型;“库存量:1000件”需要提取数字部分。这些逻辑我统一放在Pipeline中处理,不在Spider里做,这样代码更模块化,后续如果要扩充爬虫也不用改清洗逻辑。
数据入库时,使用Scrapy内置的Item Pipeline配合SQLAlchemy写入MySQL。这里有个性能优化的点:Scrapy默认每条数据入库一次,如果数据量较大,会有大量IO操作。我的做法是用一个缓冲区积攒数据,达到100条后批量插入,效率提升非常明显。
6. Spark数据处理与指标计算
6.1 从MySQL到HDFS的数据导入
数据采集完成之后,原始数据存储在MySQL中。接下来的问题是:怎么把MySQL的数据送到Spark手里?
方法有很多种,我选择的最简单方式:先把MySQL的数据导出为CSV格式,然后通过hadoop fs -put命令上传到HDFS。虽然这种方式不优雅,但胜在简单直观,适合数据量不大的场景。
如果想更正规一些,可以使用Sqoop工具,一条命令就能完成MySQL到HDFS的导入,还支持增量导入。但Sqoop的安装配置稍微复杂一些,我在课程设计里没有使用,而是选择手动导出。对于后续要做实时计算的同学,可以考虑使用Canal监听MySQL的binlog,将数据实时同步到Kafka,再由Spark Streaming消费,这就是另一套架构了。
6.2 Spark数据清洗逻辑
拿到CSV数据后,我先用Spark读取文件,生成DataFrame,然后开始清洗。这一阶段我处理了几个典型的脏数据问题:
- 缺失值处理。有些商品没有评论数,对应的字段是空值。我的策略是:如果评论数缺失,填充为0;如果价格缺失,则删除这条记录,因为价格是核心分析字段。
- 格式统一。日期字段有的是“2024-01-01”,有的是“2024/01/01”,统一转换成标准格式。价格字段统一转换为Decimal类型。
- 异常值过滤。销量为负数的记录显然是异常数据,直接过滤掉。价格超过一定阈值的记录也需要检查,排除数据录入错误。
这些清洗逻辑中,我强烈推荐一个Spark特性:使用withColumn方法来添加或修改列,配合when和otherwise函数实现条件判断。整个清洗过程可以写成一行行链式调用,代码可读性极高,调试也方便。
6.3 指标计算与结果输出
数据清洗完成后,进入核心的分析阶段。针对电商数据,我计算了以下指标:
- 商品价格的分布区间统计,如0-100元、100-200元等各区间有多少商品。
- 销量Top10的商品排名。
- 不同品类的商品数量和平均价格。
- 商品评论数的分布情况。
- 按店铺统计的商品数量和总销量。
这些指标基本覆盖了可视化大屏的主要图表。计算过程主要使用groupBy和agg组合操作,在Spark中就是非常标准的聚合计算。结果输出时,我选择写回MySQL数据库,因为Django读取MySQL比读取HDFS方便得多。
这里涉及到一个常见的架构争议——分析结果到底存哪里?有人认为应该直接存HDFS,由Django通过API读取HDFS文件;但我的实践经验是:在课程设计这种规模下,结果数据量很小,存MySQL更合适。Django的ORM直接读取MySQL,开发效率高,而且MySQL对结构化数据也支持得好。HDFS的优势在于海量数据,别为了用得着而用。
7. Django后端与可视化大屏实现
7.1 Django项目结构与API设计
Django项目我按照标准的mvt模式组织。models.py中定义了Product(商品)和AnalysisResult(分析结果)两张表,用Django的ORM映射MySQL数据。
API层使用django-rest-framework实现,设计了几个核心接口:
- /api/products/ 返回全部商品数据,支持分页和条件过滤。
- /api/analysis/price-distribution/ 返回价格区间分布数据。
- /api/analysis/top-sales/ 返回销量Top10数据。
- /api/analysis/category-stats/ 返回品类统计数据。
之所以把分析接口拆得这么细,是为了配合前端大屏的各个图表模块。大屏上的每个图表组件负责请求一个独立接口,互不干扰。这样前端渲染效率高,后端接口也职责清晰。
7.2 使用ECharts绘制可视化大屏
可视化大屏用的技术是ECharts,这是百度开源的一个JavaScript图表库,在数据可视化领域使用率极高。ECharts的优势在于开箱即用,图表类型丰富,性能极佳,而且对中文社区的支持非常友好。
大屏的布局我采用了一个常见的设计:深色背景作为整个大屏的底色,中间区域放着一个大号的数字指标,比如总商品数、总销售额、平均价格等。围绕这个中心指标,四周围绕着多个图表:
- 左上角放着一个柱状图,展示价格区间分布。
- 右上角放着一个折线图,展示商品销量趋势。
- 左下角放着一个饼图,展示品类占比。
- 右下角放着一个排行榜,展示销量Top10商品。
每个图表区域都通过AJAX请求Django的API接口,获取数据后用ECharts的setOption方法渲染。数据是动态加载的,刷新页面后图表数据会实时更新。
7.3 前后端联调与页面优化
联调阶段我遇到最常见的问题是跨域请求。前端的静态页面和后端的Django服务可能运行在不同的端口上,浏览器会拦截跨域请求。解决方案是使用django-cors-headers库,在Django的settings.py中配置允许跨域的请求来源。
另一个可以提升体验的细节是数据格式约定。前端ECharts需要的JSON格式通常是一系列键值对的数组,比如[{name: “0-100元”, value: 320}, {name: “100-200元”, value: 180}]。后端返回的数据格式必须精确匹配,否则前端只能再做一层转换,白白增加复杂度。我在设计API时,就直接按照ECharts的数据格式来组织响应内容,后端一次到位,前端免去二次处理,开发效率提升明显。
8. 常见问题与排查技巧实录
8.1 Hadoop和Spark相关
| NameNode启动失败 | 格式化时未清理数据目录 | 删除hdfs-site.xml中dfs.namenode.name.dir对应目录下的文件,重新格式化 |
| DataNode启动后立即消失 | 副本数配置超过节点数 | 将dfs.replication设为1 |
| Spark作业总是失败,日志无明确错误 | 内存不足或配置不当 | 在spark-submit中显式指定executor-memory和driver-memory,确保小于机器可用内存 |
| HDFS读写速度极慢 | 虚拟机磁盘IO瓶颈 | 将HDFS数据目录从共享文件夹移到本地磁盘,必要时关闭系统的文件系统sharing功能 |
Spark部分最容易踩的坑是配置模板直接复制粘贴导致的错误。不同版本的Spark配置文件格式差异很大,务必逐行检查spark-env.sh中是否有明显的路径错误。还有一点,跑Spark作业时如果报错“Container killed by YARN for exceeding memory limits”,这说明内存设置得过大,把executor-memory调小即可,而盲目调大只会让问题更严重。
8.2 爬虫相关
| 中文乱码 | 网页编码不是UTF-8 | 在Spider中检测response.encoding,适时转换 |
| 频繁被服务器限制 | 请求频率过高 | 在Middleware中设置随机延时和代理轮换 |
| 爬虫抓到重复数据 | 没有去重逻辑 | 在Pipeline中基于商品ID或标题做去重处理 |
| 动态网页无法抓取 | 网站使用JavaScript渲染数据 | 使用Selenium或Playwright模拟浏览器,或找到隐藏的JSON接口 |
针对动态网页,我的经验是优先寻找网页中隐藏的Ajax接口。很多看起来是动态渲染的页面,其实数据是通过一个JSON接口加载的,直接请求这个接口比模拟浏览器效率高得多,也不容易触发反爬机制。Chrome开发者工具中的Network面板就是最重要的武器,点开Fetch/XHR类型的请求,很快就能找到数据源。
8.3 Django与可视化相关
| API返回跨域错误 | 未配置CORS | 安装django-cors-headers并配置允许的域名 |
| 前端图表不显示数据 | 数据格式不匹配 | 按ECharts要求的标准格式返回数据 |
| 后台执行任务时前端卡顿 | 同步请求阻塞 | 使用Celery异步执行,或用Django Channels实现WebSocket推送 |
| 大屏在大屏幕显示模糊 | 使用了位图图片 | 全部使用矢量图表和SVG元素,提高清晰度 |
关于Django执行耗时任务,有一个经验值得分享:不要把耗时的数据计算放在正常的请求响应链路里。如果需要在后台跑数据更新任务,可以在前端页面加载时异步触发,或者写一个管理命令用cron定时执行,而不是直接写在视图函数里。请求响应时间太长,浏览器会超时,用户体验极差。
9. 个人实操心得与后续扩展建议
9.1 一点真实的体会
整套系统从零搭起来,大概用了一个半月时间,其中环境搭建和联调占了大头。说实话,对于这个级别的项目,真正的业务逻辑反而简单,难点全在分布式环境的搭建和排错上。Hadoop和Spark的报错信息非常不友好,经常是一堆堆栈信息看不到真正的根因。我的经验是:遇到问题先查日志,特别是Hadoop的logs目录下各种日志文件,大部分情况下答案都藏在日志里。
另一个体会是:对于没有大数据基础的同学,建议先通过官方例子跑通一次WordCount,再开始搭项目。很多人一上来就雄心勃勃,想直接跑项目,结果环境问题消磨了信心。如果把WordCount跑通,至少能证明Hadoop和Spark环境是正常的,后续项目如果出错,就可以集中在代码层面排查,问题范围缩小很多。
9.2 后续可以怎么扩展
如果后续想在这个项目上继续深入,有几个方向可以参考:
- 把离线计算改成实时计算。引入Kafka和Spark Streaming,实现数据从采集到展示的准实时链路。
- 增加机器学习模块。用Spark MLlib对销量做预测,或者对用户评论做情感分析,让系统从“展示现状”升级为“预测未来”。
- 增加用户功能。Django本身就有完善的用户认证体系,可以增加登录、注册、权限管理功能,把系统从演示型升级为可用型。
- 数据可视化方面,可以接入Grafana和Kibana,但这会偏离Django大屏的路线,需要重新设计架构。
最后再分享一个细节:整个项目中,我觉得最值得花时间琢磨的是数据可视化大屏的UI设计。评委和观众的第一印象就来自大屏的视觉效果。整洁的布局、协调的色彩、合理的图表搭配,这些主观感受比技术栈是否新颖更影响评价。我当时重新调整了三次大屏的布局和配色,每一次调整都让整体的观感提升不少。如果有余力,可以学习一些大屏设计规范,参考一些云厂商的展示模板,这套系统的完成度会更高。


