基于百度地图API利用协程高并发获取企业的经纬度

简介

本文探讨了如何利用 协程 高效调用百度地图 API 进行批量地理编码,Python 的 asyncio + aiohttp 结合 aiolimiter,能够实现高并发请求并智能限流。相比多线程,协程不仅具备更高的并发能力,还对硬件资源要求更低。
文章还提供了完整的代码示例,涵盖 数据存储优化、错误处理并发优化方案,适用于大规模地理编码任务。

介绍

有一些企业的地址信息需要获取的它们的经纬度地址。下表是我的表格地址一部分,我使用企业的详细地址。

登记名称行业类别住所
中渔(福建)船舶设计工程有限公司金属船舶制造福建省福州市晋安区金鸡山路59号金鸡山文化创意产业园鼎鑫建筑设计创意分园D区308-3场所
福建北强船舶管理有限公司其他水上运输辅助活动福建省福州市台江区宁化街道江滨西大道北侧海西金融大厦(IFC)办公主楼1307单元-4号
福建智达海洋船舶科技服务有限公司工程和技术研究和试验发展福建省福州市晋安区新店镇秀峰路185号高佳苑三期5#楼1层11店面
福州长远海运船舶技术咨询有限公司其他未列明商务服务业福建省福州市鼓楼区五四路158号环球广场19层04室

为获取地址的经纬度信息,我们可以调用地图 API,如百度地图或高德地图。然而,高德地图 API 充值门槛较高(最低 1000 元),因此本文选择使用百度地图 API 进行地理编码。

在使用 python 调用 API 的时候,可以使用多线程加速。如果你给百度地图充了钱,每秒钟可以调用100次API,那么显然多线程不是最优的,因为电脑没办法在1秒钟内并发100个线程。那么使用协程就可以实现高并发,并且性能远远好于多线程。

协程简介

协程是一种轻量级的并发方式,它与线程的区别在于:

线程:由操作系统管理,是真实存在的,并行执行任务。

协程:是由程序内部调度的,切换时不会引入线程的上下文切换开销。

协程的执行流程如下:

  • 系统维护一个协程任务队列。

  • 轮询检查是否有可执行的协程任务。

  • 若某个任务需要等待(如网络请求),系统会自动切换到其他任务,避免阻塞

示例:使用 asyncio 实现协程请求

import asyncio
from aiolimiter import AsyncLimiter
from tqdm import tqdm

# 每秒最多 3 个请求
limiter = AsyncLimiter(10, 1)


async def func(item):
    # 遵守速率限制
    await asyncio.sleep(3)
    return item


async def fetch(url, progress):
    async with limiter:
        result = await func(url)
        # 更新进度条
        progress.update(1)
        return result


async def main():
    urls = [str(item) for item in range(100)]
    # 创建进度条
    with tqdm(total=len(urls), desc="Processing") as progress:
        tasks = [fetch(url, progress) for url in urls]
        results = await asyncio.gather(*tasks)
    return results


result = asyncio.run(main())
print(result)

使用 func 函数类比网络请求函数,AsyncLimiter 限制协程调用速度。asyncio.sleep(3) 是非阻塞的,可用于测试协程。

使用 tqdm 展示进度条。

调用百度地图API的例子

百度地图地理编码文档地址:https://lbsyun.baidu.com/faq/api?title=webapi/guide/webservice-geocoding-base

下述是一个 GET 请求输入到浏览器中,即可获取到返回的结果:

https://api.map.baidu.com/geocoding/v3/?address=北京市海淀区上地十街10号&output=json&ak=您的ak&callback=showLocation

批量地址获取经纬度的代码:

import os
import pandas as pd
import asyncio
from aiolimiter import AsyncLimiter
from tqdm import tqdm
import aiohttp
import json
import pandas as pd

file = "data.xls"
df = pd.read_excel(file)

# 每秒最多 3 个请求
MAX_WORKERS = 3  # 根据API限制调整并发数
limiter = AsyncLimiter(MAX_WORKERS, 1)


# 接口配置
API_URL = "https://api.map.baidu.com/geocoding/v3"
AK = "自己申请的百度API的KEY"

RETRY_TIMES = 3  # 失败重试次数
TIMEOUT = 10  # 请求超时时间


def func(item):
    if not isinstance(item, str):
        return None
    item = item.replace("#", "")
    return item.strip()


ADDRESSES = df["住所"].map(func).to_list()


async def geocoding(session, idx, address, progress, retry=RETRY_TIMES):
    """地理编码请求函数(带重试机制)"""
    params = {
        "address": address,
        "output": "json",
        "ak": AK,
        "ret_coordtype": "gcj02ll",  # 可根据需要修改坐标系, 国测局坐标
    }

    for _ in range(retry):
        try:
            async with limiter:
                async with session.get(
                    API_URL, params=params, timeout=TIMEOUT
                ) as response:
                    if response.status == 200:
                        data = await response.text()
                        data = json.loads(data)
                        progress.update(1)
                        # 添加辅助信息,便于后续数据提取
                        data.update({"idx": idx, "address_input": address})
                        if data.get("status") == 0:
                            return data
                        else:
                            return {"err_msg": "api_error"}
                    elif 500 <= response.status < 600:
                        # 服务器错误,重新发起请求
                        continue
        except (aiohttp.ClientError, asyncio.TimeoutError):
            continue
        except Exception as e:
            return {"err_msg": str(e.args)}

    return {"err_msg": {}}


# # 运行多个 API 调用并保存结果
async def fetch_all():
    tasks = []
    with tqdm(total=len(ADDRESSES), desc="Processing") as progress:
        async with aiohttp.ClientSession() as session:
            for idx, address in enumerate(ADDRESSES):
                if address is None:
                    continue
                tasks.append(geocoding(session, idx, address, progress))
            results = await asyncio.gather(*tasks)  # 并发执行所有任务
            return results


results = asyncio.run(fetch_all())

output_fold = "API_results"
os.makedirs(output_fold, exist_ok=True)
base_name = os.path.basename(file).split(".")[0]
output_file = os.path.join(output_fold, f"{base_name}.json")

with open(output_file, "w") as f:
    json.dump(results, f, ensure_ascii=False, indent=2)

在上述代码中,没有从 API 调用的结果中提取数据,而是选择把结果保存在json文件中,后续再从json文件中提取需要的数据。API调用返回的结果,有很多数据,为了防止后续有需要则选择了全部保存到文件中。

在其中添加一些 data.update({"idx": idx, "address_input": address}) 标识信息,便于后续的数据提取。

数据提取代码,实现中json文件中提取所需要的代码:

import os
import json
import pandas as pd

def load_json(file):
    with open(file, 'r') as f:
        return json.load(f)
    
file = "data.xls"
api_result_file = "API_results/data.json"

lng_lat_d = load_json(api_result_file)
df = pd.read_excel(file)

lngs = []
lats = []

for idx, item in enumerate(lng_lat_d):
    assert idx == item["idx"]
    lngs.append(item["result"]["location"]["lng"])
    lats.append(item["result"]["location"]["lat"])
df["经度"] = lngs
df["纬度"] = lats
base_name = os.path.basename(file).split('.')[0]
df.to_excel(f"{base_name}_经纬度_百度API.xlsx", index=False)

下图是最终的效果,实现根据地址添加经纬度坐标:

在这里插入图片描述

代码使用

若想使用上述代码,需要修改 data.xls,确保有一个住所属性列。上述代码使用住所属性列的地址转化为经纬度。

优化建议

  1. 数据存储优化

    • 目前 API 结果在程序结束后一次性存入文件,若中途崩溃,已请求的数据会丢失。
    • 改进方式:逐步保存请求结果(如增量写入 JSON 文件),或其他方式。
  2. 并发优化

    使用多个key,不仅加速也可增加API调用量。每人每天可以调用API的额度有限,通过注册多个账号,获取多个key。

    • 简单方式:手动切换 API Key。手动运行多个python脚本。
    • 进阶方式:多进程/线程并行运行协程,每个协程使用不同的 API Key。

本方法适用于批量获取地理编码数据,并可扩展用于其他类似的 API 调用任务。

【新版代码】优化更新

为了适应不同读者的需求,如果上述的代码,已经能够满足你的需求,使用上述的代码即可。
在这里插入图片描述

我在调用API的过程中,发现很多数据在请求的过程中发生了错误,这导致使用很不方便,这需要针对发生错误的数据重新调用API。
于是我想给上