天气预报大数据分析:Hadoop+MapReduce+可视化大屏实战
做天气预报数据分析这个题目的人非常多网上随手一搜全是“爬虫 pandas matplotlib”的三件套模板。不能说这种做法不对但它本质上就是单机脚本处理数据和“大数据”三个字关系不大。真正让这个项目值钱的是整条闭环能不能落到Hadoop生态里HDFS存原始数据MapReduce做分布式清洗和统计MySQL存聚合结果最后再用可视化把结论展示出来。这个链路走通之后你才会明白为什么大数据项目要分层为什么数据需要分区为什么MapReduce的Mapper和Reducer要各司其职。这篇文章我就按自己实际做完这个项目的顺序来写先讲整体方案怎么选再讲环境怎么搭、数据怎么爬、怎么入HDFS、怎么用MapReduce算指标最后讲可视化大屏和几个能加分的扩展点。每个环节我都会把“当时为什么这么选”和“踩了哪些坑”写清楚希望对正在做课程设计、毕业设计或者刚入门想拿Hadoop练手的朋友有实际帮助。1. 项目整体架构与方案选型1.1 为什么不能只写个脚本交差很多人做这类项目会陷入一个误区认为只要用了Python、爬到了数据、画出了图就算完成了“大数据分析”。但你去答辩或者给别人介绍项目时最怕被问“你这里哪里用了大数据技术”如果回答不出来整个项目的含金量就瞬间掉一半。所以我在设计这个项目时第一原则是把Hadoop体系真正嵌入到流程里而不是把它当摆设。爬下来的天气数据不能直接进Excel得先落HDFS统计平均值、最大温差这些指标不能拿pandas算完拉倒得走MapReduce最终结果不能只存在本地得导到MySQL里作为数据服务层。这样做工作量确实会大一些但每一步都能讲出“为什么需要”和“解决了什么问题”这在毕业设计和课程设计答辩里是非常关键的。1.2 五层架构的设计思路整个项目我分成五层每一层的职责都比较清晰采集层Python requests BeautifulSoup负责抓取目标城市的天气历史数据清洗成结构化CSV存储层HDFS按年月建立目录分区保存原始CSV文件计算层MapReduce做ETL清洗和指标统计产出聚合结果服务层MySQL存储聚合结果给上层可视化提供查询接口展示层ECharts制作数据大屏可选加Qt桌面端做数据浏览器这个分层结构对应到大厂的数仓体系也很好理解ODS层存原始数据DWD层做清洗DWS层做聚合ADS层对外提供服务。课程设计不用搞那么复杂但有这个意识写出来的项目文档和PPT会比单纯写代码高出不少档次。技术选型上我要特别说一句没用Spark是因为这个项目的数据量在几十万行级别单机伪分布式的MapReduce完全可以胜任而且Hadoop课程本身要求的是掌握MapReduce原理。如果一上来就上Spark集群既增加了环境复杂度又容易把重心带偏。先把Hadoop生态跑通之后再迁移到Spark时思路会顺畅很多。2. Hadoop伪分布式环境搭建与避坑2.1 版本组合和核心配置环境这块我推荐一套我自己验证可用的组合Ubuntu 20.04虚拟机上装Hadoop 3.3.4JDK用8。千万别图新鲜装JDK 17Hadoop 3.x对JDK 8支持最稳JDK 17会触发各种奇怪的ClassNotFound问题。伪分布式搭建的核心是三个配置文件core-site.xml、hdfs-site.xml、mapred-site.xml。这里给出我实际使用的关键配置!-- core-site.xml -- property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/opt/hadoop/tmp/value /property!-- hdfs-site.xml -- property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name valuefile:///opt/hadoop/dfs/name/value /property property namedfs.datanode.data.dir/name valuefile:///opt/hadoop/dfs/data/value /property注意伪分布式副本因子必须设为1。很多新手直接把默认的3带过来然后发现DataNode只在本机副本根本写不齐最后一直在刷“There are 0 datanodes running”之类的错其实就是这个概念没想明白。启动流程也容易乱记住顺序先格式化NameNode再启动dfs和yarn。格式化是一次性操作不是每次开机都做。hdfs namenode -format start-dfs.sh start-yarn.sh jps启动后用jps检查进程正常情况下能看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager五个进程。接着浏览器访问http://localhost:9870验证Web UI注意Hadoop 3.x的NameNode Web端口是9870网上很多老教程写的50070早就没用了。2.2 伪分布式搭建的核心避坑清单这块是我踩坑最多的环节写出来给大家参考。第一NameNode格式化次数多了DataNode会“失联”。原因是每次格式化会生成新的clusterID而DataNode自己落盘的clusterID没变两边对不上。解决方法是格式化前把dfs.name和dfs.data目录删干净再重新格式化。日志报错时去看/opt/hadoop/logs/下的hadoop-hadoop-datanode-*.log里面会明确提示clusterID不一致。第二虚拟机内存不能小于2GB。伪分布式虽然只有一个节点但NameNode、DataNode、ResourceManager、NodeManager全挤在一台机器上内存不够就会直接在日志里报Native memory allocation failed。我自己一开始分的是1.5GB启动NameNode都要等半分钟后来改成3GB才流畅。第三防火墙和ssh免密虽然伪分布式不强制要配但建议还是把ssh localhost免密配好后面跑MapReduce任务、调试脚本都省事。命令很简单ssh-keygen -t rsa -P -f ~/.ssh/id_rsa然后cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys。3. 天气预报数据爬取设计与实现3.1 数据源选择与字段设计数据源我选了公开免费、不需要登录、反爬策略宽松的天气历史数据页面。课程设计场景里完全没必要去碰商业付费API也不需要挑战高难度的反爬站点选一个稳定可靠的数据源把精力放在后续的大数据流程上才是正事。字段设计要同时考虑展示和分析两个层面的需求我最终定了7个字段城市名称、日期、最高温、最低温、天气现象、风向、风力等级。其中天气现象特意保留文本形式因为后期要做“晴、多云、小雨”这类天气类型的占比统计只有数字温度是撑不起地理维度分析的。爬虫我用Python的requests加上BeautifulSoup实现核心逻辑就是遍历城市列表和日期区间把页面里的表格数据解析出来逐行写成CSV。每个城市每个月生成一个独立文件命名规则是beijing_202401.csv这样后续上传HDFS时能够很好地配合目录分区。3.2 爬虫稳定性与增量抓取策略写过爬虫的朋友都明白一次性脚本跑通并不难难的是让它在长时间、大批量抓取时不翻车。时间长了很容易遇到连接超时、页面结构微调、被临时限流等情况所以我在设计上做了三个保障第一每个月文件作为一个抓取单元已存在且文件行数正常的月份直接跳过。这个设计让断点续爬变得非常简单中途挂了只需要重新运行脚本不用从头再来。第二请求间隔用random.uniform(1.2, 2.5)做随机延迟同时设置随机User-Agent。这是最朴素也最有效的防封手段——抓这种公开历史数据页面只要不像洪水一样冲对方服务器基本不会被封IP。第三做了数据完整性校验。每个月爬下来检查CSV行数是否在当月天数范围内最少不能少于28行字段数必须是7列。如果某个月份解析出来的行数异常就把这一页标记为失败重新爬一次确实不行就记录到日志方便人工核对。这一步看起来笨但非常关键因为脏数据一旦混进HDFS后面MapReduce解析时要么报错、要么统计结果完全不可信返工成本远比重爬一次高。4. 数据入HDFS与MapReduce处理4.1 HDFS目录规划与上传爬下来的CSV不能一股脑全塞进HDFS根目录必须按时间分区规划。我的目录结构是这样hdfs dfs -mkdir -p /weather/raw/2024/01 hdfs dfs -mkdir -p /weather/raw/2024/02 hdfs dfs -put ./data/2024/01/*.csv /weather/raw/2024/01/这样做的原因很现实后续做统计时MapReduce的输入路径可以直接指向/weather/raw/2024/01或/weather/raw/2024要算哪个月就传哪个月不需要全量扫描。这就是大数据里常说的“分区裁剪”思想数据量一大扫描和不扫描的差距是几十倍的。我当时爬了10个城市、2年数据原始CSV总大小50MB左右24万个样本点。这个体量对Hadoop来说小得可怜但用来演示完整的分布式数据处理流程完全没有问题。上传后可以用hdfs dfs -du -h /weather/raw确认数据落位。4.2 MapReduce清洗和统计任务实现MapReduce我设计了两个Job。第一个Job做ETL清洗Mapper逐行解析CSV跳过表头、跳过空行、过滤掉“最低温大于最高温”这类明显异常的数据输出格式统一为“城市、日期、最高温、最低温、天气现象”。Reducer在这一阶段不做聚合只是直接透传相当于用分布式框架做了一次数据清洗。第二个Job是统计任务这也是项目的核心计算逻辑。Mapper端把(city, month)作为Map输出的key温度、天气现象作为valueReducer按key聚合计算每个城市每个月的平均最高温、平均最低温、最大温差以及当月出现频次最高的天气类型。统计时我在Mapper端挂了Combiner提前做一轮本地聚合把相同key的临时结果合并减少Shuffle阶段的数据传输量。这套设计在答辩时非常好讲Combiner的含义、Shuffle的过程、Reducer的分组逻辑每一个概念都能落到自己的代码里而不是背教科书上的定义。我自己更建议顺手跑一遍MapReduce任务完成后part-r-00000文件里前几行数据确认输出格式符合预期再往后继续。4.3 Hive交叉验证的补充思路MapReduce写起来确实啰嗦所以我后期做数据验证时引入了Hive用CREATE EXTERNAL TABLE指向同一个HDFS目录跑了几条SQL和MapReduce的结果做对比。两条技术路径算出来结果一致才能确定统计逻辑没有算错。这里多说一句Hive本身不存数据它只是把SQL翻译成MapReduce或Tez作业来跑所以它并不“抢”MapReduce的功劳。课程设计里两者都用既能展示代码能力又能展示SQL能力而且被问到“为什么不直接用Hive”时可以说清楚两者的关系反而变成加分项。5. 聚合结果落库与ECharts可视化大屏5.1 结果导出到MySQLMapReduce的输出默认是HDFS上的多个part文件需要先合并再导MySQL。我用的是hdfs dfs -getmerge把目录下的所有part文件合成本地单个CSV再通过LOAD DATA导入表。hdfs dfs -getmerge /user/hadoop/output/weather_stat /tmp/weather_stat.csv mysql -uroot -p -e LOAD DATA LOCAL INFILE /tmp/weather_stat.csv INTO TABLE weather_stat FIELDS TERMINATED BY \t;MySQL表结构设计得很简单城市、月份、平均最高温、平均最低温、最大温差、主导天气、统计天数。字段越简单越好后面可视化查询越方便。数据量只有几十个城市乘几个月MySQL随便查完全不需要搞什么索引优化。5.2 大屏可视化的三块核心图可视化我选的是ECharts因为免费、图表类型全、社区资料多。大屏内容规划了三块第一块是中国地图热力图按城市填充颜色颜色深浅代表月均最高温。这里有个容易踩的坑ECharts 5及以后版本不再内置地图GeoJSON数据必须单独引入china.js文件或注册地图JSON否则地图区域一片空白。第二块是折线图展示指定城市全年温度变化趋势。X轴月份排序这里有一个很容易出错的地方字符串月份“10”会排在“2”前面必须在数据处理阶段转成数字类型排序否则折线图会从1月直接跳到10月再回2月乍一看还以为是统计出错其实是排序问题。第三块是饼图或柱状图展示天气类型占比。这个图的数据直接来自MapReduce统计里的dominant_weather字段能直观看出一个城市一年里晴天占多少、雨天占多少配合前面两个图一起讲整个大屏就完整了。大屏布局我采用一行三列的栅格左列放地图中间是核心温度指标卡片右列放饼图和趋势图。前端通过简单的HTTP接口从MySQL取数返回JSON格式每隔30秒轮询一次模拟实时刷新效果。数据量小怎么玩都跑得动。6. 加分扩展Qt表格大数据渲染优化实战6.1 QTableWidget卡顿的根源课程设计做到这儿其实已经完成了“爬取 存储 计算 展示”的闭环但如果想让项目再多一个亮点我建议用Qt写一个桌面端的数据浏览器把SQL里查出来的几十万行明细数据展示在表格里。但这里几乎所有新手都会撞上同一堵墙用QTableWidget加载上万行数据后界面卡得没法看拖动滚动条时CPU占用直接拉满。原因很本质QTableWidget是Item-based控件它会给每个单元格都创建一个QTableWidgetItem对象。假如表里有50万行、7列内存里就要创建350万个对象不卡才怪。很多人项目就是死在这上面最后不得不用QTableWidget只加载前1000行代码又难看又鸡肋。6.2 QTableView加自定义model的正确姿势正确的做法是用QTableView搭配QAbstractTableModel自定义数据模型。核心原理是view只通过model获取当前可视区域内的单元格数据滚动过程中不断请求新的数据未显示的部分不创建任何对象。所以无论底层数据有多少行view始终只渲染视口附近的内容这就是数据量再大也不掉帧的根本原因。自定义model至少要重写四个方法int rowCount(const QModelIndex parent) const override; int columnCount(const QModelIndex parent) const override; QVariant data(const QModelIndex index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override;data函数里只做一件事根据行列号从数据结构里取值并返回不要在里面做计算、不要访问数据库。我建议在加载数据时一次性把所有行读入QVector容器data函数直接按下标索引这样取数路径最短。配套还要设setUniformRowHeights(true)告诉view所有行高一致省去每行重新计算高度的开销。我在测试机上跑过对比50万行数据QTableWidget加载耗时约12秒内存占用飙升到将近1GB换成QTableView 自定义model后加载时间降到0.3秒以内滚动流畅度基本不掉帧。这个扩展代码量不大但在课程设计展示环节属于一眼就能看出来的亮点。7. 常见问题与排查技巧实录整个项目从零到跑通我整理了一张高频问题速查表基本覆盖了最容易卡住人的几个环节现象可能原因处理方法NameNode启动失败或元数据不一致多次格式化导致clusterID冲突删掉dfs.name和dfs.data目录后一次性重新格式化DataNode不在线Web UI只看到NameNodeclusterID不匹配或目录权限不足对比VERSION文件统一clusterID检查目录属主MapReduce任务卡在Running状态YARN内存配置不足或分配不合理调整yarn-site.xml内存参数后重启YARN爬虫请求被拒绝或返回验证码请求频率过高或缺少随机UA加随机延迟、设置Session、更换数据源CSV中文乱码编码不统一统一UTF-8读写HDFS侧注意reader编码ECharts地图空白未注册GeoJSON地图数据单独引入china.js或地图JSON文件折线图X轴月份乱序字符串排序导致10月排在2月前转数字类型映射后再传给前端7.1 YARN内存参数的稳妥配置MapReduce任务频繁OOM的大概率是yarn-site.xml里的内存参数没有配置合理。我给出一组伪分布式环境可用的稳妥配置property nameyarn.nodemanager.resource.memory-mb/name value2048/value /property property nameyarn.scheduler.maximum-allocation-mb/name value2048/value /property property namemapreduce.map.memory.mb/name value1024/value /property property namemapreduce.reduce.memory.mb/name value1024/value /property注意不要和MRv1里的mapred.child.java.opts混淆这个是JVM堆参数配错地方内存调整就不会生效。7.2 排查问题的通用方法论项目做到最后真正的收获不是代码跑通而是建立了排查问题的思路。我的经验是先看日志、再看配置、最后才怀疑代码。Hadoop日志路径一般在/opt/hadoop/logs/NameNode和DataNode的问题基本都能从hadoop-hadoop-namenode-*.log和hadoop-hadoop-datanode-*.log里找到答案。MapReduce任务失败则用yarn logs -applicationId app_id拉取具体某个Application的日志定位到Mapper或Reducer报错的堆栈。这个排查顺序看起来简单但能帮你节省大量无头绪翻代码的时间。最后分享一个我自己做完项目后的体会这类题目真正难的不是写某一段代码而是把每个环节衔接起来。爬虫要想着下游能不能解析HDFS分区要想着统计能不能剪枝MapReduce输出要想着MySQL表结构能不能直接装载任何一个环节只想着“自己跑通就行”最后拼起来的系统就会漏洞百出。我在实际演示数据大屏时也一定会提前确认MySQL服务已经启动、ECharts依赖的GeoJSON文件已经放在本地避免临场手忙脚乱。希望这篇记录对正在做类似课程设计或者刚接触大数据的朋友有帮助少踩一点我踩过的坑。