Python爬虫实战:5分钟搞定高德地图公交站点坐标采集(附完整代码)
Python爬虫实战:5分钟搞定高德地图公交站点坐标采集(附完整代码)
最近在做一个城市通勤分析的小项目,需要用到全市公交站点的精确坐标。一开始想着找现成的数据集,结果发现要么是几年前的旧数据,要么就是格式混乱、坐标不准。折腾了半天,最后还是决定自己动手,直接从高德地图上把数据“拿”下来。没想到,整个过程比预想的要顺畅得多,核心代码写下来也就几十行,从零开始到拿到第一批数据,真的用不了五分钟。如果你也遇到过类似的需求——比如做交通规划分析、商业选址,或者只是想做个公交到站提醒的小工具——那么这篇实战指南应该能帮你省下不少时间。我们不谈复杂的爬虫框架,也不深入反爬机制,就聚焦一件事:如何用最直接、最有效的方式,从高德地图获取到你想要的公交站点经纬度,并且让代码即拿即用。
1. 准备工作:思路梳理与工具选择
在动手写代码之前,花两分钟理清思路至关重要。我们的目标很明确:获取指定城市、指定公交线路的所有站点名称及其经纬度坐标。高德地图作为国内主流的地图服务商,其数据更新及时、覆盖面广,是我们理想的数据源。
那么,数据从哪里来?直接爬取网页界面(Web Scraping)是一种方法,但面对动态加载的复杂页面和可能频繁变动的页面结构,这种方法维护成本高。更高效、更稳定的方式是寻找其背后的数据接口(API)。幸运的是,高德地图在为用户提供地图服务时,其前端页面会通过一系列接口与后端服务器通信,获取公交线路、站点等数据并渲染在地图上。我们的任务就是找到这个接口,并模拟合法的请求来获取数据。
提示:任何数据获取行为都应遵守相关服务条款和数据使用规范。本文介绍的方法仅用于个人学习与技术研究,请勿用于大规模、高频次的商业数据抓取,以免对目标服务器造成不必要的压力。
接下来,我们需要准备几样“工具”:
- Python 3.7+:我们的主力编程语言。
- Requests库:用于发送HTTP请求,比内置的
urllib更简洁易用。 - Pandas库:用于处理和保存数据,非常方便。
- 一个文本编辑器或IDE:比如VS Code、PyCharm,甚至Jupyter Notebook都可以。
你可以通过以下命令快速安装所需的库:
pip install requests pandas
如果安装速度慢,可以考虑使用国内的镜像源,例如:
pip install requests pandas -i https://pypi.tuna.tsinghua.edu.cn/simple
环境就绪后,我们还需要一个关键的“钥匙”:高德地图的Web服务密钥(Key)。虽然我们模拟的是网页端的请求,但了解其官方API的申请和使用逻辑有助于我们理解数据请求的构成。实际上,对于本文要使用的特定接口,可能不需要正式的Key,但为了方法的通用性和可扩展性,建议你注册一个高德开放平台开发者账号,获取一个Web服务的Key。这个过程是免费的,只需要几分钟。
2. 核心探索:定位数据接口与参数解析
这是整个过程中最具“技术侦探”色彩的一步。我们打开浏览器(推荐Chrome或Edge的开发者工具),访问高德地图官网,在搜索框输入一条公交线路,比如“北京 1路”。
按下F12打开开发者工具,切换到 “网络”(Network) 选项卡。在搜索结果显示地图和公交线路详情的同时,你会看到开发者工具里捕获到了一系列的网络请求。我们的目标是找到那个包含了公交线路和站点详细数据的请求。
通常,这类数据请求是XHR(异步请求)类型的,返回的数据格式是JSON,因为它结构清晰,便于前端解析。你需要在一堆请求中仔细筛选,寻找包含“bus”、“line”、“station”等关键词的请求URL。这个过程可能需要一点耐心和尝试。
假设我们找到了一个疑似接口:https://bus.amap.com/api/search/businfo。点击这个请求,查看它的“标头”(Headers)和“负载”(Payload)或“参数”(Parameters)。
在请求标头中,我们主要关注:
- User-Agent: 用于标识浏览器身份,服务器可能据此判断请求来源。
- Referer: 表示请求的来源页面,有时是必填的反爬措施。
在请求参数中,我们会发现一些关键的查询参数(Query Parameters),它们通常以?key1=value1&key2=value2的形式附加在URL后面。对于公交线路查询,关键的参数可能包括:
| 参数名 | 示例值 | 说明 |
|---|---|---|
keywords | 1路 | 要查询的公交线路名称 |
city | 110000 | 城市编码(高德特定的城市adcode) |
extensions | all | 返回信息的详细程度 |
output | JSON | 返回数据格式 |
key | 你的高德Key | 开发者密钥(部分接口需要) |
其中,city参数的城市编码需要特别注意。高德使用一套自己的行政区划编码(adcode),比如北京是110000,上海是310000。你可以通过高德开放平台的“行政区域查询”API来获取,或者更简单一点,直接在高德地图网页版搜索城市,从URL或网络请求中捕捉到这个编码。
找到接口和参数后,我们可以先用浏览器直接访问构造好的URL试试看。将参数拼接起来,在浏览器地址栏输入:
https://bus.amap.com/api/search/businfo?keywords=1路&city=110000&extensions=all&output=JSON&key=你的Key
如果一切正常,浏览器会返回一个结构清晰的JSON数据。这就是我们需要的“宝藏”!接下来,就是如何用Python程序化地获取并解析这个“宝藏”。
3. 代码实战:从请求到数据落地的完整流程
理论清晰后,我们开始编写代码。我会将整个过程封装成一个函数,力求清晰和复用性。我们创建一个新的Python文件,比如叫做 fetch_bus_stations.py。
首先,导入必要的库,并定义一个主函数,它接收线路名和城市编码作为输入。
import requests
import pandas as pd
import time
import random
def get_bus_station_coordinates(keywords, citycode, api_key=None):
"""
根据公交线路名称和城市编码,获取所有站点的坐标。
参数:
keywords (str): 公交线路名称,如 '1路'
citycode (str): 高德城市adcode,如 '110000'(北京)
api_key (str, optional): 高德Web服务Key。部分接口可能不需要。
返回:
pandas.DataFrame: 包含站点信息的DataFrame,如果失败则返回None。
"""
# 1. 构建请求URL和参数
base_url = "https://bus.amap.com/api/search/businfo"
params = {
'keywords': keywords,
'city': citycode,
'extensions': 'all',
'output': 'JSON'
}
# 如果接口需要key,则添加
if api_key:
params['key'] = api_key
# 2. 设置请求头,模拟浏览器行为
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Referer': 'https://bus.amap.com/'
}
# 3. 发送HTTP GET请求
try:
response = requests.get(base_url, params=params, headers=headers, timeout=10)
response.raise_for_status() # 检查请求是否成功
data = response.json() # 解析JSON响应
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
return None
except ValueError as e:
print(f"解析JSON失败: {e}")
return None
# 4. 检查API返回状态
if data.get('status') != '1':
print(f"API返回错误: {data.get('info')}")
return None
# 5. 解析数据
buslines = data.get('buslines', [])
if not buslines:
print(f"未找到线路: {keywords}")
return None
stations_list = []
# 一条线路可能有不同方向的子线路(如上行、下行)
for busline in buslines:
line_name = busline.get('name', '')
stations = busline.get('stations', [])
for station in stations:
station_info = {
'线路名称': line_name,
'站点ID': station.get('id', ''),
'站点名称': station.get('name', ''),
'经度': station.get('location', '').split(',')[0] if station.get('location') else '',
'纬度': station.get('location', '').split(',')[1] if station.get('location') else '',
'区域': station.get('area', ''),
'站点序号': station.get('index', '')
}
stations_list.append(station_info)
# 6. 转换为DataFrame并返回
if stations_list:
df = pd.DataFrame(stations_list)
return df
else:
print("该线路未解析到站点信息。")
return None
这段代码做了以下几件事:
- 构建请求:将线路名、城市码等参数组合成完整的请求URL。
- 模拟浏览器:通过设置
User-Agent和Referer头,让请求看起来更像来自真实浏览器。 - 发送与接收:使用
requests.get发送请求,并处理可能的网络或超时异常。 - 状态检查:检查返回的JSON数据中
status字段是否为'1'(通常表示成功)。 - 数据解析:从复杂的JSON结构中,提取出我们关心的字段:站点ID、名称、经纬度(
location字段通常是"经度,纬度"的字符串)。 - 结构化返回:将提取的信息存入字典列表,最后转换为Pandas DataFrame,便于后续处理。
现在,我们来调用这个函数,并保存结果。
if __name__ == '__main__':
# 示例:获取北京市1路公交的站点
keywords = '1路'
citycode = '110000' # 北京
# 如果你有高德Key,可以在这里传入
# api_key = 'your_amap_api_key_here'
df_stations = get_bus_station_coordinates(keywords, citycode) #, api_key)
if df_stations is not None and not df_stations.empty:
print(f"成功获取到 {keywords} 的 {len(df_stations)} 个站点信息。")
print(df_stations.head()) # 预览前几行数据
# 保存到CSV文件
filename = f"{keywords.replace(' ', '_')}_stations.csv"
df_stations.to_csv(filename, index=False, encoding='utf-8-sig')
print(f"数据已保存至: {filename}")
else:
print("未能获取数据。")
运行这段代码,如果网络和接口都正常,你会在当前目录下得到一个名为1路_stations.csv的文件,用Excel或文本编辑器打开,里面就是整齐的站点数据了。
4. 效率提升与稳健性增强
上面的代码已经可以完成单条线路的抓取。但在实际项目中,我们往往需要获取一个城市成百上千条线路的数据。这时,我们需要考虑效率和稳健性。
批量处理与速度控制 我们需要一个公交线路名称的列表。这个列表可以从本地文件(如Excel、CSV)或简单的Python列表中读取。然后使用循环进行批量抓取。
def batch_fetch_bus_data(line_list, citycode, output_dir='./bus_data'):
"""
批量获取多条公交线路的站点数据。
参数:
line_list (list): 公交线路名称列表
citycode (str): 城市编码
output_dir (str): 输出目录
"""
import os
os.makedirs(output_dir, exist_ok=True)
all_data_frames = []
failed_lines = []
for i, line_name in enumerate(line_list):
print(f"正在处理 ({i+1}/{len(line_list)}): {line_name}")
df = get_bus_station_coordinates(line_name, citycode)
if df is not None and not df.empty:
# 保存单条线路数据
single_file = os.path.join(output_dir, f"{line_name.replace(' ', '_')}.csv")
df.to_csv(single_file, index=False, encoding='utf-8-sig')
# 同时添加到总表
all_data_frames.append(df)
print(f" 成功,获取到 {len(df)} 个站点。")
else:
failed_lines.append(line_name)
print(f" 失败。")
# 关键:添加随机延迟,避免请求过快触发反爬
sleep_time = random.uniform(1, 3) # 随机等待1-3秒
time.sleep(sleep_time)
# 合并所有成功的数据并保存
if all_data_frames:
combined_df = pd.concat(all_data_frames, ignore_index=True)
combined_file = os.path.join(output_dir, f'all_bus_stations_{citycode}.csv')
combined_df.to_csv(combined_file, index=False, encoding='utf-8-sig')
print(f"\n所有数据已合并保存至: {combined_file}")
if failed_lines:
print(f"\n以下线路获取失败: {failed_lines}")
错误处理与重试机制
网络请求充满不确定性。除了基本的try...except,对于偶发的请求失败(如超时、服务器临时错误),我们可以加入简单的重试逻辑。
def robust_request(url, params, headers, max_retries=3):
"""带重试机制的请求函数"""
for attempt in range(max_retries):
try:
resp = requests.get(url, params=params, headers=headers, timeout=15)
resp.raise_for_status()
return resp
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
if attempt < max_retries - 1:
wait = (attempt + 1) * 2 # 退避等待
print(f"请求超时或连接错误,{wait}秒后重试... (尝试 {attempt + 1}/{max_retries})")
time.sleep(wait)
else:
raise e # 重试多次后仍失败,抛出异常
return None
在get_bus_station_coordinates函数中,将requests.get替换为对这个robust_request函数的调用,可以显著提升程序在不太稳定网络环境下的健壮性。
数据去重与坐标系统注意
在合并多条线路数据时,同一个换乘站点可能会出现在不同线路的数据中,导致重复。我们可以根据站点ID或站点名称+经纬度进行去重。
# 在合并DataFrame后,进行去重
if all_data_frames:
combined_df = pd.concat(all_data_frames, ignore_index=True)
# 根据站点ID去重(假设ID唯一)
combined_df.drop_duplicates(subset=['站点ID'], inplace=True, ignore_index=True)
# 或者根据名称和坐标去重(精度要求高时)
# combined_df.drop_duplicates(subset=['站点名称', '经度', '纬度'], inplace=True, ignore_index=True)
另外,需要明确一点:高德地图返回的经纬度坐标,通常采用的是GCJ-02坐标系(火星坐标系)。这是国内出于地理信息安全考虑,在WGS-84坐标系(国际通用GPS坐标系)基础上进行非线性加密后的坐标系。如果你需要将获取的坐标与其他来源的WGS-84坐标(例如GPS设备记录、某些国际地图数据)一起使用,就需要进行坐标转换。高德开放平台也提供了官方坐标转换API,但这超出了本文“5分钟快速获取”的范畴。在大多数仅用于高德地图本身显示或相对位置分析的应用中,直接使用GCJ-02坐标是没有问题的。
5. 数据应用与扩展思路
拿到干净、结构化的公交站点坐标数据后,它的用武之地就非常广泛了。这里分享几个我实践过或看到过的应用场景:
1. 地理可视化分析
这是最直观的应用。你可以使用Python的Folium、Pyecharts库,或者专业的GIS软件如QGIS,将站点数据在地图上打点显示。通过可视化,你可以快速洞察公交网络的密度分布、识别覆盖盲区,或者结合人口热力图分析线路规划的合理性。
# 使用Folium进行简单可视化的示例
import folium
# 假设df_stations是包含'纬度'和'经度'列的DataFrame
# 以第一个站点的坐标为中心创建地图
center_lat = df_stations['纬度'].astype(float).iloc[0]
center_lon = df_stations['经度'].astype(float).iloc[0]
m = folium.Map(location=[center_lat, center_lon], zoom_start=13)
# 为每个站点添加标记
for idx, row in df_stations.iterrows():
folium.CircleMarker(
location=[float(row['纬度']), float(row['经度'])],
radius=3,
popup=row['站点名称'],
color='blue',
fill=True
).add_to(m)
# 保存为HTML文件
m.save('bus_stations_map.html')
2. 结合路径规划API进行通勤模拟 有了站点坐标,你可以进一步调用高德或百度的路径规划API(步行、公交、驾车),计算从任意点到这些站点的距离和时间。这对于分析特定区域(如一个住宅小区或商业中心)的公交可达性非常有价值。你可以批量计算该点到所有周边站点的时间,找出“最近”的公交站,并统计在特定时间阈值(如5分钟、10分钟步行)内的站点数量,作为衡量该地点公交便利度的指标。
3. 构建自定义的公交查询工具 如果你对某几条固定线路特别关心(比如上下班线路),可以编写一个脚本,定期抓取这些线路的站点数据,并结合实时公交API(如果高德提供的话)获取车辆到站信息,做一个推送到手机或电脑桌面的个性化到站提醒工具,这比通用APP的提醒可能更符合你的动线。
扩展挑战:获取线路走向
本文聚焦于站点坐标。有时我们还需要公交线路的几何形状,即车辆行驶的路径。在高德返回的JSON数据中,busline对象里可能包含一个polyline字段,它是经过编码的一串坐标序列(通常是“经度,纬度;经度,纬度;...”的格式,或类似Google的Encoded Polyline Algorithm压缩格式)。解析这个字段,就能得到绘制线路走向所需的所有路径点。这比站点数据量更大,但也为更精细的时空分析(如计算线路长度、分析弯道)提供了可能。
整个流程走下来,你会发现核心的获取逻辑非常简洁。真正的功夫往往花在前期对数据接口的探索、对返回数据结构的理解,以及后期为了让脚本更稳定、更高效而添加的“外围”代码上。数据采集从来不是一次性任务,构建一个鲁棒、可维护的数据管道,才能让这些数据持续为你创造价值。最后一个小建议,定期(比如每季度或每半年)运行一下你的脚本,更新数据,因为城市的公交线路总是在优化调整之中。
更多推荐
所有评论(0)