从58同城租房数据到可视化大屏,这套毕设系统我前后改了四版。最初选的爬虫框架是Scrapy,折腾了两周,最后换成了Requests,反而半小时就把核心流程跑通了。这里面有不少弯弯绕绕,今天把这套基于Python+Django的租房数据分析可视化系统从选型到落地的完整思路写出来,给正在做毕设、或者想练手数据分析项目的朋友做个参考。
这套系统能做的事很明确:用Requests爬取58同城租房频道的房源信息,清洗后存入数据库,Django提供Web框架支撑,最后用可视化图表把租金分布、区域热度、户型比例、价格趋势这些维度呈现在大屏上。如果你是计算机相关专业的毕业生,这套系统的技术栈覆盖面足够撑起一篇像样的毕业论文——爬虫、数据清洗、关系型数据库设计、后端框架、前端可视化全都能讲到,而且每一块都有明确的验收标准,不会出现"做了什么说不清楚"的情况。
1. 选题价值判断:为什么租房数据可视化能成为毕设优选项
每年毕业季都有大量学生卡在选题上。系统写复杂了,工作量扛不住;写简单了,导师那关过不去。租房数据分析可视化这个方向,恰好卡在一个很舒服的位置:技术难度适中、数据来源真实、展示效果直观。
1.1 技术栈覆盖面与真实业务场景的匹配
先看这套系统涉及的技术面:
- 数据采集层:Requests + BeautifulSoup(或正则表达式),模拟HTTP请求解析HTML
- 数据存储层:MySQL或SQLite,涉及表结构设计、去重策略、数据持久化
- 后端服务层:Django框架,负责API接口、业务逻辑、模板渲染
- 数据分析层:Pandas清洗聚合,统计各维度指标
- 可视化层:ECharts对接,生成柱状图、折线图、地图热力图、饼图
这个组合的巧妙之处在于,它不是一个单纯的"管理系统"——那种增删改查的模板项目在答辩桌上没有任何优势。租房数据分析的每个环节都在处理真实数据,爬虫拿到的房源信息是脏的、乱的,你得去想清洗方案;不同区域的价格差异是明确的,你得去想怎么用图表表达。整个过程处处是问题,处处有解法,写论文的时候每个决策点都有话可说。
1.2 "大屏展示"在毕设答辩里的实际分量
我参与过几次校内答辩旁听,一个强烈的感受是:导师在5分钟之内很难判断你系统写得多深,但一眼能看出你系统"好不好看"。可视化大屏恰好踩中了这个心理——地图上彩色热力区域一铺开,价格区间的动态分布图一刷,答辩现场的氛围就不一样了。这不是说花架子能糊弄人,而是说可视化是让评审快速理解你工作量的最直接方式。
58同城租房数据选择也有讲究。它不像链家那种需要应对强反爬的站点,房源页面结构相对规整,数据字段涵盖小区名、户型、面积、朝向、楼层、租金、所在区县,供给分析绰绰有余。
提示:选题时千万别一上来就盯着"爬取链家"这种方案。链家的反爬机制会浪费你大量时间在IP代理、字体反爬对抗上,而这些内容在毕业论文里其实不太好展开写——既敏感又偏离数据分析主线。58同城这些中小体量的站点,反爬压力小,字段全,更适合作为毕业设计的数据源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构与数据流转:从Request到图表大屏的完整链路
这套系统的架构可以概括为"三端一库"。三端分别是爬虫端、服务端、展示端,一库是中间的MySQL数据库。 数据流的走向是单向的:爬虫端采集数据存入数据库,服务端查询数据库聚合指标,展示端从服务端拉取数据渲染图表。
2.1 技术选型的取舍逻辑
先说为什么选Requests而不是Scrapy。Scrapy作为一个完整框架,自带并发调度、中间件机制、Item Pipeline,功能确实强大,但对毕设项目来说有点杀鸡用牛刀。它的问题在于:异步框架的学习成本高,出问题不好调试;你把时间花在理解框架机制上,而不是花在业务逻辑上。Requests在单线程下跑几百条房源数据,控制在合理频率,加上异常重试,十几分钟也能采集完毕,完全够用。
Django在这个场景下用得比较轻,用它的ORM做模型定义和数据写入,用Django REST Framework(或直接JsonResponse)提供API接口。你不需要把Django的前后台管理模板全用上,只做数据模型和接口层,前期工作量大减。
MySQL和SQLite的选择,我建议有条件的直接上MySQL。理由很简单:答辩时被问到"为什么用MySQL",回答空间更大——支持并发、数据隔离、索引优化这些都可以聊;SQLite的话一句嵌入式数据库就打发了,没什么展开空间。
2.2 数据清洗与入库流程设计
一个容易被忽视的模块设计是清洗流程。58同城爬下来数据大致长这样:
- 租金字段是字符串:"3500元/月",直接入库的话后面聚合没法算
- 面积字段混合:"45㎡"或"45平",需要清洗成数值
- 区县信息可能包含在标题或小区地址里,需要解析
- 同一个房源可能被多次爬取,需要按房源ID去重
我在Pandas里统一处理:正则表达式提取数字,类型转换,空值填充。去重逻辑放在数据库层面——房源链接作为唯一索引,重复数据直接忽略,避免每次爬虫往库里塞重复记录。
字段设计上,我最终保留了这些核心字段:
| 字段名 | 说明 | 类型 | 备注 |
|---|---|---|---|
| house_id | 房源唯一标识 | VARCHAR(64) | 主键,来自原始链接 |
| title | 标题信息 | VARCHAR(128) | 可做关键词分析 |
| district | 所在城区 | VARCHAR(32) | 地图热力图的关键字段 |
| community | 小区名称 | VARCHAR(64) | 可进一步关联 |
| rent | 月租金 | INT | 清洗后的数值型 |
| area | 面积 | FLOAT | 清洗后的数值型 |
| layout | 户型 | VARCHAR(16) | 如2室1厅 |
| floor | 楼层信息 | VARCHAR(32) | 可转为楼层区间 |
| direction | 朝向 | VARCHAR(16) | 如南、南北 |
| create_time | 入库时间 | DATETIME | 默认当前时间 |
这张表的设计足够撑起后续所有分析维度:按district聚合算平均租金、按layout统计户型占比、按rent区间做价格分布、按area和rent算每平米单价。
3. 核心模块实现:爬虫、数据处理、接口层、可视化逐个落地
这一章是实操核心,按模块拆开讲。每一步都有具体的实现思路和踩坑记录。
3.1 Requests爬虫模块:页面解析与频率控制
首先分析58同城租房频道的URL结构。租房列表页通常遵循类似 https://xxx.58.com/zufang/pn{页码}/ 的分页规律(实际字母可能因城市而异)。我最初写的版本是硬编码单城市页面,后来发现把它设计成城市参数传入更灵活,可以爬取多个城市对比。
请求头伪装是必须的。我配置的Headers里包含了User-Agent、Referer、Accept-Language等字段。UA我是从网上找的最新Chrome版本的UA字符串,实测比默认Python-Requests UA被拦截的概率低很多。
页面解析我经历了两个版本:第一版用BeautifulSoup,写起来很直观,缺点是当页面结构变化时选择器经常失效;第二版改为正则表达式配合BeautifulSoup混合提取,字段正则优先,解析速度更快。这里不推荐用Selenium——它启动浏览器实例太重了,而且58同城这类站点没有要求动态渲染才能加载数据,纯HTTP请求足够。
频率控制是个关键点。我设了请求间隔2至3秒随机化,单次采集控制在300条以内,全程大概10分钟。即使这样,中间也可能触发临时封禁,这就需要加一个异常处理——当响应状态码不是200或者页面出现验证码特征时,程序暂停5分钟再继续。
爬虫代码核心逻辑大概是这样:
python复制import requests
import time
import random
from bs4 import BeautifulSoup
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://xxx.58.com/",
"Accept-Language": "zh-CN,zh;q=0.9"
}
def fetch_list_page(city_code, page_num):
url = f"https://{city_code}.58.com/zufang/pn{page_num}/"
resp = requests.get(url, headers=headers, timeout=10)
resp.encoding = "utf-8"
return resp.text
def parse_house_list(html):
soup = BeautifulSoup(html, "html.parser")
items = soup.select("div.house-list li, ul.house-list li")
results = []
for item in items:
# 按实际HTML结构调整选择器
title_tag = item.select_one("h2 a")
if not title_tag:
continue
title = title_tag.get_text(strip=True)
link = title_tag.get("href", "")
# 其他字段的提取同样以选择器为主
results.append({
"title": title,
"link": link,
# 这里继续提取租金、面积、户型、区县等字段
})
return results
这里有个细节:不同城市的58同城页面结构可能略有差异,class名不一样。上线前先人工打开一个城市的租房列表页,F12审查元素确认选择器无误,再开写代码。我第一版爬虫写完后运行,发现一条数据都没解析到,最后检查发现是HTML的class名里有个空格导致选择器写错。
3.2 Pandas数据清洗与指标聚合:从字符串到可用数值
清洗这一步的重要性怎么强调都不为过。举个实际例子:爬虫拿到的"租金"字段长这样:"押一付三 3500元/月","整租·宜居苑 2室1厅 65平"。你直接把原始字符串入库,后面做任何数值统计分析都是白搭。
清洗流程分四步:
- 从原始字符串中正则提取核心数字
python复制import re
def extract_rent(text):
match = re.search(r"(\d+)元/月", text)
return int(match.group(1)) if match else None
def extract_area(text):
match = re.search(r"(\d+(?:\.\d+)?)\s*[㎡平米]", text)
return float(match.group(1)) if match else None
-
类型统一。所有租金额度转为int,面积转为float。这里踩过一个坑:有的房源写"65平",有的写"65㎡",如果正则只匹配一种写法就丢了数据。
-
空值和异常值处理。租金明显的异常值(比如1元/月)需要剔除,面积小于10平或大于500平的也基本不合理。我直接用了Pandas的条件过滤。
-
新增计算字段。租金和面积有了之后,就能算"每平米月租金"(租金除以面积),这是分析城市租房成本最有价值的指标之一。
聚合查询我会在Django ORM里直接做,或者把数据导入Pandas后groupby。比如算各城区平均租金:
python复制import pandas as pd
df = pd.read_sql("SELECT district, rent, area FROM house_info", conn)
avg_rent_by_district = df.groupby("district")["rent"].agg(["mean", "count", "median"])
这样做的好处是,一旦你有了Pandas的DataFrame,后续生成ECharts需要的JSON格式就很简单了——转成列表字典,再json.dumps输出。
3.3 Django接口层设计:前后端数据桥接的三种方式
Django这套系统我前后迭代了三种接口方案,从最原始的模板渲染到完全分离的API模式,其中的取舍值得说道。
方案一:Django模板直接渲染图表。用Django的模板语言把data传到Html里,然后ECharts用这些数据绘图。优点是简单,一个服务端全搞定;缺点是每次切换分析维度都要刷新页面,交互体验一般,而且模板里混着一堆数据渲染逻辑,代码不干净。
方案二:Django REST Framework提供JSON API。后端只负责根据参数返回聚合好的JSON,前端页面用Ajax拉数据,ECharts在浏览器端渲染。这个方案交互流畅,前后端职责分离,也方便答辩时单独演示"接口层"的成果。缺点是会多写不少序列化代码和API路由。
方案三:Django + 轻量JSON视图。介于前两者之间,不引入DRF,直接用Django自带的JsonResponse,手动拼接JSON结构。
我实际采用的是方案三和方案二的混合:核心大屏页面用DRF风格API,便于展示;工具类小页面用普通JsonResponse。你如果时间紧,纯方案三也够用。
一个关键的接口设计思路是让"数据分析维度"变成可配置的URL参数。比如:
/api/rent/avg?city=beijing&group=district返回各城区平均租金/api/rent/distribution?city=beijing&bins=0-1500,1500-2500,2500-4000,4000+返回租金区间分布/api/rent/trend?city=beijing&months=6返回近6个月租金走势
接口参数化最大的好处是,前端上调整图表维度时不需要改后端代码,答辩演示的时候可以现场演示"换个参数,图表就变了",效果非常加分。
3.4 可视化大屏实现:ECharts五种常用图表的对接要点
可视化层我选择ECharts,理由很直接:文档全、中文社区活跃、图表类型丰富、配置项灵活。以下五种图表是这个项目的高频使用图表,每一个都有明确的适用场景。
第一个是地图热力图。用ECharts的map系列,需要引入对应城市的地图GeoJSON数据。在配置文件里通过echarts.registerMap("beijing", beijingGeoJson)注册。热力图颜色渐变从浅黄到深红,代表租金从低到高。一个容易踩的坑是GeoJSON的区县名称必须和你的数据district字段完全一致,比如数据里写"朝阳区",GeoJSON里也得是"朝阳区",大小写和空格都不能差,否则无法匹配。
第二个是柱状图,展示各城区平均租金对比。关键配置是label显示数值、颜色渐变、横轴文字旋转——如果区县名太长会重叠,设axisLabel.rotate = 30。
第三个是饼图,展示户型分布(1室、2室、3室、4室及以上占比)。需要注意在数据清洗时把户型归类,不要保留"2室1厅""2室2厅"这种粒度太细的分类,统一映射为"2室"。
第四个是散点图或气泡图,展示面积与租金的关系。每个点是某个房源,横轴面积、纵轴租金,一眼能看出租房市场里面积越大单价越低的基本趋势。
第五个是折线图,展示租金随时间的变化趋势。这里依赖数据库里的create_time字段,按月份聚合平均租金,前提是爬虫数据采集覆盖了多个月份,否则折线图没有实际意义。
大屏布局我个人常用的是Grid布局,分成左上、右上、中间、左下、右下几个区块。顶部放标题和关键数字指标(总房源数、平均租金、最高租金、房源最多城区),中间放大面积的地图热力图,两侧分布其他图表。
4. 开发中遇到的高频问题:排查链路与解决方案
任何项目做完回头看,真正的成长都来自报错和修bug的过程。这里整理几个我遇到最多的、也是很多人问过的问题,帮大家提前排雷。
4.1 爬虫数据量不足或全空的排查链路
这是爬虫类项目最高频的问题,现象是运行后数据库里没有数据,或者只抓到几条。
我第一次遇到这个问题时,用的排查链路是这样的:先用浏览器手动打开目标URL,确认页面可以正常访问;然后用requests.get请求同样的URL,打印返回的response.status_code——发现返回200但页面里根本没有房源列表(实际是跳转到了验证页)。问题定位出来:缺少Referer头,或者User-Agent被识别。
如果确认请求头没问题但依然解析不到,第二步需要检查解析逻辑。我会打印HTML片段,看实际页面里选择的class名和代码里是否一致。58同城页面有一次改版把li的class从house-cell换成了house-cell-wrapper,代码里如果还按旧的来就全空了。
第三步是检查入库逻辑。有时候爬虫正常、清洗正常,但ORM写入时因为唯一键冲突导致事务回滚,数据没落库。我的处理方式是捕获每批插入的异常并打印具体报错,而不是让整个爬虫崩溃。
最后一层是数据量太少的问题。这种情况基本是采集页数不够。我的爬虫代码里设置了最大页数参数,但如果58页面列表就十几页,抓下来可能两三百条,也够分析用了。此时可以放宽城市数量多采集几个城市。
4.2 数据库中区县名不统一导致地图无显示的排查
这个问题的特征是接口返回数据正常,但地图热力图区域一片空白。原因基本是行政区划名称对不上。
58页面里"朝阳""朝阳区""朝阳区(望京)"这三种写法都可能出现。如果清洗阶段没有归一化到标准区县名,地图匹配就会失败。我的解决办法是在Pandas清洗时加一个映射函数:
python复制district_map = {
"朝阳": "朝阳区",
"朝阳区(望京)": "朝阳区",
"朝青": "朝阳区",
# 其他区域同理
}
def normalize_district(name):
for key, value in district_map.items():
if key in name:
return value
return name # 未知的保留原样,便于排查
然后数据入库前统一走normalize_district。这样地图才能正确匹配。还有一个细节:有些城市的地图GeoJSON里市区区划郊区名称未覆盖,比如"房山"和"房山区"是同一个区,但在JSON里只能有一种写法,需要去GeoJSON文件里对照。
4.3 可视化大屏白屏或图表不渲染的排查
大屏空白的原因通常不在后端,而在前端JS的某个环节。最典型的问题有三个。
第一个是ECharts初始化的DOM元素尚未就绪。JS代码如果放在</body>之前,DOM是加载完的,但如果放在<head>里并通过window.onload触发,就很容易出现容器高度为0。我的做法是给大屏容器设置固定高度或视口高度百分比,并确保init在DOMContentLoaded之后执行。
第二个是数据格式不匹配。ECharts的data字段是一个数组,有人会直接从接口拿到{data: [...]}然后整个塞进series.data,导致图表无法渲染。排查方式是console.log打印接口数据,检查格式是否是[{name: "朝阳区", value: 6500}, ...]。
第三个是GeoJSON加载失败。地图系列如果地图未注册或异步加载路径不对,控制台会报Map beijing not exists。解决方案是确认echarts.registerMap在setOption之前执行,注册的数据必须是对象,而不是字符串。异步加载的话需要确保回调时机。
javascript复制fetch("/static/geo/beijing.json")
.then(res => res.json())
.then(geoJson => {
echarts.registerMap("beijing", geoJson);
// 在拿到地理数据之后再初始化图表,避免地图引用不存在
renderMapChart();
});
还有一个性能问题值得注意:大屏上同屏渲染多张地图和图表,如果数据量大,初次渲染会有卡顿。解决思路是把初始数据拆成按需加载,先在页面加载时只渲染上一屏的内容,其他图表等用户切换或视图可见时再初始化。不过对毕设来说,数据量基本在千级以内,性能优化不用做得很激进,但答辩时如果被问到"数据量大怎么办",能说出按需加载和节流渲染的思路,就是加分项。
5. 部署、测试与答辩准备:让系统在关键时刻不出岔子
开发完了还要过部署和演示关。很多同学代码写得没问题,一到答辩现场就翻车,原因基本都是环境问题。
5.1 本地环境搭建与依赖管理
我一向建议毕设项目用虚拟环境隔离依赖。在项目根目录创建虚拟环境后,pip install django requests pandas beautifulsoup4。requirements.txt记得提前导出,不要等到答辩前才换电脑跑环境——在别人电脑上装依赖踩坑是常事。
Django的静态文件处理也在本地演示时坑过一次。ECharts的JS文件放在static目录下,DEBUG=False时静态文件可能找不到。项目里记得设置STATIC_ROOT,并在发布前执行:
bash复制python manage.py collectstatic
如果懒得搞复杂部署,答辩演示时保持DEBUG=True是最省事的方案,功能正常优先。
数据库初始化也是个高频踩坑点。新环境拉下来代码,忘了python manage.py makemigrations和python manage.py migrate,启动直接报表不存在。我会建议把这两条命令写进项目的README里,作为一个新手也能照做的启动流程。
5.2 功能自测清单与数据一致性验证
答辩前花半天做一次完整的自测,比什么都重要。我列了一个测试清单,按模块来:
- 爬虫模块:重新运行一次爬虫,确认能有增量数据入库,重复运行不产生重复记录
- 数据接口:逐个访问API,确认返回JSON结构正常,字段名与前端代码一致
- 可视化模块:把每张图表的数据源切换成"实时从接口拉取",确认图表渲染无误
- 地图模块:确保障碍最多的地图热力图能正常显示,且数据匹配
- 异常场景:断网时访问页面,确认有友好提示而不是白屏
数据一致性验证有个常见问题:爬虫爬到的数据经过清洗后,与页面上展示的原始内容对不上,比如租金从"3500元/月"变成数值3500,答辩时最好提前准备几张"原始数据展示页"截图,说明清洗前的样子和清洗后的效果,这是一个很好的论文素材。
5.3 答辩讲解的有序组织与常见提问应对
答辩时讲系统,按照数据流的方向讲是最顺的:先讲数据来源(爬虫如何采集、如何绕开反爬限制)、再讲数据清洗(如何把脏数据变成可用数据)、然后讲系统后端(Django怎么组织接口)、最后演示可视化大屏。这个顺序遵循了数据产生到消费的链路,逻辑性强,评审也容易跟。
提前准备几个针对技术难点的回答。问到"你爬虫有没有被封过",如实讲遇到过但通过限速和请求头伪装解决即可。问到"数据分析得出哪些结论",准备几条有洞察的结论,例如:
- "朝阳区平均租金明显高于其他城区,主要因为核心商圈集中"
- "面积与每平米单价存在负相关,小户型每平米租金更高"
- "近几个月租金整体平稳,个别区域有小幅上涨趋势"
这些结论能体现你真的分析了数据,而不是只做了一张图表摆在那里。
注意:答辩时不建议过度依赖线上演示。如果现场网络状况不佳,可视化大屏的图表数据要准备一份本地Mock数据作为兜底。把大屏页面做成"优先请求本地数据,失败则加载内置示例数据"的模式,能有效避免现场事故。
6. 从毕设到项目经验的进阶玩法与扩展方向
最后聊点超纲的内容。如果你时间有富余,或者答辩完还想继续迭代,这系统有几个很好的扩展方向,每一项都能让项目含金量更上一层。
把数据源拓宽到多个平台是思路之一。除了58同城,还可以采集自如、贝壳这类平台的公开信息,做跨平台的租金对比。每个平台都有各自的解析难点,但这也意味着你简历上可以写"多源异构数据采集与整合"。
加入时间序列分析的深度是另一个方向。目前的大屏主要展示静态分布,如果数据积累到一定量级,可以加入"租金走势预测"。用简单的线性回归或ARIMA模型做未来3个月的租金预测,前端折线图多画一条预测线,这个"数据分析+预测"的组合写在简历上比单纯的图表展示要高级不少。
还有一个方向是引入Docker容器化部署。把Django应用、MySQL、爬虫脚本分别封装到容器里,通过docker-compose启动。这个能力在找工作时基本是标配了,在毕设项目里提前练一遍,答辩时提一嘴,很能体现工程化意识。
我个人经验是,这类数据分析可视化项目最值得投入精力的地方不在代码本身,而在于"数据清洗的严谨程度"和"图表表达的准确性"。很多学生的系统看起来颜色花哨,但仔细看全是无效信息——地图热力图上各个区的颜色差别不大,因为数据没有做规范化处理(比如用最大值最小值做归一化),或者柱状图的Y轴起始值设置不合理,导致差异被放大。做可视化第一原则是尊重数据,不要为了视觉效果去欺骗观者。
这套系统从写第一版到最终稳定运行,大概花了三周时间。其中爬虫占一周,清洗占两三天,Django接口占三天,大屏设计占三四天,剩下一周在处理各种边角问题。时间节奏给大家做个参考,如果你按这个节奏走,一般不会陷入"写不完"的焦虑。
另外源码和文档建议用Git管理,每次大修改提交一次,理由很简单:答辩前你一定会改回旧版,没有版本控制就只能在懊悔中度过。
