高德地图批量抓取北京库迪咖啡门店及营业执照完整指南(营业执照抓取失败)
一、 准备工作:申请高德开放平台 Key
- 登录 高德开放平台 (lbs.amap.com) 并完成个人开发者认证。
- 进入 控制台 -> 应用管理 -> 我的应用 -> 创建新应用。
- 在创建好的应用右侧点击 添加 Key:
- Key 名称:自定义(如
python_key) - 服务平台:必须勾选【Web服务】(切勿选择 Web端 JS API)
- IP 白名单:保持留空
- Key 名称:自定义(如
- 提交后复制生成的 32 位 Key 字符串。
二、 环境依赖安装
为防止虚拟环境(.venv)移动文件夹后导致 pip.exe 路径失效报错,统一使用 python -m 方式安装所需依赖:
python -m pip install requests pandas openpyxl playwright
python -m playwright install chromium
三、 第一步:批量抓取北京 16 个区全量门店信息(含分店名提取)
核心设计说明
- 突破 200 条限制:高德 POI 搜索接口单次最多返回约 200 条记录。北京市库迪咖啡门店数量庞大,因此通过遍历北京市 16 个行政区分区抓取,并在最后按
门店ID去重,确保全量覆盖。 - 防并发超限 (
CUQPS_HAS_EXCEEDED_THE_LIMIT):个人免费版 Key 的 QPS(每秒并发请求数)上限为 3 次。代码内置了time.sleep(0.6)固定限速及触发限流后的1.5秒自动重试机制。 - 自动提取分店名:利用正则表达式自动提取
库迪咖啡(xxx店)括号内的文字(兼容中英文括号),单独生成【分店名】列。
完整 Python 脚本 (step1_amap_beijing_stores.py)
import os
import re
import time
import requests
import pandas as pd
# =====================================================================
# 【必填】将此处替换为你申请的 32 位高德【Web服务】Key
AMAP_KEY = "填入你的高德Key"
# =====================================================================
KEYWORD = "库迪咖啡"
EXCEL_FILE = "1_高德_北京库迪全量门店.xlsx"
# 北京市 16 个行政区,分区抓取突破单次搜索条数上限
DISTRICTS = [
"东城区", "西城区", "朝阳区", "丰台区", "石景山区", "海淀区",
"门头沟区", "房山区", "通州区", "顺义区", "昌平区", "大兴区",
"怀柔区", "平谷区", "密云区", "延庆区"
]
def extract_branch_name(name):
"""提取店名括号里的分店名称,兼容中英文括号 () 和 ()"""
if pd.isna(name):
return ""
match = re.search(r"[\((]([^\))]+)[\))]", str(name))
if match:
return match.group(1).strip()
return re.sub(r"^库迪咖啡\s*", "", str(name)).strip()
def fetch_beijing_cotti_stores():
if AMAP_KEY == "填入你的高德Key" or len(AMAP_KEY) < 20:
print("❌ 错误:请先在代码第 10 行填入正确的 32 位高德 Key!")
return
print("🚀 开始从高德地图拉取北京市 16 个行政区的库迪咖啡门店...")
all_stores = []
for district in DISTRICTS:
page = 1
while True:
url = "[https://restapi.amap.com/v3/place/text](https://restapi.amap.com/v3/place/text)"
params = {
"key": AMAP_KEY,
"keywords": KEYWORD,
"city": district,
"citylimit": "true",
"offset": 20,
"page": page,
"extensions": "all"
}
# 固定间隔 0.6 秒,避开免费版 QPS 并发限制
time.sleep(0.6)
res = requests.get(url, params=params).json()
# 若偶发触发并发超限,自动休息 1.5 秒后原地重试
if res.get("info") == "CUQPS_HAS_EXCEEDED_THE_LIMIT":
print(f" ⏳ 触发并发限流,休息 1.5 秒后重试【{district}】第 {page} 页...")
time.sleep(1.5)
continue
if res.get("status") == "0":
print(f"❌ 高德 API 报错:{res.get('info')}")
return
pois = res.get("pois", [])
if not pois:
break
for poi in pois:
full_name = poi.get("name", "")
if "库迪" in full_name:
all_stores.append({
"门店ID": poi.get("id"),
"门店名称": full_name,
"分店名": extract_branch_name(full_name),
"所属区域": poi.get("adname"),
"高德地址": str(poi.get("address", "")),
"门店电话": poi.get("tel") if poi.get("tel") else "",
"经纬度": poi.get("location", "")
})
page += 1
print(f" -> 【{district}】抓取完毕,当前累计获取 {len(all_stores)} 条记录")
# 按门店ID去重并导出 Excel
df = pd.DataFrame(all_stores).drop_duplicates(subset=["门店ID"])
df.to_excel(EXCEL_FILE, index=False)
print(f"\n🎉 抓取完成!去重后共获取 {len(df)} 家北京库迪咖啡门店。")
print(f"📁 文件保存路径:{os.path.abspath(EXCEL_FILE)}")
if __name__ == "__main__":
fetch_beijing_cotti_stores()
四、 第二步:接管真实浏览器获取门店营业执照(避开滑块报错)
核心原理
高德 API 不直接返回营业执照图片,而直接用 Playwright 启动的内置浏览器带有底层自动化协议特征,在滑动阿里“霸下”验证码时会报 验证失败,点击框体重试[cite: 3, 5]。
通过 Windows 原生命令行开启 Edge 调试端口 + connect_over_cdp 接管真浏览器,即可保证 100% 真人浏览器指纹,滑块一滑即过。
操作步骤
1. 启动真实 Edge 浏览器
先关闭电脑上所有已打开的 Edge 窗口,在终端运行以下命令:
Start-Process "msedge.exe" -ArgumentList "--remote-debugging-port=9222"
2. 运行接管与存图脚本 (step2_amap_beijing_licenses.py)
import os
import re
import requests
import pandas as pd
from playwright.sync_api import sync_playwright
EXCEL_FILE = "1_高德_北京库迪全量门店.xlsx"
def scrape_licenses_with_real_browser():
if not os.path.exists(EXCEL_FILE):
print(f"❌ 找不到 {EXCEL_FILE},请先运行第一步脚本!")
return
df = pd.read_excel(EXCEL_FILE)
os.makedirs("高德_北京营业执照原图", exist_ok=True)
os.makedirs("高德_北京营业执照截图", exist_ok=True)
license_img_urls = []
with sync_playwright() as p:
try:
# 接管刚才通过 9222 端口打开的真实 Edge 浏览器
browser = p.chromium.connect_over_cdp("http://localhost:9222")
except Exception:
print("❌ 连接失败!请先关闭所有 Edge 窗口,并在终端运行:")
print(' Start-Process "msedge.exe" -ArgumentList "--remote-debugging-port=9222"')
return
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
# 监听网络请求,捕获大于图标尺寸的证照原图
def handle_response(response):
url = response.url
if response.request.resource_type == "image":
if not any(w in url.lower() for w in ["icon", "logo", "marker", "avatar", "tile", ".svg", ".gif"]):
if url not in license_img_urls:
license_img_urls.append(url)
page.on("response", handle_response)
print("✅ 已成功接管真实 Edge 浏览器!")
input("👉 准备好后,按【回车键】打开第 1 家北京门店...")
results = []
# 若需断点续传(如从第 50 家开始),可将 df.iterrows() 改为 df.iloc[49:].iterrows()
for idx, row in df.iterrows():
poi_id = str(row["门店ID"]).strip()
store_name = str(row["门店名称"]).strip()
branch_name = str(row.get("分店名", "")).strip()
district = str(row["所属区域"]).strip()
safe_name = re.sub(r'[\\/:*?"<>|]', '_', f"{idx+1}_{district}_{branch_name}")
detail_url = f"[https://m.amap.com/detail/index/poiid=](https://m.amap.com/detail/index/poiid=){poi_id}"
print(f"\n[{idx+1}/{len(df)}] 正在打开:{store_name}")
license_img_urls.clear()
try:
# 使用 domcontentloaded 防止地图图层后台持续加载导致 30 秒超时报错
page.goto(detail_url, wait_until="domcontentloaded", timeout=15000)
except Exception:
pass
cmd = input(" 👉 请在浏览器手动点开【营业详细 -> 服务须知及信息公示】,看到执照后按【回车】保存(跳过输 s,退出输 q):").strip().lower()
if cmd == 'q':
break
if cmd == 's':
continue
# 1. 保存当前屏幕截图
shot_path = f"高德_北京营业执照截图/{safe_name}.png"
try:
page.screenshot(path=shot_path)
print(f" 📸 截图已存:{shot_path}")
except Exception as e:
print(f" ⚠️ 截图失败: {e}")
# 2. 下载嗅探到的最新大图(过滤小于 10KB 的小杂图)
saved_paths = []
for i, img_url in enumerate(license_img_urls[-4:]):
try:
img_data = requests.get(img_url, timeout=8).content
if len(img_data) > 10240:
raw_path = f"高德_北京营业执照原图/{safe_name}_执照{i+1}.jpg"
with open(raw_path, "wb") as f:
f.write(img_data)
saved_paths.append(raw_path)
print(f" 📥 原图已下:{raw_path}")
except Exception:
pass
results.append({
"门店ID": poi_id,
"门店名称": store_name,
"分店名": branch_name,
"所属区域": district,
"高德地址": row.get("高德地址", ""),
"门店电话": row.get("门店电话", ""),
"营业执照截图": shot_path,
"营业执照原图路径": "; ".join(saved_paths)
})
pd.DataFrame(results).to_excel("2_高德_北京营业执照抓取汇总.xlsx", index=False)
print("\n✅ 采集已保存并退出!")
if __name__ == "__main__":
scrape_licenses_with_real_browser()
五、 常见报错与排坑速查表
| 报错信息 / 现象 | 原因分析 | 解决方案 |
|---|---|---|
Unable to create process using ... pip.exe |
项目文件夹移动或重命名后,.venv 虚拟环境中的固定路径失效 |
改用 python -m pip install ... 执行安装 |
CUQPS_HAS_EXCEEDED_THE_LIMIT |
每秒请求次数超过高德个人免费版上限(3次/秒) | 每次请求增加 time.sleep(0.6),并加入捕获限流后暂停 1.5秒 重试的逻辑 |
'latin-1' codec can't encode characters |
手动复制的 Cookie 中包含中文字符或换行符 |
使用 cookie.encode('utf-8').decode('latin-1') 转码并去除换行符 |
Timeout 30000ms exceeded (waiting until "load") |
高德后台持续加载地图瓦片,导致 load 状态一直不结束 |
将 page.goto() 的等待策略改为 wait_until="domcontentloaded" |
验证失败,点击框体重试[cite: 3, 5] |
Playwright 直接启动的浏览器带有自动化特征,被阿里霸下风控拦截 | 用系统命令行开启 --remote-debugging-port=9222,再用 connect_over_cdp 接管真实浏览器 |
浙公网安备 33010602011771号