首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >微信小店商家采集器 批量导出视频号卖家电话的方案

微信小店商家采集器 批量导出视频号卖家电话的方案

原创
作者头像
vx-taobao01221
发布于 2026-11-09 10:52:43
发布于 2026-11-09 10:52:43
110
举报
文章被收录于专栏:爬虫使用教程爬虫使用教程

微信小店商家公开信息聚合实践:基于官方接口的增量同步与字段边界

说明:本文讨论的是在商家完成微信小店开放平台授权后,对授权范围内的店铺公开经营信息进行增量同步与结构化导出。根据微信小店官方文档,“获取店铺基本信息”接口(/channels/ec/basics/info/get)。

一、先确认一个事实:官方接口返回什么、不返回什么

在动手写任何同步代码之前,需要先把“微信小店商家电话批量导出”这个需求拆到接口层面,看清官方能力的实际边界。

微信小店开放平台中,与店铺基础信息最直接相关的接口是“获取店铺基本信息”:

代码语言:javascript
复制
GET https://api.weixin.qq.com/channels/ec/basics/info/get?access_token=ACCESS_TOKEN

其返回体中的 info 对象包含以下字段:nickname(店铺名称)、headimg_url(店铺头像)、subject_type(企业/个体工商户)、status(开店状态)、username(店铺原始ID)、is_local_life(是否本地生活)、open_timestamp(开店时间戳)。联系电话不在这个接口的返回字段列表中。

另一个容易被误读的接口是 /channels/ec/merchant/privatenumber/getphone,它的名称中包含“手机号”,但实际用途是“获取当前手机号的认证状态”,服务于虚拟号拨打实名认证场景,而不是返回商家的联系电话。-11

这意味着:如果目标是“批量导出商家电话”,在微信小店的官方接口体系中,没有一个可以直接调用的标准字段来满足这个需求。商家是否在店铺页面上主动公示电话,属于页面展示层面的行为,不在基础信息接口的覆盖范围内。

因此,本文把技术目标调整为:在授权范围内,对店铺公开经营信息(名称、类型、状态、原始ID、开店时间等)做增量同步与结构化导出。这是一个有官方接口支撑、字段边界清晰、可以工程化的任务。如果某些商家在页面中主动公示了联系方式,那属于页面展示内容,不在本文的接口聚合范围内。

二、授权与权限集:代码能不能跑起来的前提

微信小店的接口调用凭证分为两种模式,直接决定了代码的写法。

商家自研模式:商家在“微信小店后台 - 服务市场 - 经营工具 - 自研”中获取 AppID 和 AppSecret,调用“获取稳定版接口调用凭据”接口拿到 access_token,后续业务 API 均携带该凭证。-12

第三方服务商模式:服务商需要引导商家完成授权,获取 authorizer_access_token 来代商家调用接口。能否成功调用某个接口,取决于商家是否将对应的权限集授权给了服务商的第三方平台账号。以“获取店铺基本信息”为例,它所属的权限集 ID 为 129、131、192,服务商获得其中之一即可调用。-1-10

这里有一个工程上必须提前确认的点:权限集不等于全部字段。即使服务商拿到了某个权限集,也需要核对目标接口的实际返回字段,不能假设“有权限就什么字段都能拿到”。本文的代码把“字段是否存在”作为正常分支来处理,而不是当作异常。

三、增量同步的骨架:凭证门卫、清洗、幂等、落盘

一次可重复运行的同步任务,包含四个环节:

代码语言:javascript
复制
凭证检查 → 拉取增量(模拟) → 字段清洗与边界处理 → 幂等落盘 → 导出

凭证门卫:微信小店的接口调用凭证有明确的有效期。access_token 的有效期由 expires_in 决定,过期后必须刷新或重新获取。代码在每次同步开始前检查凭证状态,不可用就直接停止,而不是带着失效凭证继续请求。

字段清洗与边界处理:官方返回的 nickname、username 等字段可能包含前后空格或特殊字符,需要做基础清洗。更重要的是,不存在的字段不能被当作“异常”——如果某个字段在返回体中缺失,代码应该记录为“字段未返回”,继续处理其他记录,而不是中断整个批次。

幂等落盘:增量拉取不可避免地会出现时间窗口重叠。幂等键的选择取决于业务语义:对于店铺基础信息,username(店铺原始ID)是稳定的唯一标识,以它作为幂等键是合理的。如果同一店铺的信息在同一批次中出现多次,只保留最新的一条。

依赖只有标准库:

代码语言:javascript
复制
import csv
import json
import os
import re
from dataclasses import dataclass, field
from typing import List, Set

STATE_FILE = "sync_state.json"
SEEN_FILE = "seen_shops.json"
OUTPUT_CSV = "output/wechat_shop_public_info.csv"


@dataclass
class ShopInfo:
    username: str
    nickname: str
    subject_type: str
    status: str
    raw_fields: dict = field(default_factory=dict)


def clean_text(value) -> str:
    if not isinstance(value, str):
        return ""
    return re.sub(r"\s+", " ", value).strip()


def load_state() -> dict:
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {"cursor": 0, "token_valid": True}


def save_state(state: dict):
    with open(STATE_FILE, "w", encoding="utf-8") as f:
        json.dump(state, f, ensure_ascii=False, indent=2)


def ensure_token(state: dict) -> bool:
    if not state.get("token_valid", False):
        print("[停止] 凭证态无效,需刷新或重新授权")
        return False
    return True


def fetch_increment(cursor: int):
    """
    本地模拟增量响应,替代真实接口调用。
    真实场景中携带 access_token / authorizer_access_token 请求官方接口。
    返回字段严格对应 /channels/ec/basics/info/get 的 info 对象。
    """
    demo_pages = {
        0: [
            {"username": "shop_gh_001", "nickname": "示例店铺A",
             "subject_type": "企业", "status": "opening"},
            {"username": "shop_gh_002", "nickname": " 示例店铺B ",
             "subject_type": "个体工商户", "status": "open_finished"},
            {"username": "shop_gh_003", "nickname": "",
             "subject_type": "企业", "status": "opening"},
        ],
        1: [
            {"username": "shop_gh_001", "nickname": "示例店铺A",
             "subject_type": "企业", "status": "opening"},
            {"username": "shop_gh_004", "nickname": "示例店铺D",
             "subject_type": "企业", "status": "closing"},
        ],
    }
    data = demo_pages.get(cursor, [])
    next_cursor = cursor + 1 if cursor + 1 in demo_pages else cursor
    return data, next_cursor


def load_seen() -> Set[str]:
    if os.path.exists(SEEN_FILE):
        with open(SEEN_FILE, "r", encoding="utf-8") as f:
            return set(json.load(f))
    return set()


def save_seen(seen: Set[str]):
    with open(SEEN_FILE, "w", encoding="utf-8") as f:
        json.dump(sorted(seen), f, ensure_ascii=False, indent=2)


def parse_shop(raw: dict) -> ShopInfo:
    return ShopInfo(
        username=clean_text(raw.get("username", "")),
        nickname=clean_text(raw.get("nickname", "")),
        subject_type=clean_text(raw.get("subject_type", "")),
        status=clean_text(raw.get("status", "")),
        raw_fields=raw,
    )


def idempotent_filter(shops: List[ShopInfo], seen: Set[str]) -> List[ShopInfo]:
    """以 username(店铺原始ID)为幂等键"""
    new_shops = []
    for shop in shops:
        if not shop.username:
            continue
        if shop.username not in seen:
            seen.add(shop.username)
            new_shops.append(shop)
    return new_shops


def export_csv(shops: List[ShopInfo], 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", "店铺名称", "店铺类型", "店铺状态"])
        for s in shops:
            writer.writerow([s.username, s.nickname, s.subject_type, s.status])
    print(f"[导出] {len(shops)} 条新增店铺信息 → {path}")


def main():
    state = load_state()
    if not ensure_token(state):
        return

    seen = load_seen()
    cursor = state.get("cursor", 0)

    raw_batch, next_cursor = fetch_increment(cursor)
    shops = [parse_shop(item) for item in raw_batch]

    missing_nickname = sum(1 for s in shops if not s.nickname)
    new_shops = idempotent_filter(shops, seen)

    save_seen(seen)
    state["cursor"] = next_cursor
    save_state(state)

    export_csv(new_shops, OUTPUT_CSV)

    print(f"[统计] 本批 {len(shops)} 条,{missing_nickname} 条缺少店铺名称,"
          f"{len(new_shops)} 条为新增")


if __name__ == "__main__":
    main()

运行两次的差异会展示幂等的作用:第二次因为重叠窗口中 shop_gh_001 的 username 已经在 seen 集合中,新增记录会减少,sync_state.json 中的游标也会推进。missing_nickname 的统计则展示了“字段缺失是正常分支”的处理方式——shop_gh_003 的 nickname 为空,它仍然被记录,只是昵称为空,而不是被丢弃或导致异常。

四、几个决定代码能不能长期跑下去的细节

1. username 是比 nickname 可靠的幂等键。 店铺名称可能修改,官方文档中也存在“小店修改名称回调事件”set_shop_nickname,说明昵称不是稳定标识。username(店铺原始ID)才是适合做幂等键的字段。-2

2. 字段缺失不是异常。 parse_shop 对空字符串、非字符串、缺失字段都做了 clean_text 兜底,导出结果中空昵称的店铺仍然出现在 CSV 中,只是对应列为空。这种设计让“本批有多少字段缺失”可以被统计和观察,而不是在代码里报错中断。

3. 游标推进放在幂等落盘之后。 先确认 seen 集合已经持久化,再更新 cursor。顺序反了,一旦导出环节失败,游标已经前进,那批数据就永久丢失了。

4. 凭证失效要有明确的停止信号。 ensure_token 返回 False 时主流程直接退出。真实场景中,刷新失败往往意味着商家撤销了授权或凭证已过期,继续重试没有意义。服务商模式下,无权限的常见错误码是 48001 和 61007,应当作为停止信号处理,而不是重试信号。

5. 权限集匹配是前置条件。 如果使用第三方服务商模式,调用前应确认商家授权的权限集包含目标接口所需的范围(“获取店铺基本信息”对应 129/131/192 之一)。权限集不匹配时,代码应当记录并跳过,而不是反复请求。

五、小结

“微信小店商家电话批量导出”在官方接口层面缺乏直接字段支撑,但“店铺公开经营信息的增量聚合”是一个有接口可依、字段边界清晰、可以工程化的任务。凭证门卫决定代码能不能启动,权限集匹配决定接口能不能调通,username 作为幂等键决定数据是否可信,字段缺失的降级处理决定任务能不能跑完,游标与落盘的顺序决定中断后能不能续跑。把这五件事做扎实,导出的 CSV 才是一份可以重复运行、可以回溯、不会随时间膨胀的数据资产。如果业务确实需要商家的联系方式,正确路径是在授权范围内通过平台提供的客服或沟通能力触达,而不是试图从基础信息接口中“提取”一个并不存在的字段。

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

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

目录
  • 微信小店商家公开信息聚合实践:基于官方接口的增量同步与字段边界
    • 一、先确认一个事实:官方接口返回什么、不返回什么
    • 二、授权与权限集:代码能不能跑起来的前提
    • 三、增量同步的骨架:凭证门卫、清洗、幂等、落盘
    • 四、几个决定代码能不能长期跑下去的细节
    • 五、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档