☰
Python二手房数据采集与可视化分析:爬虫、Pandas与Flask实战
2026/10/29 20:18:46 网站建设 项目流程

简介:面向Python毕业设计场景的二手房数据采集及可视化分析项目资源包,适合需要完成期末大作业或毕设项目的本科生、Python初学者参考。项目完整覆盖从数据采集、清洗、存储到分析可视化的流程:以Requests、BeautifulSoup编写爬虫抓取链家、房天下等平台的房源信息,再通过Pandas完成去重、缺失值处理与数据类型转换,清洗后的数据可写入SQLite、MySQL或MongoDB备查;分析环节则提供Matplotlib、Seaborn、Plotly绘制柱状图、折线图等方案的示例,帮助观察房价、面积、地理位置等字段的分布与趋势。zip压缩包大小约48.05MB,内含可直接运行的项目源码和全套讲解PPT等资料,便于对照学习与二次修改。资源已有691人学习,适合用来自学爬虫与数据分析全流程、理解从数据采集到可视化展示的完整项目结构,也可作为毕业设计开题、答辩环节的素材支撑。

1. 二手房数据采集与可视化分析,毕业设计为什么值得选这个方向

答辩现场最容易出现的场景是:老师问数据从哪来,你说爬虫;老师问有多少条,你说几千条;老师问你爬虫遇到反爬怎么处理,你支支吾吾。基于 Python 的二手房数据采集及可视化分析正好把这三问都堵上——它既是一条完整的 Python 爬虫链路,也是一套 Pandas 数据清洗和 Pyecharts 可视化分析的项目源码,最后还能用 Flask 把图表串成可交互的看板,答辩演示效果非常直观。加上所有资料做成 PPT 文件递上去,导师一眼就能看出工作量。适合三类人:基础薄弱想用套壳项目保底的,学过爬虫和 Pandas 想系统串一遍的,以及想往数据分析方向找实习作品的人。这个方向技术栈清晰、源码可复现、坑都可预期,作为毕业设计性价比很高。

2. 数据采集层:用 Requests 和解析库把链家二手房列表页变成结构化数据

2.1 先搞清楚数据长什么样,再写爬虫

写爬虫之前的第一件事不是上手写 requests.get,而是打开浏览器,按 F12 切到 Network 面板,刷新一次列表页,看 HTML 结构。链家和贝壳的二手房列表页,房源信息通常在 class 为 house-title 或 sellListContent 的 li 标签里,每套房子的标题、小区名、位置、总价、单价、关注人数都散落在不同的 div 和 span 中,字段之间没有固定规律,全靠 class 名区分。

我一般会先用浏览器的“检查”功能选中某一套房子的标题,确认它所在的标签层级,再决定解析逻辑。这里有个常见做法是直接用 BeautifulSoup 配合 lxml 解析,速度比正则快,代码写起来也直观。另一个关键点是,链家列表页对高频访问有明显限制,连续翻页太快会弹验证页,所以采集脚本里必须带请求间隔。建议把请求间隔写成可配置参数,而不是写死在代码里。

import time import requests from bs4 import BeautifulSoup session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Referer": "https://sh.lianjia.com/ershoufang/" # 链家上海二手房列表页 }) url = "https://sh.lianjia.com/ershoufang/pg{page}/" house_list = [] for page in range(1, 3): # 先爬两页验证结构 resp = session.get(url.format(page=page), timeout=10) soup = BeautifulSoup(resp.text, "lxml") items = soup.select("li. clear LOGCLICKDATA") if not items: items = soup.select("div.info") # 不同城市 class 名可能不一样 for item in items: title_tag = item.select_one("a.maidian-detail") if title_tag is None: continue title = title_tag.get_text(strip=True) total_price = item.select_one("div.totalPrice span") unit_price = item.select_one("div.unitPrice span") house_list.append({ "标题": title, "总价_万": total_price.get_text(strip=True) if total_price else "", "单价_元平": unit_price.get_text(strip=True) if unit_price else "" }) time.sleep(1.5) # 请求间隔,不能省

这段代码的关键点有两个。第一,selector 里 class 名含空格时,直接用 select 会失效,我用 lxml 解析后改用 div.info 兜底,这种页面的 class 命名经常改版,建议第一版先打印前 100 行 HTML 确认。第二,总价和单价的文本是类似“560万”和“46251元/平米”的字符串,后续必须在清洗阶段拆数字,不要在爬虫阶段强行转 float,因为缺失值会导致异常断掉。先把原始文本存下来,清洗时统一处理,是数据采集最常见的可靠做法。

2.2 多字段抽取与详情页补数据

列表页能拿到标题、总价、单价、小区名和基本位置,但真正支撑可视化分析的字段,比如建筑年代、面积、户型、朝向、楼层、装修情况,都在详情页里。毕设如果想做楼龄和单价的关系分析,详情页就是绕不开的一步。

常见做法是把列表页解析出的房源链接队列化,再串行进详情页采集。需要注意的是,详情页的 URL 是数字编号格式,比如 https://sh.lianjia.com/ershoufang/107123456789.html,别把 URL 列存成字符串就以为完事了,还要防重。我习惯在采集前先用一个 set 保存已访问过的房源编号,重访直接跳过。

import re from urllib.parse import urlparse def parse_house_from_detail(url, html): soup = BeautifulSoup(html, "lxml") # 基础信息在 introContent 里 intro = soup.select_one(".introContent") if intro is None: text = soup.get_text(" ", strip=True) else: text = intro.get_text(" ", strip=True) # 用正则抽面积,比定位元素更抗改版 area_match = re.search(r"建筑面积[::]\s*([\d.]+)㎡", text) year_match = re.search(r"建成年代[::]\s*(\d{4})年", text) toward_match = re.search(r"朝向[::]\s*([^\s]+)", text) house_id = urlparse(url).path.split("/")[-1].replace(".html", "") return { "房源编号": house_id, "建筑面积_平": float(area_match.group(1)) if area_match else None, "建成年代": int(year_match.group(1)) if year_match else None, "朝向": toward_match.group(1) if toward_match else "" }

正则在这里比 BeautifulSoup 的 select 更实用,因为详情页的布局会在不同城市下浮动,introContent 内部结构不统一,而文字排版的规律相对稳定。加了一个 nums 参数来控制采样数量,避免采集时间过长。

2.3 采集策略:翻页、多区域与请求间隔的边界

整个列表往往是几十万条,直接全量爬既慢又容易被封。合理的采集中间态是先按区域拆。比如上海二手房可以按浦东、徐汇、静安等行政区拼接 URL 参数,每个区域只爬前五页,这样数据量约数百到上千条,对毕业设计完全够用,又能展示多区域对比的图表效果。

请求间隔的边界值我习惯设为 1 到 2 秒之间。太快会触发滑块验证,太慢则几万条数据要爬大半天。另一个容易被忽略的坑是 Cookie 时效:浏览器登录后 cookie 里的 SESSIONID 会在几小时后失效,requests 复现不了浏览器环境,所以不要在请求头里硬编码登录态。还有,链家对同一个 IP 的访问频率有记录,如果项目演示要保证现场稳定,建议把采集好的数据落成 CSV 或 SQLite 存档,演示时优先读本地文件,爬虫只在更新数据时运行。

3. 数据清洗与特征工程:把爬回来的脏数据变成可分析的 DataFrame

3.1 清洗流程:类型转换、单位规整、缺失值处理

爬虫返回的原始数据通常是一个 CSV,长这样:标题列混着“XX小区 2室1厅”,总价是“560万”,单价是“46251元/平米”,建成年份可能有 1998 年也有“暂无”。直接拿这样的数据进 Pandas 做聚合,结果全是字符串拼出来的假数字。

清洗第一步是把价格和面积拆出来。第二步是统一单位:总价字段里去掉“万”字,单价去掉“元/平米”字样,并把可能出现的“暂无”“--”替换成 NaN。第三步是筛选采样数据,把面积明显异常的大于 500 平的记录剔除,因为这些要么是别墅搞的栋数不对,要么就是爬到了车位。还要注意层高、朝向、装修等枚举字段的空值,删掉不是好选择,填充成“未知”,可视化时才能如实展示。

import pandas as pd import re df = pd.read_csv("lianjia_sh_raw.csv", encoding="utf-8-sig") def clean_price(value): """把 560万 -> 560.0, 1.2万/平米 -> 1.2的文本清洗""" if pd.isna(value) or str(value) in ("暂无", "--", ""): return None match = re.search(r"([\d.]+)", str(value)) return float(match.group(1)) if match else None df["总价_万元"] = df["总价_万"].apply(clean_price) df["单价_元平"] = df["单价_元平"].apply(clean_price) def area_from_text(text): match = re.search(r"([\d.]+)平米", str(text)) return float(match.group(1)) if match else None df["建筑面积_平"] = df["建筑面积_平"].apply(area_from_text) df = df.dropna(subset=["总价_万元", "单价_元平", "建筑面积_平"]) df = df[df["建筑面积_平"] < 500] # 排除异常超大户型

参数说明:encoding="utf-8-sig" 是为了让 Excel 打开 CSV 时不出现中文乱码,这类细节在毕设答辩时经常被问到。正则提取正则表达式采取“匹配第一个数字”策略,好处是总价里出现“1.2万”这类带小数的文本也能兼容;坏处是“非住宅”这类字样可能污染单价列,所以我只在文本如“46251元/平米”时提取,保证列语义一致。

整个清洗过程要和后续可视化分开写。Pandas 的 apply 是逐行处理,数据量大时速度一般,好在毕业设计的数据规模通常在五千条以内,性能不是问题。真正要注意的是清洗后要做一次 describe,看看列的最大最小值合不合理,单价 10 万一平米的豪宅明显会让图表失真,这类数据是否保留要在答辩时给出理由。

3.2 特征工程:单价计算、楼龄分段、总价区间

数据清洗完,剩下来的是原始字段,可视化之前还要做特征加工。常用的是四个衍生字段:单价(如果爬虫没直接给,就用总价除以面积)、楼龄(当前年份减建成年代)、总价区间(比如 200 万以下、200-400 万等 5 档)、面积段(60 平以下、60-90 平、90-140 平、140 平以上)。

原始数据里单价可能存在 1 楼带花园的折价房,总价和面积都属实,但单价看起来异常低。要不要单独剔除?我建议不要,因为这类房源本身是真实存在,在做均价计算时可以被拉低数值,处理办法是使用中位数而不是均值。

import numpy as np df["单价_计算"] = df["总价_万元"] * 10000 / df["建筑面积_平"] df["楼龄"] = 2025 - df["建成年代"] bins_price = [0, 200, 400, 600, 800, np.inf] labels_price = ["200万以下", "200-400万", "400-600万", "600-800万", "800万以上"] df["总价区间"] = pd.cut(df["总价_万元"], bins=bins_price, labels=labels_price) bins_area = [0, 60, 90, 140, np.inf] labels_area = ["60平以下", "60-90平", "90-140平", "140平以上"] df["面积段"] = pd.cut(df["建筑面积_平"], bins=bins_area, labels=labels_area)

pd.cut 的关键是区间左闭右开,200万以下这一档的口径是 0 到 200 万(含 200 万),如果你希望 200 万整归入下一档,就把 bins 改成 [0, 199.99, 400, ...],但这样代码就变丑了。我一般直接用默认,答辩时讲清楚口径即可。特征工程这一步不只是为了可视化好看,它还是模型类分析的输入基础,如果毕设内容里有房价预测或特征相关性分析,这些区间字段可以直接做 one-hot。

4. 可视化分析:从 Pandas 聚合到交互式看板的落地路径

4.1 用 Pandas 做聚合统计,产出一张基础分析表

可视化分析的核心不是画图,而是“分析”。画图前要先想清楚要说明什么观点,比如“总价在 400-600 万区间是主力户型”或者“90 平以下房源集中在哪些商圈”。用 Pandas 做 groupby 聚合得到一张二维表,再交给图表库。

summary = df.groupby(["区域", "面积段"], observed=False)["总价_万元"].agg( 挂牌均价="mean", 房源数量="count", min_price="min", max_price="max" ).reset_index() summary["挂牌均价_万"] = summary["挂牌均价"].round(1) summary.sort_values("房源数量", ascending=False, inplace=True) print(summary.head(20))

这段代码的意图是:每个区域在不同面积段内的挂牌均价、房源数量和价格区间。直接打印出 20 行数据,是最原始的“用数据说话”的证据。注意 groupby 里用了 observed=False,当分组列是分类类型时,它会保留空组,避免某些面积段没有房源时被忽略,这种细节到了答辩时能被评审老师一眼看到。

4.2 用 Pyecharts 做交互图表的配置注意点

Pyecharts 是基于 ECharts 的 Python 封装,优势是生成 HTML,浏览器直接打开就能交互缩放、悬停显示数值。很多新手上来就画饼图,堆了一堆五颜六色的扇区,答辩时老师一问“这个图想说明什么”就答不上来。合理做法是先做三个图:区域均价柱状图、总价区间分布环形图、朝向与楼龄散点图。

from pyecharts.charts import Bar, Pie, Scatter from pyecharts import options as opts bar = Bar() bar.add_xaxis(summary["区域"].unique().tolist()) bar.add_yaxis("挂牌均价_万", summary.groupby("区域", observed=False)["挂牌均价_万"].mean().tolist()) bar.set_global_opts( title_opts=opts.TitleOpts(title="各区域二手房挂牌均价对比"), yaxis_opts=opts.AxisOpts(name="均价(万元)"), ) bar.render("区域均价对比.html") scatter = Scatter() x_data = df["建筑面积_平"].tolist() y_data = df["总价_万元"].tolist() scatter.add_xaxis(x_data) scatter.add_yaxis("总价/面积", y_data, symbol_size=8) scatter.set_global_opts( title_opts=opts.TitleOpts(title="面积与总价的散点分布"), xaxis_opts=opts.AxisOpts(name="建筑面积(平)"), yaxis_opts=opts.AxisOpts(name="总价(万元)") ) scatter.render("面积总价散点.html")

参数说明:x 轴数据用列表,y 轴数据也要用列表,如果直接把 Series 传进去,Pyecharts 内部会转成奇怪的对象导致图表空白。add_yaxis 里 symbol_size 控制散点大小,数据量超过 1000 条时可以调到 5,否则点太密。render 方法生成独立 HTML 文件,图片上传到 PPT 或现场打开浏览器演示都可以。这个环节我习惯把每个图表单独存一个 html,最后用 Flask 统一集成或直接用 iframe 放进页面。

4.3 用 Flask 把图表整合成 Dashboard 的可选路径

如果不想在答辩时开五个 HTML 页来回切换,可以在项目里加一个 Flask 应用,把多个图表的 HTML 文件通过 iframe 嵌入一个总览页面。这是毕设里最常见的“可视化分析平台”形态,数据存储用 CSV 或 SQLite 都可以,Flask 只负责提供静态文件。

from flask import Flask, render_template import os app = Flask(__name__) @app.route("/") def index(): chart_files = ["区域均价对比.html", "面积总价散点.html", "总价区间分布.html"] chart_files = [f for f in chart_files if os.path.exists(f)] return render_template("dashboard.html", charts=chart_files) if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=False)

这里有个经验:开发时 debug=False 不是让你看不到报错,而是 Flask 的调试器会暴露本机端口,答辩环境里跑外部设备演示时容易造成安全隐患。真正调 bug 时我用 print 加日志,不用 debug 模式。还有,iframe 引用本地 HTML 直接用相对路径是可行的,Flask 默认的 static 路径不好管时,把图表的 html 放到 templates 同级目录下,再用 url_for 动态生成地址会更规范,不过毕设规模下,用 os.path.exists 判断文件存在与否已经能应付答辩现场。

联动组件不是必须的,但拖一个区域下拉框联动图表,会让整个项目看起来有增量价值。常见实现是用 Flask 路由传参数,后端重新筛选 DataFrame,再 render 对应图表,这样做成本可控,也适合回答“你做了哪些功能”的问题。

5. 二手房采集可视化避坑:反爬、乱码与浏览器兼容问题排查

5.1 现象:列表页爬下来为空,连服务器返回的 HTML 都不对劲

抓包时直接打印 resp.text,发现内容变成了几个 base64 的图片或“访问过于频繁”提示。原因很简单:访问频率超过对方阈值,触发了人机验证。解决是靠“跨间隔 + User-Agent 轮换”来模拟真人浏览模式,另外把抓取时间调到凌晨或工作日白天。

还有一个比较隐蔽的坑,我踩过:requests 的 Session 持有一个连接池,长时间不关闭会占用端口;当爬取上千个页面时,偶发 connection reset 会导致中断。应对是把连接池上限调小、遇到异常就 sleep 后重试一次:

from requests.adapters import HTTPAdapter session.mount("https://", HTTPAdapter(pool_connections=5, pool_maxsize=10)) for retry in range(2): try: resp = session.get(url, timeout=10) break except requests.ConnectionError: time.sleep(5)

5.2 现象:数据写入 CSV 后 Excel 打开中文乱码

现象是 Excel 里所有中文变成一堆类似“浜”的乱码。原因是 Python 默认 utf-8 而 Excel 默认以 GBK 读取,两者编码体系不一致。解决是 df.to_csv 时指定 encoding="utf-8-sig",这个编码会带 BOM 头,Excel 能正确识别。这是所有用 Python 导出中文 Excel 数据的人必踩的一处细节,答辩时主动说出来反而是加分项。

df.to_csv("clean_lianjia.csv", index=False, encoding="utf-8-sig")

5.3 现象:图表 x 轴标签挤成一条黑线

这类现象出现在 Pyecharts 柱状图 x 轴区域名太多时,默认每个标签都旋转 0 度,几十个区域名叠加在一起。解决方法是增设 axis_label 的 interval 和 rotate 参数,或者直接限定只展示 Top15 区域。我个人更倾向于先排序筛选再画图,既保证图清晰,又避免图表库卡顿。

bar.set_global_opts( xaxis_opts=opts.AxisOpts( axis_label_opts=opts.LabelOpts(rotate=45, interval=0) ) )

5.4 现象:把“建成年代”当整数用,计算楼龄出错

清洗时建成年代可能是“1998年”或“暂无”,直接用 int 会报错。更隐蔽的是把解析后的年份当成字符串和 2025 相减,得到一串乱码。解决方式是统一先清理到干净的数值列,再参与计算。建议在清洗阶段用 pd.to_numeric 强制转换,coerce 参数把异常置为 NaN:

df["建成年代_clean"] = pd.to_numeric(df["建成年代"], errors="coerce") df["楼龄"] = 2025 - df["建成年代_clean"]

这里还有个玄学:如果数据源给的是“建成年代:1998年”这种含中文的字符串,to_numeric 直接返回 NaN,必须先正则抽出数字,这就是前面清洗函数里先 re.search 再转 float 的原因。

6. 进阶与答辩技巧:用最小代价验证分析与数据可信度

评审最常问的一句话是“你的数据怎么证明是对的”。如果只是把爬来的数据画成图,不具备可证伪性,等于没做分析。我会额外加一个“数据抽样核验”脚本,随机抽 20 条记录,把房源编号重新请求一次链家网页,断言标题和总价一致。这个脚本不用跑全量,只需要作为答辩演示的一部分,现场抽 3 条即可。另外,源码包里最好留一个 data_quality_report.xlsx 或 CSV,记录每个字段的空值率、去重数、异常值数,这些都是可量化的可信度证据。

关于交付资料,我建议源码按三个目录组织:crawler、cleaning、dashboard。crawler 放采集脚本和原始 CSV;cleaning 放清洗脚本和最终 DataFrame 结果;dashboard 放 Flask 应用和图表 HTML。全部资料 PPT 文件里放三页最关键的内容:系统架构图、数据采集流程图、可视化效果截图。架构图不追求花哨,用文字框画清楚“Requests 采集 → Pandas 清洗 → Pyecharts 可视化 → Flask 展示”这条链路即可。PPT 页数控制在 12 到 15 页,功能讲清楚,不要贴满代码。

我在讲单个图表时有一个习惯:先展示结论文字,再切图。比如“上海二手房的主力总价段集中在 300 到 600 万之间,占样本的 54%”,然后再显示对应环形图,图片只是佐证,不是主角。

最后写代码时建议给每个文件加注释,特别是解释“为什么 sleep 1.5 秒”这类决策,因为导师看源码时会顺带看注释是否成体系。如果你准备把这个项目放进简历,把数据和可视化复现一遍,能跑通比会讲概念重要得多。希望这个方向能帮你在毕业设计里少走弯路,做出一个真正能用、能解释、能演示的作品。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询