文章目录
简介
本文探讨了如何利用 协程 高效调用百度地图 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
,确保有一个住所
属性列。上述代码使用住所属性列的地址转化为经纬度。
优化建议
-
数据存储优化
- 目前 API 结果在程序结束后一次性存入文件,若中途崩溃,已请求的数据会丢失。
- 改进方式:逐步保存请求结果(如增量写入 JSON 文件),或其他方式。
-
并发优化
使用多个key,不仅加速也可增加API调用量。每人每天可以调用API的额度有限,通过注册多个账号,获取多个key。
- 简单方式:手动切换 API Key。手动运行多个python脚本。
- 进阶方式:多进程/线程并行运行协程,每个协程使用不同的 API Key。
本方法适用于批量获取地理编码数据,并可扩展用于其他类似的 API 调用任务。
【新版代码】优化更新
为了适应不同读者的需求,如果上述的代码,已经能够满足你的需求,使用上述的代码即可。
我在调用API的过程中,发现很多数据在请求的过程中发生了错误,这导致使用很不方便,这需要针对发生错误的数据重新调用API。
于是我想给上