简介:基于Spark大数据平台开发的二手房信息爬虫分析与预测系统毕业设计资源,面向需要完成大数据实战项目的计算机专业学生及开发者。系统采用Scrapy爬取二手房数据,存入MySQL 5.7,再由Spark完成统计分析并将结果写入HDFS,网站后端使用Flask、前端使用Vue并搭配大屏展示,同时结合线性回归算法对房价趋势进行预测,覆盖数据采集、存储、分析、可视化与预测全流程。压缩包共397个文件,大小约39.71MB,包含44个Python代码、34个Vue前端组件、159个SVG图标/图形资源、2个SQL数据库脚本、10个能直接运行的BAT脚本以及MP4操作视频、Markdown说明文档等,目录清晰,环境配置与启动方式均有注明。已有186人学习下载,项目经运行测试可用,下载后如遇运行问题可私信远程教学,适合作为课程设计或毕业设计的完整参考。
1. 毕业设计里的重头戏:Spark与爬虫如何凑到一块
一个典型的毕设题目写成“基于Spark大数据平台二手房信息爬虫分析预测系统”,拆开看其实是四条链路的串联:网络爬虫负责把二手房网页上的房源字段抓下来,Spark接手做离线清洗和统计分析,再基于干净数据训练房价预测模型,最后把分析结果通过前端图表砸到大屏上。这套架构本身不新鲜,但它是少数能在一个毕设周期内把“数据采集—数据工程—机器学习—可视化交付”整条流水线完整跑通的项目,这也是它能反复出现在各类毕业设计题目里的原因。
做这个系统之前,先要认清它的技术选型边界:爬虫部分用 Python 生态,Spark 用 PySpark 接入,存储落到 HDFS 或本地文件系统,可视化的数据接口通过 Spark 分析结果落地成表格或 JSON 喂给前端。这里最容易犯的错是“为了 Spark 而 Spark”——如果数据量只有几万条,单机 DataFrame 完全能处理,引入 Spark 集群反而让数据倾斜和 shuffle 调优成为新的痛点。一个常见做法是:先让爬虫和预测逻辑在单机跑通,再用 Spark 做全量重放,集群只是放大计算能力,不是解决一切问题的银弹。
适合读这篇文章的人,是打算自己做毕设、或者接手类似“爬虫+大数据分析”题目的学生和初级工程师。下面章节按“采集 — 清洗分析 — 建模预测 — 大屏展示”的顺序展开,每步都会给出可复现代码和参数说明。
2. 二手房爬虫的架构设计:并发与反爬的平衡
2.1 爬虫技术选型:Requests 还是 Scrapy
二手房网站的结构差异很大,有的数据直接渲染在 HTML 里,有的走接口返回 JSON。第一步先分析目标站点的数据来源,用浏览器开发者工具查看 Network 面板:如果列表页数据出现在XHR请求中,直接用 Requests 模拟接口更高效;如果数据嵌在 HTML 里,则要考虑解析成本,Scrapy 的 Selector 在处理这类 DOM 提取时更方便。
import requests import json def fetch_lianjia_data(page=1): url = "https://example.com/api/list" params = {"page": page, "limit": 30} headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "https://example.com/" } resp = requests.get(url, params=params, headers=headers, timeout=10) if resp.status_code == 200: return resp.json() else: print(f"请求失败: {resp.status_code}") return None这段代码的核心是params和headers两个参数:params把翻页参数独立出来便于循环调用,headers中的 User-Agent 和 Referer 是绕过基础反爬的最小配置。真实项目中,这里还应该加一个指数退避重试机制,即请求失败后等待 1 秒、2 秒、4 秒再重试,而不是立即重发。Requests 适合小规模采集,量级在万条以内时足够稳定。
如果目标是百万级房源数据,需要引入 Scrapy 的并发调度能力。Scrapy 的CONCURRENT_REQUESTS参数控制并发请求数,DOWNLOAD_DELAY控制请求间隔。这两个参数直接决定采集速度和被封风险之间的平衡:并发高、延迟低,采集快,但 IP 容易被封;反之则慢但稳。常见配置是并发 8,延迟 1.5 秒,既保证效率又不会触发网站风控。
2.2 爬虫并发设计:线程、协程还是分布式
热词里反复出现“爬虫 并发设计 到底哪个好”,这其实是每个做爬虫的人都会卡住的问题。对于二手房信息这种中等规模采集,三种方案的选择依据可以这样概括:
| 方案 | 适用场景 | 关键实现 | 风险点 |
|---|---|---|---|
| 线程池 | 单机万级数据 | ThreadPoolExecutor | GIL 限制,IO 密集时可接受 |
| 协程 | 单机十万级 | asyncio+aiohttp | 代码复杂度上升,对库依赖多 |
| 分布式爬虫 | 百万级以上 | Scrapy + Redis 任务队列 | 需要额外维护中间件 |
from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_one_page(page): data = fetch_lianjia_data(page) # 这里做字段解析和数据落盘 return len(data) with ThreadPoolExecutor(max_workers=4) as executor: futures = [executor.submit(crawl_one_page, p) for p in range(1, 101)] for future in as_completed(futures): result = future.result() print(f"本页采集房源数: {result}")max_workers=4并不是拍脑袋定的:二手房网站的单次请求响应时间一般在 200-500ms,4 个线程能让 CPU 和网络等待时间重叠,也不会给对方服务器造成过大压力。协程方案虽然性能更好,但 Python 的异步爬虫库生态不如 Requests/Scrapy 成熟,对毕设而言维护成本偏高。分布式爬虫在数据量和部署复杂度上都超出毕设需求,除非题目明确要求“高并发”字样,否则不建议为它多花时间。
2.3 反爬应对和 IP 代理池
二手房网站的常见反爬手段有三种:User-Agent 检测、请求频率检测、验证码。前两种通过构造合理的请求头和限速可以解决,验证码则需要识别服务或人工介入,毕设阶段一般用打码平台或直接绕过——绕过方式是找站点的 APP 接口,APP 端的验证码频率通常比 Web 端低。
IP 代理池是绕不过去的话题。常见做法是维护一个代理列表,每次请求随机取一个 IP,失效则剔除并补充。注意,代理池只对 IP 维度限流的站点有效,如果对方通过设备指纹或账号行为分析限流,换 IP 意义不大。
proxies = [ "http://1.2.3.4:8080", "http://5.6.7.8:8080", ] def fetch_with_proxy(url, headers): proxy = random.choice(proxies) try: resp = requests.get(url, headers=headers, proxies={"http": proxy, "https": proxy}, timeout=10) return resp except Exception: proxies.remove(proxy) # 简单失效剔除 return None这段代码里有两个注意点:proxies参数要同时指定 http 和 https,否则 HTTPS 请求会报代理错误;失效剔除要加锁,否则多线程下会抛list.remove并发异常。真正生产级的代理池会定期验证代理可用性并计算响应延迟,但毕设只要保证不崩即可。
采集后的数据格式要保持统一,常见的做法是每页抓取结果直接追加为 JSON Lines 格式,一行一条房源记录,字段包括小区名、户型、面积、朝向、楼层、总价、单价、所在区域和挂牌时间。这样下游 Spark 读取时只需要一次spark.read.json就能加载完毕,不需要额外的 ETL 转换。
3. Spark 数据清洗与分析:DataFrame 的正确打开方式
3.1 Spark 集群搭建的最小步骤
数据分析需要 Spark 环境,本地开发机和毕设演示机配置一般不高的化,用伪分布式模式最简单,即一台机器同时扮演 Master 和 Worker。由 Spark 官方下载对应 Hadoop 版本的预编译包,解压后配置spark-env.sh中的 JVM 参数即可启动。测试环境用local[*]模式就能跑通全流程,集群模式只影响最终演示时的吞吐量。
启动集群后,用 PySpark 创建一个 SparkSession,这是所有分析的人口:
from pyspark.sql import SparkSession spark = SparkSession.builder \\ .appName("HousePriceAnalysis") \\ .master("local[4]") \\ .config("spark.sql.shuffle.partitions", "8") \\ .config("spark.executor.memory", "2g") \\ .getOrCreate()这里需要解释两个关键参数:spark.sql.shuffle.partitions默认值 200 是针对大规模集群设的,本地跑 4 核时调节成 8 能减少小文件碎片;spark.executor.memory控制每个 Executor 堆内内存,超出物理内存会触发频繁 GC,表现就是任务卡住不动。本地开发时这两个参数是比较常见的调试点。
3.2 DataFrame 清洗和字段标准化
加载原始 JSON 后,第一时间要做的工作是类型推断和缺失值处理。爬虫采集到的字段往往全是字符串,比如面积字段可能是“89.5平”,总价可能是“450万”,这些都要转换后才能做后续聚合。
from pyspark.sql.functions import col, regexp_replace, to_numeric, when, avg df = spark.read.json("/path/to/house_data.jsonl") df_clean = df \\ .withColumn("area", regexp_replace(col("area"), "平", "")) \\ .withColumn("area", to_numeric(col("area"))) \\ .withColumn("total_price", regexp_replace(col("total_price"), "万", "")) \\ .withColumn("total_price", to_numeric(col("total_price"))) \\ .withColumn("unit_price", col("total_price") * 10000 / col("area")) \\ .where(col("area").isNotNull() & col("unit_price") > 0) \\ .dropDuplicates(["小区名", "户型", "面积", "总价"])逻辑说明:第一步去掉单位后缀,第二步转数值类型,第三步计算单价,第四步过滤掉面积为空和单价异常的数据,最后按业务唯一键去重。这里的dropDuplicates看的是房源天然维度,比distinct()更能防止同一条房源在不同爬取批次中重复入库。
Spark 的 DataFrame API 与 pandas 在写法上有相似之处,但底层是 lazy 执行,以上所有withColumn和where在调用count()或write前不会真正跑数据。调试阶段建议先df_clean.limit(100).show()查看中间结果,确认清洗逻辑没有问题后再做全量处理。
3.3 区域维度聚合与统计指标计算
清洗完数据后,常见的分析内容是对各区房源做挂牌均价、总价中位数、面积分布和供需比统计。这里要用到groupBy和聚合函数,同时需要区分均值和中位数的适用性——二手房价格存在明显长尾,豪宅房源会把均值拉高,中位数更能反映普通购房者的感知。
from pyspark.sql.functions import count, avg, median, round region_stats = df_clean \\ .groupBy("区域") \\ .agg( count("*").alias("房源数"), round(avg("unit_price"), 0).alias("均价"), round(median("total_price"), 0).alias("中位数总价") ) \\ .orderBy(col("房源数").desc()) region_stats.show()这里agg里可以混合多个聚合函数,Alias 用于重命名结果列。注意median在某些 Spark 版本中需要通过approxQuantile或 percent_rank 实现,如果报错可以换成expr("percentile_approx(total_price, 0.5)"),两者逻辑一致。统计结果直接写回文件或数据库,作为大屏展示的数据来源。
region_stats.write.mode("overwrite").parquet("/path/to/output/region_stats")输出用 Parquet 格式比 CSV 更适合下游系统,原因有三:列式存储便于按指标做部分读取,自带 schema 不用重复定义列类型,压缩率高节省空间。如果后续大屏展示要通过 SQL 查询,直接把 Parquet 注册成临时表的方式是:
region_stats.createOrReplaceTempView("region_stats") spark.sql("SELECT * FROM region_stats WHERE 房源数 > 100 ORDER BY 均价 DESC LIMIT 10").show()Spark 分析完成后的落盘数据一般不大,大屏展示完全可以直接读 CSV 或 Parquet 文件,不需要特意引入 Elasticsearch 或 Doris 这类重型存储组件。
4. 房价预测模型:从特征工程到 MLlib 调参
4.1 特征工程的边界与设计
预测二手房总价或单价,输入特征可以分为三类:房源自身属性(面积、户型、朝向、所在楼层)、小区特征(建筑年代、绿化率、物业费)、区位特征(距离地铁站距离、所属商圈、教育资源等级)。爬虫能采集到的一般只有第一类,部分小区信息可以从链家小区详情页补采,区位特征如果要自己做地理编码,工作量会非常大。
毕设阶段的特征是并不需要特别完整,但需要合理。建议特征集合保持如下范围:
| 特征列 | 类型 | 处理方式 |
|---|---|---|
| 面积 | 数值 | 直接使用,或取对数压缩长尾 |
| 户型(室/厅/卫) | 数值 | 拆分成三个独立数值列 |
| 朝向 | 类别 | 映射为东/南/西/北/南北等编码 |
| 所在楼层 | 类别 | 分为低/中/高层 |
| 建筑年代 | 数值 | 从“1998年建”里提取年份 |
| 区域 | 类别 | One-Hot 编码,注意稀疏性 |
| 距离地铁 | 数值 | 从描述文本中抽距离,缺失填中位数 |
特征工程的核心原则是:宁可少而准,不要多而杂。小区名不能直接作为特征,因为类别基数过高,一个小区可能只有一条样本,模型学不到泛化信息,反而导致过拟合。区域特征控制在 5-15 个类别即可,超过这个数量就要考虑是否换高层级特征。
4.2 Spark MLlib 的算法选择与训练流程
Spark MLlib 提供的回归算法中,随机森林和梯度提升树(GBT)对表格数据的预测效果较好,线性回归适合做基线。本科毕设一般建议先跑线性回归,再对比随机森林,通过 RMSE 或 MAE 指标选出更优模型。这里逻辑要严格遵循机器学习的标准流程:先切训练集和测试集,再训练,再评估,避免用全量数据训练后评估造成数据泄露。
from pyspark.ml.feature import VectorAssembler, StringIndexer, OneHotEncoder from pyspark.ml.regression import RandomForestRegressor from pyspark.ml.evaluation import RegressionEvaluator # 特征向量组装 feature_cols = ["area", "bedrooms", "living_rooms", "building_age", "floor_level_index", "region_index"] assembler = VectorAssembler(inputCols=feature_cols, outputCol="features_vec") # 类别特征编码 region_indexer = StringIndexer(inputCol="区域", outputCol="region_index") floor_indexer = StringIndexer(inputCol="所在楼层", outputCol="floor_level_index") # 索引转独热(可选) encoder = OneHotEncoder(inputCols=["region_index", "floor_level_index"], outputCols=["region_onehot", "floor_onehot"]) # 随机森林回归器 rf = RandomForestRegressor(featuresCol="features_vec", labelCol="total_price", numTrees=100, maxDepth=10, seed=42)上述代码只完成了特征向量的构建,训练时要用Pipeline把多个阶段串起来,避免手动逐列处理的顺序错乱。其中StringIndexer有一个坑:默认按频率排序,新数据中出现未见过的类别时会抛错,需要设置setHandleInvalid("keep")或"skip"来避免预测错误。
4.3 评估指标与模型诊断
训练完成后,用RegressionEvaluator分别算 RMSE 和 MAE。RMSE 对大误差更敏感,能反映模型是否在个别异常房源上严重偏离;MAE 更直观,可以直接说“预测均价平均差 X 万”。如果 RMSE 远大于 MAE,说明存在个别预测误差特别大的样本,通常来自豪宅或特殊户型。
evaluator_rmse = RegressionEvaluator(labelCol="total_price", predictionCol="prediction", metricName="rmse") evaluator_mae = RegressionEvaluator(labelCol="total_price", predictionCol="prediction", metricName="mae") rmse = evaluator_rmse.evaluate(predictions) mae = evaluator_mae.evaluate(predictions) print(f"RMSE: {rmse:.2f} 万") print(f"MAE: {mae:.2f} 万")模型调参需要控制变量:先固定numTrees=100,在maxDepth为 5、10、15 三档中对比验证集误差,选最优后再调节numTrees。不要同时动多个参数,否则分不清是哪个参数带来的效果改变。如果分析完特征重要性后发现面积和区域占了 80% 以上权重,可以适当删减弱特征,降低过拟合风险。
5. 大屏展示的数据对接与核心防坑
5.1 可视化大屏的技术选型
二手房分析预测系统的大屏展示通常分两块:静态指标卡片(总房源数、均价、最高单价、区域排名)和动态图表(区域价格柱状图、价格分布直方图、预测价格趋势)。前端框架在 ECharts 和 AntV 之间选一个即可,ECharts 的文档更全,社区样例多,适合快速完成毕设演示。
大屏数据对接方式有两种:一是后端提供 REST 接口返回 JSON 给前端异步加载,二是把 Spark 分析结果导出为静态 JSON 文件,前端启动时直接 fetch。毕设演示场景下方案二更省事,不需要额外写一个 Web 服务,也避免了 CORS 跨域配置带来的麻烦。
// 前端读取静态 JSON 数据 fetch("/data/region_stats.json") .then(response => response.json()) .then(data => { initBarChart(data); }); function initBarChart(data) { const chart = echarts.init(document.getElementById("bar-chart")); const option = { title: { text: "各区二手房均价" }, tooltip: { trigger: "axis" }, xAxis: { type: "category", data: data.map(item => item.区域) }, yAxis: { type: "value" }, series: [{ type: "bar", data: data.map(item => item.均价), itemStyle: { color: "#5470c6" } }] }; chart.setOption(option); }静态 JSON 的关键在于字段命名要和 Spark 落盘时的列名保持一致。Spark 写入 JSON 时会自动生成part-00000-xxx.json这样的分片文件,如果数据量小,用coalesce(1)合并成单个文件再丢给前端目录会更方便。
5.2 大屏参数调整与性能优化
大屏调整时最常遇到两个问题:图表刷新卡顿和不同分辨率下布局错位。刷新卡顿的原因是每次setOption都重新构建整个图表,处理办法是把series数据单独提取出来,用setOption({ series: [{ data: newData }] })做局部更新。布局错位则要用rem单位配合屏幕宽度计算基准字体大小,而不是写死像素值。
提示:大屏展示要求的是“看得清、不花哨”,不要在一屏里堆超过六个图表。视觉过载会让答辩老师抓不住重点,数据指标的选择比图表样式重要得多。
5.3 预测结果的展示策略
预测模型的结果不只是“某个房子的价格是多少”,更有价值的是“价格随面积变化的曲线”。将测试集样本按面积分桶,计算每个桶的平均预测价格和真实价格,两条线放在同一张折线图里,既能直观看到模型拟合度,也适合答辩时解释模型效果。实现方式是在完成模型预测后,用groupBy按面积段聚合得到预测均价,再写入 JSON 供前端读取。
from pyspark.sql.functions import floor # 按面积分桶,桶宽 10 平米 df_with_pred = df_clean.join(predictions.select("原文ID", "prediction"), on="原文ID") \\ .withColumn("面积段", (floor(col("area") / 10) * 10).cast("int")) \\ .groupBy("面积段") \\ .agg(round(avg("prediction"), 0).alias("预测均价"), round(avg("total_price"), 0).alias("真实均价")) \\ .orderBy("面积段") df_with_pred.write.mode("overwrite").json("/path/to/output/price_curve")这里用floor(area / 10) * 10实现的10 平方米分桶方式比较实用:面积 45-55 的房源会被分到 50 这一桶,桶的数据量相对均匀,不会出现大户型样本过少导致曲线尾部剧烈抖动。如果分桶太细(比如 5 平米),90 平米以上区间可能每个桶只有几条记录,平均值不稳定。
5.4 集群资源不足时的兜底方案
如果你在演示现场出现集群内存不足或节点掉线的情况,最快的兜底方案是切换 Spark 运行模式。代码侧不用改,只把SparkSession.builder.master("local[4]")改为local[2]甚至local[1],减少分配的 Executor 资源,虽然计算慢一些,但能保证不崩溃。另外一个常见做法是提前把分析结果用 CSV/JSON 缓存一份,万一 Spark 环境在答辩现场起不来,仍能通过预先生成的数据文件完成展示。
5.5 一段前端防白屏技巧
大屏页面作为展示入口,它的容错很重要。封装一个fetchData函数,当 JSON 数据请求失败时,用本地内置的 mock 数据兜底渲染,这样即使后端数据文件被误删或路径配错,演示也不会出现白屏尴尬。这个技巧只需要在前端实现,和后端 Spark 没有耦合,属于纯前端兜底逻辑,但它的实战价值很高——毕设答辩现场的网络环境往往不可控,静态文件的读取也可能因为相对路径问题需要调整。
本文还有配套的精品资源,点击获取