ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Hadoop+Hive+Spark+Django租房推荐系统与数据分析大屏实战

2026/9/8 3:44:06 拓冰建站 浏览量
Hadoop+Hive+Spark+Django租房推荐系统与数据分析大屏实战 选毕设题目这件事属实是个玄学。我盯着题目列表看了一下午最后锁定了hadoopsparkhive租房推荐系统 58同城租房数据分析可视化大屏 Django框架 大数据。说实话当时这个题目在我脑子里就四个字的印象不明觉厉。真正动手之后才发现这套技术栈组合其实特别常规难的不是单个组件而是把它们串起来的思路。整个项目做下来我最大的感受是租房这个场景选得好。数据好拿、字段丰富、贴近真实需求而且推荐和数据分析大屏两个核心功能都有足够的发挥空间。更重要的是它把 Hadoop 负责存储、Hive 负责数仓、Spark 负责计算、Django 负责对外服务这条链路完整走了一遍做完之后你对大数据项目到底是怎么运转的会有一个非常具体的认知而不是停留在面试题里背概念。这篇就把我当时从数据采集、数仓搭建、推荐实现到大屏展示的完整过程和踩坑经验整理出来给正在做同类课题、或者想拿一套完整项目练手的人一个参考。1. 四个组件各司其职这套技术栈到底怎么配合1.1 为什么是这四个不是别的选题确定之后我第一步不是急着写代码而是先把技术栈的角色分工搞清楚。很多人容易陷入一个误区觉得 Hive 和 Spark 都能写 SQL、都能做计算那是不是重复了其实不是。Hive 的核心价值在于数据仓库管理——它把 HDFS 上的文件映射成一张张逻辑表让你能用 SQL 描述我要什么数据而不用关心底层文件怎么切分。Spark 的核心价值在于分布式计算——数据量大了之后单机跑不动需要把任务拆到多台机器上并行处理。两者在同一个项目里完全不冲突Hive 管表结构Spark 管计算逻辑底层数据都躺在 HDFS 上。Django 在这个项目里的角色则更偏向对外服务。它是 Python 的 Web 框架负责把 Spark 分析出来的结果、推荐系统算出来的候选房源通过 HTTP 接口提供给前端大屏展示。为什么选 Django 而不是 Spring Boot因为数据清洗和特征处理阶段大概率要用 Python 的生态pandas、sklearn用 Django 可以让整个项目的开发语言保持统一不用在 Java 和 Python 之间来回切换。所以最终的技术分工是这样的组件角色定位核心职责Hadoop HDFS分布式存储存放采集的原始租房数据和分析结果Hive数据仓库构建分层表结构提供类 SQL 查询能力Spark分布式计算做 ETL 清洗、跑推荐算法、产出聚合指标DjangoWeb 服务提供大屏数据接口、推荐查询接口1.2 数据从采集到展示的完整流向这一套系统听起来复杂但数据流向捋清楚之后就很简单采集端用 Python 脚本抓取租房信息落到本地 CSV 文件。CSV 上传到 HDFS然后通过 Hive 建立外部表映射这就是 ODS 层原始数据层。接下来用 Spark SQL 做清洗和标准化把结果写入 Hive 的 DWD 层明细表。再往下一部分数据通过 Spark 跑推荐算法产出针对某个用户推荐的房源列表另一部分数据通过 Spark 做聚合统计产出大屏需要的指标结果。这两类结果都落到 Hive 的 ADS 层Django 后端直接查询 ADS 层数据包装成 JSON 接口交给前端。这个链路里最容易被忽略的是结果数据怎么给 Django 用。一开始我想的是 Django 直接连 Hive后来发现查询响应时间太不可控。最终方案是Spark 跑完把聚合结果写回 MySQLDjango 走 MySQL 查询。大屏展示讲究低延迟Hive 底层的 MapReduce/Spark 作业冷启动就要几秒完全不适合实时查询。这个取舍后面在第五章细讲。2. 采集清洗从58页面拿到一行干净数据2.1 字段设计先想清楚要分析什么采集之前一定要先把字段定好。我当时对着 58 同城的租房列表页和详情页梳理了一遍最终确定的字段如下小区名称如阳光家园区域如朝阳区商圈如望京厅室如2室1厅面积单位平方米朝向如南北楼层如低层/共28层装修如精装租金单位元/月租赁方式整租/合租发布时间房源链接作为唯一标识为什么房源链接也要存因为它是天然的房源 ID。58 同城页面上的每套房源详情页 URL 都带一串数字编号这串编号在全网是唯一的后期做去重、做关联查询都靠它。没有这个字段后面去重会非常痛苦。2.2 页面解析与反爬应对采集方案我用了 requests BeautifulSoup 的组合。58 同城的租房列表页结构相对规整房源块都在li标签里通过 class 属性可以定位到具体的标题、价格、详情链接。解析详情页的时候我用的是页面里内嵌的 JSON 数据比直接解析 HTML 标签稳定得多。58 的详情页有一段page-data的 script 标签里面包含完整的房源结构化数据用正则把它抠出来再用json.loads解析字段基本都齐了。反爬方面我做了三件事。第一请求头伪装成浏览器 UA不能让人一眼看出是脚本。第二请求间隔随机控制在 3 到 6 秒避免高频请求给目标站点造成压力。第三如果遇到验证码或者异常跳转直接停止当前循环休眠 30 秒再继续。有一点必须说清楚爬虫采集数据只应该用于学习研究要遵守目标网站的 robots 协议控制合理的请求频率。我采集的数据量控制在万条级别完全够做分析和推荐测试没必要追求把整个站都扒下来。2.3 清洗时最容易翻车的三个细节原始抓下来的数据非常脏清洗阶段我踩了三个坑这里重点说一下。第一个坑是租金单位不统一。58 同城大部分房源价格是xxxx 元/月但有一部分合租房源显示的是xxxx 元/月押一付三甚至有面议的。如果直接当成数字处理后面统计平均租金时会出大问题。我的处理方式是正则匹配出第一个数字丢弃面议类数据再统一转成 int 类型的月租金。第二个坑是虚假房源干扰。58 上1 元/月1 室 1 厅 200 平米这种明显是引流帖的数据不少。我按两个规则过滤租金小于 200 元/月的剔除面积大于 300 平且月租金低于 2000 元的剔除。宁可少一些样本也要保证进入分析的数据质量。第三个坑是重复房源。同一个房源可能被中介重复发布多次或者出现在不同的列表中。我的去重策略是按小区厅室面积租金四个字段分组保留第一条记录。这个逻辑在 Python 里用 pandas 的drop_duplicates一行就能搞定。清洗完成后数据落成 CSV 文件等待上传到 HDFS。3. Hive数仓到底怎么设计才能让后续分析不返工3.1 分层思想ODS、DWD、ADS各放什么很多人在做类似项目的时候拿到数据就直接建一张表开跑。这种做法在数据量小的时候没什么问题但一旦分析需求变多你会发现表结构东拼西凑改一个字段要牵连一堆 SQL。所以还是要老老实实分层。我设计了三层ODS 层原始数据层原封不动存采集到的 CSV 数据字段类型都不改。这一层存在的意义是留底万一后面想重新清洗原始数据还在。DWD 层明细数据层清洗和标准化之后的数据字段命名统一、类型规范、去除脏数据。这一层才是真正供分析和推荐使用的明细数据。ADS 层应用数据层面向具体业务需求产生的聚合表。比如各区域平均租金统计表户型分布统计表热门商圈 TOP10 表等。这些表直接给大屏展示用。这个分层思路来自真实企业级数仓虽然项目体量不大但按这个规范走后面写分析 SQL 的时候非常省心——你永远知道去哪一层取数。3.2 建表实操与分区策略Hive 建表我用的是外部表数据放在 HDFS 指定目录。DWD 层核心表的建表语句大致是这样CREATE EXTERNAL TABLE dwd_house_info ( house_id STRING COMMENT 房源ID, community STRING COMMENT 小区名称, district STRING COMMENT 区域, biz_circle STRING COMMENT 商圈, layout STRING COMMENT 户型, area DOUBLE COMMENT 面积平米, orientation STRING COMMENT 朝向, decoration STRING COMMENT 装修, rent INT COMMENT 月租金, rent_type STRING COMMENT 整租/合租, publish_date STRING COMMENT 发布日期 ) PARTITIONED BY (dt STRING COMMENT 数据日期) ROW FORMAT DELIMITED FIELDS TERMINATED BY , STORED AS TEXTFILE LOCATION /warehouse/house/dwd/dwd_house_info;这里特别强调一下分区字段。按dt分区可以在查询时只扫描某一天的数据大大减少 I/O。如果后续每天增量采集数据每次都往对应分区写入即可不会污染历史数据。比较常见的问题是不建分区表全量数据放在一个目录里查询时每次都要扫描全表Spark 作业跑得又慢又费资源。3.3 外部表优先以及删表不删数的坑为什么用EXTERNAL外部表而不是内部表区别在于内部表删表时HDFS 上的数据文件也会被删掉外部表删表只删元数据HDFS 上的文件还在。做项目过程中我吃过一次亏。当时图省事建了内部表后来因为表结构设计不合理要删了重建一条DROP TABLE下去数据直接没了。好在 ODS 层的 CSV 还有备份重新上传再建表才恢复。从那之后我所有的表都改成了外部表。建议你也养成这个习惯特别是还在调试阶段表结构改来改去是常态外部表明显更安全。数据导入用LOAD DATA INPATH命令即可把 HDFS 上的 CSV 文件加载到对应分区LOAD DATA INPATH /home/hadoop/house/csv/20250110 INTO TABLE dwd_house_info PARTITION (dt2025-01-10);4. Spark推荐推荐质量取决于特征设计不取决于算法多高级4.1 为什么没直接上协同过滤项目标题里有推荐系统大多数人第一反应就是协同过滤。我一开始也打算用 ALS 算法但真正把数据捋清楚之后放弃了这个方案。协同过滤的核心依赖是用户行为数据——用户看了哪些房源、收藏了哪些房源、咨询了哪些房源。但实际采集的时候这类数据非常难拿到即便拿到也是稀疏的一塌糊涂。没有行为数据协同过滤就是无米之炊。所以我把方案调整为基于内容和规则的综合推荐不依赖用户的历史行为而是通过用户给出的需求比如预算、通勤区域、户型偏好和房源本身的特征地段、租金、面积、户型、朝向做匹配打分。这套方案不需要大量的历史行为数据冷启动问题天然规避而且逻辑透明答辩的时候也容易讲清楚。4.2 房源特征向量与用户画像推荐的核心是把人和房都抽象成向量然后算相似度。房源侧我提取了五个核心特征做了标准化处理租金归一化到 0~1面积归一化到 0~1所在区域热度评分根据该区域历史房源数量和数据计算热门区域得分高户型匹配度与用户需求户型的匹配程度朝向得分南北通透得分最高每个房源最终都得到一个五维特征向量。用户侧通过前端让用户选择预算范围、期望区域、期望户型、面积偏好系统把用户偏好也转成对应的特征向量。4.3 召回排序的完整实现推荐流程分两步先召回再排序。召回阶段做的事情是粗筛。根据用户选择的区域和租金上限从全量房源中筛出一个候选集排除掉明显不符合的房源。这一步用 Spark SQL 直接查 Hive 就能完成过滤条件很直观val candidates spark.sql( SELECT * FROM dwd_house_info WHERE district 朝阳区 AND rent 6000 AND layout LIKE %2室% )排序阶段是对候选集里的每个房源算综合得分。得分函数我用的是加权求和val score 0.4 * rentScore 0.2 * areaScore 0.2 * districtScore 0.2 * layoutScorerentScore的算法是如果房源租金在用户预算区间内给满分超出预算越多得分线性下降。areaScore类似面积越接近用户需求的越分高。districtScore来自区域热度统计表layoutScore则是根据是否匹配用户户型需求来给。完整跑一遍之后按得分降序取前 20 条写入推荐结果表ads_recommend_result。整个计算过程用 Spark 执行分布式跑几万条房源也就是几秒的事。这段优化的代码示例import org.apache.spark.sql.expressions.Window import org.apache.spark.sql.functions._ val baseDf spark.sql(SELECT * FROM dwd_house_info WHERE dt2025-01-10) val resultDf baseDf .withColumn(rent_score, when(col(rent) lit(userMaxRent), 1.0) .otherwise(1.0 - (col(rent) - lit(userMaxRent)) / lit(userMaxRent))) .withColumn(area_score, when(abs(col(area) - lit(userArea)) 10, 1.0) .otherwise(1.0 - abs(col(area) - lit(userArea)) / lit(userArea))) .withColumn(total_score, col(rent_score) * 0.4 col(area_score) * 0.3 col(district_score) * 0.2 col(layout_score) * 0.1) .withColumn(rank, row_number().over(Window.partitionBy(district).orderBy(col(total_score).desc))) .filter(col(rank) 20)这段逻辑实际是写进 Spark 作业的main方法里的用户参数通过命令行参数传入。5. 可视化大屏怎么取数Django的角色远比想象的简单5.1 大屏指标清单与SQL落地大屏要展示什么决定了 Spark 要聚合什么。我梳理了六个核心模块总房源数与在租房源数各区域房源占比饼图各区域平均租金排行柱状图租金价格区间分布直方图户型分布饼图或柱状图商圈热度 TOP10横向柱状图这些指标全部来自 DWD 层明细数据用 Spark SQL 逐条聚合。比如各区域平均租金排行对应的 SQL 大致是SELECT district, AVG(rent) AS avg_rent FROM dwd_house_info WHERE dt 2025-01-10 GROUP BY district ORDER BY avg_rent DESC跑完后的结果写入 ADS 层不同的表每张表对应一个大屏模块。5.2 Django对接大数据结果的两种方式Django 对接大数据结果有两条路我实际对比过之后选了性价比更高的一条。第一种方式Django 直接查 Hive。理论上可行但 Hive 的查询延迟太高冷启动一个作业要数秒大屏每次刷新都要等体验非常差。而且 Django 要连 Hive 需要引入额外的 JDBC 依赖配置成本高。第二种方式Spark 聚合结果写 MySQLDjango 查 MySQL。Spark 作业跑完之后把 ADS 层的数据以 DataFrame 的形式写入 MySQL 的对应表中。Django 后端走常规的 MySQL 查询。实测接口响应时间从秒级降到了百毫秒以内体验完全不一样。我推荐第二种。大数据链条负责算Web 服务负责取两个环节解耦出问题也好排查。Spark 写 MySQL 的代码很简单val df spark.sql(SELECT district, AVG(rent) AS avg_rent FROM dwd_house_info GROUP BY district) df.write.mode(overwrite) .jdbc(jdbc:mysql://localhost:3306/house_analysis, ads_district_avg_rent, prop)5.3 接口设计与前端实现要点Django 这边我用了 Django REST Framework一个大屏数据接口把多个指标打包返回前端只需请求一次class DashboardDataView(APIView): def get(self, request): total_house HouseStat.objects.filter(stat_nametotal_house).first() district_rent DistrictAvgRent.objects.all() layout_data LayoutDistribution.objects.all() data { total_house: total_house.stat_value, district_rent: [ {name: item.district, value: item.avg_rent} for item in district_rent ], layout_distribution: [ {name: item.layout, value: item.cnt} for item in layout_data ], } return Response(data)前端大屏我用的是 ECharts。官方模板里有不少现成的 dashboard 布局直接把接口数据填进 option 就好。大屏的视觉设计不是重点但要注意一点数据刷新机制。我用了每个 30 秒定时拉一次接口的方式让大屏上的数字动起来演示效果比静态页面好很多。这个改动量很小就几行 JavaScript 的事但答辩时观感提升明显。6. 调试路上真实踩过的坑能救一个是一个6.1 Spark on YARN只用了1个CPU核问题出在哪这是我在搭建集群后跑第一个 Spark 作业时遇到的事。作业跑起来了但看 YARN 的资源监控页发现 executor 只用了 1 个 vcore不管数据量多大并行度都上不去。作业慢得像单机跑。排查链路是这样的先检查 Spark 应用提交参数--executor-cores设的是 2没问题。再检查 spark-defaults.confspark.executor.cores也配置了。最后发现卡在 YARN 的调度器配置上——yarn.nodemanager.resource.cpu-vcores默认值是 1 或未配置YARN 认为每个节点只有 1 个 CPU 核可用Spark 申请更多核也分配不到。解决办法是把该参数改成节点的真实逻辑核数比如 8property nameyarn.nodemanager.resource.cpu-vcores/name value8/value /property改完重启 YARN 的 nodemanager再跑作业CPU 核数立刻上来了作业时间直接砍半。这个坑在伪分布式环境不明显因为伪分布式节点少但只要你上了集群早晚会撞上。6.2 Hive/Spark分区概念绕不清的典型错误partition by和distribute by这两个概念特别容易混淆。我在设计分区表的时候一开始也把两个用法搞反了。Hive 里建表的PARTITIONED BY是静态分区把数据按照某个字段的值划分到不同的 HDFS 目录。而distribute by是 Spark SQL 中控制Shuffle 阶段数据如何分发到下游分区的关键字它影响的是数据落盘时的分布方式不改变表结构。实际项目中容易坑人的是动态分区写入。Spark 往 Hive 分区表写数据时需要在写入 SQL 里声明partitionBy(district)并开启动态分区参数INSERT OVERWRITE TABLE ads_district_avg_rent PARTITION (district) SELECT district, AVG(rent) FROM dwd_house_info GROUP BY district;如果忘了开hive.exec.dynamic.partition.modenonstrict作业会直接报错提示动态分区模式为 strict 时至少需要指定一个静态分区。这类报错信息里其实已经把原因说清楚了但新手很容易因为它出现在 Spark 日志深处而忽略掉。6.3 伪分布式环境的内存OOM与格式化问题如果只是做课程设计很多人的机器跑不了完整的三节点集群会先搭伪分布式环境。伪分布式踩的坑更多。第一个是HDFS NameNode 格式化失败。最典型的原因就是/etc/hosts里localhost映射错了。格式化时如果报ERROR namenode.NameNode: java.io.IOException: Cannot create directory /tmp/hadoop-xxx/dfs/name先检查/etc/hosts再检查 HDFS 目录权限。还有一点很关键每次修改完配置文件重新格式化前一定要把 Hadoop 的临时目录通常是你自己设置的 tmp 目录和 NameNode/DataNode 的数据目录全部删干净否则会二次格式化失败。第二个是Spark 作业 OOM。伪分布式环境下物理内存就那么大Spark 默认的 executor 内存动不动 1G~2G再加上 Hadoop 自己占用的内存轻松把机器打爆。我把spark.executor.memory调到 512m、spark.driver.memory调到 512m同时限制 Hive 的mapreduce.map.memory.mb为 512这才在 8G 内存的笔记本上勉强跑顺。如果你的数据量不大这不是丢人的配置反而是务实的做法。第三个跟项目本身相关Spark 重复跑推荐任务时结果表数据翻倍。原因是我用了saveAsTable每次执行都会追加写入。改成insertOverwrite或者用mode(overwrite)就正常了。这类问题通过观察表内数据总量和分区数量就能发现属于典型的作业成功但结果不对的隐性 bug。做完整套项目再回头看其实每个组件的安装配置都不算难网上教程一抓一大把。真正拉开差距的是你能不能把数据从哪儿来、表怎么设计、指标怎么算、结果怎么展示这条链路想清楚。我做完这个项目最大的体会是大数据技术栈的难点不在于单个工具的 API 怎么调而在于你怎么把一个具体问题映射到分布式系统的思维方式里去。最后分享一个小技巧给在做类似课题的朋友每次改动表结构或清洗逻辑之前先在本地把数据样例导出几行确认无误再全量跑。不要问我为什么强调这一点——我在 Hive 和 Spark 之间反复调字段类型的时候靠这个习惯保住了好几次头发。这个项目做完之后如果你想继续扩展可以考虑把实时数据流引进来比如接入 Spark Streaming 做实时房源热度统计或者把推荐算法从基于规则升级成基于 ALS 的协同过滤再往前就是一套完整的实时推荐系统了。底子已经打好往哪个方向扩都顺手。