首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >高德地图商家电话导出工具 百度高德腾讯实体门店采集教程

高德地图商家电话导出工具 百度高德腾讯实体门店采集教程

原创
作者头像
vx-taobao01221
发布于 2026-10-20 09:56:53
发布于 2026-10-20 09:56:53
190
举报
文章被收录于专栏:爬虫使用教程爬虫使用教程

高德地图商家电话导出工具 百度高德腾讯实体门店采集教程

本文讨论的是通过高德、百度、腾讯地图官方开放平台的 Web 服务 API,检索并导出商家主动公开的电话信息。示例代码仅调用官方授权接口,不涉及网页爬取、登录绕过或风控对抗。数据使用需遵守各平台服务条款,不得用于骚扰或未经同意的商业推广。

一、为什么用官方 API,而不是“采集器”

实体门店电话本质上属于地图 POI(Point of Interest)数据的一部分。三家平台都在官方开放平台提供了地点检索接口,返回结构化 JSON,其中直接包含电话字段。相比模拟浏览器抓取地图前台页面,调用官方 API 有三个明显好处:

  1. 字段稳定:电话、地址、名称都有明确的字段名,不依赖前端 DOM 结构。
  2. 有配额和文档:每个 Key 的每日调用量、QPS 限制都是公开的,代码里做限频即可。
  3. 合规边界清晰:你在使用平台允许的方式获取平台公开的数据,而不是对抗平台。

代价是每个平台返回电话的条件不一样,需要分别处理。

二、三平台接口对比

高德

百度

腾讯

地点搜索接口

/v3/place/text(关键字搜索)

/place/v2/search

/ws/place/v1/search

电话字段

pois[].tel

results[].detail_info.telephone(仅 scope=2 时返回)

data[].tel

获取电话的关键参数

extensions=all

scope=2

filter=tel<>null(筛选有电话的地点,需 URL 编码)-21

分页

page / offset(默认每页 20 条)

page_num / page_size

page_index / page_size(最大 20 条)-21

返回结构

status=1 为成功

status=0 为成功

status=0 为正常-21

高德的 tel 字段可能包含多个号码,用分号隔开;百度如果不加 scope=2,detail_info 整个字段都不会出现;腾讯的 filter=tel<>null 是最直接的“只要带电话的结果”的筛选方式。

三、统一代码实现

下面的代码把三个平台的检索封装成同一套输出结构,方便后续合并导出 CSV。依赖只有 requests:

代码语言:txt
复制
pip install requests
代码语言:txt
复制
import csv
import os
import time
from dataclasses import dataclass, field
from typing import List, Optional
from urllib.parse import quote

import requests

OUTPUT_CSV = "poi_contacts.csv"
REQUEST_INTERVAL = 0.5  # 秒,自行根据配额调整


@dataclass
class PoiContact:
    platform: str
    name: str
    address: str
    phone: str
    category: str = ""
    poi_id: str = ""


# ---------------- 高德 ----------------

def search_amap(key: str, keywords: str, city: str, page: int = 1) -> List[PoiContact]:
    url = "https://restapi.amap.com/v3/place/text"
    params = {
        "key": key,
        "keywords": keywords,
        "city": city,
        "citylimit": "true",
        "offset": 20,
        "page": page,
        "extensions": "all",  # 不加这个拿不到 tel
        "output": "json",
    }
    results = []
    try:
        resp = requests.get(url, params=params, timeout=10)
        data = resp.json()
    except Exception as e:
        print(f"[高德] 请求异常:{e}")
        return results

    if data.get("status") != "1":
        print(f"[高德] 失败:{data.get('info')}")
        return results

    for poi in data.get("pois", []):
        tel = poi.get("tel", "") or ""
        # 高德 tel 可能是空的,也可能用分号分隔多个号码
        results.append(PoiContact(
            platform="高德",
            name=poi.get("name", ""),
            address=poi.get("address", "") if isinstance(poi.get("address"), str) else "",
            phone=tel,
            category=poi.get("type", ""),
            poi_id=poi.get("id", ""),
        ))
    return results


# ---------------- 百度 ----------------

def search_baidu(key: str, keywords: str, region: str, page: int = 0) -> List[PoiContact]:
    url = "https://api.map.baidu.com/place/v2/search"
    params = {
        "ak": key,
        "query": keywords,
        "region": region,
        "output": "json",
        "page_size": 20,
        "page_num": page,
        "scope": 2,  # 必须为 2,才会返回 detail_info,其中包含 telephone
    }
    results = []
    try:
        resp = requests.get(url, params=params, timeout=10)
        data = resp.json()
    except Exception as e:
        print(f"[百度] 请求异常:{e}")
        return results

    if data.get("status") != 0:
        print(f"[百度] 失败:{data.get('message')}")
        return results

    for item in data.get("results", []):
        detail = item.get("detail_info") or {}
        results.append(PoiContact(
            platform="百度",
            name=item.get("name", ""),
            address=item.get("address", ""),
            phone=detail.get("telephone", "") or "",
            category=detail.get("tag", "") or "",
            poi_id=item.get("uid", ""),
        ))
    return results


# ---------------- 腾讯 ----------------

def search_tencent(key: str, keywords: str, region: str, page: int = 1) -> List[PoiContact]:
    url = "https://apis.map.qq.com/ws/place/v1/search"
    # filter=tel<>null 用于筛选有电话的地点,参数值需要 URL 编码
    params = {
        "key": key,
        "keyword": keywords,
        "boundary": f"region({region},0)",
        "page_size": 20,
        "page_index": page,
        "filter": quote("tel<>null"),
        "output": "json",
    }
    results = []
    try:
        resp = requests.get(url, params=params, timeout=10)
        data = resp.json()
    except Exception as e:
        print(f"[腾讯] 请求异常:{e}")
        return results

    if data.get("status") != 0:
        print(f"[腾讯] 失败:{data.get('message')}")
        return results

    for item in data.get("data", []):
        results.append(PoiContact(
            platform="腾讯",
            name=item.get("title", ""),
            address=item.get("address", ""),
            phone=item.get("tel", "") or "",
            category=item.get("category", ""),
            poi_id=item.get("id", ""),
        ))
    return results


# ---------------- 汇总与导出 ----------------

def collect_all(
    amap_key: Optional[str],
    baidu_key: Optional[str],
    tencent_key: Optional[str],
    keywords: str,
    city: str,
    max_pages: int = 3,
) -> List[PoiContact]:
    all_contacts: List[PoiContact] = []

    if amap_key:
        for p in range(1, max_pages + 1):
            batch = search_amap(amap_key, keywords, city, page=p)
            all_contacts.extend(batch)
            time.sleep(REQUEST_INTERVAL)
            if len(batch) < 20:
                break

    if baidu_key:
        for p in range(max_pages):
            batch = search_baidu(baidu_key, keywords, city, page=p)
            all_contacts.extend(batch)
            time.sleep(REQUEST_INTERVAL)
            if len(batch) < 20:
                break

    if tencent_key:
        for p in range(1, max_pages + 1):
            batch = search_tencent(tencent_key, keywords, city, page=p)
            all_contacts.extend(batch)
            time.sleep(REQUEST_INTERVAL)
            if len(batch) < 20:
                break

    return all_contacts


def dedupe_and_export(contacts: List[PoiContact], output: str = OUTPUT_CSV):
    # 只保留有电话的记录,并按“电话 + 名称”去重(同一门店可能在多个平台出现)
    with_phone = [c for c in contacts if c.phone.strip()]
    seen = set()
    unique = []
    for c in with_phone:
        key = (c.phone.strip(), c.name.strip())
        if key not in seen:
            seen.add(key)
            unique.append(c)

    os.makedirs(os.path.dirname(output) or ".", exist_ok=True)
    with open(output, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerow(["平台", "名称", "地址", "电话", "分类", "POI ID"])
        for c in unique:
            writer.writerow([c.platform, c.name, c.address, c.phone, c.category, c.poi_id])

    print(f"[完成] 有电话记录 {len(unique)} 条 → {output}")


if __name__ == "__main__":
    # 在环境变量中设置 Key,不要硬编码到代码里
    amap_key = os.getenv("AMAP_KEY")
    baidu_key = os.getenv("BAIDU_KEY")
    tencent_key = os.getenv("TENCENT_KEY")

    contacts = collect_all(
        amap_key=amap_key,
        baidu_key=baidu_key,
        tencent_key=tencent_key,
        keywords="咖啡馆",
        city="北京",
        max_pages=2,  # 先小批量验证
    )
    dedupe_and_export(contacts)

四、几个容易踩的坑

1. 百度不加 scope=2,电话永远是空的。 telephone 藏在 detail_info 里,而 detail_info 只有在 scope=2 时才返回。这是百度接口最容易被忽略的一点。

2. 腾讯的 filter 参数必须 URL 编码。 tel<>null 里的 <> 直接拼进 URL 可能被错误解析,代码中用 quote() 处理是必要的。-21

3. 高德的 tel 可能包含多个号码。 返回格式可能是 "010-xxxx;138xxxx",如果要拆开单独分析,需要在导出后按分号切分。

4. 分页终止条件用“返回条数 < page_size”来判断。 不要直接用 max_pages 硬翻到底,既浪费配额,也容易触发限流。

5. 先小批量、低频率验证。 把 max_pages 设为 1~2,确认三个平台的 Key 都能正常返回、字段解析无误之后,再扩大范围。每家平台的免费配额不同,代码里用 time.sleep 做基础限频是下限而不是保证。

五、合规提醒

  • 只调用各平台官方开放平台提供的 Web 服务 API,不爬取地图前台页面,不绕过任何登录或验证机制。
  • 每个平台的 Key 都应在控制台设置配额告警或 IP 白名单,避免意外超量。
  • 电话属于个人信息,导出的 CSV 应控制访问权限。
  • 使用场景应限于市场调研、门店信息核对等正当用途,不得用于电话营销骚扰。
  • 如果某个平台返回的电话字段为空,说明该 POI 没有公开电话,不要尝试从其他渠道“补全”。

六、小结

高德、百度、腾讯三家地图开放平台的地点检索 API 都能返回商家公开电话,但触发条件不同:高德靠 extensions=all,百度靠 scope=2,腾讯可以直接用 filter=tel<>null 筛掉无电话的结果。把三个平台统一成同一套数据结构后,去重和导出 CSV 就变得很直接。代码里的限频、分页终止和 Key 环境变量管理,是让它稳定运行的关键。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么用官方 API,而不是“采集器”
  • 二、三平台接口对比
  • 三、统一代码实现
  • 四、几个容易踩的坑
  • 五、合规提醒
  • 六、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档