ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python爬虫+Geopy:从二手房数据挖掘房价与地理位置的关系

Python爬虫+Geopy:从二手房数据挖掘房价与地理位置的关系 干了几年数据采集和城市数据分析的活儿我一直有个特别直观的感受爬虫本身不难难得是把数据爬下来之后你能从里面看见什么。以前接过一个二手房相关的分析需求当时手上攒了三千多条挂牌房源字段无非是小区名称、户型面积、朝向楼层、总价单价。表格拉到一半眼睛就花了除了知道“市中心贵、郊区便宜”这种废话之外基本得不出任何能让人眼前一亮的结构性结论。后来把快烂在Excel里的数据接进Python用Geopy把每个小区的名字换算成经纬度再算一算到地铁站、到商圈核心的直线距离整个事情突然就变得不一样了。房价和地理位置之间的“隐藏关系”根本不是简单一句“地段决定价值”能概括的它藏在距离衰减曲线、配套密度和板块分化这些细节里。这篇博文就把我这套“Python爬虫Geopy”的组合玩法完整拆一遍包括抓取思路、地理编码的工程细节、相关性测算方法以及我在实际操作中踩过的坑。适合刚学完Python基础、想拿真实项目练手的读者也适合已经写过几个爬虫但想做更深层数据分析的朋友。1. 挂牌数不等于房价真相地理位置才是关联杠杆1.1 为什么同一板块挂牌价差距巨大先把一个很容易被忽略的事儿说明白——我们平时在房源APP上看到的挂牌价其实是一堆极其“不均匀”的样本。同一个板块里有2000年以前的楼梯房有2021年交付的次新电梯房有靠马路的噪声房也有小区中心花园边的楼王。如果不引入地理位置坐标只按“板块”这个行政或商业概念做聚合那么板块内部的方差很可能会吞掉真正的规律。我举个例子。假设某个板块A平均挂牌单价是3.2万/平米但如果你把每套房子的经纬度画出来再把单价渲染成颜色会发现从板块东北角到西南角单价沿着一条主干道呈现出明显的下降。原因是东北角紧挨着一个开通不久的地铁换乘站而西南角是老旧工业区改造的安置房片区。单纯看“板块均价”你会以为整个板块都值这个价但实际上真正支撑价格的只是那一小块地。这就是我坚持在分析链路里引入Geopy的核心原因——地理位置不是一列冷冰冰的字段它是所有房源销售述求和买家决策的底层编码。没有坐标的房价分析等于只看了故事的封面。1.2 地理信息能给数据叠加的三层价值把地理位置信息加进房价分析通常能带来三层递进式价值距离维度的量化每一套房源到地铁站、商圈、学校、医院的距离都可以被计算出来距离变量可以替代模糊的“板块”概念成为回归或相关性分析中的连续特征。这一点对建模非常重要因为连续变量比离散的行政区划更能暴露真实规律。空间分布的直观性通过结合底图的可视化人类视觉系统能快速识别高值聚集区、价格断裂带、沿交通线延伸的“溢价走廊”。这种模式在纯表格里几乎不可能被发现。比价与套利逻辑相邻小区、甚至同一小区的不同期楼栋因为地理位置微小的差异可能出现每平米几千块的差价。坐标对齐后这种“相邻高差”会非常刺眼也更有分析价值。明白了这层关系后面所有技术操作——爬取、清洗、地理编码、可视化——就都有了明确的目的不再是为了跑通代码而跑通代码。2. 房源页面抓取从字段设计到反爬节奏控制2.1 目标网站选择与列表页结构分析开始写爬虫之前得先想清楚从哪儿拿数据。我个人的选源标准有四条列表页能拿到足够详细的房源字段减少二次请求的次数页面结构稳定不要今天改版明天变样房源信息更新相对及时最好有明确的挂牌时间robots规则和访问频率控制友好至少别逼着人上各种强对抗手段。很多综合房产平台的二手房列表页通常会展示小区名称、几室几厅、面积、朝向、楼层、总价、单价。这些字段足够做第一轮分析。但要拿到更细的“建筑年代”“物业类型”或“附近配套”就得进详情页。我的建议是先评估“能不能不做详情页请求”尽量用列表页解决战场。因为详情页请求量动辄翻几倍被限制的风险也随之上升。拿到列表页之后先用浏览器的开发者工具看一眼页面结构。现在的房源列表大部分是服务端渲染配合部分异步加载数据可能嵌在HTML里也可能从接口返回JSON。判断方法很简单用Python的requests把页面拉下来然后搜一个房源标题里特有的字符串如果能在HTML源码里找到那就是服务端渲染用解析库直接处理如果找不到就得去Network面板里找XHR请求了。2.2 请求策略、解析逻辑与字段落地这里给一套我常用的最小可行代码框架配合注释说明每一步的意图。利用requests获取页面配合解析HTML。import requests from bs4 import BeautifulSoup import pandas as pd import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.5, } def fetch_list_page(city_code, page_no): 抓取某个城市二手房的列表页返回HTML文本 url fhttps://example-realestate.com/{city_code}/ershoufang/pg{page_no}/ resp requests.get(url, headersHEADERS, timeout15) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def parse_list_page(html): 从列表页HTML中解析房源字段 soup BeautifulSoup(html, html.parser) items [] for li in soup.select(.house-item): title_node li.select_one(.title a) if not title_node: continue title title_node.text.strip() community li.select_one(.community).text.strip() house_info li.select_one(.house-info).text.strip().split(|) total_price li.select_one(.total-price).text.strip() unit_price li.select_one(.unit-price).text.strip() items.append({ title: title, community: community, layout: house_info[0].strip() if len(house_info) 0 else , area: parse_area(house_info[1]) if len(house_info) 1 else None, orientation: house_info[2].strip() if len(house_info) 2 else , total_price: parse_price(total_price), unit_price: parse_price(unit_price), }) return items这只是演示性质的伪代码不同的网站需要替换选择器。写解析逻辑的时候有一个很重要的原则每个字段都要单独写一个小函数做清洗不要直接在解析列表推导里塞一堆replace。比如面积字段原始文本可能是“89.5平米”或者“89.5㎡”统一转成浮点数总价字段可能是“650万”也可能是“6,500,000”要转成以“万”为单位的数值。把这些清洗逻辑集中起来后面做数据分析会省很多事。2.3 增量抓取与去重机制爬数据最怕的不是慢而是重复和脏。尤其是二手房这种“同一套房挂牌信息每天变”的场景如果没有唯一标识后期去重就是灾难。我建议在字段设计阶段就给每套房源留一个house_id字段通常列表页链接里会带一长串数字编号把它提取出来作为主键。抓取时先读一下本地已有的主键集合新数据只有在这个集合里不存在时才写入。这套逻辑用十几行就能实现import os SAVED_IDS set() DATA_PATH houses.csv if os.path.exists(DATA_PATH): df_old pd.read_csv(DATA_PATH) SAVED_IDS set(df_old[house_id].astype(str)) new_rows [] for page_no in range(1, 101): html fetch_list_page(某市, page_no) items parse_list_page(html) for item in items: hid item[house_id] if hid not in SAVED_IDS: new_rows.append(item) SAVED_IDS.add(hid) # 关键每次请求之间随机sleep避免固定频率触发反爬 time.sleep(random.uniform(1.5, 4.0))两次请求之间的停顿是给你自己留退路的操作。别小看这个随机sleep很多反爬策略第一步就是检测请求间间隔是否完全规律。你把自己伪装成一个“有点磨蹭但还算正常”的用户比那种毫秒级扫页的脚本存活率高得多。3. Geopy地理编码把小区地址换算成经纬度的工程细节3.1 地址标准化清洗不是小事拿到房源数据之后第二个大工程就是把“小区名称”变成经纬度。我最早犯过的错是直接把“某小区”这种名字扔给地理编码服务结果大量请求返回歧义结果全国叫“阳光花园”的小区可能有几十个光靠名字根本无法确定到底是谁。所以做地理编码前必须先做地址标准化。通常要把“城市 行政区 板块 小区名”拼成一个完整地址字符串。比如“某市 朝阳路街道 远大板块 阳光花园二期”。但很多时候列表页只给了小区名行政区与板块要从当前列表页的URL参数或面包屑导航里提取。这批数据得在抓取阶段就跟着一起存下来等到了编码阶段再拆分链条容易断。另外一个容易踩坑的点是小区名里的各种后缀噪声比如“阳光花园二期”“阳光花园B区”“阳光花园东区”这些在后缀上不同的名对地理编码来说往往指向同一个坐标。所以我一般会先做一步标准化把“一期/二期/三期”“东区/西区/南区/北区”“A/B/C栋”里的这些规则词去掉先生成一个community_base字段用于编码同时保留原始名称用于展示。3.2 Nominatim调用与并发控制Geopy本身不是一个地理编码服务它是一个封装了多个地理编码服务接口的Python库最常用的是OpenStreetMap的Nominatim。调用方式非常简洁from geopy.geocoders import Nominatim from geopy.distance import geodesic geolocator Nominatim(user_agenthousing_analysis_demo/1.0 (your_emailexample.com)) def geocode_address(address, retries3): for i in range(retries): try: location geolocator.geocode(address, timeout10) if location: return location.latitude, location.longitude except Exception as e: print(f第{i1}次尝试失败: {address}, {e}) time.sleep(2) return None, None这里有两个必须注意的点。第一user_agent必须按照官方要求改成自己的应用名称和联系方式一堆爬虫脚本都用默认的geopy/2.x很容易被服务端直接拒掉。第二Nominatim的公共实例有严格的频率限制官方要求是每秒最多1次请求。如果房源样本上万条建议用本地OSM数据库或者换用Mapbox/Baidu/高德等付费或认证服务不要把公共Nominatim当成生产级工具来用。我实际处理5000条房源时用单线程加sleep的方式跑完大概花了两个多小时。这个速度确实不快但胜在稳定。如果你实在要提速可以开多进程但每个进程的请求频率加起来还是不能超过服务限制否则IP被临时封了更浪费时间。3.3 坐标系、坐标偏转与可视化对齐地理编码得到的坐标是WGS84坐标系也就是GPS使用的国际标准。但国内大多数地图底图在实际叠加标注时用的可能是GCJ-02或者BD-09坐标系。直接把WGS84坐标撒到某些国内地图SDK上会出现几十米到几百米不等的偏移在住区密集区域很容易把属于A小区的点落到B小区头上。解决方式也很直接要么在采集阶段直接用平台自带的位置信息如果平台页面里标注了经纬度就直接用平台坐标不要自己再编码一遍要么在做可视化时选择支持WGS84的底图来源比如光栅切片方式加载OSM底图或者自己做一个坐标纠偏的转换函数把WGS84转成GCJ-02。我在实际项目中会同时存两套坐标一套是地理编码得到的原始坐标用于距离计算一套是转换后的展示坐标用于底图叠加。距离计算用原始坐标的好处是所有房源在一个统一的坐标系下距离度量不打架可视化再单独做坐标转换。不要为了可视化方便把距离分析也建立在偏移过的坐标上那样算出来的“距离”很容易掩盖真实的地理关系。4. 距离、通勤、配套三个与房价强相关的地理特征测算坐标到手之后数据分析才真正开始。给你分享三个我实际验证过、和房价相关性最稳定的地理特征维度。4.1 到商圈核心的距离所有城市都有一个或几个公认的商圈核心比如老城区的中央商业街或者新区的CBD。这个核心点的坐标需要你手动确定——通常是当地租金最贵、写字楼最集中、或者商场人流最大的那一小块区域。把每一套房源坐标和这个核心点坐标做球面距离计算用Geopy一行就能搞定from geopy.distance import geodesic core_point (39.9042, 116.4074) # 示例用目标城市核心坐标替换 def distance_to_core(lat, lon): if lat is None or lon is None: return None return geodesic((lat, lon), core_point).kilometersgeodesic计算的是椭球体上的真实最短距离比平面直角坐标系的欧氏距离更接近实际步行或行车的直线参考。虽然它不是道路距离但在宏观趋势分析里直线距离已经能解释相当大比例的房价变异。分析时我建议把房源按照距离核心点的远近分成几个环带比如0-3公里、3-6公里、6-10公里、10公里以上然后分别统计各环带的单价中位数。这样看比直接画散点图要更加稳健因为避免了个别极端房源对散点的干扰。4.2 到地铁站步行距离第二个高价值特征是“到最近地铁站的距离”。这里有的做法是直接调地图API算步行导航距离但请求量大、成本高。一个折中的办法是用爬虫抓取当地所有地铁站点的名称和坐标这一步通常比较轻松一次性搞定对每一套房源计算它到所有地铁站的最小直线距离如果想更精细可以用OSM路网数据做一次离线步行距离估算。第2步的计算量看起来很大但实际上用KD树或者scipy.spatial.distance.cdist来批量算几万条数据也是秒级完成import numpy as np from scipy.spatial import cKDTree # metro_coords: 所有地铁站经纬度数组 # house_coords: 所有房源经纬度数组 tree cKDTree(metro_coords) distances, indices tree.query(house_coords, k1)灵魂拷问直线距离2公里和步行距离2.5公里对房价的影响方向一样吗在大多数情况下方向一致但量级会有偏差。如果项目要求比较严谨建议至少抽样几十套房源人工用地图App计算真实的步行时间和直线距离做一个线性校准。不需要太准能校准出“这个城市的直线距离折算步行时间的平均系数”就够了。我测过的一个城市样本里直线距离每减少100米二手房挂牌单价大约上涨0.8%到1.5%但这个系数在市中心和远郊差别很大。市中心的步行尺度竞争激烈溢价敏感远郊人们更依赖地铁地铁距离的影响反而陡峭。这种非线性关系正是简单说“离地铁越近越贵”的人看不透的地方。4.3 周边POI密度配套丰富度第三个特征是“配套密度”。这里的思路是以每套房源为中心画一个1公里或1.5公里的缓冲区统计缓冲区里有多少个餐饮、购物、学校、医疗、休闲类POI点。POI数据的来源可以是公开的地图接口也可以是自己爬取的兴趣点列表。缓冲区分析用GeoPandas会比较顺手import geopandas as gpd from shapely.geometry import Point gdf gpd.GeoDataFrame(houses, geometrygpd.points_from_xy(houses[lon], houses[lat])) gdf.set_crs(epsg4326, inplaceTrue) gdf gdf.to_crs(epsg3857) # 转投影坐标系方便算面积 poi_gdf gpd.GeoDataFrame(pois, geometrygpd.points_from_xy(pois[lon], pois[lat])) poi_gdf.set_crs(epsg4326, inplaceTrue) poi_gdf poi_gdf.to_crs(epsg3857) buffers gdf.geometry.buffer(1000) # 1公里缓冲区统计每个缓冲区内的POI数量之后你会得到一个“配套分”字段。把配套分和单价做相关性分析通常能看到比单纯距离更强的解释力因为一套房周围的配套密度本身就是多重地理因素的综合结果。5. 热力图与散点图谱让隐藏关系自己浮现5.1 单价热力图距离计算完之后一定要把结果画出来看。我最常做的一件事是画“单价热力图”。用Folium配合热力插件可以直接把经纬度和单价数据叠加到交互式底图上from folium.plugins import HeatMap import folium m folium.Map(location[city_lat, city_lon], zoom_start12) heat_data [[row[lat], row[lon], row[unit_price]] for _, row in houses.iterrows() if row[lat]] HeatMap(heat_data, radius18, blur12, min_opacity0.3).add_to(m) m.save(house_price_heatmap.html)热力图是探索性分析里最直观的手段。你会非常清楚地看到房价的“脊骨”和“洼地”某个片区的红色高亮可能沿着地铁线像脊柱一样延伸某个看起来离中心不远的板块却因为断头路、铁路切割或者大型公园阻隔在图上形成一个明显的蓝色空洞。上次我分析某个样本城市的数据时就发现一个很有意思的现象单价最高的区域并不是城市地理中心而是在中心和地铁换乘大站之间的一小段弧线上。周边老破小多、路面狭窄、以小店为底商离核心商圈也不是最近的但就是贵。再细看那段弧线上集中了三个新建的改善型楼盘房龄新、得房率高、学区稳定叠加上地铁通勤便利和成熟底商才形成了这个“隐藏高价带”。这种结论如果不把价格渲染到地图上只看表格里的均价排序是得不出任何可操作判断的数据样本会被淹没在均值之中差异被平均掉规律自然就隐藏起来了。5.2 面积-总价-距离散点组合热力图适合看空间分布但要看变量之间的相互作用还是得回归到散点图和二维分面。我常用的一个组合是X轴到核心的距离Y轴总价点大小面积点颜色单价import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(12, 8)) sc ax.scatter( houses[dist_to_core], houses[total_price], shouses[area] * 0.5, chouses[unit_price], cmapviridis, alpha0.6, ) plt.colorbar(sc, label单价元/平米) ax.set_xlabel(到核心商圈直线距离km) ax.set_ylabel(总价万元) plt.show()这个图能同时看出几个维度的信息。正常情况下随着距离增加总价应该总体下降但是点的颜色单价和大小面积会告诉你这个下降到底是因为面积变小还是因为单价变低。如果距离远、面积大、总价仍然坚挺说明购买力正在向外围改善型区域迁移。还有一招很实用把画布按照市场总价分成几个区间。比如总价200万以下、200-400万、400-800万、800万以上四张小分面图分别画距离-单价关系。不同总价段对距离的敏感度差别很明显高端市场往往对距离不那么敏感但对着资源稀缺度极其敏感刚需市场则相反距离几乎是生命线。这种结构性差异只有分面下来才看得清。6. 合规边界与数据质量的四个常见坑6.1 robots与个人信息边界写到这必须专门提醒一下合规问题。爬虫的合规边界不是“能访问就能爬”而是要尊重目标网站的robots协议和使用条款。房源信息如果是公开挂牌信息商家本身也希望通过搜索被获取这类数据的采集风险相对较低但依然要注意采集频率不要影响目标网站的正常运维抓下来的数据不要用于骚扰性推销或者变相骚扰如果涉及联系人和电话信息坚决不入库、不分析——个人联系方式不是你应该爬的字段。我在做分析时只保留房源属性、价格和位置这三类信息联系人字段在解析阶段就直接丢弃。这既是自我保护也是对数据伦理的基本尊重。6.2 反爬识别与请求频率控制现在的房源平台普遍有比较成熟的Web应用防火墙和UA识别、IP频率限制。我的建议是尽量用“低强度、长周期、分类别”的方式来取数。比如每天固定跑两轮增量抓取每轮间隔5~8秒最多不超过100页。这样一天下来大概能更新几百到上千条房源对分析来说完全够用。不要指望一次爬完整个城市几十万条数据那种玩法很容易把自己的IP送进小黑屋。代理IP不是不能用但那是个无底洞成本高、维护麻烦除非真要做大规模持续采集否则不建议新手一上来就折腾代理池。另外请求失败一定要做退避策略。连续失败3次立刻停下来至少休息30分钟再继续。这个“停手”的功夫比盲目重试更有效。我在脚本里通常设置一个指数退避的计数器第一次失败等30秒第二次等60秒第三次等180秒再失败就直接退出程序并发送通知。自动提醒能让你在被封之前及时止损。6.3 房源数据撒谎面积、报价、挂牌价与成交价的偏差即使爬下来的数据再干净也要知道它天生带偏。最大的一层偏差是挂牌价不是成交价。房主的心理预期和市场真实成交之间往往有一道需要谈判才能磨平的缝隙。有些房主挂牌就是纯试水价格高出市场两成挂着几个月也不降价有些房主急于出手挂牌价已经低于最近成交价。这些都是挂牌数据里无法避免的噪声。第二个常见问题是面积注水。不同年代的小区对“建筑面积”和“套内面积”的标注口径不一样有的赠送阳台算一半面积有的把地下室也摊进去。如果拿单价总价/面积作为主要因变量要先做一步面积异常值清洗。我通常会把面积小于30平米或者大于300平米的房源单独看看确认一下是不是车库、商铺混进来了。另外单价低于5000或者高于同板块均值3倍的数据也大概率是录入错误或者产权特殊比如只有使用权建议打上异常标记后再决定要不要剔掉。6.4 地理编码失败后的兜底方案地理编码一定会遇到失败的情况。地址写的太模糊、小区名太新、服务端返回结果偏差太大这些都会造成坐标缺失或错标。我的兜底策略是分三层第一层重试与候选修正。把搜索关键词从完整地址改为“小区名”如果还是失败就尝试去掉“小区”“花园”这类通名后缀只保留核心地名。第二层人工抽样修正。把所有未编码成功的地址列出来用地图App快速查询一批把坐标更新回数据表命中率通常在70%以上。第三层放弃与标记。剩余实在查不到的保留到geocode_status字段里标为failed分析的时候不参与地理特征计算但不能整体删除整条房源——因为价格字段可能是可用的只是位置维度缺失。还有一点地理编码返回的坐标不是绝对精准。Nominatim有时会把地址定位到小区入口附近误差一般在几十米到一两百米级别。你要分析的是“到地铁站的直线距离”这个误差会被放大。所以我自己对“离地铁站距离”这个特征会在结果里加一个“±0.15km”的容错带结论解读时不过度抠细节。最后分享一个我自己的数据分析习惯做完一轮房源数据的地理分析之后不要着急下结论。样本量不够时先把“异常值”单独列出来看判断它们是录入错误、特殊房源还是真正值得关注的趋势苗头。有一次我在数据里发现某个远离市中心的楼盘单价特别高几乎和市中心齐平。最初以为是编码错误后来交叉验证发现是本地一个著名的高端养老社区带医疗配套总价门槛极高单价自然惊人。这种形态恰恰又是“地理位置隐藏关系”的一种表现——它不在常规的区位逻辑里但坐标一旦展开它自己就跳出来了。用Python抓取二手房数据只是第一步把Geopy的距离空间算清楚、把热力图画出来你才真正拥有了对一座城市房价形成机制的解释能力。也正因为如此我始终觉得这种“爬虫地理信息”的项目比单纯写一百个通用爬虫Demo有意义得多。
返回列表