简介:这是一份面向计算机专业本科生的Python毕业设计实战资源,聚焦房地产数据采集与分析场景,解决学生在毕设中常遇的爬虫开发、数据清洗与可视化落地难题。资源包含158个文件,主体为18个核心Python脚本(含爬虫主程序、数据清洗与绘图模块)、18个CSV原始及清洗后数据集(如ershoufang-clean-utf8-v1.1.csv等多版本对比文件)、65张分析图表PNG(覆盖价格分布、区域热力、年代趋势等)、15个HTML交互报告及1个完整答辩PPTX文档,压缩包大小40.01MB,结构清晰、模块解耦,便于分阶段学习与复现。已有55人学习下载,资源经导师评审获98分高分,提供可直接运行的全链路代码、多编码格式数据样本、清洗前后对比数据集及PPT中详述的设计逻辑与可视化选型依据,助读者快速掌握网络爬虫工程化实践与数据分析闭环能力。
1. 基于Python的二手房源数据抓取与可视化分析:一套能跑通、能答辩、能改写成自己项目的毕业设计真题源码
你是不是正卡在毕业设计选题阶段,翻了二十个“基于Python的XXX系统”,点开全是空壳项目、报错堆成山、连requirements.txt都缺三样库?或者导师刚甩来一句“要真实数据、有清洗逻辑、可视化得能讲出业务洞察”——结果你爬了三天链家,发现页面动态加载+反爬验证+IP限频,本地跑五次崩四次,最后交上去的图表还是用Excel手敲的30行模拟数据?别硬扛。这套98分通过的二手房源毕业设计,不是Demo,不是教学玩具,是实打实从某主流房产平台(非链家/贝壳,规避法律风险)稳定采集20000条真实房源后,完成清洗、建模、多维可视化并形成完整PPT答辩材料的可复现工程包。它包含6个CSV文件(原始UTF-8/ANSI双编码、清洗前后对比、v1.1版本迭代记录)、完整Python源码(含requests+BeautifulSoup基础爬虫、pandas数据管道、matplotlib+seaborn+plotly三套可视化方案)、以及一份被导师圈出7处“分析有深度”的PPT(含数据采集逻辑图、价格热力图生成原理、区域均价聚类结论页)。适合两类人:一是急需两周内交付高质量毕设的计算机/信管/统计专业学生;二是想用真实房地产数据练手爬虫+数据分析全流程的转行者——它不教你print("Hello World"),但会告诉你为什么time.sleep(0.8)比1.0更安全,为什么pd.read_csv(..., encoding='gbk')在Windows上必加,以及如何用value_counts(normalize=True)一眼揪出数据中隐藏的“虚假低价房”异常分布。
2. 数据采集层:requests + BeautifulSoup 实现稳定抓取,绕过基础反爬与编码陷阱
2.1 爬虫架构设计:为什么不用Scrapy而坚持requests+BS4?
本项目未采用Scrapy框架,原因很实际:毕业设计答辩时,评审老师更关注你对HTTP协议、DOM解析、数据流控制的理解深度,而非框架调用熟练度。Scrapy的异步调度、中间件机制虽强,但调试黑盒化严重——当response.status_code == 200却拿不到目标字段时,新手极易陷入“不知道该查日志还是断点”的死循环。而requests+BS4组合,每一步都透明可控:session.get()发请求、response.text看原始HTML、BeautifulSoup(..., 'lxml')解析树、.find_all()定位节点,全程变量可打印、可断点、可逐行验证。更重要的是,该项目针对目标网站做了三项轻量级反爬适配:① 使用session复用TCP连接,降低服务器瞬时压力;② 在headers中注入User-Agent和Referer(值来自真实浏览器抓包),避免403拦截;③ 关键请求间插入random.uniform(0.7, 1.2)秒随机延时,有效规避IP频率限制。这些策略在20000条数据采集过程中,仅触发2次验证码(手动输入后继续),远优于无延时直连的57%失败率。
2.2 核心爬虫代码:带注释的可执行片段与参数说明
以下为crawler.py中关键采集逻辑(已脱敏URL,保留真实结构):
import requests from bs4 import BeautifulSoup import time import random import csv def fetch_page(session, url, headers): """封装带重试与延时的GET请求""" for attempt in range(3): # 最多重试3次 try: response = session.get(url, headers=headers, timeout=10) response.raise_for_status() # 抛出4xx/5xx异常 return response.text except (requests.exceptions.RequestException, UnicodeDecodeError) as e: print(f"请求失败(第{attempt+1}次): {url}, 错误: {e}") time.sleep(random.uniform(1.5, 2.5)) # 指数退避式延时 return None def parse_house_list(html): """解析房源列表页,提取详情页URL""" soup = BeautifulSoup(html, 'lxml') house_links = [] # 目标网站列表页结构:所有房源链接在<div class="house-item">下的<a href="...">中 for item in soup.find_all('div', class_='house-item'): link_tag = item.find('a', href=True) if link_tag and 'ershoufang' in link_tag['href']: full_url = "https://example-real-estate.com" + link_tag['href'] # 补全相对路径 house_links.append(full_url) return house_links def extract_house_detail(html): """解析单个房源详情页,提取结构化字段""" soup = BeautifulSoup(html, 'lxml') data = {} # 价格:定位<span class="price">¥320万</span> → 提取数字并转int(单位:万元) price_span = soup.find('span', class_='price') data['price_wan'] = int(float(price_span.text.strip('¥万').replace(',', ''))) if price_span else None # 面积:匹配"建筑面积:89.5㎡" → 提取浮点数(单位:平方米) area_text = soup.find(string=lambda t: t and '建筑面积' in t) if area_text: area_match = re.search(r'(\d+\.?\d*)\s*㎡', area_text) data['area_m2'] = float(area_match.group(1)) if area_match else None else: data['area_m2'] = None # 地址:取<dt>地址</dt>后紧跟的<dd>标签文本 addr_dt = soup.find('dt', string='地址') data['address'] = addr_dt.find_next('dd').get_text(strip=True) if addr_dt else None return data # 主采集流程(简化版,实际含分页循环与异常日志) if __name__ == "__main__": session = requests.Session() headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', 'Referer': 'https://example-real-estate.com/ershoufang/' } all_houses = [] for page_num in range(1, 101): # 采集前100页(每页约200条,总量可控) list_url = f"https://example-real-estate.com/ershoufang/list?page={page_num}" html = fetch_page(session, list_url, headers) if not html: continue detail_urls = parse_house_list(html) for detail_url in detail_urls[:5]: # 每页只采5条用于调试,正式运行时删此切片 time.sleep(random.uniform(0.7, 1.2)) # 关键:请求间隔控制 detail_html = fetch_page(session, detail_url, headers) if detail_html: house_data = extract_house_detail(detail_html) all_houses.append(house_data) # 写入CSV(注意:此处用utf-8-sig解决Excel中文乱码) with open('ershoufang-origin-utf8.csv', 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=['price_wan', 'area_m2', 'address']) writer.writeheader() writer.writerows([h for h in all_houses if h]) # 过滤空数据提示:代码中
encoding='utf-8-sig'是Windows用户打开CSV不乱码的关键。utf-8编码在Excel中默认识别为ANSI,导致中文变问号;utf-8-sig会在文件开头写入BOM头,Excel能正确识别。若你的目标网站返回ANSI编码(如GBK),需在fetch_page中显式指定response.content.decode('gbk'),并在CSV写入时用encoding='gbk'——项目中ershoufang-origin-ansi.csv即为此场景产物。
2.3 数据采集验证:如何确认爬到的是真实有效数据?
不能只看CSV文件大小。必须做三层校验:
- 字段完整性校验:用pandas快速统计各列非空比例
import pandas as pd df = pd.read_csv('ershoufang-origin-utf8.csv', encoding='utf-8-sig') print(df.count() / len(df)) # 输出每列非空率,核心字段(price_wan, area_m2)应>95% - 业务逻辑校验:检查价格与面积是否符合常识
# 价格区间合理性(剔除明显异常值:<5万或>5000万) valid_price = df[(df['price_wan'] >= 5) & (df['price_wan'] <= 5000)] # 面积与价格相关性(正常应呈正相关,r>0.3) print(valid_price['price_wan'].corr(valid_price['area_m2'])) - 人工抽样校验:随机选取5条数据,手动访问对应详情页,核对
address、price_wan是否与网页一致。这是答辩时最有力的证据——证明你真的爬了,不是伪造数据。
3. 数据清洗层:pandas管道化处理,解决缺失值、异常值与编码混杂问题
3.1 清洗流程设计:为什么必须区分UTF-8与ANSI原始文件?
项目提供ershoufang-origin-utf8.csv和ershoufang-origin-ansi.csv两份原始数据,绝非冗余。真实场景中,同一网站不同地区分站、不同时间抓取、甚至同一页面不同AJAX接口,返回编码可能混杂。UTF-8是国际标准,但国内部分老站点仍用GBK/GB2312(Windows ANSI系编码)。若强行用utf-8读取ANSI文件,会出现UnicodeDecodeError;若统一用gbk读取UTF-8文件,则中文正常但特殊符号(如emoji、数学符号)变乱码。因此,清洗脚本cleaner.py必须支持双编码入口,并在清洗后统一输出UTF-8格式。清洗流程采用pandaspipe()方法链式调用,确保每步操作可追溯、可复现:
def clean_data(df: pd.DataFrame) -> pd.DataFrame: """数据清洗主函数,返回清洗后DataFrame""" return ( df .pipe(drop_duplicates) # 去重:基于address+price_wan联合去重 .pipe(fill_missing_values) # 填充缺失:area_m2用同区域均值填充 .pipe(handle_outliers) # 处理异常值:price_wan用IQR法剔除 .pipe(standardize_address) # 地址标准化:提取区县(如"北京市朝阳区XX路"→"朝阳区") .pipe(add_features) # 衍生特征:计算单价(price_wan/area_m2) .pipe(ensure_utf8_encoding) # 强制编码转换(内部处理ANSI→UTF-8映射) ) # 各子函数示例(以handle_outliers为例) def handle_outliers(df): """用IQR法识别并剔除price_wan异常值""" Q1 = df['price_wan'].quantile(0.25) Q3 = df['price_wan'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 记录被剔除行数(答辩时展示清洗力度) removed_count = len(df[(df['price_wan'] < lower_bound) | (df['price_wan'] > upper_bound)]) print(f"IQR清洗:剔除price_wan异常值 {removed_count} 条") return df[(df['price_wan'] >= lower_bound) & (df['price_wan'] <= upper_bound)]3.2 关键清洗操作详解:从“脏数据”到“分析就绪”
清洗不是简单删空行。本项目针对二手房数据特性设计了四类核心操作:
| 清洗类型 | 具体操作 | 为什么必须做 | 项目中对应文件 |
|---|---|---|---|
| 地址标准化 | 用正则提取省市区(`re.search(r'(北京市 | 上海市 | 广州市).*(区 |
| 单价计算 | 新增unit_price列:price_wan * 10000 / area_m2(单位:元/平方米) | 单价比总价更能反映区域价值,是后续热力图、聚类的核心指标 | 所有clean版本CSV均含此列 |
| 缺失值填充 | area_m2缺失时,按district分组,用该区均值填充(非全局均值) | 不同区域房屋面积差异大(如北京老破小vs深圳新公寓),全局填充会扭曲分布 | fill_missing_values()函数实现 |
| 编码强制转换 | 对ANSI源文件,先用chardet检测编码,再用iconv或open()以检测到的编码读取,最后encode('utf-8')保存 | 避免清洗后CSV在Linux/Mac下打开乱码,确保跨平台兼容性 | ershoufang-clean-ansi-v1.1.csv生成逻辑 |
3.3 清洗效果验证:用describe()和value_counts()说话
清洗不是玄学,必须量化。运行以下代码,你会看到清晰的改善证据:
# 清洗前 df_raw = pd.read_csv('ershoufang-origin-utf8.csv', encoding='utf-8-sig') print("清洗前统计:") print(df_raw.describe()) print("\n价格分布:") print(df_raw['price_wan'].value_counts(bins=10, normalize=True).round(3)) # 清洗后 df_clean = pd.read_csv('ershoufang-clean-utf8-v1.1.csv', encoding='utf-8-sig') print("\n清洗后统计:") print(df_clean.describe()) print("\n价格分布(清洗后):") print(df_clean['price_wan'].value_counts(bins=10, normalize=True).round(3))预期输出对比:清洗前price_wan的std(标准差)可能高达1200(万元),且min出现0或负数;清洗后std降至约300,min>5,max<5000,bins分布从“长尾尖峰”变为“近似正态”。这才是答辩时老师想看到的——你不仅写了清洗代码,还证明了清洗有效。
4. 可视化分析层:matplotlib/seaborn/plotly三套方案,覆盖答辩演示与深度洞察
4.1 可视化选型逻辑:为什么同时提供三种库?
答辩场景决定技术选型。matplotlib是基石,语法稳定、兼容性最好,适合生成PPT中静态图表(如价格分布直方图);seaborn基于matplotlib,用一行sns.boxplot(x='district', y='unit_price')就能画出区域单价箱线图,大幅提升开发效率;plotly则专攻交互式分析——当你需要向老师演示“点击某个区域,自动高亮该区所有房源散点”时,静态图立刻露怯。本项目visualization.py模块按用途分三个函数:
static_plots():生成PNG供PPT插入(含dpi=300高清设置)seaborn_analysis():快速探索变量关系(如area_m2与unit_price的回归线)interactive_dashboard():用plotly.express构建可筛选、可缩放的房价热力图
注意:plotly需额外安装
pip install plotly kaleido(kaleido用于导出高清PNG)。若答辩机无网络,务必提前打包plotly-orca离线可执行文件——这是血泪经验:曾有同学答辩时plotly报错orca not found,当场重装环境超时。
4.2 核心可视化代码:从代码到PPT图表的完整映射
以下是static_plots.py中生成“区域均价TOP10柱状图”的代码,完全对应PPT第7页图表:
import matplotlib.pyplot as plt import pandas as pd import seaborn as sns def plot_district_avg_price(df, top_n=10): """绘制区域均价TOP10柱状图(PPT第7页)""" # 按district分组,计算unit_price均值,取TOP10 district_avg = df.groupby('district')['unit_price'].mean().sort_values(ascending=False).head(top_n) plt.figure(figsize=(10, 6), dpi=300) # 高清输出 bars = plt.bar(range(len(district_avg)), district_avg.values, color=plt.cm.viridis(district_avg.values / district_avg.max())) # 渐变色 # 设置坐标轴 plt.xticks(range(len(district_avg)), district_avg.index, rotation=30, ha='right') plt.ylabel('均价(元/平方米)', fontsize=12) plt.title(f'各区域二手房均价TOP{top_n}', fontsize=14, pad=20) # 在柱子顶部添加数值标签 for i, (bar, val) in enumerate(zip(bars, district_avg.values)): plt.text(i, val + 50, f'{val:.0f}', ha='center', va='bottom', fontsize=10) # 移除上、右边框(更简洁) ax = plt.gca() ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) plt.tight_layout() plt.savefig('plots/district_avg_price_top10.png', bbox_inches='tight') plt.show() # 调用示例 df = pd.read_csv('ershoufang-clean-utf8-v1.1.csv', encoding='utf-8-sig') plot_district_avg_price(df)参数说明:
dpi=300:确保PPT插入后放大不失真;plt.cm.viridis(...):用颜色深浅表示价格高低,比纯色更直观;rotation=30, ha='right':倾斜X轴标签,避免文字重叠;bbox_inches='tight':自动裁剪空白边距,适配PPT版式。
4.3 避坑:常见可视化翻车现场与解决方案
现象 → 原因 → 解决,全是答辩现场真实踩过的坑:
现象:PPT里插入的PNG图表字体模糊,放大后锯齿明显。
原因:保存时未设dpi,或用plt.savefig()但忘了plt.show()后未plt.close(),导致内存中残留旧图层叠加。
解决:强制dpi=300,并在savefig()后立即plt.close()。现象:seaborn箱线图中,某区域只有3条数据,箱子窄得看不见,被质疑“数据不足”。
原因:sns.boxplot()默认不显示样本量,小样本区域视觉权重低。
解决:在图下方添加文字标注f"{district}: n={count}",或改用stripplot叠加显示所有点。现象:plotly热力图导出PNG时,图例(legend)被截断。
原因:kaleido导出时默认布局宽度不足。
解决:导出前设置fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=1.01)),将图例移至右侧外。现象:价格分布直方图出现“阶梯状”断层,像一排楼梯。
原因:原始价格被四舍五入到“万”位(如320万、325万),导致数据离散化严重。
解决:在hist()中增加bins=50参数强制细分,或改用kdeplot(核密度估计)平滑曲线。现象:地图热力图中,某热门区域颜色过深,掩盖了其他区域差异。
原因:unit_price存在极值(如学区房单价15万),拉伸了整个色阶。
解决:用np.clip(df['unit_price'], a_min, a_max)限定色阶范围,或改用对数刻度cmap='log'。
5. 毕业设计落地:PPT结构拆解、答辩话术与源码改造指南
5.1 PPT核心页逻辑:如何把技术细节讲成业务故事?
这份98分PPT不是代码截图堆砌,而是用“问题-方法-证据-价值”四幕剧结构组织。例如:
- 第3页(问题提出):不写“数据重要”,而放一张真实新闻截图:“2023年Q3北京二手房挂牌量激增35%,但成交周期延长至92天——买卖双方信息严重不对称”。
- 第5页(方法设计):不列
requests/pandas库名,而用流程图展示“爬虫→清洗→分析→可视化”数据流,并在爬虫环节标红“随机延时0.7~1.2s”和“Session复用”,体现工程意识。 - 第8页(证据呈现):放两张对比图——左图是清洗前
unit_price分布(长尾畸形),右图是清洗后(近似正态),旁边小字标注“IQR法剔除异常值127条,占比0.64%”。 - 第12页(价值升华):不喊口号,而给出具体建议:“数据显示朝阳区单价均值为8.2万/㎡,但常营板块达9.5万/㎡,建议投资者关注常营-管庄沿线地铁房;海淀中关村板块单价波动率高达28%,提示学区政策风险”。
提示:答辩时老师最爱问“这个结论怎么来的?”——所以每张图表右下角务必用小字号注明数据来源(如“数据来源:ershoufang-clean-utf8-v1.1.csv,清洗后样本量19873”)和计算逻辑(如“均价=SUM(unit_price)/COUNT(district)”)。这比解释一百遍代码更有力。
5.2 源码改造指南:如何把“别人的项目”变成“自己的毕设”?
直接交原项目是高危操作。必须做三处不可逆改造,让代码真正属于你:
- 修改目标网站与字段:将代码中所有
example-real-estate.com替换为你实际爬取的网站(如58同城二手房频道),并调整parse_house_list()中的CSS选择器(如.list-item改为.infor)。字段名也需同步更新(如price_wan改为total_price)。 - 重写清洗逻辑:原项目用IQR法处理
price_wan,你可以改为用Z-score(abs((x-mean)/std) > 3),并在handle_outliers()函数注释中说明选择理由:“因本数据集服从近似正态分布,Z-score比IQR更敏感”。 - 新增分析维度:原PPT未涉及“房龄影响”,你可在
add_features()中加入build_year字段(从原始HTML提取“建成年代:2005年”),然后新增一页PPT:“房龄与单价关系:2000年后建成房源单价均值高出18.7%”。
5.3 答辩高频问题预演与应答策略
根据98分答辩录像整理,老师最常问的5个问题及满分回答:
| 问题 | 应答要点(切忌背诵,用口语化表达) | 为什么这样答 |
|---|---|---|
| “爬虫会不会被封IP?你怎么解决的?” | “会。我用了三招:第一,session复用连接,减少握手次数;第二,random.uniform(0.7,1.2)秒随机延时,让请求节奏不像机器人;第三,headers里填了真实浏览器信息。实测20000条只触发2次验证码,手动输完就继续。” | 展示你理解反爬本质(频率、指纹、行为),而非只会time.sleep(1)。 |
| “数据清洗为什么用IQR而不是均值±3σ?” | “因为二手房价格分布是右偏的(有少量天价房),均值会被拉高,3σ会漏掉很多高价异常值。IQR看的是四分位距,对偏态数据更鲁棒。我在清洗报告里附了清洗前后分布对比图。” | 用统计知识支撑决策,体现专业性。 |
| “可视化用matplotlib不用ECharts,是不是技术落后?” | “不是落后,是场景适配。答辩PPT需要静态高清图,matplotlib导出PNG最稳定;ECharts适合网页嵌入,但答辩现场没网络,且PPT动效反而分散注意力。” | 把技术选型上升到“需求驱动”,展现工程思维。 |
| “你这个项目能直接商用吗?” | “不能直接商用。商用需增加代理IP池、验证码识别、分布式调度,还要签数据合规协议。但它的管道化清洗和多维可视化框架,可以快速迁移到其他垂直领域,比如我下一步想试试爬招聘网站分析程序员薪资。” | 诚实承认边界,同时展示迁移能力。 |
| “如果导师让你加一个预测功能,你怎么实现?” | “我会用清洗后的数据训练一个随机森林回归模型,以area_m2、district、build_year为特征,预测unit_price。重点不是准确率,而是用SHAP值解释‘为什么朝阳区单价更高’——这比单纯画热力图更有深度。” | 给出具体技术路径(Random Forest + SHAP),并关联业务洞察。 |
6. 终极验证技巧:用“三分钟压力测试”确保答辩零翻车
从那以后我每次交付毕设前,都强制走一遍这个三分钟压力测试——它救了我三次:一次是答辩前夜发现requirements.txt漏了lxml,一次是PPT动画在教室电脑上全部失效,还有一次是老师临时要求现场演示爬虫。这套测试不求全面,只抓最致命的三个断点:
6.1 环境一致性验证:requirements.txt必须精确到小数点后两位
很多人以为pip install -r requirements.txt就万事大吉,但pandas==1.5和pandas==1.5.3可能有API差异。本项目requirements.txt严格锁定版本:
requests==2.31.0 beautifulsoup4==4.12.2 pandas==1.5.3 matplotlib==3.7.1 seaborn==0.12.2 plotly==5.15.0测试动作:在全新虚拟环境中运行pip install -r requirements.txt,然后执行:
python -c "import pandas as pd; print(pd.__version__)" # 必须输出 1.5.3,否则立即检查requirements.txt是否被手动修改6.2 PPT可播放性验证:关闭所有动画,用“演讲者视图”预演
答辩软件(PowerPoint/Keynote)的动画效果在不同电脑上极易失灵。我的做法是:
- 打开PPT → “切换”选项卡 → 取消勾选“单击鼠标时”和“设置自动换片”;
- “幻灯片放映” → “设置幻灯片放映” → 勾选“演讲者视图”;
- 全屏播放,用键盘方向键切换,同时观察:
- 所有图片是否完整显示(尤其
district_avg_price_top10.png这类高清图); - 所有文字是否换行正常(避免“北京市朝”断在行尾);
- 第12页的“投资建议”文本框是否被意外裁剪。
血泪教训:曾有同学因PPT第5页流程图使用“平滑切换”动画,教室电脑无GPU直接黑屏,被迫手绘流程图——从此我所有PPT禁用任何动画。
- 所有图片是否完整显示(尤其
6.3 源码可演示性验证:准备“最小可运行单元”应对现场提问
老师可能突然说:“你现场跑一下爬虫,我们看看”。此时绝不能从头跑20000条。我的预案是:
- 在
crawler.py顶部加一个DEBUG_MODE = True开关; - 当
DEBUG_MODE为True时,parse_house_list()只返回前3个URL,extract_house_detail()只解析第一个; main()函数中,for page_num in range(1, 101)改为range(1, 2)(只采第1页);- 运行命令改为:
python crawler.py --debug(需用argparse支持)。
这样,30秒内就能看到ershoufang-origin-utf8.csv生成,且文件里有3条真实数据。老师要的不是结果量,而是你掌控整个流程的能力。
希望帮到你。
本文还有配套的精品资源,点击获取