欢迎光临
我们一直在努力

基于Python的二手房数据采集与可视化分析实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

简介:面向Python毕业设计及期末大作业场景,这份资源整合了二手房数据采集与可视化分析项目的完整源码和配套PPT资料。项目基于Requests、BeautifulSoup等爬虫库完成链家、房天下等平台的数据采集,借助Pandas进行清洗预处理,支持将数据存入SQLite、MySQL等数据库,并使用Matplotlib、Seaborn、Plotly绘制柱状图、折线图等图表,形成从爬虫到可视化展示的闭环实践方案。压缩包大小约48.05MB,以.py源码文件与PPT演示资料为主,目录结构清晰,便于按数据采集、清洗、存储、分析等模块直接参考或二次开发。目前已有691人学习下载,适合需要完成毕业设计、课程项目或想系统掌握二手房数据获取与可视化流程的Python学习者。

1. 基于Python的二手房数据采集与可视化分析:项目拆解与机会点

“基于Python的二手房数据采集及可视化分析”高频出现在毕设和实战项目清单里,原因很朴素:它把采集、清洗、分析、可视化这条完整数据流水线,压缩进了普通笔记本就能跑动的代码量。立项不需要申请开放平台API,公开网页上的挂牌信息就是天然数据集;答辩时又能输出区域均价排行、户型分布饼图、价格时间线这类看得见的结果。这类项目的源码和资料PPT文件常被成套整理,因为代码只是交付的一半,另一半是能讲清数据的图表和讲解材料。适合两类人:刚入门Python、想串联requests和pandas的学生,以及能写脚本但缺可视化输出经验的从业者。这篇直接把选型理由、参数边界和踩坑位置讲透。

2. 数据采集模块:用 requests + BeautifulSoup 把房源列表页抓回本地

2.1 开工前先定字段清单:要哪些列、从哪个页面取

很多初学者一上来就写爬虫,抓到什么算什么,后面清洗时才发现缺朝向、缺楼层,重跑一遍又要重新限速。我一般的做法是先把字段字典定死,再决定抓哪些页面。二手房列表页通常已经给出了核心字段:标题、位置、户型、面积、朝向、楼层、总价、单价、关注人数。详情页多出来的主要是建筑年代、环线、挂牌时间,对于校级毕业设计或技能展示来说,列表页信息已经足够回答大部分问题。

以主流房产平台的二手房频道为例,我建议第一批落库字段按下表设计:

字段名 数据示例 来源 用途
title 远洋山水 3室2厅 东南 列表页标题 舆情导出,房源唯一标识辅助
community 远洋山水 标题/位置 小区聚合统计
house_type 3室2厅 列表页 户型拆分解读
area_text 128.43㎡ 列表页 面积转数值
toward 东南 列表页 朝向偏好分析
floor_text 中楼层/共28层 列表页 楼层高低对价格的影响
total_price 790万 列表页 挂牌总价
unit_price 61516元/平 列表页 单价异常识别

值得强调的是,总价单位是“万”,单价在部分城市会换成“元/平”,这个差异如果不在抓取阶段处理,清洗阶段会非常痛苦。确定字段清单后,项目目录也顺手安排一下:fetch.py 负责请求和解析,clean.py 负责清洗,plot.py 负责可视化,data/raw 存放原始 CSV,docs 放 PPT 和报告。数据结构先于代码定型,后面所有分析环节都围绕这同一份字典进行,尽量少返工。

2.2 最小可用的列表页解析代码:先跑通一页再谈全量

采集模块的主选项是 requests 加 BeautifulSoup,原因很直接:页面结构相对规整,选好选择器就能稳定提取字段,不依赖浏览器渲染。Selenium 能解决动态加载,但开销大、容易被检测,适合作为备选方案,不适合作为主力。

下面这段是一个能跑通单页抓取的骨架代码,我保留了两个参数对外可变:请求重试次数和超时时间。

import time
import random
import requests
import pandas as pd
from bs4 import BeautifulSoup

# 用 Session 保持连接,减少 TCP 握手开销
session = requests.Session()
session.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0",
"Accept": "text/html,application/xhtml+xml",
"Accept-Language": "zh-CN,zh;q=0.9",
}

def fetch_html(url, max_retry=3, timeout=10):
"""抓取 HTML,返回字符串;失败时按指数退避重试。"""
for attempt in range(max_retry):
try:
resp = session.get(url, timeout=timeout)
if resp.status_code == 200:
resp.encoding = "utf-8"
return resp.text
print(f"HTTP {resp.status_code},第 {attempt + 1} 次重试")
except requests.RequestException as err:
print(f"请求异常:{err}")
time.sleep(3 * (attempt + 1))
return ""

def parse_list_html(html):
"""解析列表页,只保留字段完整的数据行。"""
soup = BeautifulSoup(html, "html.parser")
rows = soup.select("ul.sellListContent li")
records = []
for li in rows:
title_el = li.select_one(".title a")
position_el = li.select_one(".positionInfo")
total_el = li.select_one(".totalPrice span")
unit_el = li.select_one(".unitPrice")
if not title_el or not total_el:
continue
records.append({
"title": title_el.get_text(strip=True),
"position": position_el.get_text(strip=True) if position_el else "",
"total_price": total_el.get_text(strip=True),
"unit_price": unit_el.get_text(strip=True) if unit_el else "",
})
return records

if __name__ == "__main__":
test_url = "https://城市频道示例/ershoufang/pg1/"
html = fetch_html(test_url)
data = parse_list_html(html)
print(f"本页解析到 {len(data)} 条房源")
df = pd.DataFrame(data)
df.to_csv("data/raw/list_pg1.csv", index=False, encoding="utf-8-sig")

代码里的两个细节值得注意:第一, resp.encoding = "utf-8" 在拿到响应后立即执行,避免部分中文页面被 requests 用误判编码读成乱码;第二,解析时每个字段都用 select_one 加存在性判断再取文本,而不是直接链式调用,否则某一条房源缺标签时会抛 AttributeError 中断整轮抓取。 max_retry=3 配合 timeout=10 对大页面足够, 3 * (attempt + 1) 让重试等待时间递增,对偶发的网络抖动和接口限流都比较友好。

2.3 分页与限速参数:跑一站数据要多久、怎么不被封

列表页一般支持 pg2 、 pg3 这种路径分页方式。把单页函数套进循环后,最关键的是 sleep 时间和失败策略。固定 sleep 反而容易被识别,因为真实用户操作间隔是随机的;我习惯用 random.uniform(2.0, 4.5) 生成随机等待,每 20 页插入一次 60 秒的长暂停,让请求行为更接近手动翻页节奏。

以下是一段带断点续传思想的分页采集代码:

all_records = []
for page in range(1, 51):
url = f"https://城市频道示例/ershoufang/pg{page}/"
html = fetch_html(url)
page_records = parse_list_html(html)

if not page_records:
print(f"第 {page} 页无数据,暂停 30 秒")
time.sleep(30)
continue

all_records.extend(page_records)

# 每页都写盘,而不是等全部跑完,避免中途断网丢全部数据
pd.DataFrame(all_records).to_csv(
"data/raw/raw_list.csv", index=False, encoding="utf-8-sig"
)
print(f"第 {page} 页完成,本页 {len(page_records)} 条,累计 {len(all_records)} 条")
time.sleep(random.uniform(2.0, 4.5))

这里最关键的习惯是每页循环后都执行一次 to_csv 增量覆盖。爬虫最怕的不是封锁,而是跑了一个小时,内存崩了,发现一页都没存。分页循环里一旦遇到连续三页空白,就应该主动中断而不是无限重试,指数退避只能处理瞬时错误,页面结构变化或 IP 被限时,重试是拿不到结果的。

常见的限速参数配置大概是:单页抓取间隔 2 到 4.5 秒,单页解析耗时约 0.1 秒,一页 30 条房源,跑完一个 3000 套房子的城区大约需要 10 到 15 分钟。如果想抓全部 50 页,预留 20 分钟比较稳妥。若页面返回 403 或出现验证页面,用普通浏览器打开确认是否触发验证码,再决定要不要调整抓取频率。

3. 数据清洗与字段工程:把“3室2厅 建筑面积97.55㎡”变成能算的数值

3.1 文本字段的正则拆分:户型、面积、楼层、朝向

抓下来的原始数据里, house_type 是“3室2厅”, area_text 是“97.55㎡”, total_price 是“590万”。这些文本在 DataFrame 里默认是 object 类型,画图前必须拆成数值。常见的清洗策略是用正则从字符串里提取第一个数字,而不是用字符串切分。因为真实数据里存在“3室2厅1卫”、“5室3厅”这类变体,单纯按“室”切分得到的结果不稳定。

import re
import pandas as pd

df = pd.read_csv("data/raw/raw_list.csv", encoding="utf-8-sig")

def extract_room(text):
"""从"3室2厅1卫"中提取室、厅,缺省部分返回 pd.NA。"""
room_match = re.search(r"(\\d+)室", str(text))
hall_match = re.search(r"(\\d+)厅", str(text))
room = int(room_match.group(1)) if room_match else pd.NA
hall = int(hall_match.group(1)) if hall_match else pd.NA
return room, hall

df["室数"] = df["house_type"].apply(lambda x: extract_room(x)[0])
df["厅数"] = df["house_type"].apply(lambda x: extract_room(x)[1])

df["面积"] = df["area_text"].str.replace("㎡", "", regex=False).astype(float)

df["总价_万"] = df["total_price"].str.replace("万", "", regex=False).astype(float)

df.head()

str.replace 与 astype(float) 是整条链路里最容易翻车的两步。 area_text 里如果混入“暂无数据”这样的占位文本, astype 会直接抛 ValueError,所以清洗时建议先做一步强制转换判断:

df["面积"] = pd.to_numeric(
df["area_text"].str.replace("㎡", "", regex=False),
errors="coerce",
)

errors="coerce" 会把无法解析的值变成 NaN,而不是让整个程序中断。这也是一个重要的清洗习惯:宁可在后面处理 NaN,也不能让每一步异常都把流程打停。总价字段同理,部分房源会标“价格待定”, to_numeric 会把它转成 NaN,后面统一处理。

3.2 缺失值和“极端挂牌价”处理:先看分布,再决定删还是留

清洗的顺序不要反过来。很多教程上来就让删缺失值,实际业务中应该先承认两个现实:第一,二手房挂牌数据天然包含缺失,例如部分房源不展示建筑面积;第二,挂牌价和成交价之间有明显溢价,而且存在大量“钓鱼房源”把单价挂得高得离谱。

先看分布再动手,是清洗阶段最稳的顺序。我一般会先跑一个 describe() 或者画直方图,确认数据量级和区间,再决定过滤边界。

low = df["单价"].quantile(0.02)
high = df["单价"].quantile(0.98)
df_clean = df[(df["单价"] >= low) & (df["单价"] <= high)]

为什么用分位数而不是均值加减标准差?因为房价分布呈现右偏态,少数几百万元的豪宅会把均值拉高,用 3σ 法则会把城区里正常的次新房误删。用 2% 和 98% 分位点做截断,既不引入主观阈值,又能在保留主体数据的同时剔除极端挂牌。

缺失值处理上,朝向缺失可以填“未知”,面积缺失建议直接删除,因为面积是后续所有分析的核心字段。户型中“厅数”缺失可以保留,因为部分一居室确实没有厅。这不是玄学,而是基于字段在分析链路中的权重做决策:主分析字段缺失必须删,辅助字段缺失可以留。

3.3 单位对齐与去重:单价、总价、面积必须在同一数量级里

原始数据抓下来时,单价可能是“61516元/平”,也可能是“6.1万元/平”。这个差异来自不同小区页面的展示逻辑。清洗时统一换算成“元/平方米”能省掉后续大量麻烦。

def clean_unit_price(text):
text = str(text).replace("元/平", "").strip()
if "万" in text:
return float(text.replace("万", "")) * 10000
return float(text)

df_clean["单价_元"] = df_clean["unit_price"].apply(clean_unit_price)

这套函数只处理最常见的两种格式,真实数据里还会出现“暂无”、“约”字前缀等变体,遇到时先输出到日志里人工判断,不要试图用通配规则吞掉所有异常。

去重是另一个容易被忽略的点。同一套房源可能被不同中介重复挂牌,标题里带“满五唯一”“急售”等标签反而不利于去重。我习惯用小区名、户型、面积、总价四个字段组合去重,字段越少越容易误删,字段越多越容易漏重。

df_clean = df_clean.drop_duplicates(
subset=["community", "house_type", "面积", "总价_万"],
keep="first",
)

keep="first" 会保留同一条房源第一次出现的版本。去重后建议再次检查总条数,如果删掉的比例超过 15%,就要怀疑是不是去重字段太窄,把不同楼层的房源误判成重复了。

4. 可视化分析:用单价分布、户型占比和区域排名回答“买哪值”

4.1 单价分布直方图与户型占比环形图:三行代码一个结论

数据清洗完成之后,可视化才是把结论交给观众的一步。我先从最不会出错的两张图开始:单价分布直方图和户型占比环形图。直方图能一眼看出挂牌单价的主体区间,环形图能说明市场里主流供给是两居还是三居。

绘图前需要先设置中文字体,否则所有标签都会变成方块,这是新手最常见的中文乱码问题。

import matplotlib
import matplotlib.pyplot as plt

matplotlib.rcParams["font.sans-serif"] = ["SimHei", "Noto Sans CJK SC"]
matplotlib.rcParams["axes.unicode_minus"] = False

fig, ax = plt.subplots(figsize=(8, 5))
ax.hist(df_clean["单价_元"] / 10000, bins=40, color="#2171b5", alpha=0.85)
ax.set_xlabel("挂牌单价(万元/㎡)")
ax.set_ylabel("房源数")
ax.set_title("二手房挂牌单价分布")
plt.tight_layout()
plt.savefig("output/price_distribution.png", dpi=150)

把单价除以 10000,是为了让横轴刻度变成“万元/㎡”的直观数值,省去后期在图表里再标一个单位。bins 参数从 30 调整到 40,能让长尾更平滑,也能保留主峰形状。alpha=0.85 是为了让直方图在叠加参考线时不会完全遮住背景网格。

户型占比用 value_counts 加饼图是个标准操作,但如果户型种类太多,比如“4室2厅”和“4室3厅”都只有几套,饼图会挤成一团。我一般只取占比前 6 的户型,其余归为“其他”,这样成图更干净,向别人讲解时也更有说服力。

4.2 区域均价 Top10 与面积-总价散点图:看清承受范围内的次优区

区域均价排行是二手房分析里最出彩的图。先按小区或板块聚合均价,再取前 10 名画横向条形图。横向条形图比纵向更适合这里,因为板块名称是中文,纵向标签会挤成一串。

avg_price = (
df_clean.groupby("板块")["单价_元"]
.mean()
.sort_values(ascending=False)
.head(10)
)

fig, ax = plt.subplots(figsize=(8, 6))
ax.barh(avg_price.index, avg_price.values / 10000, color="#2ca02c")
ax.set_xlabel("平均挂牌单价(万元/㎡)")
ax.set_title("板块均价 Top10")
plt.tight_layout()
plt.savefig("output/plate_top10.png", dpi=150)

groupby("板块") 的前提是清洗阶段已经把“朝阳门内大街”这类位置信息拆出了板块字段。如果 position 字段直接可用,可以跳过 groupby 直接画总价-面积散点图。

散点图用来回答“面积和总价到底存在什么关系”。普通线性关系在图上不直观时,可以叠加一条回归趋势线,但 matplotlib 本身不提供回归计算,需要调用 numpy.polyfit 拟合一次一次项系数。

import numpy as np

x = df_clean["面积"]
y = df_clean["总价_万"]
coeff = np.polyfit(x, y, 1)
fit_line = np.poly1d(coeff)

fig, ax = plt.subplots(figsize=(9, 6))
ax.scatter(x, y, s=6, alpha=0.4, color="#d62728")
ax.plot(sorted(x), fit_line(sorted(x)), linewidth=2, color="#1f77b4")
ax.set_xlabel("建筑面积(㎡)")
ax.set_ylabel("挂牌总价(万)")
ax.set_title("面积与总价的关系:参考回归线")
plt.savefig("output/area_price_scatter.png", dpi=150)

散点图的价值有两个:一是直观看出关系是否线性,二是暴露异常点。图里那些远离趋势线的点,往往是装修溢价或老破小降价的典型样本,追问这些点能帮你写出更有深度的分析结论。

4.3 图表命名与输出规范:直接给 PPT 用的参数建议

分析做完了,图只是半成品,需要输出成文件才能放进 PPT。这里我建议每张图都设置 dpi=150 以上,字号用默认大小,尺寸横图 8×5 或 8×6,存成 PNG。太大的 PDF 格式在 PPT 里反而难排版,PNG 最通用。

输出文件 分析问题 建议图表类型 关键参数
price_distribution.png 市场挂牌单价集中区间 直方图 bins=40,dpi=150
house_type_pie.png 主力户型占比 环形图 只保留前6类
plate_top10.png 哪个板块最贵 横向条形图 按均值降序
area_price_scatter.png 面积和总价的关系 散点图+趋势线 以“面积”为横轴
total_price_trend.png 不同板块价格对比 分组条形图 按板块分面

5. 常见问题与避坑清单:采集、清洗到可视化最容易翻车的 5 个位置

5.1 爬着爬着突然返回空白页,数据量不再增长

现象:脚本还在跑,日志显示 HTTP 200,但解析出来的 records 列表是空的,而且持续多个页面为空白。

原因:页面结构没有变,而是触发了反爬拦截,返回的是一个包含验证逻辑的空白壳页面,状态码依然是 200,代码里的「非空判断」无法覆盖这种情况。

解决:在解析前检查 HTML 是否包含列表标签特征,比如 ul.sellListContent 。如果连续 3 页都没有该特征,停止抓取并休眠 10 分钟或更换 User-Agent。这个检查逻辑一定要放在解析函数之前。

5.2 总价字段明明有数字,聚合计算时却报错

现象: df["总价"].mean() 报 TypeError 或返回明显错误的结果。

原因:清洗阶段没有做类型转换, "790万" 依然是含「万」的字符串,pandas 试图做字符串拼接而不是数值计算。

解决:用 str.replace("万", "", regex=False).astype(float) 统一转数值,转换过程中发现无法兼容的值先输出到日志,而不是静默删除。我踩过这个坑一次之后,所有抓取字段的 dtype 在清洗第一步就会被列出来检查一遍。

5.3 同一个小区在多个区域的重复挂牌,导致板块统计翻倍

现象:板块均价图里某板块特别高,但人工核对后发现板块内只有 2 套房源。

原因:同一个小区在不同门店重复挂牌,去重字段没有覆盖“小区+户型+面积+总价”组合,导致统计中出现同一套房的两条记录。

解决:先执行 3.3 节里的 drop_duplicates,再去做任何 groupby 统计。统计前还可以再看一眼每个板块的房源总量,样本低于 5 条的板块不进入均价排行,从源头避免小样本带来的偏差。

5.4 Matplotlib 中文显示成方块,图表输出全部是豆腐块

现象: ax.set_title("二手房挂牌单价分布") 执行正常,保存出来的 PNG 里中文全部变成空心方块。

原因:matplotlib 默认字体不支持中文, SimHei 和 Noto Sans CJK SC 至少需要一种存在于本地系统。

解决:在脚本开头统一设置 matplotlib.rcParams["font.sans-serif"] 和 ["axes.unicode_minus"] = False 。如果你在 Linux 服务器上跑,还需要先安装字体包并把字体缓存重建一次,否则即使设置了字体名也不会生效。

5.5 PowerBI 或者 Excel 打开 CSV 全是乱码

现象:写出的 CSV 用 Excel 打开中文乱码,但用 pandas 读取完全正常。

原因: to_csv 默认用的编码可能是 utf-8 ,Excel 默认用 gbk 打开文件。

解决:写 CSV 时统一指定 encoding="utf-8-sig" ,BOM 头会让 Excel 自动识别编码。这个细节虽然和分析无关,但如果你要把数据交给指导老师或团队成员,乱码会大大降低数据交付的可信度。

6. 进阶用法:从图表走向估价模型与自动生成汇报材料

图表是把现状讲清楚,进阶的玩法是向前走一步——预测。基于清洗后的字段,可以用一个简单的线性回归做一个板块内单价预测模型,回答“一套 80 平米、5 层、南北向的两居,在该板块大概多少钱”。

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression

features = df_clean[["面积", "室数", "厅数", "总楼层", "所在楼层"]]
features["所在楼层"].fillna(features["所在楼层"].median(), inplace=True)

X = features
y = df_clean["总价_万"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

model = LinearRegression()
model.fit(X_train, y_train)
print("R²:", model.score(X_test, y_test))

这个模型的精度不会太高,二手房定价受装修和学区影响极大,这些字段很难从列表页直接抓全。建模的意义不在预测准确性,在于把清洗阶段沉淀出的特征拼成一个可解释的闭环:数据结构决定特征,特征决定模型,模型再反过来验证清洗逻辑是否存在瑕疵。真实成交数据往往比挂牌数据能给出更好的拟合度,所以如果你能拿到成交记录,这个方向会有更多价值。

自动汇报这方面,我会把图表输出和 PPT 生成脚本串成一条命令。用 python-pptx 批量把 PDF、PNG 放进模板页,再自动生成结论页。图表是分析过程的快照,但结论必须由人来写。成交价与挂牌价的价差、不同板块的库存房源对比,这些内容都需要结合业务常识才能讲清楚,不能靠脚本自动生成文字。如果让我重新做一遍这个项目,我会先把数据字典固定下来,再写每一行清洗代码。把字段、单位、缺失值处理规则定义清楚,后续所有分析都会事半功倍。手上有完整可靠的数据,比跑一堆漂亮的图更值得投入时间。希望帮到你。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

赞(0)
未经允许不得转载:171主机测评 » 基于Python的二手房数据采集与可视化分析实战
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址