资讯动态

Python租房市场数据可视化平台:Django+爬虫+ECharts实战全解析

发布时间:2026/10/9 5:59:17 来源:尧图企业网站定制
每年毕业季“计算机毕业设计选什么题”都是个绕不开的坎。选纯算法怕数学底子撑不住选管理信息系统又觉得没什么亮点。如果让我给一个稳妥又能出效果的建议我会毫不犹豫推荐“Python租房市场数据可视化平台”——它把Django框架、Requests爬虫、数据分析、可视化串成一条完整链路既是能动手的工程又能产出视觉效果不错的展示大屏更重要的是答辩时有完整的故事可讲数据从哪来、怎么处理、怎么分析、有什么结论。这篇文章就基于我实际完成这个项目的经验把从选题、爬虫、数据建模、分析到可视化上线的全过程拆开讲清楚包括踩过的坑和最终的解决办法希望能给正在做或者准备做类似毕设的同学省点时间。1. 项目整体设计与思路拆解1.1 为什么选“租房数据可视化”毕业设计选题的性价比分析先说选题逻辑。毕设最怕的不是工作量不够而是“做完了但讲不出东西”。租房市场数据可视化这个题目天然具备三个优势数据好拿。租房信息是公开数据房源网站上有大量的挂牌数据字段丰富小区、户型、面积、租金、朝向、楼层结构也比较规整比爬那种需要登录、需要处理复杂验证码的场景省心得多。链路完整。这个题目不是单一技术点而是一条完整的数据流水线Requests爬虫采集 → Pandas清洗 → Django ORM存储 → 统计分析 → ECharts可视化。每一环都能单独拿出来讲答辩时随便问哪个环节你都有实际内容支撑。展示效果好。可视化大屏是毕设答辩的加分项。一张地图上按区域分布展示租金均价旁边配户型占比饼图和价格趋势图老师进来第一眼就大概知道你做了什么不用你费太多口舌解释。当然这个题也有它容易踩的坑比如房源网站的反爬虫策略、数据清洗时的脏数据、ECharts图表在页面加载时的性能问题等。这些我会在第6节专门展开。1.2 技术栈选型这套Python组合拳为什么打得通先亮出整个项目用到的技术栈一览层技术选型承担职责数据采集Python Requests BeautifulSoup获取房源列表页、详情页数据数据存储Django ORMSQLite/MySQL数据表模型定义、读写操作数据处理Pandas清洗、去重、补充计算字段后端框架Django 3.2提供页面渲染和JSON接口前端可视化ECharts 5柱状图、地图、饼图、趋势图数据分析Django聚合查询 Pandas透视表统计各区域均价、户型占比等这个组合的核心思路是用最成熟、资料最多的工具减少试错成本。Django是Python Web领域的老牌框架迁移、数据模型、Admin后台都能直接复用Requests搭配BeautifulSoup是爬虫入门级的经典组合遇到问题随便一搜就有答案ECharts是百度开源的图表库文档全、社区活跃、中文API友好毕业设计用它能省很多时间。我见过有人在这个题目里用Scrapy框架做爬虫也不是不行但如果你是第一次做爬虫Scrapy的异步机制和中间件配置会带来额外的学习成本而Requests写起来更直观调试更简单。毕设的核心目标是“在限定时间内把链路跑通”所以我的建议是爬虫层用Requests起步如果以后想扩展成更大的采集系统再换Scrapy也不迟。这里也要提前强调一下合规问题**爬虫部分只采集公开挂牌数据控制请求频率不碰个人隐私信息并且只用于学习研究绝不用作商业用途。**这个底线一定要守住毕设本身是学习性质学校和老师也认可但你自己心里要有数。2. 数据采集层Requests爬虫的工程化实践2.1 爬虫设计不是“拿到网页就行”而是“能持续稳定拿到”很多人写爬虫时喜欢直接写一个循环挨个请求URL能拿到就完事。但毕业设计稍微会遇到一个尴尬情况数据量太少后面可视化根本做不出趋势。所以爬虫这块要考虑的是“可持续采集”。我在项目里设计了三个关键点请求头伪装。直接裸用默认的User-Agent很容易被服务器识别并拒绝。我的做法是定义一组常见的浏览器UA每次随机取一个来用同时携带Accept、Accept-Language等常规请求头字段。import requests from bs4 import BeautifulSoup import time import random HEADERS_POOL [ { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, }, { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15, Accept-Language: zh-CN,zh;q0.9, }, ] def get_random_headers(): return random.choice(HEADERS_POOL)请求频率控制。这是爬虫能否持续跑下去的关键。我在每两个请求之间加了time.sleep(random.uniform(2, 5))既避免给目标站点造成压力也让自己的采集更稳定。实测下来不加延迟连续请求几十次之后很容易触发风控加了延迟之后能稳定跑完整个采集任务。解析只抽核心字段。页面HTML结构复杂如果你把整张页面都视为数据不仅存储冗余后面清洗也麻烦。我用BeautifulSoup定位到房源列表项只抽取小区名、户型、面积、朝向、楼层、总价月租金、每平米单价、所在区域这些字段。至于发布时间、标签等信息我一开始也爬了但发现很多页面结构不统一清洗成本高最后只在详请页做了补充采集没有大规模展开。下面是我当时列表页采集的简化版代码def parse_list_html(html_text): soup BeautifulSoup(html_text, html.parser) house_list [] items soup.select(.house-list .house-item) # 以目标站点实际选择器为准 for item in items: try: title item.select_one(.title).get_text().strip() detail item.select_one(.detail).get_text().strip().split(|) # 例如 detail 形如 3室1厅 | 89.5平米 | 南 北 | 中楼层 layout detail[0].strip() area float(detail[1].replace(平米, ).strip()) orientation detail[2].strip() floor_info detail[3].strip() if len(detail) 3 else price item.select_one(.price).get_text().replace(元/月, ).strip() rent int(float(price)) district item.select_one(.district).get_text().strip() community title.split( ) [0] if title else house_list.append({ community: community, district: district, layout: layout, area: area, orientation: orientation, floor: floor_info, rent: rent, }) except Exception as e: # 单个条目的解析异常不影响整体跳过并记录 logging.warning(f解析条目失败: {e}) return house_list这里有一个很容易忽略的点解析单个条目必须使用异常隔离。你有可能会遇到一条数据格式异常比如面积字段不是数字、标题为空如果不用try包裹整个循环会直接中断前面爬的所有数据都白费。我的习惯是每一条单独try失败就打日志跳过后续再统一处理脏数据。2.2 多页采集与断点续采不给毕设留“一次性”遗憾租房列表通常分页第1页和第20页的页面结构一般只是页码参数不同。我直接构造页码参数循环采集但此时遇到了一个很现实的问题如果爬到第15页时IP被限制了怎么办总不能每次都从头开始。我用的方案是“断点续采”维护一个采集进度记录文件记录已经爬到的页码下次启动时自动从上次停下的位置继续。不需要引入消息队列或者数据库状态一个简单的JSON文件就够了。import json import os PROGRESS_FILE crawl_progress.json def load_progress(): if os.path.exists(PROGRESS_FILE): with open(PROGRESS_FILE, r, encodingutf-8) as f: return json.load(f) return {completed_pages: []} def save_progress(page): progress load_progress() progress[completed_pages].append(page) with open(PROGRESS_FILE, w, encodingutf-8) as f: json.dump(progress, f, ensure_asciiFalse, indent2)这个思路不大但特别能体现你的工程思维答辩时说出来会加分。3. 数据存储与清洗从裸数据到可用数据3.1 Django ORM建模用最少的表承载完整的分析结构租房数据在毕设层面不需要设计特别复杂的表结构。我最终只建了一张核心房源表外加一张区域统计表。两张表就够了。房源表字段设计如下from django.db import models class House(models.Model): district models.CharField(max_length50, verbose_name所在区域) community models.CharField(max_length100, verbose_name小区名称) layout models.CharField(max_length20, verbose_name户型) area models.FloatField(verbose_name面积(㎡)) rent models.IntegerField(verbose_name月租金(元)) unit_price models.FloatField(verbose_name每平米月租金(元/㎡/月), nullTrue, blankTrue) orientation models.CharField(max_length20, verbose_name朝向) floor models.CharField(max_length30, verbose_name楼层, blankTrue) source_url models.URLField(uniqueTrue, verbose_name来源链接, nullTrue) crawl_time models.DateTimeField(auto_now_addTrue, verbose_name采集时间) class Meta: db_table house verbose_name 房源信息为什么要把unit_price每平米月租金单独抽出来因为租房分析时单纯看总租金不能完全说明问题北京和上海的豪宅和普通住宅的总租金差异大但单位面积租金更能体现区域价值。我选择在导入时直接用rent / area计算好避免每次查询都临时计算减轻接口压力。另外source_url加唯一约束天然用于去重。爬虫重复采集时数据会以Duplicate entry的形式直接报错我在写入逻辑里捕获这个异常并跳过即可不需要额外写一长串去重逻辑。3.2 数据清洗实操哪些脏数据会让你直接翻车租房数据的脏主要体现在这几种情况面积或租金为0或者异常大。比如某房源面积写成0.1平米明显是测试数据如果直接参与统计均价会被拉得很离谱。我的清洗规则是面积低于5平方米或高于300平方米、月租金低于200元或是超过50000元的记录直接剔除。户型格式不统一。有的写“3室1厅”有的写“3房1厅”有的甚至写“三室一厅”。我统一用正则把数字和“室”“房”替换成统一格式。import re import pandas as pd def clean_layout(text): # 统一“房”为“室” text text.replace(房, 室) # 提取数字 室 数字 厅例如 3室1厅 match re.search(r(\d)室[(]?\d*[)]?厅, text) if match: return match.group(0) return text朝向字段有缺失。缺失的我不硬猜直接填空字符串后面分析的时候单独算成一个“未标注”类别即可。清洗整块我是交给Pandas完成的。Django ORM的bulk_create可以批量写数据但清洗还是Pandas更顺手所以我先导出到DataFrame处理再写回。这样分工比较清晰。import pandas as pd from django_pandas.io import read_frame df read_frame(House.objects.all()) df df[(df[area] 5) (df[area] 300)] df df[(df[rent] 200) (df[rent] 50000)] df[unit_price] (df[rent] / df[area]).round(2) df df.dropna(subset[district, rent, area])一个实际的体会**清洗工作在毕设里的占比比你想象要高。**很多同学把时间压在写代码上最后发现辛辛苦苦爬了5000条数据能用得上的只有3000条。清洗这一步最好在采集入库时顺手做不要留到最后统一处理不然数据量大时排查具体某条问题记录会很麻烦。4. 数据分析与可视化让数据真正“讲故事”4.1 分析维度怎么定答辩展示的叙事逻辑数据分析不是把图表堆上去就完了。我的经验是可视化平台的叙事逻辑要能自洽。我当时定的展示主线是总体概况平台上一共有多少条房源、平均租金、平均面积、最高租金区域在哪。区域租金对比哪个区最贵、哪个区性价比高每平米租金维度。户型结构分析一室、两室、三室分别的供应占比和平均租金。价格区间分布大部分房源集中在哪个价位段适合做人群画像描述。面积与租金关系面积越大单价是否一定越低这个点能讲出一点分析深度。这套分析逻辑一方面贴近大众直觉大家都会关心哪个区租房贵另一方面数据都能用最简单的统计得到不需要复杂的机器学习模型。但它又比单纯展示一堆数字有信息量得多因为你能回答“为什么某一个区域均价高”这种追问。4.2 ECharts可视化的实战配置四类核心图表实现可视化部分是整个项目最直观的“脸面”。我选了ECharts原因是它的地图组件在展示区域数据时比Matplotlib好看太多而且交互效果对非技术背景的人冲击力更强。区域均价横向柱状图按均价降序排列一眼看出区域梯度。// 数据由后端接口返回形如 [{ district: 朝阳, avg_rent: 6800 }, ...] function renderBar(domId, data) { var chart echarts.init(document.getElementById(domId)); var districts data.map(item item.district); var rents data.map(item item.avg_rent); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: districts, axisLabel: { rotate: 30 } }, yAxis: { type: value, name: 元/月 }, series: [{ type: bar, data: rents, itemStyle: { color: function(params) { var colors [#c23531, #2f4554, #61a0a8, #d48265, #91c7ae, #749f83]; return colors[params.dataIndex % colors.length]; }} }] }); }区域地图这个最能撑场面。ECharts的地图组件需要先引入地图GeoJSON数据可以在代码里通过AJAX加载然后注册地图再渲染散点或色块。我这里给大家一个思路$.get(https://geo.datav.aliyun.com/areas_v3/bound/110000_full.json, function(geoJson) { echarts.registerMap(beijing, geoJson); var chart echarts.init(document.getElementById(mapChart)); chart.setOption({ tooltip: { formatter: params params.name params.value 元 }, visualMap: { min: 3000, max: 12000, text: [高, 低], inRange: { color: [#d94e5d, #eac736, #50a3ba] } }, series: [{ type: map, map: beijing, data: mapData }] }); });注意一个坑ECharts地图在离线环境可能无法加载GeoJSON资源。我记得当时在学校实验室答辩准备时机房的网络不稳定地图一直空白排查了半天才发现是GeoJSON文件没加载出来。解决办法是把这个JSON文件下载到本地静态目录然后用fetch()请求本地路径。户型占比饼图这个用ECharts的饼图即可不需要特殊配置但要注意把占比太小的类别合并成“其他”不然图例太长不好看。租金价格区间直方图我用binSize1000将租金离散化然后统计各区间数量画成阶梯柱状图。这个图能直观看到“一线城市租房集中在3000—6000元/月”这种结论。4.3 数据接口怎么设计让Django和前端各司其职可视化前端的数据来源我的做法是通过Django提供JSON接口而不是渲染模板时直接塞数据。这样前后端职责分离接口也能复用以后换前端框架不用动后端。我写了一个统一的stats/路由挂在Django的URLConf下from django.urls import path from . import views urlpatterns [ path(api/overview/, views.overview, nameoverview), path(api/district_avg/, views.district_avg, namedistrict_avg), path(api/layout_dist/, views.layout_dist, namelayout_dist), path(api/price_hist/, views.price_hist, nameprice_hist), path(api/area_rent/, views.area_rent, namearea_rent), ]视图部分用Django的聚合查询可以直接出结果例如from django.db.models import Avg, Count from django.http import JsonResponse from houses.models import House def district_avg(request): data (House.objects .values(district) .annotate(avg_rentAvg(rent), cntCount(id)) .order_by(-avg_rent)) return JsonResponse(list(data), safeFalse)这个接口返回的JSON数组可以直接喂给ECharts。接口和图表字段保持一致是这个项目能快速跑起来的关键。我在刚开始做时后端返回的字段名是avg_month_rent前端的变量叫avg_rent结果页面一直报undefined排查了半天才发现是对不上。建议前后端的字段命名从一开始就统一或者至少完成后用console.log打印一遍数据再绑到图表上。5. Django Web层与前后端整体集成5.1 Django目录与项目结构整洁不等于多一层很多教程喜欢建一堆App把功能拆得很细毕设时间紧张的时候确实没有必要。我用的结构是常规的三App架构project/ ├── manage.py ├── config/ # 项目配置settings.py, urls.py ├── houses/ # 房源数据app模型、爬虫脚本、清洗脚本 ├── stats/ # 统计接口app聚合查询 JSON返回 └── dashboard/ # 可视化页面app模板渲染 静态资源这里有一个容易混淆的点Django的App划分不是越多越好。我把爬虫脚本放在houses下面是因为它和房源数据模型强相关方便直接调用ORM读写数据库。如果你单独建一个spiderApp还要在houses和spider之间引模型多一层跳转反而麻烦。静态资源ECharts库、GeoJSON文件、自定义JS/CSS统一放在dashboard/static/下。页面模板用的是Django模板语言但只在入口页渲染图表数据全部走后端接口模板里的代码量很少。5.2 首页大屏布局不用前端框架也能撑起大局首页大屏的布局我用的是CSS Grid Flex把屏幕分成左中右三栏。左侧放户型分布和数据概况中间放区域地图右侧放价格区间直方图和Top10小区列表。这个布局在笔记本上表现良好答辩时投到投影仪上也能自适应拉伸。这里分享一个小技巧大屏作为答辩展示的“开场画面”加载性能要优先保证。如果接口一次性返回所有图表数据数据量大时接口响应会变慢页面卡顿。我当时优化为“页面先渲染框架图表按需加载数据”每个图表的init和数据请求分开触发实测页面初始加载时间从3秒降到了1秒以内。此外还要注意ECharts容器需要有明确高度否则图表绘制时会因为容器高度为0而出现空白。我当时踩过这个坑最后统一给每个图表容器设置了height: 360px;才稳定显示。6. 常见问题与排查技巧实录6.1 爬虫跑到一半被限制怎么办这是爬虫毕设里发生率最高的问题没有之一。我自己的经历是某次采集了大约2000条数据后突然连续出现503错误而且之后的请求全部被重定向到验证码页面。我的处理思路分三步先停再想。先暂停爬虫不要继续硬试。等待几分钟让限制缓解同时检查是不是请求频率设置得太快。我最终把sleep调整为4到6秒随机值稳定很多。降低单轮采集量。把每天采集量控制在几千条以内分多次执行。不要指望一次脚本把所有数据都拉下来数量越大越容易触发风控。检查请求头是否完善。被限制后我回看了日志发现部分请求的Accept-Encoding和Referer字段缺失补全请求头并保持每个请求的“行为轨迹”像真实用户浏览问题出现的概率显著下降。切记不要在毕设阶段去研究所谓“高并发采集”“绕过风控”这些方案那既不是毕设重点也容易触碰合规边界。低频率、小批量、误伤少才是这个项目最稳妥的路线。6.2 中文编码与乱码问题爬虫拿到网页后解析出的中文变成“锟斤拷”是初学者最容易遇到的坑。这个问题的本质是页面返回的编码和代码里声明的编码不一致。排查口诀先看响应头里的Content-Type再试resp.apparent_encoding最后手动指定编码。当时我遇到的站点没有明确声明编码Requests默认用ISO-8859-1解析导致中文乱码。解决方式是在拿到响应后主动指定UTF-8解码resp requests.get(url, headersget_random_headers(), timeout10) resp.encoding utf-8后来我还把PostgreSQL里的中文没有问题的但SQLite在部分旧版本下对中文搜索不友好所以字段尽量都存标准字符串排序时尽量走ORM而不是原生SQL。6.3 图表加载缓慢与数据量增长后的优化当房源数据超过1万条后接口响应时间明显变长尤其是地图散点图。优化方案很直接统计结果不是实时查询而是缓存。我用Django内置的cache框架把统计结果缓存在内存里每天定时刷新一次。from django.core.cache import cache def district_avg(request): cache_key district_avg_data data cache.get(cache_key) if data is None: query (House.objects.values(district) .annotate(avg_rentAvg(rent), cntCount(id)) .order_by(-avg_rent)) data list(query) cache.set(cache_key, data, 86400) return JsonResponse(data, safeFalse)这样处理之后接口响应时间从原来的几百毫秒降到几十毫秒。虽然毕业设计的数据量远远到不了“大数据”但能体现你的性能优化意识答辩时也是一个可聊的加分点。6.4 常见异常速查表现象可能原因解决思路爬虫返回503请求频率过高或UA被识别降低请求频率更换UA池中文乱码解码编码不对主动指定resp.encoding utf-8单条解析报错中断页面结构变化或脏数据每条独立try记录日志跳过bulk_create报重复source_url唯一约束捕获 IntegrityError跳过重复项地图空白GeoJSON文件加载失败将GeoJSON下载到本地通过本地路径引入图表不显示容器高度为0给图表容器设置固定高度接口返回慢统计查询无缓存使用Django cache缓存统计结果写在最后毕设做完我的一点体会这套系统做下来我最深的体会是毕设项目的价值不在于用了多前沿的技术而在于你有没有把一个环节做到位、讲清楚。“Python租房市场数据可视化平台”这个题目每个技术点单看都不难难的是把它们串起来。爬虫采集要稳定数据清洗要严谨ORM模型要多想一步接口字段要前后对齐图表布局要为一个坐标轴的显示反复调样式。当你把这些问题一个个解决下来答辩时你能聊的内容绝对不止是“我做了个网站”而是“我怎么把一个需求从数据源头打通到最终展示”。如果要在这个基础上继续扩展我建议可以从三个方向选一个一是引入大模型的API做一个租房问答助手用户直接问“哪个区域性价比高”系统自动调取图表数据生成回答二是把统计结果做成定时任务每天自动增量采集并更新大屏三是把Django换成前后端分离架构用Vue或React重写展示页面。这几个方向都顺着现有项目的根往上生长很自然。最后分享一个非常细节的坑项目答辩前一定把完整的项目跑一遍至少模拟一次从爬虫到页面展示的全流程。我那时候以为一切就绪结果答辩前一天发现数据库里居然全是历史遗留的旧缓存页面显示的和最新采集的数据对不上差点在台上翻车。后来我清理了缓存并重建统计才算顺利过关。这种问题提前踩比现场踩好得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑