淘宝店铺联系电话导出实践:基于开放平台 API 的字段解析与数据清洗
本文讨论的是在商家主动授权的前提下,通过淘宝开放平台官方 API 获取店铺公开信息,并在本地对返回的结构化数据进行解析、清洗与导出。示例代码不包含网页爬虫、不模拟登录、不绕过任何平台防护机制。淘宝法律声明明确禁止未经许可使用爬虫或自动化程序获取平台内容;开放平台合规指南同样指出,合法方式仅限 TOP API + 商家 OAuth 授权。数据使用需遵守《个人信息保护法》及平台规则。
淘宝店铺联系电话的导出,在网上常见两类做法:一类是写爬虫模拟浏览器访问店铺页面、从 HTML 里正则匹配号码;另一类是直接购买“采集器”成品。
爬虫路线的问题不在于技术上能不能跑通,而在于它从根上不符合规则。淘宝法律声明写明,未经许可不得以爬虫、自动化程序等方式擅自获取、使用平台内容。TOP 开放平台的合规指导更进一步:商品公开信息也不能无限制批量爬取,即使数据公开,批量抓取也可能构成不正当竞争。这意味着“公开数据”四个字本身并不自动赋予批量采集的合法性。
真正可行的路线是:商家授权 → 调用官方 API → 解析返回字段 → 本地清洗导出。这条路线下,开发者拿到的是商家同意提供的、该店铺范围内的数据,合规边界是清晰的。
taobao.shop.get 与 tel 字段淘宝开放平台的 taobao.shop.get 接口用于获取卖家店铺基本信息,请求时必须传入 nick(卖家昵称)和 fields(需返回的字段列表)。在部分对接实践中,返回的店铺对象中包含 tel 字段,类型为 String,示例值形如 1891226351。
这里有两个容易被误解的地方需要说清楚:
第一,tel 字段是否返回,取决于权限与授权。 开放平台对 API 和字段做了安全等级标记,并非所有应用都能拿到全部字段。代码里不能假设“调了接口就一定有电话”,必须有字段缺失时的降级处理。
第二,nick 是卖家昵称,不是店铺 ID。 taobao.shop.get 用 nick 来定位店铺,这一点与很多“按店铺 ID 查”的直觉不同。批量处理时,昵称列表的来源和准确性需要单独保证。
下面的代码不涉及如何“拿到”授权,而是聚焦于授权完成后,如何把 API 返回的数据稳妥地解析、清洗、去重并导出。这才是开发者真正需要工程化处理的部分。
依赖只有 requests(用于构造签名请求,本文不展开签名细节)和标准库。为了保持代码可独立运行,下面的示例以一个本地模拟的 API JSON 响应为输入,演示解析、清洗、去重和导出的完整流程。
import csv
import json
import os
import re
from dataclasses import dataclass, field
from typing import List, Optional
# ============ 数据模型 ============
@dataclass
class ShopContact:
nick: str
title: str
raw_tel: str
phones: List[str] = field(default_factory=list)
sid: str = ""
status: str = ""
# ============ 清洗规则 ============
# 宽松召回:从 tel 字段中拆出可能的号码
PHONE_CANDIDATE = re.compile(
r"(?:(?:\+|00)86[\s-]?)?"
r"(?:1[3-9]\d[\s-]?\d{4}[\s-]?\d{4}"
r"|400[\s-]?\d{3}[\s-]?\d{4}"
r"|0\d{2,3}[\s-]?\d{7,8})"
)
def normalize(raw: str) -> str:
s = re.sub(r"[\s\-()]", "", raw)
for p in ("+86", "0086"):
if s.startswith(p):
return s[len(p):]
return s
def is_plausible(phone: str) -> bool:
if re.fullmatch(r"1[3-9]\d{9}", phone):
return phone[1] not in "012"
if re.fullmatch(r"400\d{7}", phone):
return True
if re.fullmatch(r"0\d{9,11}", phone):
return True
return False
def clean_tel(raw: str) -> List[str]:
"""从 tel 字段中提取并清洗号码"""
if not raw or not isinstance(raw, str):
return []
candidates = PHONE_CANDIDATE.findall(raw)
# 如果正则没命中但字段本身像单个号码,也纳入
if not candidates:
stripped = normalize(raw)
if stripped:
candidates = [stripped]
result = []
for c in candidates:
n = normalize(c)
if n and is_plausible(n):
result.append(n)
# 去重保序
seen = set()
return [p for p in result if not (p in seen or seen.add(p))]
# ============ 响应解析 ============
def parse_shop_response(data: dict) -> Optional[ShopContact]:
"""
解析 taobao.shop.get 风格的 JSON 响应。
兼容 shop 对象嵌套在 shop_get_response 下的常见结构。
"""
shop = None
if isinstance(data, dict):
shop = data.get("shop")
if shop is None:
resp = data.get("shop_get_response") or {}
shop = resp.get("shop") if isinstance(resp, dict) else None
if not isinstance(shop, dict):
return None
nick = shop.get("nick", "")
title = shop.get("title", "")
raw_tel = shop.get("tel", "")
sid = str(shop.get("sid", ""))
phones = clean_tel(raw_tel)
return ShopContact(
nick=nick,
title=title,
raw_tel=raw_tel,
phones=phones,
sid=sid,
status="ok" if phones else "no_phone",
)
# ============ 批量处理与导出 ============
def process_responses(responses: List[dict]) -> List[ShopContact]:
results = []
for data in responses:
contact = parse_shop_response(data)
if contact is not None:
results.append(contact)
return results
def export_csv(contacts: List[ShopContact], path: str):
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["卖家昵称", "店铺标题", "店铺ID", "原始tel字段", "清洗后电话", "状态"])
for c in contacts:
writer.writerow([
c.nick, c.title, c.sid,
c.raw_tel,
";".join(c.phones),
c.status,
])
total = len(contacts)
with_phone = sum(1 for c in contacts if c.phones)
print(f"[完成] 共 {total} 个店铺,{with_phone} 个含有效电话 → {path}")
# ============ 演示入口 ============
if __name__ == "__main__":
# 本地模拟响应,仅用于演示解析逻辑
demo = [
{
"shop_get_response": {
"shop": {
"sid": 44750468,
"nick": "demo_shop_a",
"title": "示例店铺A",
"tel": "0571-88886666",
}
}
},
{
"shop": {
"sid": 44750469,
"nick": "demo_shop_b",
"title": "示例店铺B",
"tel": "",
}
},
{
"shop": {
"sid": 44750470,
"nick": "demo_shop_c",
"title": "示例店铺C",
"tel": "13800138000;021-12345678",
}
},
]
contacts = process_responses(demo)
export_csv(contacts, "output/taobao_shop_contacts.csv")运行后会输出:
[完成] 共 3 个店铺,2 个含有效电话 → output/taobao_shop_contacts.csv1. tel 字段不能当作“一定有值”来处理。 代码里 clean_tel 对空字符串、非字符串、无命中候选的情况都做了兜底,导出时用 状态 列标记 ok 或 no_phone。这样批量跑下来,能直接看出有多少店铺没有返回公开电话,而不是在代码里报错中断。
2. tel 可能是多号码拼接。 实际返回值中,tel 不一定是一个干净号码,可能包含分隔符或多个号码。先宽松召回、再规范化、最后用号段规则校验,比直接对一个正则假设“整个字段就是一个号码”要稳健。
3. 保留 原始tel字段 列。 导出结果里同时保留原始值和清洗后的值,是为了人工复核时有据可查。如果某个号码被清洗规则误过滤,可以回溯原始字段,而不是只能对着空结果猜。
4. 去重按“店铺 + 号码”维度,而不是全局去重。 不同店铺可能公开同一个 400 客服号,但它们在业务上属于不同店铺的记录,不应该跨店铺合并。代码中清洗结果按店铺内去重,保留了这种区分。
代码本身不包含获取授权、调用线上接口的部分,但实际使用时,这两件事决定了方案能不能持续跑下去。
授权必须是商家主动完成的。 TOP 开放平台的合规要求写得很直接:必须由商家主动授权,拿到 sessionKey,只能获取该商家店铺下的数据,不能代授权、不能跨店铺拿数据。nick 列表如果来自非授权渠道,即便技术上能调通接口,数据来源的合法性也存在问题。
调用频率要按配额来设计,而不是按“能跑多快”来设计。 开放平台有明确的频次限制,time.sleep 只是下限。更稳妥的做法是把批量任务做成可中断、可续跑的结构:每处理完一批就落盘,记录已经成功的 nick,下次从中断处继续,而不是每次从头请求。
淘宝店铺联系电话的导出,真正的技术分水岭不在“能不能抓到”,而在数据来源是否经过授权、返回字段是否被稳妥处理、异常情况是否有降级路径。taobao.shop.get 的 tel 字段存在返回空值、多号码拼接、权限受限等多种可能,代码必须把这些当作正常分支来设计,而不是 happy path 之外的意外。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。