
简介一份基于Python的二手房数据采集与可视化分析毕业设计项目资料面向计算机相关专业学生尤其适合需要完成课程设计、期末大作业或毕设答辩的开发者。项目完整覆盖网络爬虫采集、Pandas数据清洗与预处理、SQLite/MySQL/MongoDB数据库存储以及Matplotlib/Seaborn/Plotly可视化分析等环节可帮助理解二手房房价、面积、地理位置等信息的抓取、清洗与图表展示流程。资料以zip压缩包形式提供大小约48MB内附项目源码及配套PPT演示文件便于直接运行调试也能作为论文撰写和答辩展示的参考素材。目前已有691人学习下载。通过源码与完整流程读者可掌握Requests、BeautifulSoup等爬虫库的实战用法学会对原始数据进行去重、缺失值处理与类型转换并利用多种图表呈现房价分布和区域趋势整套代码结构清晰、路径完整是毕业设计或期末大作业的优质参考。1. 基于 Python 的二手房数据采集及可视化分析这个毕设题目到底在考察什么第一次看到这个题目的人多半觉得它就是“爬虫 画图”的拼盘抓点二手房数据用 pyecharts 画几张均价图再把 PPT 一拼就能交差。实际做完你会发现真正的难点从来不是 urllib 还是 requests 这种选型问题而是“数据抓下来了怎么保证它是干净、口径一致、能支撑结论的”。答辩时评委问你“单价怎么算的”“为什么这个区和那个区均价差这么多”答不上来代码写得再花哨也白搭。这个项目适合两类人一是 Python 语法学过但没做过完整数据链路的本科应届生想用一份能讲清楚“采集—清洗—分析—可视化”全流程的毕设证明动手能力二是已经工作但想补一个高质量练手项目的从业者把平时零散的爬虫和图表技能串成一条主线。说白了它考察的不是你会不会调库而是你能不能从一堆混乱的网页文本里整理出一份经得起追问的分析结果。下面直接按我平时做这类小项目的顺序展开先拆请求与字段设计再讲清洗入库然后落到图表和答辩现场。每段都会给可以直接抄的代码和参数也会把最容易翻车的地方单独拎出来说清楚。2. 二手房数据采集列表页请求、详情页解析与反爬节奏设置2.1 采集链路与选型为什么 requests BeautifulSoup 比 Scrapy 更适合毕设答辩二手房数据采集的常规链路是先抓列表页拿到房源链接和标题再逐个进入详情页补全价格、面积、朝向、楼层、关注人数等字段。选型上有两条路一是 Scrapy 这种重型爬虫框架二是 requests BeautifulSoup 的组合。我的建议很直接毕设选后者。原因有三条。第一Scrapy 的并发调度和中间件机制本身就能讲半小时答辩时你很难三句话说清“这个 request 为什么走这个 pipeline”反而容易被追问卡住。第二二手房站点普遍有基础反爬但单机、低频率、带真实请求头的方式基本够用不需要 Scrapy 的分布式能力。第三requests BeautifulSoup 的代码可读性强导师翻源码时能一眼看懂你在做什么这对“项目源码全部资料”这类交付物来说比性能重要得多。下面这段是列表页请求与解析的最小实现我一般会把字段先打出来确认页面结构再批量跑import requests from bs4 import BeautifulSoup import random import time HEADERS { 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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } def fetch_list_page(city_url, page): 抓取某一页列表城市URL以https://xx.xxxx.com/为例 url f{city_url}pg{page}/ resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding # 避免中文乱码 return resp.text def parse_list_page(html): 从列表页提取房源标题和详情链接 soup BeautifulSoup(html, html.parser) items [] for div in soup.select(div.info): a div.select_one(a.title) if not a: continue items.append({ title: a.get_text(stripTrue), link: a.get(href), }) return items if __name__ __main__: html fetch_list_page(https://xx.xxxx.com/, 1) for item in parse_list_page(html)[:3]: print(item)逻辑说明fetch_list_page负责请求parse_list_page负责解析两者拆开是为了方便中途断点保存。resp.encoding resp.apparent_encoding这行很重要很多中文乱码问题都是因为忽略了它。select_one(a.title)的选择器以你抓的站点实际结构为准我第一次写时直接照抄别人博客的选择器结果解析出来全是 None浪费了半天时间。参数方面timeout10是必须的否则某个页面卡住时整个程序会一直挂在那里不往下走。详情页的解析类似只是字段更多代码量也更长。核心做法是拿列表页的链接逐个请求把每个字段塞进一个字典最后统一append到列表里。这里有一个经验详情页字段不要一次全解析完先取“总价、单价、面积、户型、区域”这五个核心字段跑通再加“朝向、装修、楼层、关注人数”这些次要字段。2.2 字段设计与解析代码把“三室一厅 89.6平米”拆成结构化字段采集之前先把字段表定下来。我比较推荐的二手房数据字段结构如下这份表也可以直接抄进毕设文档的需求分析章节字段名示例值说明titleXX小区 3室2厅 89.6平米列表页标题含冗余信息region天河-体育中心-XX板块区县-板块需要拆分total_price528万挂牌总价带单位unit_price58920元/平平台口径单价area89.6平米建筑面积layout3室2厅户型floor低楼层/共33层楼层/总层数orientation南朝向decoration精装装修程度follow_count100人关注热度指标urlhttps://...房源链接crawl_time2025-04-01 21:00采集时间这里最容易踩的坑是“标题信息重复解析”。比如标题里已经有“3室2厅 89.6平米”详情页里又有独立的户型、面积字段很多新手图省事直接从标题里正则抠数据。短期看没问题但平台调整标题格式后你的清洗脚本会批量失效。正确的做法是标题只作为展示字段保留结构化字段一律从详情页的独立节点里取。详情页解析的示意代码def parse_detail(html, url): soup BeautifulSoup(html, html.parser) total_node soup.select_one(div.total) unit_node soup.select_one(div.unitPrice) area_node soup.select_one(div.area) layout_node soup.select_one(div.layout) record { total_price: total_node.get_text(stripTrue) if total_node else None, unit_price: unit_node.get_text(stripTrue) if unit_node else None, area: area_node.get_text(stripTrue) if area_node else None, layout: layout_node.get_text(stripTrue) if layout_node else None, url: url, } return record逻辑说明每个字段都做了空值兜底避免个别房源缺字段时整个程序崩溃。get_text(stripTrue)会把节点里的空白字符清掉比如“ 528万 ”变成“528万”后面清洗时少很多麻烦。参数说明这里的select_one选择器一定要先手动打开一个详情页在浏览器开发者工具里确认节点结构再写宁可多花半小时也不要写完后对着空结果猜。2.3 反爬应对与采集节奏请求头、限速、断点续采二手房平台的页面结构并不复杂真正麻烦的是请求频率稍高就会被限流表现是连续几页返回 503 或验证页。这里分享一套我常用的稳妥节奏单线程、每次请求后随机睡 1 到 2 秒、同一页面失败重试 3 次、每抓 50 页打印一次进度。这套参数对毕设级别的数据量完全够用几千条数据大概跑半小时到一小时。def fetch_with_retry(url, headersHEADERS, retries3): 带重试的请求函数失败时随机休眠后再次尝试 for i in range(retries): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: return resp print(f非200状态: {resp.status_code}, 重试 {i 1}/{retries}) except requests.RequestException as e: print(f请求异常: {e}, 重试 {i 1}/{retries}) time.sleep(random.uniform(1, 2)) return None逻辑说明重试前随机休眠是为了避免失败后立刻发起请求形成“越失败越被封”的恶性循环。random.uniform(1, 2)产生 1 到 2 秒的随机小数让请求间隔不是固定值这是最基本的反反爬习惯。参数说明retries3是性价比比较高的设置超过 3 次还在失败大概率是你的请求头或 URL 本身有问题重试再多也没意义。关于断点续采最朴素的做法是每抓完一个列表页就把已获得的房源链接追加写入一个links.csv下次启动时先读这个文件跳过已抓链接。毕业设计不需要搞分布式队列文件追加就够了。注意抓取行为要遵守目标网站的 robots 协议和服务条款控制频率数据仅用于课程设计与学习展示不要商用。3. 数据清洗与入库从 JSON 文件到 DataFrame 的整洁数据加工3.1 为什么先落 CSV 再做 DataFrame一条便于纠错与复查的流水线很多人的习惯是爬虫跑完直接pd.DataFrame(data)开始分析。我吃过这个亏分析到一半发现面积字段里有“暂无数据”单价字段里有“价格待定”回头想查原始数据时发现原始变量已经被覆盖了。所以我现在做这类项目一律先把原始数据逐条落盘成 CSV再做清洗。这条流水线的好处有三个第一清洗脚本跑挂了可以随时从原始 CSV 重新开始不用重新爬第二答辩时评委问“这个字段原始长什么样”你可以现场打开 CSV 演示可信度极高第三CSV 是通用格式后续无论是用 pandas 还是导入 Excel 做补充分析都不受限制。原始数据落盘的代码很简单核心是csv.DictWriterimport csv import os def append_to_csv(filename, records): 把解析后的记录追加写入CSV自动处理字段名 if not records: return file_exists os.path.exists(filename) fieldnames list(records[0].keys()) with open(filename, a, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesfieldnames) if not file_exists: writer.writeheader() writer.writerows(records)逻辑说明file_exists判断是避免重复写表头fieldnames从第一条记录动态生成这样后面加了字段也不用改代码。参数说明encodingutf-8必须显式指定Windows 下默认编码可能是 gbk用 Excel 打开 CSV 时中文会乱码这个坑几乎每年都有人踩如果你后续要用 Excel 直接打开建议这里用utf-8-sigExcel 对带 BOM 的 UTF-8 识别更友好。3.2 字段级清洗面积、总价、单价、户型的解析与归一化爬下来的原始字段大多是带单位的字符串比如“89.6平米”“528万”“58920元/平”还有“低楼层/共33层”“3室2厅”这种复合信息。清洗的目标是把它们变成数值或规范分类让 pandas 能直接做分组和聚合。import re import pandas as pd def clean_area(value): 89.6平米 - 89.6失败返回None m re.search(r([\d.])\s*平米, str(value)) return float(m.group(1)) if m else None def clean_total_price(value): 528万 - 5280000统一转为元 m re.search(r([\d.])\s*万, str(value)) return round(float(m.group(1)) * 10000, 2) if m else None def clean_floor(value): 低楼层/共33层 - (低楼层, 33) if not isinstance(value, str) or / not in value: return None, None level, total value.split(/, 1) total_num re.search(r\d, total) return level.strip(), int(total_num.group()) if total_num else None def clean_layout(value): 3室2厅 - (3, 2) m re.match(r(\d)室(\d)厅, str(value)) if m: return int(m.group(1)), int(m.group(2)) return None, None逻辑说明每个函数都坚持“解析失败返回 None”而不是抛异常因为房源数据里总有异常值一个“车位”或“商铺”混进住宅列表就能让整批数据清洗中断。这里用正则而非split是为了兼容“89.6 平米”中间有空格、数字带小数点这类变体。参数说明clean_total_price统一把“万”转成“元”是为了后面算“总价/面积”时单位一致如果你觉得万元更方便也可以保留 528但全项目只能用一个单位否则图表纵轴会莫名其妙多出几个量级。清洗后的校验也要跟上我一般会打印这几项字段非空率、每列数据类型、区域分布的去重计数。这些数字能快速暴露清洗逻辑有没有写错。3.3 把清洗逻辑封装为可复用的函数并在答辩演示中留下“检查点”清洗函数不要写在一个巨大的 Jupyter Notebook 单元格里建议按功能拆到clean.py中主流程只做“读取原始 CSV → 逐列清洗 → 写出干净 CSV”。这样做好处是答辩现场可以说清楚“数据加工分几步”每一步的输入输出都是明确的检查点。import pandas as pd def load_and_clean(raw_csv): df pd.read_csv(raw_csv) df[area_num] df[area].apply(clean_area) df[total_num] df[total_price].apply(clean_total_price) df[floor_level], df[floor_total] zip(*df[floor].apply(clean_floor)) df[bedrooms], df[halls] zip(*df[layout].apply(clean_layout)) df[district] df[region].apply(lambda x: str(x).split(-)[0].strip()) df df.dropna(subset[area_num, total_num]) return df def audit(df): print(总记录数:, len(df)) print(区域分布:\n, df[district].value_counts()) print(面积缺失:, df[area_num].isna().sum()) print(总价缺失:, df[total_num].isna().sum()) if __name__ __main__: df load_and_clean(data/house_raw.csv) audit(df) df.to_csv(data/house_clean.csv, indexFalse, encodingutf-8-sig)逻辑说明zip(*df[floor].apply(clean_floor))会把函数返回的元组拆成两列这是 pandas 里处理多返回值的常用写法。dropna(subset[area_num, total_num])是最后一道防线把核心字段缺失的记录直接剔除避免分析时出现 NaN 参与计算。参数说明indexFalse写 CSV 时不要保留 pandas 的索引列否则后续 read 时会多出一列无名数据这是非常低级但很常见的失误。这一步做完你已经有了一个干净的结构化数据集。接下来才是最有“展示感”的部分可视化分析。4. 可视化分析四张主力图的选题逻辑与 pyecharts 配置落地4.1 图表选型从数据到结论而不是为了画图而画图如果你打开很多毕设 PPT会发现图表页堆满了直方图、折线图、饼图、热力图但说不出每张图回答什么问题。评委问“这张图说明了什么”答“可以看出分布情况”——这就是典型的数据可视化黑匣子。做这个项目时我建议只保留四张主力图每张对应一个清晰结论图表类型数据维度回答的问题面积分布直方图面积主流房源面积段集中在哪区域均价条形图区域 单价哪个区域最贵价差多大面积-总价散点图面积 总价总价与面积是否线性相关户型占比环形图户型挂牌主力户型是什么这四张图刚好覆盖了“房源结构、价格空间、关系探索、分类构成”四个分析视角也足够撑起毕业设计的需求分析、数据分析和结论三个章节。如果你采集的数据里还有朝向、装修、楼层字段可以再补一张“朝向 x 均价”的横向条形图但不要为了展示字段数量而堆图表答辩时间有限讲透四张图远比带过十张图有说服力。4.2 pyecharts 交互图表的配置细节tooltip、dataZoom 与区域条形图可视化我首选 pyecharts原因是它输出 HTML答辩现场可以直接在浏览器里交互比静态图片更有演示冲击力。环境安装很简单pip install pyecharts即可不需要额外装地图包。from pyecharts.charts import Bar from pyecharts import options as opts import pandas as pd df pd.read_csv(data/house_clean.csv) region_price df.groupby(district)[unit_price].mean().sort_values() bar ( Bar() .add_xaxis(region_price.index.tolist()) .add_yaxis(平均单价, [round(v, 0) for v in region_price.values]) .set_global_opts( title_optsopts.TitleOpts(title各区域二手房平均单价), tooltip_optsopts.TooltipOpts(triggeraxis), yaxis_optsopts.AxisOpts(name元/平米), ) ) bar.render(output/region_price.html)逻辑说明groupby(district)[unit_price].mean()先按区域分组计算均价再sort_values()排序这样条形图出来自动从低到高排列视觉上更有层次。triggeraxis让鼠标悬停在坐标轴附近时同时显示该区域所有信息比默认的 item 触发更符合条形图的使用习惯。参数说明yaxis_opts里的name会显示在纵轴旁边记得写单位“元/平米”否则评委可能看不出数据量纲。如果你想让图表更专业可以再加一个dataZoom配置让面积-总价散点图支持缩放查看密集区域from pyecharts.charts import Scatter scatter ( Scatter() .add_xaxis(df[area_num].tolist()) .add_yaxis(总价, df[total_num].tolist()) .set_global_opts( title_optsopts.TitleOpts(title面积与总价的关系), xaxis_optsopts.AxisOpts(name面积/平米), yaxis_optsopts.AxisOpts(name总价/元), datazoom_opts[opts.DataZoomOpts(range_start0, range_end100)], ) ) scatter.render(output/area_total.html)逻辑说明datazoom_opts传入列表是因为 pyecharts 支持同时存在内置和滑动条两个缩放组件这里只配一个滑动条range_start和range_end控制初始显示范围是 0% 到 100%。参数说明散点图的原始数据中如果存在个别总价极高或极低的异常房源图表会被拉伸得很难看。在清洗阶段可以用df[df[total_num] df[total_num].quantile(0.99)]做截尾去掉顶部 1% 的极端值再看效果这一步很出效果。4.3 matplotlib 中文字体与图表导出从 HTML 到 PPT 演示的可行路径pyecharts 输出 HTML 后在 PPT 里有两种常用嵌入方式一是答辩现场切到浏览器标签页直接演示交互二是在 PPT 中放静态截图。前者更有冲击力但依赖现场网络和浏览器环境后者稳妥但需要保证图片清晰、无乱码。如果你选择截图或补充用 matplotlib 画静态图第一个要处理的就是中文字体import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 黑体 plt.rcParams[axes.unicode_minus] False # 解决负号显示为方框逻辑说明font.sans-serif指定无衬线字体列表Matplotlib 默认字体不含中文字符不设置的话所有中文都会变成方框。axes.unicode_minus这一行也必加否则坐标轴负号会显示成异常符号。参数说明如果你在 Linux 服务器上跑SimHei可能不存在需要用fc-list :langzh查可用字体然后把字体文件路径传给font_manager.fontProperties比直接改全局配置更可靠。HTML 导出后转静态图的另一个思路是自动化截图用 Playwright 或 Selenium 打开本地 HTML 文件等待图表渲染完成后截取整页。这种方式生成的是真实渲染效果比手动缩放浏览器再截图要稳定得多适合批量生成多张图表后统一插入 PPT。不过这个方案会引入额外的浏览器驱动依赖如果你的答辩环境不允许装还是用虚拟数据先手动截图最保险。5. 二手房数据项目避坑清单答辩前最容易翻车的五个细节5.1 单价与总价除以面积对不上被评委当场指出来现象你用清洗后的总价除以面积算出来的“单价”和详情页展示的单价差距很大有的甚至差一倍。答辩时评委随手一除现场就露馅了。原因平台上展示的单价是挂牌价口径有时是业主调价后的最新报价而总价可能是几个月前挂出来的原始报价两边更新时间不一致。另外面积包含建筑面积和套内面积单价通常按建筑面积算但不同房源页面可能存在口径混用。解决不要在分析里自己算单价直接使用页面展示的“单价”字段。在论文的数据说明里写一句“单价为平台挂牌口径”并把总价和面积的关系分析单独做不要做成校验关系。如果确实需要对比先打印 10 条记录人工核对确认口径一致后再动手。5.2 区域名称不统一均价图凭空多出若干“新区”现象区域均价条形图做完发现多出几个奇怪的分类比如“天河”“ 天河 ”“广州天河”同时存在柱状图被切得支离破碎。原因详情页的区域字段在不同房源上格式不完全一致有的带城市前缀有的带空格有的直接用板块名替换了区县名直接groupby就会把这些当成不同类别。解决在清洗函数里统一做“取第一段”并 strip代码就是我前面写的str(x).split(-)[0].strip()。跑完value_counts()人工检查一遍如果发现“花都”“广州花都”这种变体加一个映射字典手工归一。这一步不要嫌麻烦区域字段是分析的主键主键脏了后面所有结论都是错的。5.3 pandas.to_sql 之后日期与文本类型错乱二次导入报错现象清洗好的 DataFrame 用to_sql写入 SQLite当时看着没问题隔天重新read_sql发现时间字段变成字符串有些数值列也带了奇怪的后缀。原因to_sql会根据 DataFrame 的 dtypes 推断 SQLite 列类型但 SQLite 本身是弱类型存储日期、布尔值存进去再读出来时可能被转成文本。加上如果写入时用的是if_existsreplace表结构每次都被重建类型稳定性更差。解决把入库做成固定流程统一用if_existsappend配合显式建表语句或者在读取后手动pd.to_datetime()恢复类型。最简单粗暴的做法是全程只用 CSV 和 Parquet不碰 SQLite毕竟毕设数据量根本不需要数据库如果为了展示“数据入库能力”就一定要在答辩前检查读出来的字段类型与实际报告一致。5.4 现场演示时采集脚本崩了数据集行数对不上报告现象答辩前夜重新跑采集脚本跑了二十分钟中断第二天演示时数据量只有报告里写的一半PPT 里的图表全部失效。原因没有断点续采程序重启后从头开始抓加上中途网络波动最终数据量缩水。这是“现场演示翻车”里最让人崩溃的一种。解决采集脚本必须支持断点续跑每次启动时读取已抓取的链接集合只抓新增部分。同时把“当前已抓 N 条/预计共 M 条”的进度打印到控制台用脚本输出作为数据量的唯一口径。报告里写的数据量必须是最后一次成功运行后的统计结果不要用中途估算值。我一般会在演示前一天把完整数据跑完备份到两个不同位置现场只跑一个 10 条的小样例。5.5 matplotlib 中文变方块图表一放到 PPT 就露怯现象本地跑图正常把脚本拷到答辩电脑或远程服务器上重跑中文字体全变成方块和问号。原因matplotlib 的字体配置随系统环境变化目标机器上没有 SimHei 或微软雅黑就会自动回退到不支持中文的默认字体。解决最稳妥的方法是生成图表时不依赖系统字体在代码里用font_manager.addfont(fonts/simhei.ttf)显式指定字体文件路径把字体文件一起放到项目目录里。另一种更省事的做法是最终展示用的图表统一用 pyecharts 的 HTML 截图正文配图尽量少用 matplotlib。记住别在答辩前一天才换机器跑图表所有可视化产物提前生成好并导出为图片或 HTML 放到项目包里。6. 答辩现场不翻车的最后一公里源码组织、PPT 顺序与演示数据自检这个项目拿到手之后源码结构和 PPT 的演示顺序决定了导师能不能在三分钟内看懂你做了什么。下面是我比较推荐的交付形态照着做基本不会出大问题。项目根目录按功能拆成四个文件夹data/放原始抓取 CSV 和清洗后的 CSVsrc/放采集、清洗、可视化三个脚本output/放所有图表 HTML 和截图docs/放需求说明和数据库设计文档。每个脚本的入口处打印阶段进度比如“清洗完成共保留 N 条有效记录”这样答辩现场任何一步都能被即时验证不用翻代码猜。配套的requirements.txt用pip freeze生成但只保留requests、beautifulsoup4、pandas、pyecharts、matplotlib这些用到的库把无关包清掉避免现场安装环境时踩坑。PPT 的顺序建议这样安排第一页写题目和一句话摘要第二页放技术选型表说明为什么用 requests 而不用 Scrapy第三页展示数据流图原始网页到 CSV 到 DataFrame 再到图表每步配一行关键代码第四到第五页放四张主力图每张图只写一行数据结论最后一页写不足与展望。演示时先打开output/region_price.html展示交互效果再切回 PPT 讲静态分析全程不要现场跑爬虫脚本只预跑一遍清洗和可视化命令作为辅助。最后有一个我踩过的坑想提醒你答辩前一定要把“图表里的数据”和“报告里的数字”对齐。我有一次 PPT 里写“采集房源 1200 套”但清洗后实际参与分析的只有 980 套被评委追问后只能现场解释场面非常被动。后来我养成一个习惯每次更新数据就把清洗后的行数打出来截图存到 docs 里所有汇报材料都引用这个数字。数据一致性和可追溯性才是这个项目真正值钱的地方。希望你能用自己的数据把整个流程跑通也能在答辩时对每一个数字的来源胸有成竹。本文还有配套的精品资源点击获取