租房数据分析毕设:从Python爬虫到Hadoop+Vue可视化
2026/9/18 15:25:32 网站建设 项目流程

简介:这是面向计算机相关专业毕业生的毕业设计论文资源,主题为基于Python、Hadoop、Flask与Vue的租房数据分析系统,覆盖毕业设计与毕业论文写作双重需求。文档为docx格式,压缩包共1个文件,约6.34MB,为可直接编辑的Word版论文,内容完整,包含摘要、目录、绪论、系统关键技术、功能模块与数据分析等章节。已有153人学习浏览,适合作为选题参考、项目开发或论文结构设计的样例。文中以Hadoop大数据平台为底层支撑,结合Django/Flask后端、MySQL存储与Vue前端,系统阐述管理员端和前台端各模块的划分方式、数据管理流程及实现效果,并具体涉及HDFS、MapReduce等核心技术。读者既能快速把握租房数据分析系统的整体设计方案,又能借鉴其中关于大数据处理、Web开发与论文撰写的组织思路,为完成类似课题提供直接帮助。

1. 毕业设计选型租房数据分析,先想清楚论文怎么讲

每年毕业季都有大量“XX数据分析系统”选题,租房方向因为数据易得、业务直观、可视化效果好,一直是热门选型。但很多同学把时间花在爬虫和前端页面上,答辩时讲不清 Hadoop 到底做了什么,反而被评委问住。这篇博文按“Python 采集清洗 → Hadoop 离线存储与计算 → Flask 提供接口 → Vue 可视化”这条最稳妥的链路,把每个环节的关键代码、参数和坑位一次说透。

这套四层架构要解决的核心问题很简单:租房数据量不大,但来源杂、字段乱、需要反复分析,Hadoop 的价值在于把数据从“抓下来能用”变成“存得住、算得动”,Flask 和 Vue 则是把分析结果变成可交互的展示。适合正在做毕设、需要快速跑通并可演示的读者,也适合想了解小规模数仓怎么落地的从业者——架构可以缩,思路不能省。

2. Python 采集与预处理:别让脏数据毁掉整个分析

2.1 爬虫的目标与反爬边界

租房数据的常规来源是链家、贝壳、安居客这类公开房源页面。常见做法是用 Python 的requestsBeautifulSoupparsel抓取列表页和详情页,重点提取小区名、区域、户型、面积、朝向、楼层、租金、发布时间、经纬度这几个字段。抓取要用time.sleep()控制频率,随机 User-Agent,必要时用cookies维持会话;规模控制在几千到一两万条即可,毕设不需要全量数据,重在把链路打通。

import requests import time import random from bs4 import BeautifulSoup import pandas as pd headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } base_url = "https://xxx.zu.ke.com/zufang/pg{}/" data_list = [] for page in range(1, 51): url = base_url.format(page) try: resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") for item in soup.select(".content__list--item"): title = item.select_one(".content__list--item--title").text.strip() detail = item.select_one(".content__list--item--des").text.split(" / ") price = item.select_one(".content__list--item-price").text.strip() data_list.append([title, detail[0], detail[1], price]) except Exception as e: print(f"第{page}页失败: {e}") time.sleep(random.uniform(1, 3))

这段代码的逻辑是遍历前 50 页列表页,每页解析标题、区域描述和价格三个核心信息,单页失败直接跳过不影响整体。time.sleep(random.uniform(1, 3))是请求间隔,单位是秒,取值在 1 到 3 秒之间随机,避免被服务器识别为高频请求;如果目标网站的验证码校验严格,timeout参数可以缩小到 5 秒,快速失败及时切换代理。实际项目中detail[0]通常对应区域和板块,detail[1]对应户型信息,字段具体位置要看页面结构微调。

采集完成后第一件事不是入库,而是先导出成 CSV 看一眼字段长什么样。常见问题是:租金“4500元/月”带着单位、面积“45㎡”混着汉字、有些房源朝向是 NULL、同一个小区的名字一会儿叫“阳光花园”一会儿叫“阳光花园二期”。这些脏数据不处理,后面的 SQL 分析和可视化全部会失真。

2.2 用 pandas 做字段归一化与异常过滤

数据处理用 pandas 最顺手。核心操作有四类:字符串清洗、类型转换、空值填充、重复值去重。字符串清洗用str.extract把数字抠出来,类型转换用pd.to_numericerrors="coerce"强制无效值变 NaN,空值填充按列分别处理,重复值按“小区+户型+面积+租金”联合去重。

import pandas as pd import re df = pd.read_csv("house_raw.csv", encoding="utf-8") # 租金:提取数字,按元/月统一 df["rent"] = df["price"].str.extract(r"(\d+)").astype(float) # 面积:提取数字,剔除0和异常大值 df["area"] = df["info"].str.extract(r"(\d+\.?\d*)㎡").astype(float) df = df[(df["area"] > 10) & (df["area"] < 200)] # 户型:从"3室1厅"中拆出室和厅 df["bedrooms"] = df["info"].str.extract(r"(\d)室").astype(int) df["livingrooms"] = df["info"].str.extract(r"(\d)厅").astype(int) # 经纬度:缺失记录按小区名回填(示例逻辑) df.loc[df["lng"].isna(), "lng"] = df["lng"].fillna(df.groupby("community")["lng"].transform("mean")) # 去重 df = df.drop_duplicates(subset=["community", "bedrooms", "area", "rent"]) df.to_csv("house_clean.csv", index=False, encoding="utf-8")

这段代码的关键在正则表达式:(\d+)从“4500元/月”里提取 4500,(\d+\.?\d*)兼容整数和小数面积,(\d)室把“3室1厅”中的 3 取出来。面积过滤区间定在 10 到 200 平方米,既能去掉车位、隔断房,也能过滤掉录入错误的大面积数据;经纬度回填用的是同一小区均值,比直接丢弃信息更合理。清洗结果house_clean.csv就是进入 Hadoop 的标准数据集。

清洗这一步最容易犯的错是“过度清洗”:把异常值全部剔除导致样本量不够,后面 Hadoop 分析结果没有统计意义。一般保留 3000 条以上有效记录,分布均匀即可,不必追求完美。

3. Hadoop 伪分布式 + Hive 数仓:让毕设里的大数据不做摆设

3.1 伪分布式搭建:单机跑通完整 Hadoop 生态

Hadoop 在毕设里最常见的形式是伪分布式部署,也就是一台机器上同时跑 NameNode、DataNode、ResourceManager 和 NodeManager。虽然数据量完全不需要分布式,但这一步的价值在于:答辩时能说清楚 Hadoop 组件如何协作,HDFS 的存储逻辑、MapReduce 的调度模型在你手里是真实跑过的。生产级集群要 3 台以上机器,毕设一台 8G 内存的笔记本足够。

常用版本组合是 Hadoop 3.3.x + JDK 8 + Hive 3.1.x,三者都有版本兼容要求。JDK 装好后在core-site.xml配置 NameNode 地址,在hdfs-site.xml配置副本数。注意本地跑直接把副本数设为 1,设成 3 虽然不报错,但单节点会多做两次无意义的复制。yarn-site.xml需要设置内存分配,8G 内存的机器给 YARN 分配 2G 比较稳,改大了容易把 NodeManager 拖死。

<!-- core-site.xml --> <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> </configuration> <!-- hdfs-site.xml --> <configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/usr/local/hadoop/tmp/name</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/usr/local/hadoop/tmp/data</value> </property> </configuration>

fs.defaultFS指定了 HDFS 的入口地址,所有客户端通过 9000 端口访问;dfs.replication为 1 表示只存一份副本,单机环境减少写盘开销。dfs.namenode.name.dirdfs.datanode.data.dir分别存储元数据和数据块文件,这两个路径一定要提前建好,否则启动时会在日志里看到FileNotFoundException

启动顺序有讲究:先hdfs namenode -format格式化 NameNode,然后start-dfs.sh启动 HDFS,再start-yarn.sh启动调度框架。每一步用jps命令验证进程是否存活,正常应该看到 NameNode、DataNode、ResourceManager、NodeManager 四个进程。启动过程中如果 DataNode 起不来,优先检查 logs 目录下的.log文件而不是网上搜配置,九成问题是路径权限或端口占用。Hive 装在 HDFS 之上,需要先在 HDFS 里创建/tmp/user/hive/warehouse目录并赋权,这是新手最容易漏的一步。

3.2 建 Hive 外部表:HQL 即学即用

数据放入 Hive 有两种常见方式:内部表由 Hive 全权管理数据文件,适合本地实验;外部表通过LOCATION指向 HDFS 现有路径,数据删了表还在,业务上更安全。毕设推荐外部表,理由是你可能需要在 Hadoop 跑通后回改清洗逻辑,数据还能复用。建表语句如下:

CREATE EXTERNAL TABLE IF NOT EXISTS house_rent ( community STRING COMMENT '小区名称', district STRING COMMENT '行政区', bizcircle STRING COMMENT '板块', rent FLOAT COMMENT '月租金', area FLOAT COMMENT '面积', bedrooms INT COMMENT '室', livingrooms INT COMMENT '厅', lng DOUBLE COMMENT '经度', lat DOUBLE COMMENT '纬度' ) COMMENT '租房数据清洗表' ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE LOCATION '/user/hive/warehouse/house_rent';

字段类型选型原则是能用数字不用字符串,租金用FLOAT而不是STRING,因为后面对比均价要直接avg();经纬度用DOUBLE保留精度,维度字段大小类型不一致会在 join 时出问题。FIELDS TERMINATED BY ','要与 pandas 输出 CSV 的分隔符完全一致,Hive 默认的\001分隔符在从 CSV 导入时要显式更换,否则整张表只有一列。

数据装载用一条 HQL 搞定:

LOAD DATA INPATH '/input/house_clean.csv' INTO TABLE house_rent;

INPATH是从 HDFS 路径导入,不是本地路径;UPLOAD 到 HDFS 用hadoop fs -put house_clean.csv /input/。装载后立刻验证行数和分区粒度:

SELECT count(*), count(distinct district) FROM house_rent;

正常情况能一眼看到记录总数和各区县数量,如果没有报错但 count 为 0,先回 HDFS 检查文件是否真的存在、大小是否为 0,再确认字段分隔符是不是逗号——这两个问题占了 Hive 空表原因的八成。

3.3 离线指标计算:SQL 替代 MapReduce

Hive 最有价值的地方在于把 MapReduce 封装成 SQL,三个常用指标就能覆盖毕设的核心分析诉求:各区均价排行、面积与租金相关性、户型供应占比。这些 SQL 在答辩时即使被追问底层 MapReduce 逻辑,也能从容说明。

-- 指标1:各行政区平均租金排序 SELECT district, ROUND(AVG(rent), 2) AS avg_rent, COUNT(*) AS house_cnt FROM house_rent WHERE rent > 0 GROUP BY district ORDER BY avg_rent DESC; -- 指标2:按面积段统计平均租金,观察梯度 SELECT CASE WHEN area < 30 THEN '30平以下' WHEN area < 60 THEN '30-60平' WHEN area < 90 THEN '60-90平' ELSE '90平以上' END AS area_range, ROUND(AVG(rent), 2) AS avg_rent, COUNT(*) AS cnt FROM house_rent GROUP BY CASE WHEN area < 30 THEN '30平以下' WHEN area < 60 THEN '30-60平' WHEN area < 90 THEN '60-90平' ELSE '90平以上' END;

第一个查询按行政区聚合后取租金均值,ROUND(..., 2)控制保留两位小数,ORDER BY avg_rent DESC让结果直接看出哪个区租金最高;COUNT(*)兼职验证样本量,防止某个区只有两条数据拉高均价。第二个查询用CASE WHEN分桶,注意GROUP BY后面必须重复完整的CASE表达式,不能直接写别名,这是 Hive 和 MySQL 差异最大的地方——Hive 的 GROUP BY 发生在 SELECT 别名计算之前。house_cnt的作用是过滤极端值,样本量小于 5 的分组不进入可视化展示。

4. Flask 后端 API:把分析结果变成可调用的接口

4.1 Hive 结果导出到 MySQL

Hive 查询结果不能直接被 Flask 查询,因为 Flask 应用对用户响应要求毫秒级,Hive 的启动开销通常是秒级。常见做法是把离线分析结果导出到 MySQL,Flask 作为 Web 层只和 MySQL 通信,Hive 负责“算一次存一次”。导出方式有两种:数据量小直接hive -e "SELECT ..." > result.csv再导入 MySQL,数据量大用 Sqoop。毕设场景用前者更直接。

hive -e "SELECT district, ROUND(AVG(rent),2), COUNT(*) FROM house_rent WHERE rent > 0 GROUP BY district;" > /tmp/result_district.csv mysql -uroot -p123456 -e " LOAD DATA LOCAL INFILE '/tmp/result_district.csv' INTO TABLE rent_district FIELDS TERMINATED BY '\\t' (district, avg_rent, house_cnt);"

这里的关键是分隔符:Hive 默认输出用\t分割字段,LOAD 语句里也要指定FIELDS TERMINATED BY '\t'\\t是 shell 转义后的制表符。MySQL 侧的字段顺序要和 CSV 列顺序一致,否则数据会错列错行。导入后跑SELECT COUNT(*) FROM rent_district验证条数,比预期的区县数多通常是因为 Hive 输出带了表头。

4.2 Flask 最小接口设计与 CORS 处理

Flask 要做的事情非常克制:提供 3 到 5 个 GET 接口,每个接口对应一个可视化图表的数据源。一个合理的接口清单是:/api/district_avg返回行政区均价柱状图数据,/api/area_rent返回面积-租金散点图数据,/api/top10返回租金最高的 10 个小区,/api/heatmap返回经纬度和租金用于地图气泡,/api/community_search?q=支持小区关键词搜索。

from flask import Flask, jsonify, request import pymysql app = Flask(__name__) def get_conn(): return pymysql.connect( host="localhost", user="root", password="123456", database="rent_house", charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor ) @app.route("/api/district_avg") def district_avg(): conn = get_conn() try: with conn.cursor() as cursor: cursor.execute(""" SELECT district, avg_rent, house_cnt FROM rent_district WHERE house_cnt >= 5 ORDER BY avg_rent DESC """) data = cursor.fetchall() return jsonify({"code": 0, "data": data}) except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 finally: conn.close() if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)

jsonify会把 DictCursor 返回的字典列表直接序列化为 JSON,code: 0是约定成功标志,前端判断res.code === 0再渲染数据,异常时返回 500 和错误信息方便排查。host="0.0.0.0"允许局域网访问,这样手机或另一台电脑也能打开 Vue 页面请求数据;debug=True在开发阶段有用,上线前必须关掉,否则代码报错会直接泄露路径信息。finally块里的conn.close()确保每次请求都释放连接,Flask 默认单线程模型下不关连接会很快耗尽 MySQL 的最大连接数。

from flask_cors import CORS CORS(app)

有了这行,Flask 接口就允许前端的跨域请求。本地开发时 Vue 和 Flask 通常一个是localhost:8080、一个是localhost:5000,浏览器会拦截端口不同的 AJAX 请求,CORS(app)解决的是这个问题。不要自己写Access-Control-Allow-Origin响应头,flask_cors已经处理了预检请求和各种边界情况。

5. Vue 前端可视化:ECharts 和地图让数据说话

5.1 Vue 环境与项目结构

前端部分用 Vue 2 + Vue Router + ECharts + axios 的组合最为顺手。Vue 3 的组合式 API 对新手不友好,毕设重点是做出可用的查价和看趋势界面,Vue 2 的选项式 API 心智负担小得多。用 npm 创建项目:

npm install -g @vue/cli vue create rent-frontend cd rent-frontend npm install echarts@4.9.0 axios vue-router@3.5.1

ECharts 特意固定 4.x 版本,原因是 ECharts 5 的按需引入配置变复杂,插件结构也不同,直接引用全量包反而简单。vue-router@3是对应 Vue 2 的版本号,装成 4.x 会直接报路由匹配失败。组件结构建议是views/MapView.vue放地图热力与散点,views/TrendView.vue放均价排序柱状图与面积租金散点,views/SearchView.vue放小区搜索与详情卡片。

路由配置需要处理history模式下的刷新 404 问题。Vue Router 常用createWebHistory(),但这种模式刷新页面时后端没有对应路由会返回 404。Flask 侧需要增加兜底路由,或者干脆用createWebHashHistory()#的 hash 模式,部署简单不出错。

5.2 axios 请求与 ECharts 渲染

核心页面的逻辑几乎一样:mounted里发请求,拿到数据后setOption渲染图表。以地图热力图为例,地图用 ECharts 的 scatter 类型加visualMap组件,经纬度作为坐标,租金作为权重值。

<template> <div ref="chart" style="width: 100%; height: 600px;"></div> </template> <script> import echarts from "echarts"; import axios from "axios"; export default { name: "HeatMap", data() { return { chart: null }; }, mounted() { this.chart = echarts.init(this.$refs.chart); this.loadData(); }, methods: { async loadData() { const res = await axios.get("/api/heatmap"); if (res.data.code === 0) { const points = res.data.data.map(item => ({ value: [item.lng, item.lat, item.rent], })); this.chart.setOption({ tooltip: { trigger: "item" }, visualMap: { min: 500, max: 15000, dimension: 2 }, series: [{ type: "scatter", data: points, symbolSize: 12, itemStyle: { opacity: 0.6 } }] }); } } }, beforeDestroy() { this.chart.dispose(); } }; </script>

value数组的前两个元素对应经纬度,第三个是租金,visualMapdimension: 2告诉 ECharts 用第三个维度的值映射颜色。symbolSize固定为 12 避免极端值把别的点盖住;如果数据量大,改成函数形式symbolSize: val => val[2] / 500按租金等比缩放会更直观。beforeDestroy里调用dispose()防止组件切换后内存泄漏,这是 Vue + ECharts 最容易忽略的性能点。

5.3 本地反向代理解决跨域

开发环境中即使 Flask 已经开了 CORS,推荐的做法还是用 Webpack 的 devServer 做代理。理由是对毕设来说最终要部署上线,前端和后端在同一个域名下最省事,代理让本地环境无限接近生产环境。

// vue.config.js module.exports = { devServer: { proxy: { "/api": { target: "http://localhost:5000", changeOrigin: true } } } };

前端请求/api/district_avg时,devServer 会把请求转发到http://localhost:5000同路径地址。如果后端接口路径带前缀比如/rent/api,还需要加pathRewrite: { "^/api": "/rent/api" }做路径改写。changeOrigin: true必需,它会让代理请求头中的 Host 和目标一致,后端做任何域名校验时不会误判。

6. 联调排错与答辩演示的三个关键技巧

联调阶段按“数据 → 接口 → 图表”的顺序排查能省一半时间。先确认 MySQL 表里有数据,再确认 Flask 接口浏览器访问有 JSON 返回,最后看 Vue 控制台有没有请求报错。一个常见问题是 Vue 请求成功但图空白,这时打开浏览器开发者工具看 Network 面板返回的data字段是不是空数组,通常从 SQL 条件过滤掉样本量小于 5 的分组就能暴露问题。另一个高频报错是 Flask 返回的数据里avg_rent是 Decimal 类型,jsonify无法序列化,需要在查询语句里先CAST(avg_rent AS FLOAT),或者写一个自定义 JSON 转换器统一处理。

演示顺序建议从“整体看板”切入,先打开地图页面展示房源空间分布,用户在大区域点击后进入对应行政区的均价柱状图和面积租金散点,最后搜索某个小区名看到实时详情。这套交互路径把 Hadoop、Flask、Vue 三个技术点全部串起来,评委无论从哪层深挖都有内容。答辩前预演一遍接口容错,把租房数据 CSV 里加两行脏数据重新跑清洗到 Hive 的完整链路,只操作一遍就记住每个坑的位置。

本文还有配套的精品资源,点击获取

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

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

立即咨询