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后面。对于公交线路查询,关键的参数可能包括:

参数名示例值说明
keywords1路要查询的公交线路名称
city110000城市编码(高德特定的城市adcode)
extensionsall返回信息的详细程度
outputJSON返回数据格式
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

这段代码做了以下几件事:

  1. 构建请求:将线路名、城市码等参数组合成完整的请求URL。
  2. 模拟浏览器:通过设置User-AgentReferer头,让请求看起来更像来自真实浏览器。
  3. 发送与接收:使用requests.get发送请求,并处理可能的网络或超时异常。
  4. 状态检查:检查返回的JSON数据中status字段是否为'1'(通常表示成功)。
  5. 数据解析:从复杂的JSON结构中,提取出我们关心的字段:站点ID、名称、经纬度(location字段通常是"经度,纬度"的字符串)。
  6. 结构化返回:将提取的信息存入字典列表,最后转换为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的FoliumPyecharts库,或者专业的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压缩格式)。解析这个字段,就能得到绘制线路走向所需的所有路径点。这比站点数据量更大,但也为更精细的时空分析(如计算线路长度、分析弯道)提供了可能。

整个流程走下来,你会发现核心的获取逻辑非常简洁。真正的功夫往往花在前期对数据接口的探索、对返回数据结构的理解,以及后期为了让脚本更稳定、更高效而添加的“外围”代码上。数据采集从来不是一次性任务,构建一个鲁棒、可维护的数据管道,才能让这些数据持续为你创造价值。最后一个小建议,定期(比如每季度或每半年)运行一下你的脚本,更新数据,因为城市的公交线路总是在优化调整之中。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐