二手房数据采集与可视化分析:Python爬虫与数据清洗实战
发布时间:2026/10/1 1:58:01来源:尧图网络
简介这是一份基于Python的二手房数据采集与可视化分析毕业设计项目包面向计算机、电子信息、数学等专业学生适用于课程设计、期末大作业或毕业设计参考属于高分项目作品。项目覆盖数据采集、清洗、存储与可视化分析的完整流程压缩包内含155个文件包括18个Python脚本、18个CSV数据集含原始编码与清洗后多版本便于对比预处理效果、15个HTML及11个JS可视化页面、65个PNG图表以及1份报告PPT整体约35.13MB。数据集与脚本目录结构清晰可帮助读者快速理解爬虫采集、数据清洗和可视化展示的工程化实现也可作为二次开发与功能扩展的基础。目前已有840人学习下载适合具备一定Python基础、希望参考完整高分离校毕业设计项目或进行二手房数据分析实战的读者。1. 二手房数据采集与可视化分析为什么这个毕设方向值得做每年毕业季总能看到一批学生栽在选题太大、数据拿不到、图表凑不齐这三个坑里。二手房数据采集与可视化分析这个方向的好处是数据源明确、字段结构规整、分析维度多而且Python生态里爬虫、数据采集、可视化分析三件套刚好全覆盖。我经手过几个类似的项目说句实话爬虫部分通常三天就能跑通真正拉开分数差距的是数据集的质量和可视化能不能讲出故事。这篇笔记就把这条路线完整拆开从用Python写爬虫、清洗字段、构建数据集到最后用图表支撑结论每一步都给出可复现的做法。不管你是想快速交差还是奔着高分去这套路径都经得起答辩追问。2. 技术选型与整体架构用Python生态搭一套可复现的毕设骨架2.1 为什么是Python爬虫、数据清洗与可视化的生态闭环二手房价分析看起来是个数据分析题目实际串起了三条技术线数据采集、数据清洗、可视化分析。三条线各有成熟工具但能用一个语言串起来、文档又多到查不完的Python是最稳的选择。Java用Jsoup也能写爬虫但后续清洗要么转成Python要么用一堆老旧的工具类Node.js的Puppeteer擅长动态页面可数据处理的生态不如Pandas顺手。Python这边Requests加BeautifulSoup负责采集Pandas负责清洗统计Matplotlib、Seaborn、PyECharts负责出图整个链路零切换。对毕设而言语言选型还有一个隐藏考量导师和答辩老师大概率熟悉Python。答辩时你说我用Pandas做了数据清洗老师能跟上思路你说我用Jsoup解析了HTML非Java方向的老师可能直接走神。Python数据采集与可视化分析的参考案例也最多卡壳时搜python爬虫数据采集源码很快能找到对应的代码片段。这条路是被大量人验证过的不是冷门方向出了问题也更容易找到解决方案。从工作量角度看Python还有迭代效率优势。Pandas里一行df.groupby().median()就能算出区域中位数Matplotlib调样式也是分分钟的事。换成Java或C#改一个字段类型都要重新编译毕业设计时间本来就紧把时间花在分析和结论上而不是环境编译上这是最实在的收益。2.2 目标站点与字段设计从二手房列表页反推数据模型我一般建议先定字段再写爬虫不要反着来。以链家这类主流房产信息平台的二手房列表页为例一个典型的房源卡片会暴露这些信息小区名、所在区域、户型几室几厅、面积、朝向、楼层、装修情况、单价、总价、挂牌时间以及房源编号。把这些字段先写进一张表爬虫解析按表取值后面清洗分析不会乱。数据模型建议分两个维度房源维度一套房一条记录和区域维度一个行政区或商圈一组统计。房源表存原始采集值区域表做汇总对比。字段设计大致如下字段名类型示例用途说明房源编号str1012345678平台唯一标识用于去重小区名str阳光花园定位到具体楼盘区域str朝阳-望京行政区加商圈方便分组户型str3室2厅卧室/厅数可拆成数值字段面积float89.6建筑面积平方米单价float68500每平方米挂牌价总价float613单位万元朝向str南北主要朝向可做分类统计楼层str低楼层/共6层拆成楼层序号和总层数两个数值楼层这种字段千万别只存成纯文本。后面分析高楼层是否更贵电梯房溢价多少时你需要的是楼层序号和总层数。在清洗阶段把低楼层/共6层拆成floor1、total_floors6两个字段分析维度瞬间多出好几个。房源编号也要保留它是去重的关键没有编号你只能靠小区面积总价三元组猜测是否重复误杀率很高。2.3 项目目录结构与依赖清单一套能跑通的组合能交差的毕设项目目录结构建议直接参考简化版工业项目second_hand_house/ ├── crawler/ │ ├── spider.py # 爬虫主逻辑翻页、请求、调度 │ ├── parser.py # 页面解析与字段提取 │ └── config.py # 请求头、延时、目标URL统一配置 ├── data/ │ ├── raw/ # 原始采集结果CSV │ └── clean/ # 清洗后的数据集 ├── analysis/ │ ├── clean.py # 清洗脚本 │ └── visualize.py # 可视化脚本 ├── report/ # 图表输出目录 ├── requirements.txt └── README.md依赖清单是另一个容易翻车的地方。不要一股脑装几十个库按实际用到来。我通常只用这些requests、beautifulsoup4、pandas、matplotlib、seaborn、pyecharts。Python版本3.8以上就行开发环境用VSCode装好Python扩展后直接在终端跑脚本调试方式比Jupyter更接近真实工程。环境配置不熟的话搜python安装教程vscode python环境配置基本能解决九成问题剩下的是虚拟环境。创建虚拟环境用python -m venv venv激活后pip install -r requirements.txt别把包装进系统全局后面换机器跑项目会少很多麻烦。requirements.txt内容按下面这个规模写就够了requests2.31.0 beautifulsoup44.12.3 pandas2.1.4 matplotlib3.8.2 seaborn0.13.1 pyecharts2.0.5版本号建议锁死不要用大于号宽松匹配。毕设项目半年后再打开如果不锁版本依赖升级可能导致代码行为变化到时候一边改论文一边查兼容性问题会很狼狈。锁版本是一种低成本高回报的后悔药。3. 数据采集用Requests和BeautifulSoup写一个能跑通的二手房爬虫3.1 最小可用爬虫先抓一个列表页并解析字段动手写爬虫的第一目标不是抓全量而是单页解析成功。先手动用浏览器打开一个二手房列表页右键查看网页源代码找到一条房源记录所在的HTML结构。链家这类平台的列表页极其规律房源被包在某个class的li里字段在对应的span或a标签中。不用看整个页面源码那会把人绕晕直接搜索标题里的文字定位到那一段然后从内往外看它的父节点和兄弟节点。一个最小可用的爬虫长这样import requests from bs4 import BeautifulSoup url https://example.com/ershoufang/ # 替换为目标平台列表页 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, Referer: https://example.com/ } resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding # 避免中文乱码 soup BeautifulSoup(resp.text, html.parser) items soup.select(li.item) # 根据实际页面结构调整 print(本页房源数量:, len(items)) for item in items[:3]: title item.select_one(.title a).get_text(stripTrue) total_price item.select_one(.totalPrice span).get_text(stripTrue) unit_price item.select_one(.unitPrice span).get_text(stripTrue) print(title, total_price, unit_price)这段代码的逻辑是发GET请求拿HTML用CSS选择器定位房源节点再逐个取出字段。resp.encoding这行是防中文乱码的关键——很多页面声明了charset但实际内容可能是GBK或UTF-8不设置encoding就会乱码这是爬虫新手常遇到的黑匣子。items soup.select(li.item)里的选择器必须按实际页面的class去改不同平台、不同城市站的class都可能不同。写这段代码时有个习惯值得养成先把item的个数print出来。长度是0要么选择器写错要么页面被反爬拦截返回了验证页。这时候用item.get_text()打印整个节点内容能快速判断出是结构问题还是拦截问题。先别急着写循环把单页解析彻底跑通再说。3.2 请求头、延时与重试把爬虫调成礼貌模式单页跑通后下一步是让爬虫稳定抓取几百页而不被封。所有主流平台都有反爬最常见的两道坎是UA校验和访问频率限制。UA校验就是检查请求头里的User-Agent是不是正常浏览器Python默认UA是python-requests/x.x.x一眼假直连必被拦。频率限制则是检测同一IP的请求间隔短时间密集请求直接封IP或弹验证码。更稳的做法是用requests.Session统一管理请求头、Cookie和重试策略加上随机延时import time import random import requests from requests.adapters import HTTPAdapter session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) session.mount(https://, HTTPAdapter(max_retries3)) def fetch_page(session, url, max_retry3): for attempt in range(max_retry): try: resp session.get(url, timeout10) if resp.status_code 200: return resp elif resp.status_code in (403, 429): wait random.uniform(5, 10) * (attempt 1) print(f触发限流等待 {wait:.1f}s) time.sleep(wait) else: time.sleep(2) except requests.RequestException as e: print(f请求异常: {e}) time.sleep(3) return None关键参数有三个。max_retry是应用层重试次数这里设3配合sleep递增的退避策略能扛住短时间限流。随机延时范围1到3秒不要固定sleep(2)固定间隔反而容易被频率检测识别。HTTPAdapter(max_retries3)管的是底层网络错误重试比如连接重置、DNS解析失败它和应用层的重试逻辑不冲突两者叠加才会稳。403和429必须区分处理。403大概率是UA、Cookie或Referer没通过校验延续重试也没用应该停下来检查请求头429是请求太频繁说明延时太短把sleep范围调大。还有一种情况是页面返回200但内容里没有房源节点而是验证码页——这也要在fetch_page里做一层判断看到验证码特征就暂停较长时间。3.3 换页与落盘把采集结果存成CSV还是SQLite单页解析跑通、反爬调稳后就到了真正出量的环节翻页采集并落盘。二手房列表页的翻页参数一般是URL上的pg或page字段手动翻到第二页、第三页对比URL就能看出来。翻页循环里要注意最后一页的判断如果请求的页码超过总页数平台会返回空列表页代码里要有退出条件不能在空页上反复请求。落盘我建议边采边存不要全部抓进内存最后一次性写文件。以下代码把每页解析结果追加写入CSVimport csv import time import random def crawl_list(session, base_url, total_pages, output_path): fieldnames [房源编号, 小区名, 区域, 户型, 面积, 单价, 总价, 朝向, 楼层] with open(output_path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() for page in range(1, total_pages 1): url f{base_url}pg{page}/ resp fetch_page(session, url) if resp is None: print(f第{page}页采集失败跳过) continue soup BeautifulSoup(resp.text, html.parser) items soup.select(li.item) if len(items) 0: print(f第{page}页无房源数据可能超出总页数提前退出) break for item in items: row extract_house(item) # 返回字段字典内部做了异常保护 if row: writer.writerow(row) print(f第{page}页完成写入 {len(items)} 条) time.sleep(random.uniform(1, 3)) print(f采集结束文件: {output_path})这段代码的价值在于容错。翻页失败不会中断整体采集只是跳过并打印空页检测避免无效请求翻页后的sleep在页面处理完之后执行保证相邻两次请求之间有间隔。CSV编码用utf-8-sig而不是utf-8这是Windows下Excel能正确显示中文表头的关键很多人到交报告截图时才发现乱码这个坑踩一次就记住了。SQLite作为备选也值得提。如果后续要按区域、价格区间做SQL聚合SQLite确实比CSV方便但毕设场景CSV已经足够因为清洗和可视化都用Pandas读CSV最顺手。你要是想两边兼顾采集阶段直接写CSV分析阶段按需导入SQLite不用在采集阶段陷入数据库设计。采集完成后数据集就有了原始版本记得保留raw目录下的原文件后面清洗出错还有后悔药。4. 数据集构建与可视化分析从清洗字段到产出图表4.1 数据清洗缺失值、类型转换与异常房价爬虫采集回来的数据几乎不可能是干净的。最常碰到的三个问题单价和总价带着元/平米万这样的单位文本面积字段偶尔混入暂无数据同一套房在多次采集中重复出现。不处理干净后面出的图表全是错的。清洗脚本建议单独放一个clean.py不要和爬虫混在一起这样报告里可以单独讲数据预处理章节。清洗第一步是类型转换和去单位import pandas as pd df pd.read_csv(data/raw/houses.csv) # 单价 68500元/平米 - 68500.0 df[单价] df[单价].str.replace(元/平米, ).str.strip().astype(float) # 总价 613万 - 613.0 df[总价] df[总价].str.replace(万, ).str.strip().astype(float) # 面积 89.6平米 - 89.6 df[面积] df[面积].str.replace(平米, ).str.strip().astype(float) print(df.dtypes) print(缺失值统计:\n, df.isnull().sum())str.replace配合astype是标准操作。单位文本在HTML里以字符形式存在不先清洗字段类型是object没法求均值、算相关系数。astype(float)一旦报错说明字段里还有没被replace掉的非数值内容比如暂无数据这时改用pd.to_numeric(..., errorscoerce)转换失败的进NaN再做缺失值处理。异常值要单独过滤。面积10平米卖2000万的记录要么是录入错误要么是特殊产权房源直接参与统计会让均价失真。建议按业务常识圈一个合理范围住宅面积30到500平米、单价5000到20万每平米范围外标记为异常。这里有个血泪经验过滤条件宁宽勿严把数据集砍掉一半答辩时老师问数据量为什么这么少场面会非常尴尬。先保留异常数据分析时用分位数截断做展示而不是清洗阶段就物理删除。4.2 房价分布与区域对比PandasMatplotlib出图清洗完成后先做基础统计再做可视化。基础统计就是describe、value_counts、groupby这三件套能快速判断数据集分布形态。总价分布通常是右偏的少数千万级豪宅会把均值拉高这时候中位数比均值更能代表市场水平。这个判断会决定后面所有的图表选择。第一张图建议做总价分布直方图和区域单价中位数条形图import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 中文字体 plt.rcParams[axes.unicode_minus] False # 负号显示修复 fig, axes plt.subplots(1, 2, figsize(12, 5)) # 左图总价分布去掉两端1%极端值再画 q01, q99 df[总价].quantile([0.01, 0.99]) mask df[总价].between(q01, q99) axes[0].hist(df.loc[mask, 总价], bins30, color#5B9BD5, edgecolorwhite) axes[0].set_title(二手房总价分布排除两端1%极端值) axes[0].set_xlabel(总价万元) axes[0].set_ylabel(房源数量) # 右图区域单价中位数 region_price df.groupby(区域)[单价].median().sort_values() region_price.plot(kindbarh, axaxes[1], color#ED7D31) axes[1].set_title(各区域二手房单价中位数) axes[1].set_xlabel(单价元/平米) plt.tight_layout() plt.savefig(report/基础分布图.png, dpi150) plt.show()这两行rcParams是中文乱图的解药。Matplotlib默认字体不支持中文不设置font.sans-serif图里的区域名全是方块。axes.unicode_minusFalse处理坐标轴负号显示成方块的问题。直方图里做的between(q01, q99)掩码等于把前后1%的极端值挡在图外图看上去就是大多数房源的真实分布。保存dpi设为150以上答辩PPT投影时会放大dpi低了全是锯齿。配色用的蓝橙组合对比度足够色盲也能分辨。4.3 交互式地图与相关性热力图把分析结论可视化静态图适合放论文交互式图表适合放PPT演示。PyECharts是国内用得最多的交互式图表库输出HTML文件浏览器打开能缩放、悬浮、筛选。做区域房价地图时可以用散点图把房源按经纬度撒到地图上颜色深浅表示单价高低。经纬度获取方式是通过地图API把小区名区域转成坐标申请一个免费Key就能调这一步在毕设里是实实在在的加分项。散点地图代码骨架长这样from pyecharts import options as opts from pyecharts.charts import Map data df.groupby(区域)[单价].median().round(0) pairs [[region, float(price)] for region, price in data.items()] c ( Map() .add(区域均价, pairs, maptype北京) # 按实际城市替换 .set_global_opts( title_optsopts.TitleOpts(title北京市二手房区域均价分布), visualmap_optsopts.VisualMapOpts(minint(data.min()) // 10000 * 10000, maxint(data.max()) // 10000 * 10000 10000), ) ) c.render(report/区域均价地图.html)maptype参数必须和实际城市对应地图名称不匹配会渲染空白。visualmap_opts里min和max的设定很关键直接用原始最小最大值颜色映射会被极端值拉成一片深色毫无区分度按万取整再留一点余量图例才有层次。render生成的是独立HTML文件答辩时可以嵌入PPT也可以现场打开浏览器演示动态交互。相关性热力图用Seaborn画把面积、单价、总价、楼层序号这些数值字段放进来import seaborn as sns df[楼层序号] df[楼层].str.extract(r(\d)).astype(float) corr df[[面积, 单价, 总价, 楼层序号]].corr() plt.figure(figsize(8, 6)) sns.heatmap(corr, annotTrue, cmapRdBu_r, vmin-1, vmax1, fmt.2f, linewidths0.5) plt.title(房价影响因素相关性热力图) plt.tight_layout() plt.savefig(report/相关性热力图.png, dpi150)df[楼层].str.extract(r(\d))是从低楼层/共6层里抽出第一个数字这行代码展示了字段设计前瞻性的价值。corr()计算皮尔逊相关系数面积和总价通常是强正相关和单价的关系反而弱楼层与房价的相关性在不同城市差异很大如果算出接近0的系数说明该城市楼层对房价不显著这本身就是一个可以写进论文的结论。热力图让你快速锁定值得展开讲的关系PPT里放这张图配一句面积是总价的最强解释变量比十行文字都管用。5. 避坑与常见问题反爬、字段缺失与数据偏差的排查记录5.1 请求被拒绝状态码403但浏览器访问正常现象爬虫脚本跑起来后返回的状态码一直是403可你用浏览器打开同一个URL完全正常能看到完整房源列表。原因服务器通过User-Agent识别出你是脚本。Python的requests默认UA是python-requests/x.x.x一眼假。另一个隐蔽原因是缺少Referer或Accept-Language请求头服务器对不像浏览器的请求组合直接拒之门外。如果是从详情页跳回列表页Referer没带也会触发校验。解决用requests.Session统一设置浏览器UA并补上Referer、Accept、Accept-Language。设置后先打印resp.status_code确认变成200再跑循环。如果还是403检查Cookie是否失效部分平台需要先访问首页拿到Cookie再爬列表页。不要一上来就怀疑IP被封那是最后才要考虑的因素先按请求头排查九成能解决。写代码时可以在config.py里集中管理这些头配置免得在多个脚本里复制粘贴后期维护会吐血。5.2 字段解析失败页面上有数据但select取出来是空现象用浏览器开发者工具能看到某个span里明明有钱数但BeautifulSoup的select_one取出来是None代码在get_text()抛AttributeError程序中断。原因开发者工具显示的是页面渲染后的DOM而requests拿到的是原始HTML。如果字段是页面加载后由JavaScript动态写入的原始HTML里根本不存在这个节点。另一个常见原因是同一个class在页面里出现多次select_one取到了第一个匹配项但它是隐藏模板或广告位。解决先在爬虫里把resp.text保存成html文件用编辑器打开搜字段文本确认原始HTML里到底有没有。确实没有就是动态渲染两条路一是切到目标页面加载时调用的JSON接口直接请求并解析JSON二是用Selenium驱动Chromium等渲染完成再读取DOM。如果是class重复改用更具体的选择器比如div.houseInfo span:nth-child(2)或者用find_all之后按下标取值。这个排查顺序适用于大多数浏览器有代码没有的诡异问题先确认是不是动态渲染再怀疑选择器。5.3 数据偏差只看挂牌价不看成交量导致结论失真现象分析结果显示某个区域均价奇高和新闻里说的价格阴跌完全相反论文前后矛盾被导师打回来。原因二手房平台上展示的是挂牌价不是成交价。挂牌价是房东的心理预期有一部分房源长期卖不掉价格本来就是虚高的。如果采集时只拿当前在售列表没记录在售套数的变化数据集反映的是卖家预期而不是市场真实水平。这个问题属于结构性偏差靠清洗修不掉只能在数据来源上做说明。解决在数据集说明和论文里明确标注本数据集为挂牌价不代表成交价这是数据诚实性的底线。更严谨的做法是采集时同时记录每个区域的在售套数把在售套数和均价一起分析——在售套数突然增多的区域即使均价没降也说明卖压累积。这个维度在答辩时非常加分因为展示了你对数据背后业务逻辑的理解而不只是会调库函数。5.4 图表误导区域均价被几个豪宅拉偏现象某区域均价算出来8万/平米但直方图里绝大多数房源集中在4到6万图和数字对不上答辩时被老师当场追问。原因用的是算术平均值。少数总价上亿的房源会把均价拉到离谱高度二手房价格分布是典型的右偏分布均值对异常值极其敏感中位数才是稳健的集中趋势度量。解决区域对比一律用中位数图上标注单位元/平米中位数。做分布直方图时用4.2里的between(q01, q99)掩码把两端极端值挡在显示范围外。更严谨的还可以在报告里对比一组数据全样本均值和剔除异常值后的均值告诉读者差值来自哪些小区。这种先给结论、再解释为什么不用均值的处理方式直接反映分析功底是论文质量的分水岭。6. 进阶动态渲染页面的采集方案与结果验证技巧如果目标平台的列表页是纯JavaScript渲染HTML源码里一个房源节点都看不到Requests方案直接失效。常见做法是两条路。第一条是找页面加载时调用的JSON数据接口用浏览器开发者工具的Network面板刷新一下能找到返回房源数组的XHR请求直接请求那个接口解析JSON比解析HTML还省事接口通常在反爬上比HTML页面松。第二条是Selenium驱动Chromium等页面渲染完再读取DOM要装浏览器驱动速度慢、资源占用大只建议作为兜底。毕设场景里优先找JSON接口找不到再考虑Selenium。结果验证是很多人到答辩前才发现的问题图表里的数字和爬虫日志对不上。我的习惯是清洗完先做一轮对账。比如采集5000条原始记录清洗后剩4700条那300条去哪了要能说清楚——180条重复、80条面积异常、40条单价缺失。把这个过程写进报告就是完整的数据质量说明。可视化出图后随机抽三个小区回平台上人工核对挂牌总价确认解析和清洗没有系统性错误。这个习惯让我至少避开了三次答辩翻车。项目到这里源码、数据集、报告PPT就都有了完整的素材底座。PPT不要最后一周才开始做图表每出一张就存一张到report目录写报告时直接引用。如果时间还有富余给交互式地图加上区域筛选和下钻功能答辩演示环节现场点开比任何口头描述都有说服力。这条路线最值钱的不是爬虫代码本身而是你手里那份干净的数据集和对数据的解释能力。希望这篇笔记能帮你把这条毕设路线走顺少踩几个我当年踩过的坑。本文还有配套的精品资源点击获取
网站建设高端定制企业官网