Python应用指南:利用高德地图API获取地铁站点出入口坐标

书接上回,这里防止有小伙伴对方法二运用的程度还不够举一反三,还是单独开一篇进行讲解,本篇文章依然以综合得分指数最高的地铁站——吕厝站进行分析,来获取站点的出入口位置坐标,站点的出入口位置坐标本质上属于高德POI分类的一部分,细分类别属于"交通设施服务;地铁站;出入口",所以本质上我们获取的是高德的POI数据,只是聚焦于站点出入口这个类别;

上篇文章指路:Python应用指南:利用高德地图API获取POI数据_利用高德地图api获取南京市内的商场poi及其格局分析-CSDN博客

先讲一下方法思路,一共四个步骤;

方法思路

  1. 通过高德拾取坐标,生成矩形,打印出来坐标
  2. 获取POI数据——通过调用高德地图API
  3. GeoJSON转成csv
  4. 坐标转换——高德坐标系(GCJ-02) to WGS84

第一步,通过高德拾取坐标:坐标拾取器 | 高德地图API (amap.com),先拾取要获取POI范围的坐标中心点,生成边长为500m的矩形,这里需要更改范围的话,自行调整即可,把这个side_length_meters = 500,调整成需要的范围即可,因为这里的地理坐标使用的是高德坐标系(GCJ-02),所以生成的范围也是高德坐标系下的结果,打印出来坐标;

这里通过高德地图的测距功能测得大概以吕厝站为起点,涵盖所有出入口的范围大约在500m左右,所以这里矩形创建的边长为500m;

完整代码#运行环境Python 3.11

import math
 
 
def get_rectangle_corners_gps(center_lng, center_lat, side_length_meters=500):
    # 地球平均半径,单位为米
    earth_radius = 6371000
    # 每度纬度对应的米数
    meters_per_degree_lat = math.pi * earth_radius / 180
    # 每度经度对应的米数(在给定纬度处)
    meters_per_degree_lng = meters_per_degree_lat * math.cos(math.radians(center_lat))
 
    # 计算纬度和经度的变化量
    delta_lat = side_length_meters / meters_per_degree_lat / 2
    delta_lng = side_length_meters / meters_per_degree_lng / 2
 
    # 计算四个角的坐标
    top_left = (center_lng - delta_lng, center_lat + delta_lat)
    top_right = (center_lng + delta_lng, center_lat + delta_lat)
    bottom_left = (center_lng - delta_lng, center_lat - delta_lat)
    bottom_right = (center_lng + delta_lng, center_lat - delta_lat)
 
    return [top_left, top_right, bottom_left, bottom_right]
 
 
# 使用示例
center_lng, center_lat = 121.508092, 31.098532
side_length_meters = 500
# 包含四个角的经纬度坐标的列表,顺序为左上角、右上角、左下角、右下角
corners = get_rectangle_corners_gps(center_lng, center_lat, side_length_meters)
print("矩形四个角的坐标为:", corners)

打印高德坐标系(GCJ-02)坐标结果如下;

第二步,把获取的500m的矩形进行分割,因为高德对POI获取的限制,多边形搜索接口中每个边界范围能够获取到的POI数量是有限制的,超过该限制的POI数据则不会返回,所以我们把矩形分割成尽可能小的矩形,防止数据返回失败,然后再进一步获取每个网格POI数据;

完整代码#运行环境Python 3.11

import requests
import json
import os
import math
from concurrent.futures import ThreadPoolExecutor

# 全局变量
KEYS = ["你的key"]  # 高德地图API的Key列表,可以添加多个以应对请求限制
SAVE_DIR = r"D:\data\gaode"  # 数据保存目录
GRID_SIZE = 0.001  # 网格大小为0.001度(大约为100m)
KEYWORD = "地铁出入口"  # 查询关键字,可以修改为其他关键词
polygon_coords = [
    [118.12548841420609, 24.492928304014796],
    [118.13042958579392, 24.492928304014796],
    [118.12548841420609, 24.488431695985206],
    [118.13042958579392, 24.488431695985206]
]  # 查询的多边形区域坐标
NUM_THREADS = 4  # 线程数量,可以根据需求调整


def file_exists(polygon, keyword, page):
    """检查文件是否已经存在"""
    lng_min, lat_min = polygon[0][0], polygon[2][1]
    filename = f"{SAVE_DIR}/poi_{lng_min}_{lat_min}_{keyword}_page{page}.geojson"
    return os.path.exists(filename)


def save_poi_data(polygon, keyword, page, data):
    """保存POI数据到GeoJSON文件"""
    if not os.path.exists(SAVE_DIR):
        os.makedirs(SAVE_DIR)  # 如果目录不存在则创建
    lng_min, lat_min = polygon[0][0], polygon[2][1]
    filename = f"{SAVE_DIR}/poi_{lng_min}_{lat_min}_{keyword}_page{page}.geojson"
    with open(filename, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=4)  # 保存数据为GeoJSON格式
    return True


def fetch_poi_data(polygon, key, keyword):
    """获取POI数据,支持分页"""
    lng_min, lat_min = polygon[0][0], polygon[2][1]
    page = 1
    while True:
        if file_exists(polygon, keyword, page):
            print(f"文件已存在,跳过:网格: {polygon},页数: {page}")
            page += 1
            continue

        polygon_str = f"{lng_min},{lat_min}|{polygon[1][0]},{polygon[1][1]}"
        api_url = f"https://restapi.amap.com/v3/place/polygon?polygon={polygon_str}&keywords={keyword}&key={key}&page={page}"

        try:
            response = requests.get(api_url, timeout=10)
            response.raise_for_status()  # 检查HTTP响应状态码
            data = response.json()

            infocode = data.get("infocode")
            if infocode == "10000" and data.get("pois"):
                save_poi_data(polygon, keyword, page, data)
                print(f"下载成功!网格: {polygon_str},页数: {page}")
                page += 1
                if len(data.get("pois")) < 20:  # 当POI数据少于20时,说明已经是最后一页
                    break
            elif infocode in ["10001", "10003", "10004"]:
                print(f"Key 出现问题,infocode: {infocode},切换到下一个Key进行重试...")
                return False  # 返回False以便切换Key进行重试
            else:
                print(f"请求失败或无数据,infocode: {infocode},信息: {data.get('info')}")
                break
        except Exception as e:
            print(f"请求异常,跳过此Key:{str(e)}")
            break
    return True  # 下载成功或完成所有页数时返回True


def generate_grids(polygon_coords):
    """生成网格"""
    min_lng = min([coord[0] for coord in polygon_coords])  # 获取多边形区域的最小经度
    max_lng = max([coord[0] for coord in polygon_coords])  # 获取多边形区域的最大经度
    min_lat = min([coord[1] for coord in polygon_coords])  # 获取多边形区域的最小纬度
    max_lat = max([coord[1] for coord in polygon_coords])  # 获取多边形区域的最大纬度

    grids = []
    lng_steps = math.ceil((max_lng - min_lng) / GRID_SIZE)  # 计算经度方向上的网格数量
    lat_steps = math.ceil((max_lat - min_lat) / GRID_SIZE)  # 计算纬度方向上的网格数量

    for i in range(lng_steps):
        for j in range(lat_steps):
            grid_min_lng = min_lng + i * GRID_SIZE  # 计算当前网格的最小经度
            grid_max_lng = min(grid_min_lng + GRID_SIZE, max_lng)  # 计算当前网格的最大经度
            grid_min_lat = min_lat + j * GRID_SIZE  # 计算当前网格的最小纬度
            grid_max_lat = min(grid_min_lat + GRID_SIZE, max_lat)  # 计算当前网格的最大纬度
            grid_polygon = [
                [grid_min_lng, grid_max_lat],
                [grid_max_lng, grid_max_lat],
                [grid_max_lng, grid_min_lat],
                [grid_min_lng, grid_min_lat]
            ]
            grids.append(grid_polygon)
    return grids


def download_poi_for_grid(polygon, key, keyword):
    """下载单个网格的POI数据"""
    success = fetch_poi_data(polygon, key, keyword)
    if not success:
        return False  # 返回False以便线程外层处理Key切换
    return True


def main():
    grids = generate_grids(polygon_coords)
    with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
        for idx, grid in enumerate(grids):
            key_idx = 0
            while key_idx < len(KEYS):
                key = KEYS[key_idx]
                future = executor.submit(download_poi_for_grid, grid, key, KEYWORD)
                if future.result():
                    break  # 如果下载成功或完成,则跳出循环
                key_idx += 1
            if key_idx == len(KEYS):
                print(f"所有Key均不可用,跳过此网格: {grid}")


if __name__ == "__main__":
    main()

这里进一步解释一下代码的逻辑,实现根据提供的多边形区域坐标生成一系列小网格,每个网格的大小由GRID_SIZE决定,接下来根据给定的网格、API Key和关键词发起请求,获取POI数据。该函数支持分页,当遇到请求错误或Key限制时会尝试使用不同的Key重新请求;

GRID_SIZE定义了网格的大小,单位是度(约等于100米),整个查询区域会被分割成多个这样的网格,这里的矩形均匀地划分为 25 个 100×100 的小矩形;

结果数据如下(部分);

第三步,就是把这些获取的GeoJSON转成csv,因为我们对网格进行了拆分,所以在合并的时候注意一下,这里文件的矩形顶点坐标会不一致,所以在文件合并的时候把读取文件名改成 *_地铁出入口_page1.geojson'即可,*这个星号表示选择所有列,其他注意事项可以看一下原文章;

    # 输入文件模式
    input_file_pattern = r'D:\data\gaode\*_地铁出入口_page1.geojson'

完整代码#运行环境Python 3.11

import json
import csv
from glob import glob


def merge_pois_from_files(file_pattern, output_file):
    all_pois = []

    # 获取所有匹配给定模式的文件路径
    file_paths = sorted(glob(file_pattern))

    if not file_paths:
        print("No files found matching the pattern.")
        return

    for input_file_path in file_paths:
        try:
            # 打开文件并读取内容,指定编码(根据你的文件实际编码,这里使用 utf-8 作为示例)
            with open(input_file_path, 'r', encoding='utf-8') as f:
                # 加载 JSON 数据
                data = json.load(f)

                # 检查是否存在 'pois' 键
                if 'pois' in data:
                    # 遍历 pois 列表
                    for poi in data['pois']:
                        # 提取所需字段
                        id = poi.get('id', '未找到')
                        name = poi.get('name', '未找到')
                        location = poi.get('location', '未找到')
                        address = poi.get('address', '未找到')
                        adname = poi.get('adname', '未找到')
                        cityname = poi.get('cityname', '未找到')
                        type = poi.get('type', '未找到')

                        # 将提取的信息添加到列表中
                        all_pois.append({
                            'id': id,
                            'name': name,
                            'location': location,
                            'address': address,
                            'adname': adname,
                            'cityname': cityname,
                            'type': type
                        })
                else:
                    # 如果文件中不包含 'pois' 键,打印错误信息
                    print(f"{input_file_path} does not contain 'pois' key.")
        except FileNotFoundError:
            # 捕获文件未找到的错误
            print(f"File not found: {input_file_path}")
        except json.JSONDecodeError:
            # 捕获 JSON 解析错误
            print(f"Invalid JSON format in {input_file_path}.")
        except Exception as e:
            # 捕获其他未预料的错误
            print(f"Unexpected error occurred while processing {input_file_path}: {e}")

    # 写入 CSV 文件
    write_to_csv(all_pois, output_file)


def write_to_csv(pois_list, output_file_path):
    # 定义 CSV 文件的字段名
    fieldnames = ['id', 'name', 'location', 'address', 'adname', 'cityname', 'type']

    # 写入 CSV 文件
    with open(output_file_path, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

        # 写入表头
        writer.writeheader()

        # 写入数据
        for poi in pois_list:
            writer.writerow(poi)


if __name__ == '__main__':
    # 输入文件模式
    input_file_pattern = r'D:\data\gaode\*_地铁出入口_page1.geojson'
    # 输出文件路径
    output_file_path = 'D:\data\gaode\merged_poi.csv'

    # 合并所有文件中的 POIs 并写入 CSV 文件
    merge_pois_from_files(input_file_pattern, output_file_path)

生成csv结果如下,我们只把地铁站点出入口的数据筛选出来即可;

第四步,我们把csv的坐标列手动分列一下,并把坐标从高德坐标系(GCJ-02)转到WGS84,批量转换工具:地图坐标系批量转换 - 免费在线工具 (latlongconverter.online)

文章仅用于分享个人学习成果与个人存档之用,分享知识,如有侵权,请联系作者进行删除。所有信息均基于作者的个人理解和经验,不代表任何官方立场或权威解读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图说交通

买猫粮,楼下的流浪猫在等我

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值