简介:一份面向毕业设计及Python爬虫初学者的二手房数据采集与可视化分析项目,覆盖从爬虫编写、数据清洗到图表展示与报告输出的完整流程。项目源码已获导师认可并高分通过,代码经过真实运行测试,功能完整可靠。压缩包共158个文件,包括18个Python脚本、18个CSV数据文件、65张图表PNG、15个HTML可视化页面、8个TXT说明文档、8个INI配置文件及答辩PPT等,整体约40MB,目录结构清晰便于按模块查阅。目前已有1800人学习下载。资源提供ANSI/UTF-8编码及不同记录条数的多版本原始数据,同时配有清洗后数据,便于对照理解数据预处理过程;连同详细使用教程和报告PPT,可快速掌握爬虫项目开发思路、可视化展示与答辩汇报要点,适合毕业设计选题或实战技能提升参考。
1. 二手房爬虫可视化毕业设计:先看清这个项目到底在拼什么
不少人第一次拿到“基于Python网络爬虫的二手房数据采集及可视化分析”这个题目时,以为难点在爬虫怎么写。真跑过一遍你会发现,抓数据只是开头,清洗、去重、字段解析和图表叙事才是真正耗时的地方。这个项目要解决的是一个数据闭环问题:用 Python 网络爬虫把公开二手房页面的挂牌数据采下来,落库,再交给可视化分析回答“哪个区域贵”“面积和总价是什么关系”这类具体问题。它适合计算机、大数据相关专业的毕业生,也适合正在转行数据分析的从业者。你不需要发明算法,但必须把采集、存储、清洗、展示四条线全部跑通——这正是这类项目能拿高分毕业设计的原因。
2. 采集与分析链路怎么搭:最小可跑骨架和四个选型理由
2.1 为什么优先选 Requests + BeautifulSoup,而不是一上来就学 Scrapy
做二手房爬虫,第一反应往往是去学 Scrapy。Scrapy 确实是工业级框架,但毕业设计场景下它有明显的副作用:学习曲线陡、项目结构重、调试链路长。你花两周时间搭 Scrapy 的管道、中间件和 Item Loader,可能还没跑到数据清洗环节。
我一般会先让项目跑起来,再谈架构。用 Requests 发 HTTP 请求,用 BeautifulSoup 解析 HTML,这是 Python 网络爬虫里最直接、最容易单步调试的组合。等代码能爬到 1000 条房源了,再去了解 Scrapy 的并发和去重机制也不迟。它的核心原理其实很简单,一共四步:
- 构造 HTTP 请求,带上浏览器身份信息;
- 接收服务器返回的 HTML 或 JSON;
- 用解析器从响应里抽出目标字段;
- 把字段按统一格式写入存储。
理解这四步,比熟练背诵某个框架的 API 重要得多。爬虫领域有个说法叫“爬虫的原理很简单,复杂的永远是页面和反爬”,你没有必要在入门阶段就被框架本身绊住。下面是一个典型的毕业设计源码目录结构,我建议按这个思路组织你的项目:
second_hand_house/ ├── crawler/ │ ├── spider.py # 列表页与详情页采集 │ ├── parser.py # 字段解析与清洗 │ └── utils.py # 请求头、延时、重试工具 ├── data/ │ ├── raw/ # 原始 CSV 数据 │ └── processed/ # 清洗后的数据 ├── analysis/ │ ├── clean_data.py # pandas 清洗脚本 │ └── visualize.py # PyECharts 图表生成 └── output/ └── report.html # 可视化报告这个结构的优点是边界清楚:爬虫只管采集,分析只管画图,数据和输出解耦。后面你换目标站点、换图表主题,都不需要把脚本推倒重来。
2.2 最小爬虫骨架:请求头、随机延时与字段解析
先写一个最简版本,不追求完整功能,目标是把一个页面的核心字段抓下来。这里以公开二手房页面的列表项为例子,真实项目中你需要按目标站点的 HTML 结构调整选择器。
import random import time import requests from bs4 import BeautifulSoup # 请求头尽量接近真实浏览器,缺了它很容易被识别为脚本请求 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", } def fetch_house_list(page_url): """抓取列表页并返回 BeautifulSoup 对象""" # timeout 分别设置连接超时与读取超时,避免页面卡死拖垮进度 resp = requests.get(page_url, headers=HEADERS, timeout=(5, 10)) resp.raise_for_status() # 状态码非 2xx 时直接抛异常,方便排查 resp.encoding = resp.apparent_encoding # 防止中文乱码 return BeautifulSoup(resp.text, "html.parser") def parse_house_item(item): """从单个房源节点里提取字段,字段缺失返回占位符""" title = item.select_one(".house-title") price = item.select_one(".total-price") unit_price = item.select_one(".unit-price") area = item.select_one(".house-area") return { "title": title.get_text(strip=True) if title else "未知标题", "总价": price.get_text(strip=True) if price else "暂无标价", "单价": unit_price.get_text(strip=True) if unit_price else "暂无标价", "面积": area.get_text(strip=True) if area else "暂无面积", } if __name__ == "__main__": soup = fetch_house_list("https://example.com/ershoufang/pg1/") for item in soup.select(".house-item"): print(parse_house_item(item)) # 每次请求之间随机休息 1~2 秒,是对目标站点的基本礼貌 time.sleep(random.uniform(1, 2))这段代码的关键不是选择器写法,而是三个容易被忽略的参数:timeout用元组同时限制连接和读取时长,避免某一个慢响应把整个采集流程拖死;resp.apparent_encoding用于处理中文字符集,比直接用resp.encoding更可靠;random.uniform让请求间隔不是固定值,避免形成规律的访问特征。运行这段代码时如果拿到空列表,先打印resp.status_code和Soup的前 200 个字符,判断是页面结构变化还是被反爬拦截,而不是盲目改选择器。
2.3 数据落库:CSV 与 SQLite 两种方案的分工
采集到的数据要落盘,常见做法是 CSV 和 SQLite 二选一。很多人直接全部塞进 MySQL,但对毕业设计来说这是过度设计。下面这张表是实际选择时的判断依据:
| 存储方案 | 优点 | 适用场景 | 注意事项 |
|---|---|---|---|
| CSV | 可直接用 pandas 读取,Excel 也能打开 | 数据量几千条、一次跑完即可 | 必须用utf-8-sig编码,否则 Excel 打开中文乱码 |
| SQLite | 支持 SQL 查询、增量去重、字段约束 | 需要多次采集、需要按条件筛选 | 单文件数据库,迁移方便,无需安装数据库服务 |
| MySQL | 并发写入好 | 数据量几十万条以上 | 需要本地装服务,对毕设项目偏重 |
我的习惯是:原始数据先写 CSV 留底,因为它是审计痕迹,后面清洗出问题还能回看原始字段;清洗后的数据再进 SQLite,因为去重和聚合查询用 SQL 写起来更直观。
-- 建表语句:房源唯一标识使用 id 字段,并用 UNIQUE 约束防止重复采集 CREATE TABLE IF NOT EXISTS house ( id TEXT PRIMARY KEY, title TEXT NOT NULL, total_price REAL NOT NULL, unit_price REAL, area REAL, district TEXT, crawled_at TEXT DEFAULT (datetime('now', 'localtime')) ); -- 插入时带冲突处理:同一房源 ID 再次抓到时直接忽略 INSERT OR IGNORE INTO house (id, title, total_price, unit_price, area, district) VALUES (?, ?, ?, ?, ?, ?);INSERT OR IGNORE是增量采集里最有价值的一句:第二次跑爬虫时不用写复杂的查重逻辑,重复的房源 ID 天然会被挡在库外。REAL 类型的字段存数值,而不是存“800万”“120.5平米”这种带单位的字符串,这是为第 4 章的可视化分析铺路。
3. 二手房目标站点采集落地:接口定位、翻页与反爬节奏
3.1 数据藏在哪个位置:用浏览器“检查”面板找真实请求
很多人拿到一个二手房网站,第一件事是去看页面元素,然后对着 HTML 写选择器。这没有错,但有些页面的 HTML 是后端模板渲染的,有些则是前端异步加载的。异步加载的页面里,列表数据是通过浏览器里的 XHR 请求拿 JSON 返回的,如果你只盯着 HTML 看,永远找不到核心数据。
实际操作上,我一般会打开浏览器的“开发者工具”(F12),切到“网络”(Network)面板,勾选“Fetch/XHR”过滤项,再手动翻一页列表。观察新增的请求,逐个查看“响应”标签页,如果看到 JSON 里直接有房源标题、总价、单价这些字段,那这个站点的数据接口就找到了。这种方式能拿到比 HTML 更干净的结构化数据,解析成本低得多。
需要注意一个现实问题:不是所有站点都方便用这种方式采集。有的接口带签名参数,有的 JSON 里字段含义不直观,这时候回到 HTML 解析反而更省事。判断标准很简单,对比解析 JSON 的代码量和解析 HTML 的代码量,谁短用谁。另外,动手之前先看一眼目标站点的robots.txt和页面底部的数据声明,遵守站点的访问约定,这是一个毕业设计项目应该有的基本素养。
3.2 翻页采集逻辑:从列表页到详情页的URL规律
二手房列表几乎都是分页展示的,URL 规律通常是pg1、pg2、pg3这样的页码参数。百度热搜里很多人搜“python爬虫”其实是想找到这一步的通用写法,翻页逻辑本身不复杂,但要注意页码上限和详情页补全两个问题。
def crawl_list_pages(base_url, max_page=10): """ 按页码采集列表,再进入详情页补全字段。 max_page 不要一次拉满,先跑 3 页看数据质量。 """ all_items = [] for page in range(1, max_page + 1): page_url = base_url.replace("{page}", str(page)) soup = fetch_house_list(page_url) page_items = soup.select(".house-item") if not page_items: print(f"第 {page} 页没有解析到房源,提前结束") break for item in page_items: info = parse_house_item(item) # 列表页可能只给总价和面积,详情页才有挂牌时间和户型 detail_url = extract_detail_url(item) if detail_url: info.update(parse_detail_page(detail_url)) all_items.append(info) # 每页之间随机休息,同时打点日志方便排查进度 print(f"第 {page} 页采集完成,累计 {len(all_items)} 条") time.sleep(random.uniform(1.5, 3.5)) return all_items这段代码里max_page是第一个该调的参数。我踩过的一个坑是直接把翻页上限设到 100,结果爬到第 8 页就被限流。合理的做法是先小步跑通,确认数据没有重复、字段没有残缺,再逐步提高页码上限。列表页和详情页的关系也需要明确:列表页提供的是概览字段,详情页才有完整的核心信息。理想情况是每个列表项都进详情页抓一遍,但这会让请求量放大 10 到 20 倍,所以项目中我通常只在列表页字段不够用时才补详情页。
3.3 反爬下的请求节奏:随机延时、失败重试与冷却等待
反爬是二手房采集绕不开的话题,它不是玄学,而是服务器在判断“对面是人还是脚本”。判断依据主要是请求频率、请求头完整性和行为规律。一个正常用户不可能每 0.5 秒点一次下一页,也不可能连续 20 个小时不休息。
def request_with_retry(url, max_retries=3): """带失败重试的请求函数,遇到限流状态码时递增等待""" for attempt in range(max_retries): try: resp = requests.get(url, headers=HEADERS, timeout=(5, 10)) if resp.status_code == 429: # 429 表示请求太频繁,等待时间随重试次数递增 wait_time = 30 * (attempt + 1) print(f"触发限流,等待 {wait_time} 秒后重试") time.sleep(wait_time) continue resp.raise_for_status() return resp except requests.RequestException as e: print(f"请求失败:{e},重试 {attempt + 1}/{max_retries}") time.sleep(5 * (attempt + 1)) return None429状态码是服务器在明确告诉你“太快了”。这时候继续硬闯没有意义,等待时间要按重试次数递增,比如第一次等 30 秒,第二次 60 秒。5 秒的连接超时也要保留,防止某个请求把整个采集流程卡死在半路。真正的反爬对策是把请求间隔拉长到人的操作节奏,列表页之间休息 2 到 4 秒,详情页之间休息 1 到 2 秒,让一天的采集量控制在几千条以内。这个量级已经足够支撑一套毕业设计的分析,完全不需要追求高并发。
4. 数据清洗与可视化分析:把二手房数据变成答辩结论
4.1 清洗规则:面积、单价、总价的字符转数值与去重
爬下来的数据通常是带单位的字符串,比如“800万”“120.5平米”“暂无标价”。这样的数据不能直接画图,pandas 读取后第一步就是类型转换。
import pandas as pd df = pd.read_csv("data/raw/house_raw.csv", encoding="utf-8-sig") # 总价:去掉“万”字并转成数值,"暂无标价"填 NaN df["total_price"] = ( df["总价"] .str.replace("万", "", regex=False) .astype(float) ) # 面积:去掉“平米”单位,同时处理空白字符 df["area"] = ( df["面积"] .str.replace("平米", "", regex=False) .str.strip() .astype(float) ) # 按二手房唯一标识去重,保留第一次采集到的记录 df_cleaned = df.drop_duplicates(subset="house_id", keep="first") # 过滤明显异常值:总面积小于 5 平米或总价低于 5 万的基本是虚假房源 df_cleaned = df_cleaned[ (df_cleaned["area"] >= 5) & (df_cleaned["total_price"] >= 5) ]str.replace里的regex=False是个容易被忽略的参数,不加的话“万”字在某些 pandas 版本里会被正则解析,引发警告。异常值过滤的条件是根据二手房市场常识总结的,真实房源几乎不可能出现 2 平米的住宅,这类数据往往是页面维护时的错误占位。清洗后的数据建议单独输出一个processed文件,源码包里保留清洗前和清洗后两份数据,这样答辩时你可以现场演示“清洗前什么样、清洗后什么样”,这是一个很容易打动评委的动作。
4.2 可视化图表的选型:用 PyECharts 生成可交互 HTML
可视化分析部分,我强烈建议用 PyECharts 而不是 Matplotlib。两者都能画图,但 PyECharts 生成的是 HTML 文件,图表支持鼠标悬停查看数值、缩放、平移,这些交互能力在答辩现场演示时效果比静态图好很多。你只需要把报告文件拷到答辩电脑上,用浏览器打开就可以演示,不依赖 Python 环境。
from pyecharts.charts import Bar, Scatter from pyecharts import options as opts # 区域均价柱状图:横向对比,一眼看出哪个区最贵 bar = ( Bar() .add_xaxis(district_list) # 区域名称列表 .add_yaxis("均价(元/平米)", avg_price_list) .set_global_opts( title_opts=opts.TitleOpts(title="各区域二手房均价对比"), yaxis_opts=opts.AxisOpts(name="元/平米"), ) ) bar.render("output/avg_price_by_district.html") # 面积与总价散点图:用点的分布看两者相关性 scatter = ( Scatter() .add_xaxis(area_list) .add_yaxis("总价(万)", total_price_list) .set_global_opts( title_opts=opts.TitleOpts(title="面积与总价关系"), xaxis_opts=opts.AxisOpts(name="面积(平米)"), yaxis_opts=opts.AxisOpts(name="总价(万)"), ) ) scatter.render("output/scatter_area_price.html")散点图里如果大部分点集中在一个带状区域,说明面积是总价的主要驱动因素;如果中心城区明显偏离这条带,说明地段溢价在起作用。这两个图放在报告 PPT 里,能直接支撑“区域差异”和“价格驱动因素”两个分析结论。图表主题、颜色可以调成与你的报告封面一致,这个细节会让人感觉整个项目是成套的,而不是拼凑的。
4.3 图表怎么组织成报告PPT:一图一结论的叙事顺序
可视化分析做完之后,很多人的报告 PPT 变成图表的堆砌。评委看 PPT 时最想知道的是“你从数据里看出了什么”,而不是“你用了什么库”。所以每张图表旁边一定要配一句结论,且结论只能从图上直接读出来。
| 图表 | 从图中读出的一句话结论 | 可作为答辩回答 |
|---|---|---|
| 区域均价柱状图 | 中心城区均价是近郊的 1.6 倍 | 地段是二手房定价的第一因素 |
| 面积-总价散点图 | 60-90 平米房源集中在中低总价段 | 刚需面积段供应充足 |
| 总价分布直方图 | 300-500 万是主流成交区间 | 该城市二手房以改善型需求为主 |
| 涨幅对比折线图 | 近三个月近郊均价涨幅高于中心城区 | 城市扩张带动近郊市场升温 |
这个表里的规律都是典型结论,你拿自己的数据跑完后大概率会得到类似但数值不同的版本。组织的顺序建议是:先放数据规模的说明,再放区域对比,然后放面积与总价关系,最后放一个趋势性的结论。每一步都从图表展开,而不是先写一大段文字再贴图。这也是报告 PPT 里“分析”和“展示”的根本区别:图表是证据,结论是观点,两者要挨在一起。
5. 采集与可视化路上的常见坑:现象、原因、修复顺序
5.1 翻车场景:请求头缺了 Referer,拿到的是空壳页面
现象:爬虫返回 200 状态码,但解析出来title字段全是空的,打印 HTML 发现只有框架标签,核心内容不在里面。
原因:有些站点的服务器会校验请求头里的Referer字段,它表示“你是从哪个页面跳转过来的”。脚本直接请求详情页时没有携带这个字段,服务器选择性返回空壳模板。
解决:在请求头里补上"Referer": "https://example.com/ershoufang/",让请求看起来像从列表页跳转进入的。这也解释了为什么请求头要统一封装在一个HEADERS字典里而不是散落在各处,因为反爬头字段往往是一整套一起校验的。
5.2 翻车场景:解析“暂无标价”导致类型转换崩溃
现象:清洗时astype(float)报ValueError: could not convert string to float,程序中断。
原因:房源页面上有大量“暂无标价”“价格待定”等占位文本。爬虫采集时把字符串原样存了下来,清洗阶段直接转数值必然崩溃。
解决:在清理阶段先把"暂无标价"、"待定"、""统一替换成None,并给总价单价字段指定为可空类型。这里有一个实际经验:不要用 0 填充缺失值,因为 0 在后面的分析里会被当成真实成交价,扭曲平均值。缺失就让它缺失,pandas 绘图时会自动跳过空值。
5.3 翻车场景:翻页爬到第 5 页就收到 429 状态码
现象:前 4 页数据正常,第 5 页开始连续 429,之后所有请求都返回 503。程序没有崩溃,但后面全是无效数据。
原因:固定 1 秒的定时请求形成了机器行为特征,服务器对单 IP 的请求频率阈值被触发。第六页之后触发了更严格的风控。
解决:把固定延时改成随机延时,间隔范围调大,比如 2 到 5 秒之间随机取;重试逻辑里遇到 429 至少等待 60 秒。最佳实践是每次启动爬虫前先确认当前网络出口的请求频率已经冷却,而不是直接把程序挂在那里跑。
5.4 翻车场景:CSV 用 Excel 打开字段中文全部乱码
现象:采集和清洗完全正常,print到控制台也正常,但用 Excel 打开生成的 CSV 文件后,小区名、区域名全成了“锟斤拷”之类的乱码。
原因:CSV 默认用utf-8编码写入,而 Windows 版 Excel 读取时默认按gbk解码,两者不匹配。这是数据采集项目里最常见的编码坑之一。
解决:写入 CSV 时统一使用encoding="utf-8-sig"。多出来的 BOM 头是 Excel 识别 UTF-8 的标记,不会影响 pandas 读取。代码演示如下:
df.to_csv("data/processed/house_cleaned.csv", index=False, encoding="utf-8-sig")5.5 排查思路:打印状态码和异常堆栈,别让程序变成黑匣子
现象:爬虫跑着跑着停了,日志只有一句Process finished with exit code 0,既没有输出错误,也没有打印数据进度。
原因:代码里把所有异常都吞进了except块,也没有在任何循环节点打印当前页码和数据条数。一旦出错,你并不知道它停在哪一步。
解决:每个请求函数至少返回状态码,每个翻页循环打印“当前在第几页、累计多少条”,异常捕获时把traceback.format_exc()打出来。这里有一条血泪经验:爬虫项目调试成本最高的部分不是写爬虫,而是定位“它到底停在哪”。一个可读的日志加上稳健的异常处理,比任何花哨的框架都管用。如果页面结构变了,直接去对比第一次成功时的响应文本和现在的差异,通常是某个 HTML 层级被调整了。
6. 让源码项目从“能跑到能答辩”:增量采集和自检清单
增量采集是这个项目收尾时最值得加的一层能力。简单说就是爬虫第二次运行时不要重复抓取已经入库的房源,做法是启动时先把现有house_id集合加载进内存,遇到重复 ID 直接跳过。这样每天定时跑一次,数据会持续增长,你的报告 PPT 里也能多一张“数据随时间累积”的过程说明图。
-- 增量采集前取已有 ID 集合,交给爬虫脚本去重 SELECT id FROM house;自检清单我建议按这个顺序执行:先检查采集脚本能否独立跑通 3 页;再检查清洗后 CSV 的行数和字段非空率;最后检查两个 HTML 图表能否在浏览器里正常打开。演示顺序上,先跑清洗脚本,再跑可视化脚本,最后打开report.html,评委看到的是一个完整的分析流程而不是一张静态截图。整个项目里我摔得最多的不是爬虫本身,而是“本地能跑,换个电脑跑不起来”的环境问题。后来我习惯了在源码包里附带requirements.txt并在教程里写清楚 Python 安装和依赖导入的步骤,这也是这类毕业设计源码包里“使用教程”最该承担的角色。希望这一套从采集到可视化的完整链路,能帮你把项目真正做成可以复现、可以演示、经得起追问的作品。
本文还有配套的精品资源,点击获取