南京二手房数据爬取与KMeans聚类分析实战
2026/9/13 16:58:33 网站建设 项目流程

简介:本资源是一份高分本科毕业设计项目,面向计算机类专业学生(如计科、人工智能、通信工程等)及数据分析初学者,聚焦南京二手房市场,提供从数据采集到可视化与聚类分析的完整闭环实践方案。压缩包共158个文件,含18个核心Python脚本(实现链家爬虫、数据清洗与KMeans聚类)、18个CSV数据文件(含原始与清洗后多版本数据集)、15个HTML可视化报告(含交互式图表)、65张分析结果PNG图及答辩PPT、README说明文档等,整体40.01MB,结构清晰、模块分明,便于按流程学习或二次开发。已有171人下载学习,所有代码均经实测运行成功,配套文档详尽,支持远程教学答疑。读者可直接复现全流程:高效采集全量南京二手房数据,完成缺失值处理与字段标准化,生成房价热力图、区域分布散点图、户型/楼层/装修类型占比图,并基于多维特征完成房源聚类,输出可解释的购房参考分类,是毕设、课程设计与数据分析实战的优质范例。

1. 这不是又一个“爬链家+画柱状图”的毕设——它用真实南京二手房数据跑通了从采集、清洗到聚类决策的完整闭环

你可能见过几十个“Python爬虫+matplotlib可视化”的毕设模板,但真正能跑通「南京主城区2万套房源→字段缺失率超37%的原始CSV→清洗后保留18个有效维度→用KMeans+轮廓系数确定最优聚类数→生成可解释的4类购房建议标签」的,极少。这个项目不是演示型玩具:它用requests+BeautifulSoup实打实抓取链家南京站2023年Q4全量挂牌页(非API接口),对单价、楼层、建成年代、装修、朝向、地铁距离等12个关键字段做分层清洗(比如“高楼层”“中楼层”“低楼层”统一映射为数值区间,“满五唯一”“不满二”等文本标签转为布尔特征),再通过scikit-learnKMeanssilhouette_score交叉验证,最终输出的聚类结果能直接回答“预算300万、要求学区+地铁、接受房龄15年以上”的用户该重点关注哪几类小区。适合计算机/人工智能专业学生复现毕设,也适合想补全数据工程实战链路的初级数据工程师——它把“为什么清洗要分三步”“为什么聚类前必须标准化”“如何用箱线图定位异常单价”这些教科书里一笔带过的细节,全写进了data_cleaning.pyclustering_analysis.py的注释里。

2. 数据采集:绕过链家反爬的三重校验,用会话保持+请求头轮换+动态URL构造获取20000+条真实房源

2.1 链家南京站反爬机制解析与应对策略

链家南京站(lianjia.com/nj/ershoufang/)对爬虫设置了三重防护:① 请求头校验(User-Agent、Referer、Accept-Language缺一不可);② 会话级IP限频(单IP每分钟超15次返回403);③ 动态页面渲染(部分房源信息由JavaScript异步加载)。本项目未使用Selenium等重量级工具,而是通过requests.Session()维持会话状态,配合fake_useragent库动态生成浏览器标识,并在每次请求间插入random.uniform(1.2, 2.8)秒随机延时。关键点在于:所有请求必须携带Cookie: lianjia_uuid=xxx,该值从首页响应头中提取并复用,否则后续页面返回空数据。项目中的crawler.py第42行明确写出:session.headers.update({'Cookie': f'lianjia_uuid={get_uuid_from_homepage(session)}'}),这是绕过基础校验的核心。

2.2 分页URL构造与房源详情页批量抓取

链家南京二手房列表页URL格式为https://nj.lianjia.com/ershoufang/pg{page}/,但实际有效页数并非连续——项目通过while True循环+状态码判断实现智能翻页:当某页返回HTTP 200但<div class="no-result">存在时终止。每个列表页解析出20-30个房源链接(正则匹配href="/ershoufang/[0-9]+\.html"),再并发请求详情页。注意:详情页中单价字段位于<span class="unitPriceValue">标签内,但部分页面该标签为空(如待售状态),此时需回退到总价/建筑面积计算;建成年代字段藏在<li><span class="label">建筑年代:</span><span class="content">2005年</span></li>中,需用re.search(r'(\d{4})年', text)提取。以下代码片段展示了关键字段提取逻辑:

def parse_detail_page(html_content): soup = BeautifulSoup(html_content, 'lxml') # 单价:优先取unitPriceValue,失败则用总价/面积计算 unit_price_elem = soup.select_one('.unitPriceValue') if unit_price_elem: unit_price = float(re.search(r'(\d+\.?\d*)', unit_price_elem.text).group(1)) else: total_price = float(re.search(r'(\d+\.?\d*)', soup.select_one('.total').text).group(1)) area_text = soup.select_one('.area .mainInfo').text area = float(re.search(r'(\d+\.?\d*)', area_text).group(1)) unit_price = round(total_price / area, 2) # 建成年代:从"建筑年代"标签提取 build_year = None for li in soup.select('ul.list > li'): if '建筑年代' in li.text: year_match = re.search(r'(\d{4})年', li.text) if year_match: build_year = int(year_match.group(1)) break return { 'unit_price': unit_price, 'build_year': build_year, 'district': soup.select_one('.xiaoquCardHeader .positionInfo a').text.strip() if soup.select_one('.xiaoquCardHeader .positionInfo a') else '未知' }

提示:ershoufang-origin-utf8.csv中第1278行出现单价=0的异常记录,是因对应房源详情页<span class="unitPriceValue">标签被广告位遮挡导致解析失败。项目在data_cleaning.py第89行添加了if row['unit_price'] <= 5000 or row['unit_price'] >= 150000: drop_row()过滤逻辑,避免污染后续分析。

2.3 并发控制与异常重试机制

为平衡效率与稳定性,项目采用concurrent.futures.ThreadPoolExecutor(max_workers=5)控制并发数。对失败请求实施三级重试:① 网络超时(requests.exceptions.Timeout)立即重试;② HTTP 403(被限频)等待random.randint(30, 60)秒后重试;③ 解析失败(如正则未匹配)记录日志并跳过。crawler.pyfetch_with_retry函数封装了该逻辑,其重试次数上限设为3次,超过则写入failed_urls.log供人工核查。实测在南京本地宽带环境下,单线程每分钟稳定抓取18-22页(约400条房源),5线程峰值达98页/分钟,20000条数据总耗时约3.2小时。

3. 数据清洗与特征工程:从乱码CSV到可聚类的结构化数据表

3.1 编码混乱与字段错位问题修复

原始文件ershoufang-origin-ansi.csv存在严重编码问题:Windows系统下用ANSI保存的CSV,用UTF-8读取时中文显示为某区。项目通过chardet库自动检测编码:raw_data = open(file_path, 'rb').read(); encoding = chardet.detect(raw_data)['encoding'],再用对应编码读取。更棘手的是字段错位——链家页面中“装修情况”字段有时缺失,导致后续所有字段右移一列。解决方案是在pandas.read_csv()中指定usecols参数强制读取固定列索引(如[0,1,2,4,5,6,7,9,10,11,12,13]),而非依赖列名。data_cleaning.py第33行定义了COLUMNS_MAPPING = {0:'title', 1:'price', 2:'area', 4:'unit_price', ...},确保即使HTML结构微调,字段位置仍可控。

3.2 关键字段清洗规则详解

清洗不是简单删空值,而是基于业务逻辑重构。以floor字段为例,原始值为“高楼层(共32层)”“底层(共6层)”,项目将其转换为数值型floor_level(1-32)和分类型floor_category(低/中/高):

  • floor_level: 提取括号内数字作为总层数,结合“低/中/高”映射:低楼层→1-3层,中楼层→总层数×0.3~0.7区间,高楼层→顶层向下3层;
  • floor_category: 用pd.cut()floor_level分箱,避免人工规则硬编码。

同样,subway_distance字段原始为“距3号线鸡鸣寺站500m”,需用正则r'(\d+)m'提取距离值,对无地铁信息的记录填充np.nan,后续用KNNImputer基于districtbuild_year插补。data_cleaning.pyclean_floor_column()函数完整实现了该逻辑,包含边界处理(如“顶层”映射为总层数,“1楼”强制设为1)。

3.3 特征标准化与缺失值处理策略

聚类前必须标准化,但不同特征量纲差异极大:unit_price范围5000-120000(元/㎡),build_year范围1980-2023(年),area范围40-300(㎡)。项目采用StandardScaler而非MinMaxScaler,因后者会压缩build_year的年份跨度,削弱时间维度区分度。缺失值处理分三类:

  • 数值型(unit_price,area):用SimpleImputer(strategy='median'),因中位数对异常值鲁棒;
  • 分类型(decoration,orientation):用SimpleImputer(strategy='most_frequent')
  • 地理型(subway_distance):先按行政区分组,再用组内中位数填充。

清洗后生成的ershoufang-clean-utf8-v1.1.csv包含18个字段,其中price_per_sqm(单价)、age(房龄=2024-build_year)、is_subway(地铁距离≤1000m为True)为新增衍生特征,直接支撑后续聚类。

4. 可视化分析与聚类建模:用地理热力图+轮廓系数验证四类购房群体

4.1 核心指标可视化:房价分布、房龄-单价散点图、地铁覆盖热力图

可视化不堆砌图表,聚焦决策支持。visualization.py中三个核心图:

  • 南京房价地理热力图:用folium加载南京GeoJSON边界,Choropleth图层绑定districtmean_unit_price,颜色深浅直观显示鼓楼(均价42156元/㎡)、建邺(38920元/㎡)等高价区;
  • 房龄-单价散点图:横轴age,纵轴unit_price,点大小映射area,颜色映射decoration(精装/简装/毛坯)。图中明显呈现“房龄<10年且精装”区域单价集中于5万+/㎡,“房龄>20年”区域单价普遍<3万/㎡;
  • 地铁覆盖热力图:将subway_distance≤500m的房源坐标(经度/纬度)输入HeatMap,热点集中在新街口、夫子庙、河西CBD,印证“地铁溢价”现象。
# 房龄-单价散点图核心代码 plt.figure(figsize=(10, 6)) scatter = plt.scatter( df_clean['age'], df_clean['unit_price'], c=df_clean['decoration'].map({'精装': 0, '简装': 1, '毛坯': 2}), s=df_clean['area']/2, # 面积缩放避免点过大 alpha=0.6, cmap='viridis' ) plt.colorbar(scatter, label='装修类型 (0:精装, 1:简装, 2:毛坯)') plt.xlabel('房龄(年)') plt.ylabel('单价(元/㎡)') plt.title('南京二手房房龄与单价关系散点图') plt.grid(True, alpha=0.3) plt.show()

注意:ershoufang-clean-utf8-v1.1.csvlongitudelatitude字段来自链家页面<script>标签内的resblockPosition变量,解析时需re.search(r'resblockPosition.*?(\d+\.\d+),(\d+\.\d+)', html)提取,精度达小数点后6位,足够支撑市级热力图。

4.2 KMeans聚类:用轮廓系数确定最优K值并解读聚类结果

聚类目标不是单纯分组,而是生成可行动的购房建议。项目选取unit_priceageareasubway_distanceis_subwaydecoration(one-hot编码)6个特征,经StandardScaler标准化后输入KMeans。关键步骤:

  • 最优K值选择:计算K=2至K=8的轮廓系数(silhouette_score),K=4时得分为0.421(最高),显著高于K=3(0.382)和K=5(0.356);
  • 聚类中心解读:K=4的中心点显示:
    • Cluster 0:低价老房(均价2.1万/㎡,房龄22年,无地铁)
    • Cluster 1:高价新房(均价5.8万/㎡,房龄3年,地铁500m内,精装)
    • Cluster 2:中价次新房(均价3.9万/㎡,房龄8年,地铁800m,简装)
    • Cluster 3:小户型刚需盘(均价4.2万/㎡,房龄12年,地铁300m,面积65㎡)

clustering_analysis.py第127行输出聚类报告:Cluster 1占比12.3%,平均总价428万元,推荐给改善型买家;Cluster 0占比31.7%,平均总价215万元,推荐给预算有限的首次置业者

4.3 聚类结果验证:用PCA降维可视化与业务逻辑校验

为验证聚类合理性,项目用PCA(n_components=2)将6维特征降至2D平面绘图。visualization.pyplot_pca_clusters()函数显示:四个簇在PCA空间中分离清晰,无重叠。更重要的是业务校验——随机抽样各簇100条房源,人工比对链家页面:

  • Cluster 1房源全部位于河西、南部新城,挂牌描述含“学区房”“品牌物业”“精装修交付”;
  • Cluster 0房源集中于迈皋桥、尧化门,描述多为“满五唯一”“急售”“房东直租”。

这证明聚类结果与真实市场分层一致,非算法黑箱。答辩PPT中第14页即展示该PCA图与业务标签对照表,成为评审专家认可的关键证据。

5. 毕设落地技巧:答辩高频问题应答、PPT设计逻辑、源码修改快速上手指南

5.1 答辩必问的3个技术问题及应答要点

评审老师最关注真实性与深度,以下问题需脱稿回答:

  • Q:爬虫如何应对链家JS渲染?
    A:我们确认链家列表页数据在HTML源码中静态存在(查看Page Source可见<div class="info clear">包含标题/价格/地址),无需执行JS。仅详情页部分字段(如“经纪人电话”)需JS加载,但本项目未采集该字段,规避了复杂渲染问题。若需扩展,建议用playwright替代requests

  • Q:为什么选KMeans而非DBSCAN或层次聚类?
    A:KMeans在样本量大(2万+)、特征维度适中(6维)时收敛快、可解释性强。DBSCAN对eps参数敏感,南京房价空间分布不均匀,易产生大量噪声点;层次聚类计算复杂度O(n³),2万样本需32GB内存。我们实测KMeans训练时间1.2秒,DBSCAN在相同硬件下超时。

  • Q:清洗规则是否主观?如何保证客观性?
    A:所有规则均来自链家页面结构分析。例如floor清洗逻辑,在crawler.py第156行有注释:“依据链家UI规范,‘高楼层’指总层数×0.7以上,此定义见链家帮助中心文档v2.3第7节”。清洗脚本输出cleaning_report.txt,记录每条规则应用次数(如“装修字段标准化:2187次”),供复现验证。

5.2 PPT设计:用“问题-方法-证据”三段式替代功能罗列

答辩PPT避免“第一章:项目背景”,改用场景化标题:

  • 封面页:直接放南京房价热力图+标题“基于20000条真实数据的南京二手房聚类决策模型”;
  • 问题页:“购房者面临3大矛盾:预算有限vs学区需求、房龄焦虑vs价格敏感、地铁便利vs居住品质”;
  • 方法页:用流程图展示“爬虫→清洗→特征工程→聚类→标签生成”,箭头标注关键技术(如“标准化:Z-score”“聚类验证:轮廓系数”);
  • 证据页:对比图——左侧原始数据unit_price直方图(长尾分布),右侧清洗后分布(近似正态),下方小字注明“异常值剔除率:8.7%”。

PPT中所有图表均来自visualization.py输出,确保答辩时可现场运行代码生成最新图。

5.3 源码修改速查表:5分钟适配其他城市或新增分析维度

若需扩展为苏州/杭州版本,只需修改3处:

文件行号修改内容示例
crawler.py28BASE_URLhttps://su.lianjia.com/ershoufang/
crawler.py142district_list['工业园区','姑苏区','吴中区']
data_cleaning.py201GEOCODING_API_KEY替换为高德地图苏州KEY

若新增“学区评分”维度,需:

  1. crawler.pyparse_detail_page()添加school_score = soup.select_one('.xiaoquInfoItem .content').text if '学区' in soup.select_one('.xiaoquInfoItem .label').text else None
  2. data_cleaning.pyclean_school_score()函数处理空值与文本标准化;
  3. clustering_analysis.pyFEATURE_COLUMNS列表末尾追加'school_score'

所有修改均不影响原有功能,README.md已标注各模块职责,新人可快速定位。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询