简介:一份面向毕业设计的Python二手房数据采集与分析项目,压缩包内含完整源码和PPT演示文档。项目从真实网页中抓取房源信息,覆盖请求发送、页面解析、字段提取、数据清洗、统计分析与可视化展示等环节;通过爬虫框架高效获取位置、面积、户型、单价、总价等字段,并对空值、异常值进行处理。分析阶段基于清洗后的数据完成区域供求、价格水平、户型结构等统计,借助常用可视化库绘制分布图、对比图和占比图,直观反映市场特征。压缩包共158个文件、约40MB,主要包含18个爬虫脚本、18个CSV数据文件(含原始与清洗后多版本)、15个HTML展示页面、65张PNG结果图表和1份PPT演示文档。源码模块划分清晰,便于二次修改和学习;PPT对项目背景、实现过程和最终成果做了系统梳理,可直接用于毕业设计汇报。已有52人学习/下载,适合计算机相关专业学生作为课程设计或毕设参考,也可供爬虫与数据分析初学者动手实践。
1. 毕业设计选 Python 爬虫做二手房数据可视化,别只看房价
链家和贝壳这类平台上,一个城市几万套挂牌房源字段高度结构化:小区、户型、面积、朝向、楼层、总价、单价、关注人数。这个数据量级对爬虫和可视化来说,既不像电商评论那样需要处理反爬极强的验证码,也不像微博热搜那样字段混乱,恰好适合在毕业设计周期内从零到一完整跑通。很多人以为这个题目的核心是“爬得多快”,实际上在答辩现场,评委更在意的是原始数据是否干净、分析结论是否自洽、图表能不能回答“这个城市的二手房价格到底受什么因素影响”。
这篇东西按我自己做类似项目的路径来讲:从 Python 环境与虚拟环境配置开始,到爬虫模块的字段设计、反爬参数调整,再到用 Pandas 做清洗、用 Matplotlib 和 PyECharts 做静态与交互可视化,最后落在 PPT 演示和论文配图上。源码和文档的组织方式会穿插在各章里,因为毕业设计交的不只是代码,还要让老师能顺着目录看懂你在每一步做了什么。
2. 开发环境与源代码组织:Python 版本、虚拟环境和项目结构
很多人拿到“含源代码”的参考项目后,第一件事就是双击运行,结果报错一堆 ModuleNotFoundError。原因通常不在代码本身,而是解释器版本和依赖库版本不匹配。先说清楚环境怎么搭,再谈怎么组织代码,这样后面写爬虫和画图时不会因为环境问题反复打断。
2.1 Python 安装与 VS Code 环境配置
Python 版本建议直接上 3.10 或 3.11。3.12 之后的版本对部分第三方库的二进制 wheels 支持晚一些,遇到 lxml、pandas 装不上时反而浪费时间。官网下载安装包时勾选 “Add Python to PATH”,这一步能省掉后面一堆“python 不是内部或外部命令”的报错。
python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate pip install requests beautifulsoup4 lxml pandas matplotlib pyecharts flask激活后命令行前缀会出现(venv),说明当前 shell 已经切到虚拟环境。这样做的意义是:不同项目依赖互相隔离,毕业设计文件夹打包给老师时,只要附带 requirements.txt,对方在新机器上pip install -r requirements.txt就能复现环境,避免“在我电脑上能跑”的尴尬。
requests 用于发起 HTTP 请求,获取房源列表页和详情页 HTML beautifulsoup4 解析 HTML 并提取字段 lxml BeautifulSoup 的解析器,比 Python 内置 html.parser 快数倍 pandas 数据清洗、过滤、分组聚合 matplotlib 生成论文里用的静态图表(柱状图、散点图、直方图) pyecharts 生成可交互的 HTML 图表,放入毕设系统展示更出效果 flask 可选,用来把图表和数据包装成一个本地可视化看板VS Code 里按Ctrl+Shift+P打开命令面板,输入 “Python: Select Interpreter”,选刚才那个 venv 路径下的解释器。这一步不做的后果是:终端里明明安装了包,编辑器右下角却一直报红色波浪线,因为 IDE 还指着系统中另一个 Python。环境变量 VIRTUAL_ENV 的值能帮你确认当前终端是否真的在虚拟环境内,打印一下就知道。
2.2 毕业设计源代码如何按功能拆分
很多毕业设计代码是个几百行的单文件,从 import 到爬虫再到画图全塞一起。这种写法自己调试没问题,写论文时却很难组织章节:第三章讲数据采集、第四章讲数据处理、第五章讲可视化,代码里却没有对应模块,只能硬生生在论文里贴大段代码,答辩效果很差。
second_hand_house/ ├── config.py # 请求头、目标城市、基础 URL、延迟区间 ├── requirements.txt # 依赖清单 ├── spiders/ │ ├── collector.py # 请求列表页、处理翻页和重试 │ └── parser.py # 解析 HTML、提取房源字段、返回结构化字典 ├── analysis/ │ ├── clean.py # 清洗 CSV、类型转换、异常值过滤 │ └── visualize.py # 生成静态图和交互图 ├── data/ │ └── houses.csv # 爬取结果 └── README.md # 运行步骤和模块说明config.py 单独放的意义是:请求头、目标城市、爬取页数这些“会变的东西”和“逻辑代码”分离。比如今天爬杭州,明天改成南京,只需要改 config.py 里的城市拼音和 URL 模板,不用动爬虫逻辑。我在实际项目中会额外加一个delay_range = (1, 3),每次请求之间随机 sleep,既是对目标网站的基本尊重,也是减少 IP 被临时限制的有效手段,后面爬虫章节会细说。
源代码里还要注意路径兼容。用Path(__file__).parent.parent / "data"来定位文件,而不是写死C:/Users/xxx/...,换机器运行才不容易报 FileNotFoundError。
3. 二手房数据采集:从网络爬虫原理到字段解析与翻页
第一个版本别急着上 Scrapy。Scrapy 功能强,但它的信号机制、中间件、Item Pipeline 对初学者来说是个不小的陡坡,而且调试起来不如 requests 直观。毕业设计要的是你能在答辩时讲清楚每一步,requests + BeautifulSoup 足够覆盖大多数需求,等理解了生命周期再去迁移到 Scrapy 也不迟。
3.1 爬虫原理在二手房场景里的具体体现
浏览器输入网址到看到页面,中间发生的事可以简化成:客户端给服务器发 HTTP 请求,服务器返回 HTML 文本,浏览器把 HTML 渲染成页面。爬虫做的事情就是跳过“渲染”这一步,直接拿 HTML 文本,然后用解析库把需要的字段抠出来。二手房的列表页大多在服务端直接渲染,Chrome 开发者工具里按 F12 查看 Elements,能看到完整的房源标题、价格、户型,这意味着不需要 Selenium 这种重型工具去模拟浏览器,直接 GET 请求就能拿数据。
一个常见误判是“Requests 拿到的 HTML 和浏览器里看到的不一样”。原因通常在于列表页有一部分数据是懒加载的,比如缩略图、小区均价地图。解决办法不是立刻换工具,而是先在 Network 面板里看 XHR 请求,如果页面滚动时出现了额外的 JSON 接口,那就直接请求那个接口,解析 JSON 比解析 HTML 更稳定。
3.2 用 requests 和 BeautifulSoup 抓取列表页的最小实现
这里以贝壳/链家风格的房源列表页为例,写一个能直接改城市跑通的最小采集脚本。
import random import time import requests from bs4 import BeautifulSoup def fetch_page(city: str, page: int) -> str: url = f"https://{city}.lianjia.com/ershoufang/pg{page}/" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0.0.0 Safari/537.36", "Referer": f"https://{city}.lianjia.com/ershoufang/", } resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text def parse_list(html: str) -> list[dict]: soup = BeautifulSoup(html, "lxml") items = [] for li in soup.select("ul.sellListContent li"): title = li.select_one(".title a") house_info = li.select_one(".houseInfo") total_price = li.select_one(".totalPrice span") unit_price = li.select_one(".unitPrice span") if not all([title, house_info, total_price, unit_price]): continue items.append({ "title": title.get_text(strip=True), "info": house_info.get_text("|", strip=True), "total": total_price.get_text(strip=True), "unit": unit_price.get_text(strip=True), }) return items代码里有三个关键参数值得解释:
headers里的 User-Agent 决定服务端把你当成浏览器还是爬虫。只写python-requests会遇到非常高的概率被返回验证页;Referer 表示请求是从哪个页面跳转来的,很多反爬规则会校验这个字段。timeout=10限制单个请求最大等待时间,防止某个 IP 被限速后线程卡死。raise_for_status()会在返回状态码为 4xx/5xx 时主动抛异常,比自己判断if resp.status_code != 200更简洁。
lxml解析器比默认的html.parser在复杂 HTML 上快得多,但需要单独 pip install 安装。.get_text("|", strip=True)里的竖线分隔符很重要——二手房列表页的houseInfo字段通常长这样:2室1厅 | 89.5平米 | 南 北 | 精装 | 中楼层(共18层) | 2020年建,用竖线分隔后方便后续劈列。
3.3 反爬应对策略:延迟、重试和字段丢失处理
爬虫写得快,不如写得稳。我在实际项目里给采集器加了三级处理逻辑:单页请求失败重试 3 次,每次间隔递增;连续失败 5 次就停 60 秒再继续;解析结果为空时记录日志而不是直接崩溃。这几个参数直接用 sleep 控制就行,不需要引入复杂的重试库。
def crawl_with_retry(city: str, max_pages: int = 20): data = [] for page in range(1, max_pages + 1): for attempt in range(3): try: html = fetch_page(city, page) items = parse_list(html) if not items: print(f"第{page}页解析为空,可能被重定向到验证页面") data.extend(items) break except requests.RequestException as e: print(f"第{page}页第{attempt + 1}次请求失败: {e}") time.sleep((attempt + 1) * 5) time.sleep(random.uniform(1, 3)) return datarandom.uniform(1, 3)让每次请求间隔落在 1 到 3 秒的随机区间内,而不是固定 sleep 2 秒。固定间隔很容易被访问频率统计识别出来,随机间隔更接近人工操作。这里的max_pages在毕业设计中建议控制在 20 页以下,数据量已经足够支撑分析,同时减少对目标站点的压力。不要试图爬全站几十万条数据,论文里的分析结论靠抽样就能说明问题。
数据落地时注意编码:
import pandas as pd df = pd.DataFrame(data) df.to_csv("data/houses.csv", index=False, encoding="utf-8-sig")utf-8-sig会在文件开头写入 BOM,Excel 打开时中文不会乱码。如果直接用普通utf-8,Windows 下 Excel 会按 ANSI 解析,出现“链家”变成“閾惧”这类问题。
4. 数据清洗与可视化分析:Pandas 预处理、房价分布和区域对比
爬下来的 CSV 是给电脑看的,不是给人看的。二手房数据的清洗主要集中在三类问题上:文本字段混在一起需要拆分、价格字段带单位需要转型、个别房源价格异常高或异常低需要过滤。直接拿原始数据画图,图里的坐标轴会出现 “300 万 500 万” 这种没法计算的文本,Matplotlib 也会因为字符串数据无法排序而报错。
4.1 用 Pandas 拆字段、类型转换和异常值过滤
先把原始数据按字段拆开。链家的houseInfo是竖线分隔的,用str.split拆成列表再取值:
import pandas as pd df = pd.read_csv("data/houses.csv") info_split = df["info"].str.split("|", expand=True) df["bedrooms"] = info_split[0].str.extract(r"(\d+)室").astype(float) df["hall"] = info_split[0].str.extract(r"(\d+)厅").astype(float) df["area"] = info_split[1].str.replace("平米", "").astype(float) df["orientation"] = info_split[2].str.strip() df["decoration"] = info_split[3].str.strip() df["floor"] = info_split[4].str.strip()正则(\d+)室从 “2室1厅” 中提取数字 2,astype(float)把文本转数值。这里的核心坑是expand=True拆分出的空列:不是每套房源的房源信息字段段数都一样,老小区可能缺装修信息,这时对应列是None,后面算均值时 Pandas 会默认忽略 NaN,不会报错,但如果你想检查缺失率,可以跑一下df.isnull().mean()。
价格字段的处理方式类似,把单位去掉再转 float:
df["total_price"] = df["total"].str.replace("万", "").astype(float) df["unit_price"] = df["unit"].str.replace("元/平", "").astype(float) # 过滤明显异常:单价低于1万或高于15万 df = df[(df["unit_price"] > 10000) & (df["unit_price"] < 150000)] df = df.dropna(subset=["area", "total_price", "unit_price"])dropna的subset参数指定只看这三列,缺任何一列就删除整行,避免把不完整的记录画进图里。异常值的阈值取决于城市:一线城市中心区单价超过 15 万/平米是存在的,但出现在普通挂牌列表里大概率是别墅或商业地产误挂,直接过滤掉反而让分析更聚焦普通住宅。
4.2 区域均价对比:Matplotlib 柱状图与中文字体设置
清洗完就可以分组统计了。按区域算均价和挂牌量,是两个最基本的分析维度:
region_stats = df.groupby("region").agg( avg_unit_price=("unit_price", "mean"), avg_total_price=("total_price", "mean"), house_count=("title", "count") ).sort_values("avg_unit_price", ascending=False)groupby后返回的 DataFrame 里,列名由(原列名, 聚合函数)组成,用agg传入命名元组可以让结果的列名直接叫avg_unit_price。sort_values按均价降序排列,方便画图时直接取前 10 个区域。
画图时中文字体是经典翻车点。Windows 自带 SimHei,macOS 没有;Linux 服务器上更可能只看到方框。标准做法是先用matplotlib.font_manager找系统里可用的中文字体:
import matplotlib import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["WenQuanYi Zen Hei", "Noto Sans CJK SC", "SimHei"] plt.rcParams["axes.unicode_minus"] = False top10 = region_stats.head(10) plt.figure(figsize=(12, 6)) plt.bar(top10.index, top10["avg_unit_price"]) plt.title("各区域二手房挂牌均价(前10)") plt.ylabel("单价(元/平米)") plt.xticks(rotation=45) plt.tight_layout() plt.savefig("output/region_avg_price.png", dpi=150)axes.unicode_minus设为 False 是为了让坐标轴上的负号正常显示,虽然价格数据没有负数,但设置掉又不是坏事。tight_layout()自动调整子图参数,避免横坐标文字被截断。dpi=150是论文插图的最低标准,答辩屏幕投影时不会糊。
4.3 面积与总价关系:散点图和相关系数
二手房分析里面积和总价的关系几乎必问,适合用散点图呈现:
import numpy as np plt.figure(figsize=(10, 6)) plt.scatter(df["area"], df["total_price"], alpha=0.4, s=10) plt.xlabel("面积(平米)") plt.ylabel("总价(万)") plt.title("二手房面积-总价散点图") # 拟合线性趋势线 slope, intercept = np.polyfit(df["area"], df["total_price"], 1) x_line = np.linspace(df["area"].min(), df["area"].max(), 100) plt.plot(x_line, intercept + slope * x_line, color="red", linewidth=2) plt.savefig("output/area_total_scatter.png", dpi=150)alpha=0.4降低散点透明度,数据重叠时能看出密度差异;s=10控制点的大小,点太大会把后面的点遮住。np.polyfit返回一次多项式系数,画出来的红色趋势线能直观回答“面积对总价的影响是不是线性的”。
实际操作中你会发现很多城市的面积-总价散点图有明显的拐弯:60 平米以下单价偏高,120 平米以上单价回落。这是因为学区房的面积通常不大,总价被学区溢价抬高;大面积改善房反而在非核心区。这个现象可以作为毕业设计里的一个分析亮点写进论文。
4.4 用 PyECharts 做可交互的 HTML 图表
Matplotlib 图适合放在论文 Word 文档里,但答辩现场展示时,交互式图表的观感会好很多。PyECharts 的用法和 Matplotlib 区别较大,它是链式调用配置项,输出一个独立的 HTML 文件,浏览器打开就能看,不需要启动本地服务。
from pyecharts.charts import Bar from pyecharts import options as opts bar = Bar() bar.add_xaxis(region_stats.head(10).index.tolist()) bar.add_yaxis( "挂牌均价(元/平米)", region_stats.head(10)["avg_unit_price"].round(0).astype(int).tolist(), itemstyle_opts=opts.ItemStyleOpts(color="#d48265"), ) bar.set_global_opts( title_opts=opts.TitleOpts(title="区域二手房均价对比"), yaxis_opts=opts.AxisOpts(name="元/平米"), tooltip_opts=opts.TooltipOpts(trigger="axis"), ) bar.render("output/region_bar.html")PyECharts 的add_xaxis接受的是普通 list,不能直接传 Series;round(0).astype(int)是为了让 tooltip 里的数字不带小数点。trigger="axis"表示鼠标悬停在坐标轴上时显示该轴所有系列的数据,比默认的item更适合对比类图表。
如果你想把图表嵌入到 Flask 里做可视化系统,bar.render()会生成完整 HTML 页面,render_embed()可以返回带有依赖的 HTML 片段,直接作为模板字符串传回浏览器。这个技巧在答辩演示时很加分:老师以为你只画了静态图,结果你打开网页还能悬浮看每一根柱子的数值。
5. 毕业设计收尾:PPT 演示、答辩讲解和源码交付的细节
毕业设计评分通常由三部分构成:论文文本、系统演示、答辩问答。代码写得再好,如果 PPT 上满屏代码模糊不清,或者答辩时被问到“这个功能对应你论文里的哪一节”答不上来,分数都会受影响。以下是我自己带项目时常用的处理方式,可直接参用。
5.1 答辩演示时要避开的三个坑
第一,别在现场跑爬虫。网络状况、目标网站反爬策略、本地代理都会影响实时效果。正确做法是提前把爬虫结果存成 houses.csv,演示时打开数据文件展示字段,然后运行清洗和可视化脚本,这样既展示了“会写爬虫”,又不会因为一次超时让整个答辩尬住。可以在 PPT 里放一段爬虫运行时的截图,证明数据确实是从网站采集来的。
第二,图表要少而精。不要一口气放 20 张柱状图。我一般只留四类图:区域均价对比、面积-总价散点图、户型分布饼图、单价直方图。每张图配一两句结论,比如“从区域均价来看,核心城区比外围板块高约 2.1 倍”“面积-总价在对数坐标下呈线性关系,说明改善型住宅的溢价效应显著”。
第三,源代码交付时记得删掉无用文件和敏感信息。爬虫里如果写死了 Cookie 或者登录凭证,交之前务必清掉。config.py 里的请求头保留没问题,但任何带个人特征的 token 都不该出现在毕业设计里。__pycache__目录、venv 文件夹、爬取的大体积中间文件一并清理,打包体积小,老师也更容易找到入口。
5.2 论文中“爬虫模块”怎么写才不是流水账
很多论文写爬虫时把一整段 urllib 代码贴进去,然后写“本章实现了数据采集”。更合理的写法是先画数据流图,再写出关键函数,每个函数配固定格式说明:输入参数、输出格式、异常处理逻辑。比如fetch_page的输入是城市拼音和页码,输出是 UTF-8 编码的 HTML 字符串,异常时向调用方抛出 RequestException,由上层决定重试还是跳过。处理手段也用逻辑带的词“服务端渲染的列表页直取 HTML;接口渲染的页面则解析 JSON,不能一概用解析库等待页面加载”。
需要在论文中明确说明频率限制策略:time.sleep(random.uniform(1, 3))保证请求间隔不小于 1 秒,这类描述能从规范层面增加学术诚实度。
5.3 PPT 最后一页放什么信息量最大
不需要再来一页“谢谢”或 Q&A。把你在答辩中会被问到的问题和对应回答直接放在最后一页,能显著提高主动掌控感。常见问题包括:数据量多大,字段如何确认抓取正确;反爬机制做到什么程度,会不会触发封锁;为什么选择 Matplotlib 而不是 Seaborn;数据清洗时怎么判断异常值是错误数据还是真实高价。针对每个问题给出简洁回答,列在备注里,PPT 正页只留关键词。
演示时打开output/region_bar.html的交互图表,让鼠标悬停在柱子上展示具体数值,做一次现场演示,比任何口述都有说服力。
本文还有配套的精品资源,点击获取