说明:本文讨论的是在单个商家完成 OAuth 授权的前提下,对
taobao.seller.info接口返回的店铺公开信息进行字段解析与完整性校验。淘宝开放平台规则明确规定:通过开放平台获取的商家数据仅可展现和使用于数据来源的同一卖家,不允许聚合多店铺的数据。因此,本文的代码与方案严格限定在单店铺授权场景,不涉及多店铺批量聚合。示例代码不包含网页爬虫、模拟登录或风控对抗逻辑,仅使用本地模拟响应演示字段处理。tel字段在部分场景下返回的是已脱敏值,代码必须将其作为正常分支处理,而非异常。-12
天猫店铺信息接口中,与电话最相关的是 taobao.seller.info 返回的 tel 字段,类型为 String,示例值形如 1891226351。但把文档和实际对接经验放在一起看,这个字段的语义至少包含三层含义,而不仅仅是“一个电话号码”。-1
第一,它可能被脱敏。 多个接口文档在 tel 字段的说明中明确标注“部分场景返回,已脱敏”。示例值 1891226351 只有 10 位,而标准手机号是 11 位,这个长度本身就说明它不是一个完整的号码。把脱敏值当作可用的电话来解析,会在后续环节产生误导。-12
第二,它的存在与否取决于授权与场景。 tel 不是“必返回”字段。shop_id、seller_id、nick 被标注为“必返回”,而 tel 属于“部分场景返回”。代码如果假设 tel 永远存在,在大量店铺上会直接落到 KeyError 或空指针。-12
第三,它是一个“字符串”,而不是结构化的号码对象。 这意味着 tel 中可能包含分隔符、多个号码拼接、前后空格,甚至包含与电话无关的内容。把 tel 直接当作 phone 使用,是把接口的原始输出等同于业务可用字段。
因此,本文的核心不是“提取电话”,而是设计一个 tel 字段的解析器:识别它的形态(完整/脱敏/空/多值/格式异常),根据形态决定输出什么、标记什么,并且严格限定在单个授权店铺的范围内使用。
淘宝开放平台的规则写得很直接:商家数据“仅可以展现和使用于数据来源的同一卖家,不允许将数据共享于其他组织或个人使用”,并且“不允许聚合多店铺的数据”。这意味着代码在设计上就不应该包含“传入 nick 列表、循环调用、合并导出”的批量结构。
本文的代码以一个单店铺输入为起点:给定一个已经完成授权的 sessionKey(在代码中抽象为一个可用状态位),处理该店铺的 seller_info 响应。如果需要对多个店铺分别处理,正确的方式是每次只处理一个授权店铺、各自独立输出,而不是在一个进程内聚合多店数据。
这个约束直接影响了数据结构的设计:不需要跨店铺的幂等键,不需要全局去重表,不需要“游标推进”的批量状态。代码的复杂度因此显著下降,重心回到单条记录的字段级解析上。
下面的代码定义了一个 TelField 解析器,对 tel 的原始字符串做形态判定,输出结构化结果。依赖只有标准库。
import csv
import os
import re
from dataclasses import dataclass, field
from typing import List
# ============ 字段形态枚举 ============
FORM_FULL = "full" # 完整可辨识号码
FORM_MASKED = "masked" # 明显脱敏/截断
FORM_EMPTY = "empty" # 字段缺失或空字符串
FORM_MULTI = "multi" # 包含多个号码
FORM_AMBIGUOUS = "ambiguous" # 无法归类为以上任何形态
PHONE_RE = 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})"
)
SPLIT_RE = re.compile(r"[;,,;/|]")
@dataclass
class TelField:
raw: str
form: str = FORM_EMPTY
numbers: List[str] = field(default_factory=list)
note: str = ""
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_full(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 parse_tel(raw) -> TelField:
if raw is None or not isinstance(raw, str) or not raw.strip():
return TelField(raw=str(raw) if raw is not None else "", form=FORM_EMPTY,
note="字段缺失或为空")
text = raw.strip()
# 先尝试按分隔符拆分,判断是否多值
parts = [p.strip() for p in SPLIT_RE.split(text) if p.strip()]
if len(parts) > 1:
numbers = []
for p in parts:
n = normalize(p)
if n and is_full(n):
numbers.append(n)
if numbers:
return TelField(raw=text, form=FORM_MULTI, numbers=numbers,
note=f"拆分出 {len(numbers)} 个完整号码")
# 单值:尝试完整号码匹配
candidates = PHONE_RE.findall(text)
full_numbers = []
for c in candidates:
n = normalize(c)
if n and is_full(n):
full_numbers.append(n)
if full_numbers:
return TelField(raw=text, form=FORM_FULL, numbers=full_numbers,
note="完整号码")
# 没有完整号码,检查是否为疑似脱敏/截断
digits_only = re.sub(r"\D", "", text)
if 5 <= len(digits_only) <= 11 and not is_full(normalize(text)):
return TelField(raw=text, form=FORM_MASKED, numbers=[],
note=f"疑似脱敏或截断(原始数字长度 {len(digits_only)})")
return TelField(raw=text, form=FORM_AMBIGUOUS, numbers=[],
note="无法归类,需人工复核")这个解析器的关键设计在于“不给脱敏值任何成为号码的机会” 。FORM_MASKED 分支明确将 1891226351 这类 10 位数字归类为“疑似脱敏”,numbers 列表保持为空。下游代码如果只消费 numbers,就天然不会把脱敏值当作可用电话。
在解析器的基础上,对单个店铺的响应做完整性校验,输出一份字段级体检报告。
def process_single_shop(data: dict, output_csv: str):
"""
data: taobao.seller.info 风格的本地模拟响应。
严格限定单店铺,不聚合。
"""
info = data.get("seller_info") or data.get("shop") or {}
if not info:
print("[跳过] 响应中无 seller_info 节点")
return
shop_id = info.get("shop_id", "")
shop_name = info.get("shop_name", "")
tel = parse_tel(info.get("tel"))
rows = [{
"shop_id": shop_id,
"shop_name": shop_name,
"tel_raw": tel.raw,
"tel_form": tel.form,
"tel_numbers": ";".join(tel.numbers),
"tel_note": tel.note,
}]
os.makedirs(os.path.dirname(output_csv) or ".", exist_ok=True)
with open(output_csv, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
print(f"[完成] shop_id={shop_id},tel形态={tel.form},完整号码={len(tel.numbers)} 个")
if __name__ == "__main__":
# 本地模拟响应,覆盖多种 tel 形态
demo_responses = [
{"seller_info": {"shop_id": "S001", "shop_name": "示例旗舰店",
"tel": "1891226351"}}, # 脱敏/截断
{"seller_info": {"shop_id": "S002", "shop_name": "示例专营店",
"tel": ""}}, # 空
{"seller_info": {"shop_id": "S003", "shop_name": "示例专卖店",
"tel": "400-123-4567;0571-88886666"}}, # 多值
{"seller_info": {"shop_id": "S004", "shop_name": "示例企业店",
"tel": "13800138000"}}, # 完整
]
# 单店铺独立处理,不合并
for i, resp in enumerate(demo_responses):
process_single_shop(resp, f"output/shop_{i}.csv")运行后会为每个店铺生成独立的 CSV。shop_id=S001 的 tel_form 为 masked,tel_numbers 为空,tel_note 提示“疑似脱敏或截断(原始数字长度 10)”。shop_id=S002 的 tel_form 为 empty。shop_id=S003 的 tel_form 为 multi,tel_numbers 包含两个号码。shop_id=S004 的 tel_form 为 full。
天猫店铺 tel 字段的工程难点,不在于“怎么把号码取出来”,而在于先判断它是什么。脱敏值、空值、多值拼接、格式异常,每一种形态需要不同的处理策略。本文的解析器把形态识别放在号码提取之前,单店铺处理的代码结构则直接呼应了开放平台“不允许聚合多店铺数据”的规则。输出的字段体检报告让 tel 的每一次“不可用”都有据可查,而不是在批量循环中悄悄丢失。如果业务确实需要商家的联系方式,正确路径是在授权范围内通过平台提供的沟通能力触达,而不是从一个部分场景返回、可能已脱敏的字段中“提取”一个并不存在的完整号码。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。