ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的租房数据采集、分析与可视化看板实战解析

基于Python的租房数据采集、分析与可视化看板实战解析 先说这个项目本身。一个“基于Python实现的租房数据分析和展示系统”本质上就是把网上的房源信息抓下来清洗成干净的结构化数据再从价格、区域、户型、面积这些维度去拆解市场最后把结论放到一个网页看板上让租客、中介或者房东不用自己翻几百页列表就能看懂行情。做这套东西技术上不复杂但真正花时间的不是写代码而是想清楚“数据怎么来、怎么洗干净、怎么算才有意义、怎么展示才有人愿意看”。这篇文章我按自己做这个项目的顺序把从采集到展示的完整链路拆开讲一遍里面包括我踩过的坑、调过的参数、以及最后选择这种方案而不是那种方案的原因。适合正在学Python数据分析的人、想做一个完整实战项目来练手的人也适合真的在做租房决策、想看一下自己所在城市租金结构的朋友。1. 项目整体设计与思路拆解1.1 一个租房数据系统到底在解决什么问题把“租房数据分析和展示系统”这句话拆开看它其实有三个主语数据、分析、展示。数据对应采集和存储分析对应指标和洞察展示对应可视化和交互。大多数入门项目只做了其中一两个环节比如爬完数据导成Excel就结束或者用现成的BI工具拉个图表就完事。但这个项目有意思的地方在于它要求你把整条链路串起来——数据从网页到数据库、从数据库到DataFrame、从DataFrame到图表、再从图表到用户界面。我在设计初期列过几个核心问题这些问题是整个项目的“需求说明书”租客想知道某个区域的平均租金是多少同样预算在哪些地段能租到更大的面积。房东或中介想知道不同户型的租金分布是否合理自己的挂牌价处于市场什么位置。系统本身要解决数据从哪来、多久更新一次、以什么形式呈现给不写代码的人。第一个版本我犯了一个典型错误就是先把技术栈定了再说需求。后来发现正确顺序应该反过来先定用户、再定指标、最后定工具。比如租客最关心的是“单位租金”也就是每平米每月多少钱而不是总价。因为总价是面积和单价叠加的结果同样5000块在市区可能只能租40平在郊区能租90平直接比总价没有意义。定了这个指标之后你才知道清洗数据时面积字段不能丢、租金字段不能丢、区域字段不能丢。1.2 技术选型为什么整套都用Python整套系统从爬虫到后端展示都用Python这不是情怀是合理性。租房数据量级通常在几千到几万条没到需要分布式处理的程度分析维度就是区域、户型、面积、价格这几个Pandas绰绰有余展示阶段要的是一个轻量级看板接口FastAPI或Flask都能扛住。所以选Python不是因为“流行”而是因为在这个数据规模下它是成本最低、交付最快的路线。具体技术栈我列一下后面章节会逐个讲环节工具选择理由数据采集Requests BeautifulSoup轻量、可控比Scrapy更适合中小规模采集数据存储SQLite单文件、零配置几千条数据完全够用数据清洗与分析Pandas NumPy表格操作和统计计算的主要阵地可视化Matplotlib Pyecharts静态图用于报告交互图用于网页看板后端接口Flask轻量、写接口快和前端解耦前端展示ECharts HTML图表交互能力强浏览器开箱即用有一个常见的选型纠结是“用Scrapy还是Requests”。Scrapy是爬虫框架自带并发、去重、中间件听起来很强大但对于租房这种目标站点固定、采集频率不高每天一次够用的场景它的学习成本和配置成本是不划算的。RequestBeautifulSoup加一个time.sleep()就是最朴素的稳定方案。等以后数据量上来、要分布式采集了再迁移到Scrapy也不迟。另一个纠结是“要不要用Django”。我的看法是纯做数据展示APIFlask比Django合适。Django自带Admin后台、ORM、迁移工具这些对内容型网站是优势但对一个只需要三五个JSON接口的看板系统来说太重了。Flask写路由和返回JSON非常直接几十行代码就能跑起来。2. 数据采集爬虫设计、字段处理与反爬对策2.1 字段设计你要的不只是“价格”和“面积”采集之前先设计字段表。这个环节容易被忽略但它的重要性远超写爬虫本身。字段设计得不好后面清洗和分析会非常痛苦。我第一版只存了标题、价格、链接三个字段结果做分析时发现没有面积、没有区域、没有户型整个数据几乎是废的只能重新爬。后来重新设计了这张表字段名示例说明title整租·云锦花园 2室1厅 精装修原始标题用于回溯district朝阳区行政区后续按区聚合biz_circle望京商圈/板块比行政区粒度更细layout2室1厅户型需要从标题中解析area89面积单位平方米price6500租金单位元/月unit_price73.03单位租金计算字段floor低楼层/共18层楼层信息toward南朝向publish_time2025-05-14挂牌时间用于分析时效source_urlhttps://...来源链接用于去重和溯源这里关键不是字段多而是每个字段必须有明确的“用途”。district和biz_circle是为了做区域对比area和price是为了算unit_pricelayout能拆出来是因为不同户型的租金差异很大一居和四居不能混在一起看均价。想清楚这些你才知道哪些字段是必须要的哪些可以从标题里解析不要无脑爬一堆用不上的信息。2.2 爬虫实现与反爬策略Requests BeautifulSoup的基本实现不复杂核心步骤是构造请求头、发请求、解析DOM、提取字段。下面是核心代码我会把每个关键点在代码后面解释。import requests import time import sqlite3 from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9 } def fetch_page(url, retry3): for attempt in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: resp.encoding utf-8 return resp.text except Exception as e: print(f[请求失败] {url}, 第{attempt 1}次, 错误: {e}) time.sleep(2) # 失败后等待再重试 return None def parse_listing(html): soup BeautifulSoup(html, html.parser) items [] for card in soup.select(.content__list--item): title_tag card.select_one(.content__list--item--title) price_tag card.select_one(.content__list--item-price) des_tag card.select_one(.content__list--item--des) if not title_tag or not price_tag: continue title title_tag.get_text().strip() price int(price_tag.get_text().replace(元/月, ).strip()) # des 字段形如2室1厅 | 89.00平米 | 南 | 整租 raw_des des_tag.get_text().strip() if des_tag else layout, area, toward parse_des(raw_des) # 自定义解析 items.append({ title: title, price: price, layout: layout, area: area, toward: toward, source_url: title_tag.get(href) }) return items有几个细节单独说。第一个是resp.encoding utf-8不主动设置的话Requests有时候会按响应头里的charset来解码一旦header没写或者写错你拿到的就是一堆乱码。第二个是retry逻辑网络请求不可能永远成功加上重试机制能显著提高稳定性实测下来重试3次、间隔2秒成功率从90%左右提到了接近100%。第三个是CSS选择器的定位建议先在浏览器F12里确认目标字段的DOM结构再写选择器比靠猜快得多。反爬方面做的事情按优先级排列设置合理的请求间隔。我控制在2到4秒之间随机太快会被封IP太慢又拖采集时间。几千条数据按这个速度大概一小时左右跑完完全可接受。维护一个User-Agent池每次请求随机取一个。虽然网站在普通情况下不一定会检测这个但这是成本最低的伪装手段。如果一个IP频繁被限制就需要考虑代理换IP。这里重点是必须选择稳定合规的服务不能碰非法采集和违反平台规则的工具。采集频率过高或全站高频扫描都是不可取的做数据分析爬虫要遵守目标网站的robots协议和法律法规。我自己的做法是只采集公开列表页和详情页信息不对任何一个网站施加压力数据用途也仅限于学习研究绝不用于商业竞争。2.3 数据存储为什么先用SQLite存储上我用SQLite而不是直接存CSV原因是SQLite天然支持去重、增量更新和查询。CSV每次都要全量加载数据一多就慢而且多个字段用列表形式存下来的话还原也麻烦。SQLite单文件部署Python标准库自带sqlite3模块不需要额外装服务对几千条数据来说性能毫无压力。建表语句和插入逻辑大概是这样的CREATE TABLE IF NOT EXISTS rentals ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, district TEXT, biz_circle TEXT, layout TEXT, area REAL, price INTEGER, unit_price REAL, floor TEXT, toward TEXT, publish_time TEXT, source_url TEXT, crawl_date TEXT, UNIQUE(source_url) );加UNIQUE(source_url)是去重的关键。每次采集前先查一下URL是否已存在存在就跳过这样增量更新就很方便。等数据量到了几十万条SQLite的单文件写入可能成为瓶颈那时候再迁移MySQL或者PostgreSQL也不迟Pandas的read_sql对两者都能平滑切换。3. 数据清洗与特征工程3.1 脏数据到底长什么样很多教程会在“数据清洗”章节放一堆漂亮代码但很少告诉你实际中的脏数据有多离谱。我爬下来的数据里最常见的问题有这么几类户型字段混乱“2室1厅”“两室一厅”“2居室”“3房2厅”混在一起规则不统一。面积带单位“89平米”“89㎡”“89.00平”“89平”各种写法都有。租金格式不同“6500元/月”“6500元 (押一付三)”“半年付3800/月”这种带支付条件的要单独处理。区域数据缺失部分房源没有标注行政区只能从详情页或者标题里再提取。重复数据同一套房被房东挂了两家中介URL不同但标题和价格完全一样。3.2 从文本到结构化数据清洗的核心是“把人的表达变成机器可计算的数字”。我用Pandas做这一步整体思路是先转换类型、再拆分文本、最后做去重和缺失值处理。户型解析的逻辑是先用正则把“数字室/房数字厅/居”这种模式抓出来再统一映射成“X室Y厅”的标准格式。比如import re def parse_layout(text): 把 2室1厅、两室一厅、2居室 统一成 2室1厅 if not isinstance(text, str): return None num_map {一: 1, 两: 2, 二: 2, 三: 3, 四: 4, 五: 5} # 先查阿拉伯数字模式 pattern_digit re.search(r(\d)室?\s*(\d)?厅?, text) if pattern_digit: room int(pattern_digit.group(1)) hall int(pattern_digit.group(2)) if pattern_digit.group(2) else 0 return f{room}室{hall}厅 # 再查中文数字模式 for char, num in num_map.items(): if char in text: hall 1 if 厅 in text else 0 return f{num}室{hall}厅 return None面积和单价的处理就直白了先把字符串里的数字提取出来再转成float。遇到缺失面积时我会看看同一小区其他房源的面积中位数能不能补上如果整个小区都没有数据就宁可放弃这条记录也不能让它进入分析环节因为一个错误的面积会污染单位租金这个核心指标的准确性。3.3 构建核心指标单位租金与性价比清洗完成之后最关键的指标是unit_price price / area也就是每平米月租金。这个指标的意义在于消掉了面积的影响让不同面积大小的房子之间有可比性。举个例子房源面积总价单位租金A40平4000元100元/平/月B80平6000元75元/平/月单看总价A便宜2000块但按单位租金算B才是真正的性价比之王。这个指标做完后面所有的区域对比、户型对比才有意义。除了单位租金我还计算了一个简单的“性价比分位”字段。逻辑是在同一区域内把单位租金从低到高排序按照30%和70%分位切成三个档位——低于30%的标记为“低于市场价”70%以上的标记为“高于市场价”中间是“市场价”。这个字段展示的时候很有用租客一看到自己看的房子属于高价位区域就会重新考虑预算。不要小看这种基础统计转换。一个简单的分位数比任何复杂建模都直观而且用户能理解、能解释。4. 数据可视化的维度选择与实现4.1 哪些图表真正有信息量可视化最怕的是为了画图而画图。我在这个项目里选图表的标准只有一条这个图能不能直接回答一个具体问题。我最终确定了六个维度的图表对应六类问题图表问题类型租金价格直方图当前市场的主流租金区间是多少直方图区域均价柱状图哪些区租得贵差距有多大柱状图单位租金箱线图同一个区内的价格波动大吗箱线图面积-价格散点图面积和总价是线性关系吗散点图户型成交占比饼图哪个户型供应量最大饼图/环形图时间趋势折线图近三个月租金在涨还是跌折线图这六个图能覆盖租客和房东的大部分决策场景。区域均价回答“选哪个区”箱线图回答“这个区里会不会捡漏”直方图回答“我的预算能覆盖多少选择”趋势图回答“现在下手还是再等等”。4.2 Pyecharts交互图表与页面联动Matplotlib适合出静态报告但网页看板上的交互图我用的是Pyecharts。它用的是ECharts的渲染引擎生成的HTML可以直接嵌入网页而且支持鼠标悬浮查看数值、缩放区域这对用户理解数据是有质变帮助的——静态图只能看到趋势交互图能让用户自己探索数据。核心绘图代码大致是这样from pyecharts.charts import Bar from pyecharts import options as opts def create_district_avg_bar(df, top_k10): 各区域单位租金均值TOP10柱状图 grouped df.groupby(district)[unit_price].median().sort_values(ascendingFalse) top_data grouped.head(top_k) bar ( Bar() .add_xaxis(top_data.index.tolist()) .add_yaxis(单位租金(元/㎡/月), top_data.round(1).tolist()) .set_global_opts( title_optsopts.TitleOpts(title各区域单位租金中位数 TOP10), yaxis_optsopts.AxisOpts(name元/㎡/月), ) ) return bar细节上有两点值得说。第一聚合时我用的是中位数而不是均值。原因是租金分布是典型的右偏分布几套豪宅能把均值拉得很高中位数更贴近“大部分房子租多少钱”。第二柱状图按中位数降序排列这样用户一眼就能看出最高和最低的区域不需要自己再排序。所有图表的title里我都写明“单位租金”而不是“租金”就是为了避免和总价混淆。4.3 将分析流程封装成可复用脚本分析做完一遍之后我开始考虑“数据更新了怎么办”。如果每次更新数据都要重新执行一遍笔记本那这个系统就没有落地价值。所以我把整个分析流程封装成了一个模块输入是SQLite数据库路径和日期范围输出是更新过的汇总指标表和分析图表。def run_analysis(db_path): 一键执行全部分析返回汇总指标和图表HTML路径 df load_data(db_path) df clean_data(df) summary calc_summary(df) charts { price_hist: create_price_hist(df), district_avg: create_district_avg_bar(df), unit_price_box: create_unit_price_box(df), scatter: create_scatter(df), layout_ratio: create_layout_ratio(df), trend: create_price_trend(df), } for name, chart in charts.items(): chart.render(f./output/charts/{name}.html) return summary这个脚本的好处在哪数据采集完成之后跑一次就会自动输出所有图表。你不需要记住每个分析调用什么函数也不需要担心哪次忘算了一个指标。配合系统定时任务它还能在每天固定时间自动跑一遍把最新的行情更新到看板上。我一直觉得一个数据分析项目的最终形态应该是一个“定时产出结果”的工具而不是一个“手动执行的分析报告”。后续还可以在这个基础上做同比环比把“这个月比上个月涨了还是跌了”自动算出来。5. 展示系统设计从数据到可用看板5.1 后端接口设计展示系统我分了两层后端只负责提供JSON数据接口前端负责渲染图表。这个分层的好处是以后想换一个前端框架或者做一个手机App端后端完全不用动。后端用Flask实现了三个核心接口from flask import Flask, jsonify, request import sqlite3 import pandas as pd app Flask(__name__) DB_PATH ./rentals.db def load_data(): conn sqlite3.connect(DB_PATH) df pd.read_sql(SELECT * FROM rentals, conn) conn.close() return df app.route(/api/summary, methods[GET]) def api_summary(): df load_data() summary { total: int(len(df)), avg_price: float(df[price].mean()), median_unit_price: float(df[unit_price].median()), top_district: df.groupby(district)[price].mean().idxmax(), } return jsonify(summary) app.route(/api/district, methods[GET]) def api_district(): df load_data() result ( df.groupby(district)[unit_price] .median() .sort_values(ascendingFalse) .round(1) .to_dict() ) return jsonify(result) app.route(/api/trend, methods[GET]) def api_trend(): df load_data() monthly ( df.groupby(df[crawl_date].str[:7])[unit_price] .median() .round(1) .to_dict() ) return jsonify(monthly) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)接口设计有一个经验后端尽量把聚合好的结果返回给前端而不是返回原始列表让前端自己算。比如/api/district直接返回的是每个区的单位租金中位数这样前端拿到就能直接绘图不需要在前端做任何数据统计分析。前端的任务就是画图后端的任务是算数职责清晰才不会乱。5.2 前端看板实现与布局前端部分我用了原生HTML ECharts没有引入Vue或React。原因很简单看板不需要复杂的组件状态管理一个静态页面加几个请求就能完成引入框架反而增加维护成本。看板采用经典的上中下布局顶部四个概览指标卡房源总数、平均租金、单位租金中位数、租金最高区域。用户不需要看图就能快速得到结论。中部三个主要图表。左侧是区域均价柱状图中间是价格分布直方图右侧是户型占比环形图。底部详情数据表格支持搜索和排序方便用户逐条核对原始数据。ECharts在页面里初始化很简单async function loadDistrictChart() { const resp await fetch(/api/district); const data await resp.json(); const chart echarts.init(document.getElementById(districtChart)); chart.setOption({ xAxis: { type: category, data: Object.keys(data) }, yAxis: { type: value, name: 元/㎡/月 }, series: [{ type: bar, data: Object.values(data) }], tooltip: { trigger: axis } }); }这里面有一个性能细节图表容器必须在页面加载完成后再初始化否则拿不到宽度会渲染成默认的600px。我用window.onload来保证。另外ECharts的橙色和蓝色默认主题已经够好看不需要花太多时间美化配色把精力放在“数据能不能讲清楚”上更重要。5.3 部署与性能优化部署方面这套系统我建议先本地跑通再加服务器。Flask默认的app.run()不适合生产环境但分量级下配合waitress或gunicorn就足够了。我自己的部署方式是后端用waitress跑在5000端口前端静态文件由另一个静态服务或Flask的static_folder托管数据每日由定时任务更新更新完触发一次分析脚本重新生成图表JSON只在内网或可信环境中开放访问不暴露到公网性能优化主要靠“预聚合”。看板加载时如果直接去扫原始表几万行数据每次请求都要groupby和排序体验会很差。我的方案是每天分析脚本跑完就把聚合结果单独存成一张rentals_summary表。前端请求时直接读这张小表毫秒级返回。原始数据表只在需要明细查询的时候才被访问。这个思路其实就是“结果表模式”小项目里最简单有效不需要引入Redis或者缓存框架。6. 常见问题与排查技巧实录踩坑是项目过程中最有价值的部分。我把自己遇到的典型问题整理成了一张速查表这些问题在教程里基本找不到答案。现象原因解决方法爬下来的中文全是乱码网页编码和Requests解码方式不一致显式设置resp.encoding utf-8或按响应头charset动态设置请求到一半返回403触发网站频率限制或风控增加请求间隔、轮换UA、检查是否为登录态页面面积字段有“平米”“㎡”“平”多种写法源站数据格式不统一统一正则提取数字部分再做类型转换同一套房重复出现在结果中不同URL指向同一房源用source_url做唯一约束或对标题价格面积组合去重图表加载后宽度异常ECharts容器初始化时未拿到实际宽度在window.onload或setTimeout后初始化图表SQLite读取变慢几千条以上无索引的频繁groupby给district、crawl_date字段加索引或改用预聚合表Flask接口返回中文变成ASCII码默认JSON序列化转义非ASCII字符使用jsonify即可正常返回或者设置app.config[JSON_AS_ASCII] FalsePandas读取float列出现NaN原始数据有“暂无”“待定”等文本pd.to_numeric(errorscoerce)再统一填充或丢弃除了上面的问题我再分享三个排查技巧都是实测有效的第一个技巧是“分段打印中间结果”。清洗数据时不要一口气写完所有步骤再一次性输出而是在每一步后面加print(df.shape)和print(df.head())。比如你解析完户型字段后先看看分布发现“两室一厅”没有被匹配到那你可以在正则里补充中文数字映射而不是等到最后分析时才发现数据不对。这个习惯能帮你节省大量时间。第二个技巧是“浏览器F12先看网页结构再写爬虫”。手动在目标网页上右键检查元素确认标题在.class里、价格在哪个标签下比你写好代码后再试错高效得多。爬虫的容错率天生就不高一个选择器写错整批数据可能就全错了。第三个技巧是“日志里记下每次请求的状态码”。我在爬虫脚本里用了logging而不是print因为生产环境跑任务时你需要回溯当时发生了什么。把所有非200状态码记录下来回头一查就知道是风控导致的还是目标页面结构改了。7. 扩展方向与个人总结这个项目做到后面我最大的体会是数据项目的价值不取决于用了多高深的算法而取决于数据质量和分析视角是否贴合真实决策。Pandas、Flask、ECharts这些工具都是现成的真正拉开差距的是你有没有想到“单位租金”比“总租金”更有可比性有没有注意到“中位数”比“均值”更适合描述租金分布有没有意识到“展示系统”的价值在于让一个不懂技术的人也能看懂数据。有些扩展我觉得值得在后续版本里做。第一是引入地理可视化把房源标记到地图上用散点的大小和颜色表示价格这样用户能直观看到城市的租金热点是怎么分布的。第二是增加环比和同比指标“本月和上月相比涨了多少”对租客决策有很强的参考意义。第三是接入定时采集和自动分析让系统每天自动更新数据看板始终保持新鲜而不是靠人工手动跑脚本。最后再分享一个小技巧当你做一个数据分析项目时先把最终用户的“三个问题”写下来然后让图表去回答这些问题。这个做法能保证你做的每个图表都有存在的理由。这套租房数据分析和展示系统就是在这种思路下完成的从采集到看板整条链路跑通之后你会发现自己对Python数据分析的理解比做十个零散案例都深。
返回列表