☰
二手房数据采集与可视化分析:Python毕业设计实战指南
2026/9/26 14:04:55 网站建设 项目流程

简介:这份资源是面向计算机、通信、人工智能、自动化等专业学生与教师的Python毕业设计完整项目包,围绕二手房数据采集与可视化分析展开,可用于毕业设计、期末课程设计或课程大作业,也适合作为小白入门与进阶练习的实战案例。压缩包共157个文件,约40.01MB,包含18个py源码文件、18个csv数据集、15个html页面、11个js脚本及65个png图表,另有txt说明、ini配置、ttf字体与pptx答辩材料,覆盖数据采集、清洗、存储、可视化到论文资料的全流程。项目代码经过调试测试,答辩评审分达98分,可直接运行。已有177人学习下载,读者能获得完整赛题方案、原始与清洗后的二手房数据、可视化图表、论文配套资料及清晰的目录结构,基础较好的同学还可在此基础上修改调整,实现不同功能。

1. 二手房数据采集与可视化:一个能写进简历的毕业设计选题

二手房数据采集及可视化分析,说白了就是三件事:把房源信息从公开页面上抓下来、清洗成结构化表格、再用图表把价格分布和区域差异讲清楚。这个选题在计算机毕业设计里经久不衰,原因很实在——数据源公开、技术栈成熟、可视化效果直观,答辩时老师一眼就能看懂你在做什么。适合的人群也很明确:Python 刚入门、想做一个完整项目练手、同时需要一份能写进简历的本科或专科毕业生。我见过太多人卡在第一步:环境没配好、请求被拒、数据存不进数据库。这篇笔记就按我实际带学生做项目的路径,从环境搭建一路讲到可视化出图,把每个环节的参数和坑都摊开说。

2. 环境搭建与数据采集:从零把房源数据抓下来

2.1 Python 环境与依赖库的版本选择

很多人第一步就翻车:python安装教程看了一堆,结果装完发现 pip 用不了,或者 vscode python环境配置对不上。我的建议是直接用 Python 3.10 或 3.11,这两个版本对爬虫和可视化库的兼容性最稳。不要用 3.12 以上的最新版,部分可视化库的 wheel 包还没跟上,装的时候会报编译错误,新手根本处理不了。

安装时务必勾选“Add Python to PATH”,这一步漏了后面所有命令行操作都会提示“python 不是内部或外部命令”。装完之后在终端里跑一遍确认:

python --version pip --version

两条命令都能正常输出版本号,才算环境通了。如果 pip 版本太旧,先升级:

python -m pip install --upgrade pip

接下来装核心依赖。二手房数据采集和可视化分析通常需要这几类库:请求库、解析库、数据处理库、存储库、可视化库。我一般用一条命令批量装:

pip install requests beautifulsoup4 lxml pandas matplotlib seaborn jieba wordcloud pymysql sqlalchemy

这里解释一下每个库的角色。requests 负责发 HTTP 请求拿页面内容;beautifulsoup4 配合 lxml 解析 HTML 提取房源字段;pandas 做数据清洗和表格操作;matplotlib 和 seaborn 出统计图;jieba 加分词,wordcloud 做词云;pymysql 和 sqlalchemy 负责把数据写进 MySQL。版本方面不用刻意锁死,pip 默认拉最新稳定版即可,但如果 pandas 装到了 2.x,后面读 CSV 时encoding参数要留意,默认 utf-8 对中文兼容没问题,但遇到 GBK 编码的文件要显式指定。

提示:如果你用的是 conda 环境,把 pip 换成 conda install 也可以,但部分库在 conda 源里更新慢,建议混合使用,优先 pip。

2.2 目标页面分析与请求头构造

二手房数据采集的第一步不是写代码,是打开浏览器开发者工具(F12),切到 Network 面板,刷新目标页面,找到返回房源列表的那个请求。你要确认三件事:请求方式是 GET 还是 POST、返回的是 HTML 还是 JSON、有没有分页参数。

大部分二手房平台的列表页是服务端渲染的 HTML,直接请求就能拿到房源卡片。但有些平台改成了 AJAX 加载,返回的是 JSON 数据,这时候解析方式完全不同。我一般先看 Response 的 Content-Type:如果是text/html,用 BeautifulSoup 解析;如果是application/json,直接用response.json()处理。

请求头是第二个关键点。不加 User-Agent 的请求大概率被拒,返回 403。我通常构造一个完整的 headers:

import requests import time import random headers = { "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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": "https://example.com/", } def fetch_page(url, retry=3): for i in range(retry): try: resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.text print(f"状态码 {resp.status_code},第 {i+1} 次重试") except requests.RequestException as e: print(f"请求异常:{e},第 {i+1} 次重试") time.sleep(random.uniform(2, 5)) return None

这段代码的逻辑很直白:带 headers 发 GET 请求,超时设 10 秒,失败后随机等 2 到 5 秒再重试,最多三次。timeout参数必须设,不设的话遇到慢响应会一直挂着。random.uniform做随机延迟是为了降低请求频率特征,比固定 sleep 更自然。Referer字段有些平台会校验,加上更保险。

参数说明:retry=3是经验值,超过三次还失败说明要么 IP 被限了,要么页面结构变了,继续重试没意义。timeout=10对大多数房源页面够用,如果目标服务器响应慢可以调到 15。

2.3 房源字段提取与分页采集

拿到 HTML 之后,用 BeautifulSoup 定位房源卡片。假设每个房源是一个div带 classhouse-item,里面包含标题、总价、单价、面积、户型、楼层、小区名、区域等字段。提取逻辑如下:

from bs4 import BeautifulSoup def parse_list(html): soup = BeautifulSoup(html, "lxml") items = soup.select("div.house-item") records = [] for item in items: try: record = { "title": item.select_one("h2.title").get_text(strip=True), "total_price": item.select_one("span.total").get_text(strip=True), "unit_price": item.select_one("span.unit").get_text(strip=True), "area": item.select_one("span.area").get_text(strip=True), "layout": item.select_one("span.layout").get_text(strip=True), "floor": item.select_one("span.floor").get_text(strip=True), "community": item.select_one("a.community").get_text(strip=True), "district": item.select_one("a.district").get_text(strip=True), } records.append(record) except AttributeError: continue return records

这里用select_one而不是find,因为 CSS 选择器写起来更直观。每个字段单独 try 包裹,遇到某个卡片结构不完整就跳过,不让一条脏数据打断整批采集。get_text(strip=True)去掉首尾空白和换行,后面清洗时省事。

分页采集用一个循环拼接 URL:

all_records = [] for page in range(1, 51): url = f"https://example.com/sale/pg{page}/" html = fetch_page(url) if html is None: print(f"第 {page} 页采集失败,跳过") continue records = parse_list(html) if not records: print(f"第 {page} 页无数据,可能已到末页") break all_records.extend(records) print(f"第 {page} 页采集 {len(records)} 条") time.sleep(random.uniform(3, 6))

50 页是常见二手房列表的深度上限,实际跑的时候如果某页返回空列表就提前结束。每页之间等 3 到 6 秒,一天采几百页完全够用,不会给目标服务器造成压力。

3. 数据清洗与存储:把脏数据变成能分析的表格

3.1 价格、面积字段的数值化处理

采集下来的原始数据全是字符串,比如“总价 320 万”“单价 28500 元/平米”“面积 112.3 平米”。直接拿去做统计会报错,必须转成数值类型。这一步是数据清洗里最琐碎但最不能省的环节。

import pandas as pd import re df = pd.DataFrame(all_records) def parse_total_price(text): match = re.search(r"(\d+\.?\d*)", text) return float(match.group(1)) if match else None def parse_unit_price(text): match = re.search(r"(\d+\.?\d*)", text) return float(match.group(1)) if match else None def parse_area(text): match = re.search(r"(\d+\.?\d*)", text) return float(match.group(1)) if match else None df["total_price"] = df["total_price"].apply(parse_total_price) df["unit_price"] = df["unit_price"].apply(parse_unit_price) df["area"] = df["area"].apply(parse_area) df.dropna(subset=["total_price", "unit_price", "area"], inplace=True) df = df[(df["total_price"] > 10) & (df["total_price"] < 5000)] df = df[(df["area"] > 15) & (df["area"] < 1000)]

正则(\d+\.?\d*)匹配整数或小数,兼容“320”和“112.3”两种格式。dropna去掉关键字段缺失的行,然后做业务过滤:总价低于 10 万或高于 5000 万的极可能是录入错误或异常房源,面积低于 15 平米或高于 1000 平米的同理。这两步过滤能去掉大部分噪声,让后面的分布图更干净。

注意:过滤阈值要根据目标城市的实际房价水平调整。一线城市总价上限可以放到 8000 万,三四线城市下限可以降到 5 万。

3.2 去重、缺失值填充与 MySQL 入库

二手房数据采集最容易出现的问题是重复:同一房源在不同页面出现、或者翻页时边界重叠。去重按标题加小区名加面积组合判断:

df.drop_duplicates(subset=["title", "community", "area"], keep="first", inplace=True) df.reset_index(drop=True, inplace=True)

keep="first"保留第一次出现的记录,后面的丢弃。reset_index重建索引,不然后面入库时索引跳号。

缺失值处理分两种:数值字段用中位数填充,文本字段用“未知”填充。但我的习惯是,关键字段缺失直接删行,非关键字段才填充。比如楼层缺失可以填“未知”,但面积缺失这条记录就没分析价值了。

入库用 sqlalchemy 加 pymysql:

from sqlalchemy import create_engine engine = create_engine( "mysql+pymysql://root:password@localhost:3306/house_db?charset=utf8mb4" ) df.to_sql("second_hand_house", engine, if_exists="replace", index=False)

连接串里charset=utf8mb4必须加,不然小区名里的生僻字会报错。if_exists="replace"每次全量覆盖,适合毕业设计这种一次性采集场景。如果是增量采集,改成append并加唯一索引去重。

参数说明:root:password换成你自己的 MySQL 账号密码,localhost:3306是默认地址端口,house_db是数据库名,需要提前建好。

4. 可视化分析:用图表把房价规律讲清楚

4.1 价格分布与区域对比图

可视化分析的核心不是图多好看,是图能回答什么问题。二手房数据最常回答的三个问题:价格整体分布什么样、哪个区域最贵、面积和总价是什么关系。

先看价格分布,用直方图加核密度曲线:

import matplotlib.pyplot as plt import seaborn as sns plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False fig, ax = plt.subplots(figsize=(10, 6)) sns.histplot(df["total_price"], bins=40, kde=True, ax=ax, color="#4C72B0") ax.set_xlabel("总价(万元)") ax.set_ylabel("房源数量") ax.set_title("二手房总价分布") plt.tight_layout() plt.savefig("price_dist.png", dpi=150) plt.show()

font.sans-serif设成 SimHei 是为了正常显示中文,不设的话标题全是方框。axes.unicode_minus设 False 解决负号显示问题。bins=40是经验值,数据量在几千条时 40 个柱子粒度合适,太少看不出分布形态,太多柱子太碎。kde=True叠加核密度曲线,能直观看出分布是单峰还是双峰。

区域对比用箱线图:

fig, ax = plt.subplots(figsize=(12, 6)) order = df.groupby("district")["unit_price"].median().sort_values(ascending=False).index sns.boxplot(data=df, x="district", y="unit_price", order=order, ax=ax) ax.set_xlabel("区域") ax.set_ylabel("单价(元/平米)") ax.set_title("各区域二手房单价对比") plt.xticks(rotation=45) plt.tight_layout() plt.savefig("district_price.png", dpi=150) plt.show()

按中位数排序让图表更有可读性,贵的区域排左边。箱线图能同时看到中位数、四分位距和异常值,比柱状图信息量大。rotation=45防止区域名重叠。

4.2 户型词云与面积-总价散点图

词云适合展示户型、小区名、卖点标签的分布。用 jieba 分词后喂给 wordcloud:

import jieba from wordcloud import WordCloud text = " ".join(df["layout"].dropna().tolist()) words = " ".join(jieba.cut(text)) wc = WordCloud( font_path="C:/Windows/Fonts/simhei.ttf", width=800, height=400, background_color="white", max_words=100 ) wc.generate(words) wc.to_file("layout_wordcloud.png")

font_path必须指向系统中文字体文件,不指定的话中文全是方块。Windows 下 simhei.ttf 一般在C:/Windows/Fonts/目录,Linux 下换成/usr/share/fonts/里的中文字体路径。max_words=100控制显示词数,太多会挤在一起。

面积和总价的关系用散点图加回归线:

fig, ax = plt.subplots(figsize=(10, 6)) sns.regplot(data=df, x="area", y="total_price", ax=ax, scatter_kws={"alpha": 0.4, "s": 15}, line_kws={"color": "red"}) ax.set_xlabel("面积(平米)") ax.set_ylabel("总价(万元)") ax.set_title("面积与总价关系") plt.tight_layout() plt.savefig("area_price.png", dpi=150) plt.show()

alpha=0.4让散点半透明,重叠区域颜色更深,能看出密度。s=15控制点大小,数据量大时点太大会糊成一片。regplot 自动拟合线性回归线,斜率能反映该城市每平米均价的粗略水平。

5. 避坑与排查:采集和可视化中最容易翻车的五个点

5.1 请求返回 403 或空内容

现象:代码跑起来不报错,但resp.text是空字符串,或者状态码直接 403。原因通常是请求头不完整或 IP 被临时限制。解决:先补全 User-Agent、Accept、Accept-Language、Referer 四个字段,再降低请求频率,把 sleep 调到 5 秒以上。如果还不行,换一个时间段再试,不要连续硬刚。

5.2 中文显示为方块

现象:matplotlib 出的图里标题和轴标签全是方框。原因是默认字体不支持中文。解决:在绘图前设置plt.rcParams["font.sans-serif"] = ["SimHei"],Linux 环境下如果没有 SimHei,换成["WenQuanYi Micro Hei"]或系统里已有的中文字体名。词云图单独设font_path参数。

5.3 入库时报编码错误

现象:to_sql执行到一半报UnicodeEncodeError或乱码。原因是连接串没指定 utf8mb4,或者数据库建库时字符集不对。解决:连接串加?charset=utf8mb4,建库语句用CREATE DATABASE house_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。两个地方都对了才不会出问题。

5.4 分页采集提前中断

现象:跑到第 10 页突然返回空列表,但手动打开第 10 页明明有数据。原因是目标页面做了频率检测,短时间请求太多被临时拦截。解决:在循环里加随机延迟,每页间隔 3 到 6 秒,每采 10 页额外多等 10 秒。如果已经被拦,停 10 分钟再跑。

5.5 可视化图表保存后空白

现象:plt.savefig保存的图片打开是白板。原因是savefig写在了plt.show()后面,show 之后画布被清空了。解决:先savefig再show,或者用fig.savefig代替plt.savefig。这个坑很隐蔽,因为代码不报错,只是图没了。

6. 从毕业设计到可展示项目:三个让答辩加分的技巧

第一个技巧是加一个简单的交互式筛选。用 Streamlit 把 DataFrame 包一层,几行代码就能做出区域下拉框和价格区间滑块,答辩时现场演示比静态图有说服力:

import streamlit as st st.title("二手房数据可视化看板") district = st.selectbox("选择区域", ["全部"] + sorted(df["district"].unique().tolist())) price_range = st.slider("总价范围(万元)", 0, 2000, (100, 800)) filtered = df.copy() if district != "全部": filtered = filtered[filtered["district"] == district] filtered = filtered[(filtered["total_price"] >= price_range[0]) & (filtered["total_price"] <= price_range[1])] st.metric("房源数量", len(filtered)) st.metric("平均单价(元/平米)", f"{filtered['unit_price'].mean():.0f}") st.bar_chart(filtered.groupby("district")["unit_price"].median())

跑起来只需要streamlit run app.py,浏览器自动打开。st.metric显示关键指标,st.bar_chart直接出图,不用写 matplotlib 那套配置。

第二个技巧是导出分析报告。用 pandas 的to_excel把清洗后的数据和统计结果写成一个多 sheet 的 Excel,答辩时可以作为附件提交:

with pd.ExcelWriter("house_analysis.xlsx") as writer: df.to_excel(writer, sheet_name="原始数据", index=False) df.groupby("district")["unit_price"].agg(["mean", "median", "count"]).to_excel( writer, sheet_name="区域统计" ) df.groupby("layout")["total_price"].mean().sort_values(ascending=False).head(10).to_excel( writer, sheet_name="户型均价TOP10" )

第三个技巧是给采集脚本加日志。不要用 print,用 logging 写到文件里,答辩时如果老师问“你采了多少条、失败了多少”,直接翻日志比回忆靠谱:

import logging logging.basicConfig( filename="crawl.log", level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) logging.info(f"第 {page} 页采集 {len(records)} 条") logging.warning(f"第 {page} 页采集失败")

这三个技巧花不了多少时间,但能让你的毕业设计从“跑通了”变成“像个正经项目”。我带学生做的时候,凡是加了 Streamlit 看板和日志的,答辩分数都明显高一截。血泪经验是:不要等到答辩前一天才想起来整理代码,采集脚本、清洗脚本、可视化脚本分三个文件放好,每个文件开头写清楚用途和运行顺序,老师翻代码的时候一眼就能看懂。希望帮到你。

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

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

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

立即咨询