简介:这是一份面向计算机相关专业毕业生的租房数据分析系统毕业设计论文,基于Python+Hadoop+Flask+Vue技术栈,完整呈现从需求分析、系统架构设计、功能模块开发到数据分析应用的毕业设计过程。压缩包内仅含1个docx格式文档,大小6.34MB,即论文正文,内容包括中英文摘要、目录、绪论、关键技术介绍、系统设计、系统实现等完整章节,并详细展示了管理员端与前台端各功能模块的设计思路。目前已有153人学习下载,适合正在开展类似课题或需要参考完整论文结构的学生。论文重点阐述了Hadoop分布式平台下的租房数据存储与处理方案、Django后端逻辑实现、Vue前端交互设计,以及MySQL数据库的应用;同时梳理了系统首页、个人中心、用户管理、房屋信息管理、租房数据管理、房屋资讯等核心模块的划分与实现细节。读者可通过该论文快速理清毕业设计写作脉络,获得技术选型与系统设计参考,为论文撰写和毕业答辩提供实用支撑。
1. 租房数据分析系统毕业设计技术栈的选型逻辑
租房数据分析这类毕业设计,难点通常不在算法,而在“数据能否跑通全链路”。只靠 Python 做爬虫加 pandas 统计,能在本地快速出结果,但和“大数据”方向的课程要求贴合不够;硬上 Hadoop 又容易让采集、展示变成摆设。常见做法是用 Python 解决数据获取与清洗,把清洗结果落到 HDFS,由 Hadoop 承担分布式存储和离线计算,再用 Flask 把统计结果暴露成接口,最后用 Vue 做可视化页面。这套技术栈里的每一环都有明确职责,也正好对应论文里“数据层—计算层—服务层—展示层”的章节结构。下面按这条链路把可复现的做法拆开讲,适合正在做同类系统、需要尽快拿出能演示能答辩的完整项目的读者。
2. Python 爬虫与数据清洗:租房数据进 HDFS 的最小链路
2.1 用 requests 与 BeautifulSoup 抓取租房列表页
租房数据来源不固定,链家、贝壳、58 同城都有列表页和详情页,反爬策略也各不相同。毕设场景下不需要追求高并发,用 requests 加 BeautifulSoup 就能支撑几万条数据量级。下面是一个最小可跑的抓取示例,目标是从列表页提取标题、租金、面积、户型、城区和朝向。
import requests import time import json from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" } def fetch_page(city_code, page): # city_code 是城市在目标网站的拼音或数字编码,page 从 1 开始 url = f"https://example-rental.com/{city_code}/rent/pg{page}/" resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() resp.encoding = "utf-8" return resp.text def parse_list(html): soup = BeautifulSoup(html, "html.parser") items = [] for li in soup.select("div.rental-item"): title = li.select_one("a.title").text.strip() rent = li.select_one("span.rent").text.strip() area = li.select_one("span.area").text.strip() layout = li.select_one("span.layout").text.strip() district = li.select_one("span.district").text.strip() items.append({ "title": title, "rent": rent, "area": area, "layout": layout, "district": district, "source": "example-rental" }) return items if __name__ == "__main__": result = [] for p in range(1, 6): html = fetch_page("bj", p) result.extend(parse_list(html)) time.sleep(1) # 对目标站点做基础限速,避免请求过密 with open("rent_raw.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) print(f"共抓取 {len(result)} 条")这段代码里值得注意的参数是timeout=10和time.sleep(1)。前者防止某个页面长时间挂起拖死整个爬虫进程,后者是对目标站点的基本礼貌,也能降低被识别为爬虫的概率。resp.encoding = "utf-8"必须显式指定,部分租房网站页面头里没有 charset,requests 默认会用 ISO-8859-1 去猜,导致中文乱码。选择器.rental-item这类 class 名称要按实际页面结构调整,但“每条租房信息对应一个容器节点”的解析思路是通用的。
2.2 清洗规则与字段设计:租金和面积是后续统计的地基
爬下来的字段往往是字符串,直接进 Hadoop 再算会造成大量脏数据。我的习惯是在 Python 侧先把字段清洗成规整的 CSV,再批量上传到 HDFS。清洗重点有三个:租金是否包含“元/月”后缀,面积是否混入“平米”字样,朝向是否有缺失。用 pandas 处理这类半结构化文本最顺手:
import pandas as pd import re df = pd.read_json("rent_raw.json", encoding="utf-8") # 租金统一为整数月租:去除"元/月"后转 int,失败置为 -1 df["rent"] = df["rent"].apply(lambda x: int(re.sub(r"[^0-9]", "", str(x))) if re.search(r"\d", str(x)) else -1) # 面积统一为浮点数:去除"平米"等汉字,空值填充 0.0 df["area"] = df["area"].apply(lambda x: float(re.search(r"\d+(\.\d+)?", str(x)).group()) if re.search(r"\d+(\.\d+)?", str(x)) else 0.0) # 朝向缺失统一填"未知",方便后续分组统计 df["orientation"] = df["orientation"].fillna("未知") # 过滤租金或面积明显异常的数据:租金小于 100 或面积小于 5 基本是噪声 df = df[(df["rent"] > 100) & (df["area"] > 5)] # 生成清洗后 CSV,采UTF-8编码,保证 Hive/MapReduce 读取不易乱码 df.to_csv("rent_clean.csv", index=False, encoding="utf-8")清洗字段的取舍会直接影响后面的统计分析口径。租金字段如果保留字符串,Hadoop 的 MapReduce 里还得再转一次类型,增加错误概率;面积字段混入“整租”“合租”这类说明文字,统计均值时会被过滤掉。所以我的原则是:凡是参与统计的字段都在清洗阶段转成数值类型,凡是只做展示的字段(比如标题里的“南北通透”“近地铁”)保留原文即可。
2.3 把清洗结果上传 HDFS:先建目录再 put
清洗后的 rent_clean.csv 还在本地,需要进入 Hadoop 的 HDFS 才能被后续离线分析读取。上传前先在 HDFS 上建立按日期分区的目录结构,这样可以避免后续数据全堆在一个文件里,也方便论文里写“数据按天增量更新”。
hdfs dfs -mkdir -p /rental/data/2025/05/01 hdfs dfs -put rent_clean.csv /rental/data/2025/05/01/ hdfs dfs -ls /rental/data/2025/05/01/参数说明:-mkdir -p表示多级目录一次创建,如果目录已存在也不会报错;-put的本地路径必须在当前用户有读权限的目录下,HDFS 目标路径不能带file://前缀。上传完成后用-ls检查文件是否落位,输出里能看到rent_clean.csv的副本数、块大小和文件大小。副本数默认是 3,伪分布式环境下只有 1 个 DataNode,副本数 3 也不会真正复制,但不会影响读取。
这里还有一个更工程化的做法:用 Python 的hdfs库直接写 HDFS,但毕设环境往往没有配 Kerberos,用命令行 put 反而更直观,也方便在论文的操作截图中展示。下表是两种方式的适用场景对比。
| 方式 | 依赖 | 适合场景 | 主要缺点 |
|---|---|---|---|
hdfs dfs -put | Hadoop 客户端 | 一次性批量上传,演示直观 | 无法在 Python 代码里动态控制 |
hdfsPython 库 | pip install hdfs | 爬虫与上传一体化 | 需要额外配置 HDFS HTTP 端口 |
pyarrow.fs | pyarrow | 与后续 Parquet 分析打通 | 版本敏感,部署麻烦 |
3. Hadoop 伪分布式搭建与离线分析:Streaming 方式跑通租房统计
3.1 伪分布式还是集群:毕设场景的选型与取舍
毕业设计环境里常见有伪分布式、Docker 单节点集群、多机集群三种选择。多机集群需要至少三台服务器或虚拟机,适合网络工程类课题;Docker 单节点能模拟多进程,但内存占用大;伪分布式是在一台机器上同时跑 NameNode、DataNode、ResourceManager、NodeManager 四个进程,配置最少,数据量在百万行以内完全够用。
我的建议是直接用伪分布式,理由是论文里只需要证明“系统具备 Hadoop 分布式计算能力”,而不需要证明“系统能扛多少并发”。伪分布式能完成 HDFS 文件上传、MapReduce 任务提交、YARN 日志查看这条完整链路,且排错范围小。唯一需要注意是伪分布式环境下 DataNode 和 NameNode 的端口不能冲突,默认8020和9000都有可能被占用,建议统一改到9820。
3.2 三份核心配置文件与启动顺序
Hadoop 的配置集中在etc/hadoop/目录下,伪分布式不必动workers或slaves文件,重点改三份。core-site.xml指明 NameNode 地址,hdfs-site.xml设置副本数和 NameNode 目录,yarn-site.xml配置资源调度器。
<!-- core-site.xml --> <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9820</value> </property> </configuration><!-- hdfs-site.xml --> <configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/home/hadoop/data/name</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/home/hadoop/data/data</value> </property> </configuration><!-- yarn-site.xml --> <configuration> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>4096</value> </property> </configuration>配置参数里最容易被忽略的是dfs.replication。如果虚拟机只有单节点,默认副本数 3 会让 DataNode 报 “There are X datanode(s) running and X node(s) are excluded” 之类的警告,虽然不阻塞任务,但在答辩现场会显得不专业。yarn.nodemanager.resource.memory-mb要根据虚拟机内存调整,机器只有 8G 内存时设 4096 比较稳妥,设太大会把系统内存吃光。
配置完成后按顺序格式化并启动进程:
hdfs namenode -format start-dfs.sh start-yarn.sh jpsjps输出里出现NameNode、DataNode、ResourceManager、NodeManager四个进程才算启动成功。如果缺少 DataNode,常见原因是多次执行了namenode -format,导致 NameNode 的 clusterId 与 DataNode 不一致,解决办法是删除dfs.namenode.name.dir和dfs.datanode.data.dir下的数据后重新格式化和启动。
3.3 用 Hadoop Streaming 跑 Python MapReduce:统计各城区平均租金
很多人以为用 Hadoop 就必须写 Java,对 Python 栈不友好。实际上 Hadoop 自带 Streaming 工具,允许把任意可执行文件作为 Mapper 和 Reducer,Python 脚本也能直接跑在 YARN 上。统计“各城区平均租金”只需要一个 Mapper 输出城区和租金,Reducer 做累加和计数。
# mapper.py import sys for line in sys.stdin: line = line.strip() if not line or line.startswith("district"): continue fields = line.split(",") if len(fields) < 6: continue district = fields[3].strip() rent = fields[4].strip() try: rent_val = int(float(rent)) except ValueError: continue # 输出 key\tvalue,Hadoop Streaming 默认按 tab 分隔 print(f"{district}\t{rent_val}\t1")# reducer.py import sys current_district = None total_rent = 0 total_count = 0 for line in sys.stdin: line = line.strip() district, rent, count = line.split("\t") if district != current_district: if current_district is not None: avg = total_rent / total_count if total_count else 0 print(f"{current_district}\t{avg:.2f}") current_district = district total_rent = int(rent) total_count = int(count) else: total_rent += int(rent) total_count += int(count) if current_district is not None: avg = total_rent / total_count if total_count else 0 print(f"{current_district}\t{avg:.2f}")提交命令:
hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \ -input /rental/data/2025/05/01/rent_clean.csv \ -output /rental/output/district_avg_rent \ -mapper "python3 mapper.py" \ -reducer "python3 reducer.py" \ -file mapper.py \ -file reducer.py参数说明:-input和-output都是 HDFS 上的路径,output 目录必须不存在,否则任务会直接报错;-mapper和-reducer后面跟的是实际执行命令,伪分布式环境建议显式写python3,避免在某些节点上默认 python 指向 Python 2。-file会把本地脚本分发到所有容器,这一步最容易漏,漏了会报 “File does not exist: mapper.py” 或者 “Permission denied”。
任务跑完后用hdfs dfs -cat /rental/output/district_avg_rent/part-00000查看结果。MapReduce 默认输出很多个 part 文件,数据量小时只有一个,论文截图里展示这个文件的内容正好能说明“计算完成且结果落盘”。
3.4 失败排查与参数记忆点
Hadoop Streaming 任务失败时,第一件事是看 YARN 日志,不要盲目改代码。执行yarn logs -applicationId <appId>能拿到 stdout 和 stderr。常见的exit code 1原因是 Mapper 里某个字段不是预期的类型,导致抛出未捕获异常。我的习惯是在每个可能出错的字段取用处都加try/except或长度校验,宁可跳过异常行也不让整个 task 失败。
还有一个容易被忽略的坑是 CSV 里的逗号和空格。如果抓取数据里小区名包含逗号或英文引号,字段切分就会错位。对这类数据,要么预处理时把字段里的逗号替换成中文逗号,要么在 Mapper 里写一个简单 CSV 解析函数。使用正则“整体上与字段边界不一致”导致 Mapper 无法反而丢弃数据是常见情况。数据量小时,让 Mapper 输出带 index 的调试信息到 stderr,辅助定位更快。
4. Flask 数据接口与 Vue 可视化:把 HDFS 分析结果送到可演示页面
4.1 离线分析结果先落成 JSON 中间表
MapReduce 输出的 part 文件虽然能直接 cat,但前端渲染不能依赖 HDFS 文件读取。常见做法是写一个 Python 脚本把 part 文件拉回本地,解析成 JSON 再交给 Flask。这里有一个细节:不要每次前端请求都去 HDFS 读文件,应该在 Flask 启动时或定时任务中把结果加载到内存或 SQLite,接口层面只做查询。
# load_hdfs_result.py import json import subprocess def fetch_result(hdfs_path, local_path): # 拉取 HDFS 输出到本地临时文件 subprocess.run(["hdfs", "dfs", "-getmerge", hdfs_path, local_path], check=True) data = {} with open(local_path, "r", encoding="utf-8") as f: for line in f: district, avg_rent = line.strip().split("\t") data[district] = float(avg_rent) return {"district_avg_rent": data} if __name__ == "__main__": result = fetch_result( hdfs_path="/rental/output/district_avg_rent", local_path="/tmp/district_avg_rent.txt" ) with open("static_result.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2)hdfs dfs -getmerge的作用是把 HDFS 目录下所有 part 文件合并成一个本地文件,比-get更适合 MapReduce 输出。check=True表示子进程失败时直接抛异常,避免生成半截 JSON。
4.2 Flask 接口设计与参数定义
Flask 在这里只承担轻量数据服务,不需要蓝图、不需要 SQLAlchemy,最多加一个 Flask-CORS 解决跨域。接口路径按业务语义命名,参数通过 query string 传入。
from flask import Flask, jsonify, request import json app = Flask(__name__) CORS(app) # 允许 Vue 开发服务器跨域访问 with open("static_result.json", "r", encoding="utf-8") as f: result = json.load(f) @app.route("/api/rent/district", methods=["GET"]) def district_avg_rent(): # 支持可选参数 min_count,过滤样本量过少的区域 min_count = request.args.get("min_count", default=0, type=int) data = result.get("district_avg_rent", {}) if min_count > 0: data = {k: v for k, v in data.items() if counts.get(k, 0) >= min_count} return jsonify({"code": 0, "data": data}) @app.route("/api/health", methods=["GET"]) def health(): return jsonify({"status": "ok"}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)jsonify会自动设置 Content-Type 为 application/json,这是前端 axios 能不能直接拿res.data的关键。host="0.0.0.0"表示监听所有网卡,如果你用虚拟机跑 Flask、宿主机浏览器访问 Vue 页面,就必须这么写,只写 127.0.0.1 会导致外部访问失败。接口路径加入/api前缀是为了后续如果换成 Spring Boot 或 Node.js,前端代码不用改业务逻辑。
4.3 Vue 组件与 ECharts 的联动
Vue 侧的核心是把接口数据映射成图表配置。下面是一个常用的RentBar.vue组件,用axios请求 Flask 接口,拿到数据后交给 ECharts 渲染。
<template> <div ref="chartRef" style="width: 100%; height: 400px"></div> </template> <script setup> import { ref, onMounted, watch } from "vue" import axios from "axios" import * as echarts from "echarts" const props = defineProps({ minCount: { type: Number, default: 0 } }) const chartRef = ref(null) let chartInstance = null function renderChart(data) { if (!chartInstance) { chartInstance = echarts.init(chartRef.value) } const districts = Object.keys(data) const rents = Object.values(data) chartInstance.setOption({ tooltip: { trigger: "axis" }, xAxis: { type: "category", data: districts }, yAxis: { type: "value", name: "元/月" }, series: [{ type: "bar", data: rents }] }) } onMounted(async () => { const res = await axios.get("http://localhost:5000/api/rent/district", { params: { min_count: props.minCount } }) renderChart(res.data.data) }) watch(() => props.minCount, async (val) => { const res = await axios.get("http://localhost:5000/api/rent/district", { params: { min_count: val } }) renderChart(res.data.data) }) </script>ref和watch是 Vue 3 组合式 API 的核心,chartRef绑定到 DOM 节点,ECharts 初始化时需要真实 DOM,所以必须在onMounted里执行。二次渲染时不要重复echarts.init,复用chartInstance并调用setOption即可,否则页面会出现内存泄漏的警告。如果请求频率较高,建议在setOption前调用chartInstance.clear()清除旧配置。
4.4 联调阶段的两个常见卡点
前后端联调时第一个卡点是跨域。Vue 开发服务器默认跑在 5173,Flask 跑在 5000,浏览器会拦截跨域请求。解决方案有两种:后端加 Flask-CORS,或前端 vite.config.js 里配置代理。我的建议是开发期用 Flask-CORS,最简单,部署时再改成 Nginx 同源反向代理,避免把服务端口直接暴露。
第二个卡点是 ECharts 图表在 Tab 切换或路由跳转后宽高变成 0。原因是图表容器在隐藏状态下初始化时拿不到真实尺寸。解决方案是在组件激活时调用chartInstance.resize(),或者给容器一个固定的最小高度,比如上面的height: 400px样式。表格 4.2 的接口参数在演示时也方便用来动态展示“样本量过滤”的效果,可玩性比静态图高。
| 接口 | 参数 | 返回值 | 演示用途 |
|---|---|---|---|
/api/rent/district | min_count | 各城区平均租金 | 过滤小样本后趋势更平滑 |
/api/rent/area_trend | area_bin | 面积区间与租金中位数 | 展示面积与租金的关系 |
/api/health | 无 | 服务状态 | 答辩开场快速确认服务存活 |
5. 答辩前最值得做的 3 件事:数据对账、接口压测与一键复现
5.1 数据对账:HDFS 行数与清洗后 CSV 的一致性校验
答辩被问到最多的问题就是“你保证数据没丢吗?”。这是允许验证的。想回答清楚,只需两组数字:清洗前多少条,清洗后多少条。用以下脚本对账:
# 统计 HDFS 上的 CSV 行数(减去表头) hdfs dfs -cat /rental/data/2025/05/01/rent_clean.csv | wc -l # 统计本地清洗文件行数 wc -l rent_clean.csv# reconcile.py import pandas as pd hdfs_count = 13428 # 替换为上述命令的输出 df = pd.read_csv("rent_clean.csv") clean_count = df.shape[0] assert clean_count == hdfs_count - 1, f"数量不一致: HDFS={hdfs_count}, 本地={clean_count}" print(f"对账通过: {clean_count} 条")这个脚本建议放在论文“系统测试”章节的截图里,配合清洗前后的数据量对比表,能直观说明爬虫、清洗、上传三个环节都没有丢数据。
5.2 接口压测:用 ab 验证 Flask 接口能扛住演示现场
演示时可能同时有几十个页面在刷,接口如果卡顿会非常尴尬。用 Apache ab 做一次简单压测,能提前发现接口瓶颈:
ab -n 200 -c 20 http://localhost:5000/api/rent/district如果 Requests per second 低于 50,优先检查 Flask 是否开启了 debug 模式,debug=False是必须的;其次看接口里是否每次都重新读 JSON 文件,正确做法是在模块加载时读取一次并缓存。压测结果写入论文“性能测试”一节,只需要放吞吐率和平均响应时间两个指标即可。
5.3 一键复现:用编排脚本把启动顺序写清楚
答辩演示最忌讳手动敲一串命令,对方一紧张顺序就错。把一个run_demo.sh脚本放进项目根目录,按顺序执行 Hadoop 启动、结果拉取、Flask 启动、前端构建,能大幅提高演示流畅度。
#!/bin/bash start-dfs.sh start-yarn.sh hdfs dfs -test -d /rental/data/2025/05/01 || echo "数据目录不存在,先执行上传脚本" python3 load_hdfs_result.py flask --app app run --host=0.0.0.0 --port=5000 & # 后台启动 cd frontend && npm run dev脚本里的hdfs dfs -test -d参数值得解释一下:如果目录不存在执行后面的||分支,用echo给出明确提示,避免上游数据缺失时后面所有步骤都失败却看不到原因。把这段脚本放到论文附录,能说明系统具备“一键复现”能力,这也是评审老师比较看重的工程素养。
本文还有配套的精品资源,点击获取