如果你需要 TikTok Shop 商品数据来做定价或选品调研,应当先走官方渠道。TikTok Shop Partner API 提供完整的商品目录和订单数据,但仅限于授权了你的应用的店铺。TikTok Research API 返回欧盟店铺的商品、价格和销量字段,但只对获批的非商业研究者开放。大多数研究竞品的商业团队,最终要么从分析厂商那里购买授权数据,要么搭一个慢速、克制的采集器,读取公开商品页本来就发布的结构化数据,并通过目标市场所在国家的 IP 访问。
本文面向市场调研而写,与 TikTok 没有关联,也未获其背书,不涉及 TikTok 私有的、带签名的内部 API。
团队真正需要哪些 TikTok Shop 商品数据
大多数调研问题归结起来是八个左右的字段:商品标题、当前价格、划线价(由此得出折扣)、销量、店铺、评分和评价数、类目,以及哪些达人在推广这件商品。
- 价格调研。当前价格、划线价、币种和进行中的促销,在同款或同类商品的不同卖家之间比较。这需要一致的时间戳和固定的市场。英国店面的价格和德国店面的价格是两条不同的观测,而不是一条带噪声的观测。
- 选品分析。某家店铺覆盖哪些类目,上架了多少 SKU 和变体,价格在类目中处于什么位置。通常每周一次快照就够了。
- 趋势发现。按类目统计销量和评价数随时间的变化。绝对数值不如斜率重要,这一点倒是幸运,因为显示的销量本身就不精确(下文详述)。
- 达人与联盟关系梳理。哪些达人链接到了某件商品。这是法律分量最重的字段,因为它涉及的是人,而不是商品。
如果你为其他电商平台搭建过价格追踪器,整体结构会很熟悉。我们关于竞品价格自动监控和跨市场价格追踪的指南介绍了数据管道部分。TikTok Shop 的不同之处在于,商品目录有很大一部分受市场和登录状态限制。
获取 TikTok Shop 数据的官方渠道
结构化的 TikTok Shop 数据有三个正当来源,但没有一个会直接把竞争对手的商品目录交给商业团队。即便如此也要先看看:前两个在符合条件时免费,第三个往往比自建更便宜。
TikTok Shop Partner Center 与 TTS API
TikTok Shop Partner Center 是开发者为 TikTok Shop 开放平台注册应用的地方。TikTok 官方概述称,TTS API 提供对“经授权的 TikTok Shop 数据,包括商品目录、订单、发货、支付等”的访问。其接口类别包括 Product、Order、Fulfillment、Return & Refund、Logistics、Promotion、Finance、Seller、Authorization、Events、Data Reconciliation 和 Supply Chain API。
关键词是经授权。文档列出了三种授权类型:卖家(店铺授予你的应用访问权限)、达人和合作伙伴。每一种都取决于数据所有者主动同意。如果你本身是卖家,或者是客户为卖家的工具厂商,这就是你能拿到的最好数据:这些店铺的精确价格、真实库存和真实订单数。但它不能用来查看没有接入你应用的店铺。文档还列出了支持的地区和语言,并注明可用范围会随着新市场开放而变化。
TikTok Research API:确实包含欧盟店铺数据
TikTok Research API 主要以公开视频、评论和账号数据为人所知,但它还有三个常被忽略的 TikTok Shop 接口。店铺信息接口返回店铺评分、评价数、已售件数和一个表现值。店铺商品接口返回商品名称、价格、描述、销量、评价数以及 1–5 星分布。商品评价接口返回评价文本、评分和时间戳。三个接口的范围都限于在欧盟可售的店铺和商品,每次调用最多返回 10 条结果。
门槛在于资格。访问权限面向美国、EEA、英国、加拿大和瑞士的学术机构,欧盟的非营利研究组织,以及部分研究青少年安全的巴西机构。申请者必须“独立于商业利益”。Research API FAQ 给出的标准配额是每天 1,000 次请求。研究欧盟社交电商的大学团队应当申请;品牌的定价团队不符合资格,也不应借用研究合作方的访问权限。
来自分析厂商的授权数据
市面上有一整类 TikTok Shop 分析产品,通过看板或 API 出售商品排行、估算销量、达人与商品的关联以及类目趋势。购买之前先问三件事:覆盖哪些市场?销量数字是观测所得还是模型估算?能否在内部转发这些数据?说不清数字来源的厂商给你的就是估算值,你也应当这样对待。
| 渠道 | 谁能获取 | 覆盖内容 | 能拿到竞品数据吗? |
|---|---|---|---|
| TTS API(Partner Center) | 卖家,以及获得卖家、达人或合作伙伴授权的应用开发者 | 授权店铺的完整目录、价格、库存、订单、促销、财务 | 不能,只限接入了你应用的店铺 |
| Research API 的 TikTok Shop 接口 | 获批的非商业研究者 | 欧盟店铺:店铺评分和销量、商品价格和销量、评价 | 能,但仅限非商业研究 |
| 分析厂商 | 任何付费者 | 排行、估算销量、达人关联、历史数据 | 能,在授权范围内;准确度参差不齐 |
| 公开商品页 | 处于已开通市场的任何人 | 页面对未登录访客展示的内容 | 能,但受服务条款和 robots.txt 约束 |
公开的 TikTok Shop 页面:开放网络上有什么
TikTok Shop 主要是应用内体验,但在 shop.tiktok.com 也有按市场组织的浏览器店面,商品详情页位于 /pdp/ 路径下。你看到哪个市场取决于你从哪里连接。我们从德国的服务器请求一个 www.tiktok.com/shop/pdp/… URL 时,它返回 301 跳转到 shop.tiktok.com/de/pdp/…。而从同一台机器请求不带路径的 www.tiktok.com/shop,则跳转到了 shop.tiktok.com/us。
关于市场覆盖:TikTok 新闻中心宣布奥地利、比利时、荷兰和波兰将于 2026 年 6 月 15 日上线,加入法国、德国、爱尔兰、意大利、西班牙和英国的行列。Partner Center 的地区列表(最后更新于 2026 年 7 月 6 日)还包括美国、墨西哥、巴西、日本、印度尼西亚、马来西亚、菲律宾、新加坡、泰国和越南。这样有官方来源的市场共有 20 个。第三方列表列出的更多,但我们无法用 TikTok 自己的页面核实,所以这里不收录。
这对采集意味着三点:
- 地理位置决定你看到什么。同一件商品在每个市场的价格、币种、可售情况或卖家都可能不同,甚至根本不存在。要采集英国店面,你的流量就必须来自英国。这正是这里需要支持国家定向的住宅 IP 的主要原因,例如使用
USERNAME-country-GB这样的住宅代理用户名。这与伪装无关。 - 页面高度依赖 JavaScript。店面是客户端渲染的应用。
shop.tiktok.com的响应头中包含x-processed-by: Modern.js,这是字节跳动开源的 Web 框架。预计需要真实浏览器。 - 存在拦截页。从我们的数据中心测试机访问时,
shop.tiktok.com/de和shop.tiktok.com/robots.txt都返回了一个标题为 “Security Check”、会加载 CAPTCHA 脚本的 HTML 页面。我们就此止步。破解或绕过这个挑战不在本文范围内;如果你的采集器反复碰到它,正确的做法是放慢或停止。
商品页上的数据在哪里
在现代店面上,你想要的字段很少整齐地放在 HTML 里。它们通常位于以下三处之一,按稳定性从高到低排列:遵循 schema.org Product/Offer 的 JSON-LD 块(为搜索引擎发布,参见 Google 的商品标记文档)、Open Graph 和商品 meta 标签,以及框架内嵌的 hydration 状态,也就是 <script> 标签里的一大段 JSON,应用用它来渲染页面而无需再发一次请求。
方法就是读取页面已经发给你的内容。把 JSON-LD 和 meta 标签当作有文档的格式来解析。对于 hydration 状态,先清点这些 JSON 块(ID 和大小),自己在浏览器里看一个,然后只映射你需要的字段。有两件事明确不在范围内:调用应用在后台使用的私有 XHR 接口,以及复现它们的签名或 token 参数。伪造签名是规避,不是采集。我们从测试机上无法加载商品页(见上文的拦截页),所以不会声称 TikTok Shop 页面带有 JSON-LD。下面的代码无论有没有都同样适用。
| 数据项 | 通常所在位置 | 可靠性 | 备注 |
|---|---|---|---|
| 商品标题 | JSON-LD name、og:title、hydration 状态 | 高 | 可能按市场本地化;以商品 ID 而不是标题作为键 |
| 当前价格 | JSON-LD offers.price、hydration 状态 | 中 | 因市场、变体和促销而异;始终保存币种 |
| 划线价 / 折扣 | hydration 状态;JSON-LD 中很少有 | 中低 | 划线价由卖家设定;折扣自己计算 |
| 销量 | hydration 状态、可见标签;Research API(欧盟) | 低到中 | 标签常为缩写;当作趋势使用,而不是经审计的销售额 |
| 评分、评价数 | JSON-LD aggregateRating、hydration 状态 | 中 | 不同市场的评价计数方式可能不同;Research API 提供欧盟的星级分布 |
| 店铺名称 / ID | hydration 状态、可见的卖家区块 | 高 | 保存 ID;店铺名称会变 |
| 类目 | 面包屑 JSON-LD、hydration 状态 | 中 | 各市场类目树不同;映射到你自己的分类体系 |
| 变体 / SKU | hydration 状态 | 中 | 价格区间会掩盖买家实际所选变体的价格 |
| 达人 / 联盟链接 | 主要在应用内;部分在 hydration 状态中 | 网页端低 | 属于个人数据;这里厂商或合作方数据通常更好 |
克制的采集设计
研究用采集器应当表现得像一个慢速、诚实的访客:一次一个页面、固定的 URL 集合、积极缓存、遇到挑战就停。TikTok 的条款和 robots 规则把界限划得比大多数网站都清楚,所以先从这里看起。
robots.txt 和条款实际怎么说
www.tiktok.com/robots.txt(获取于 2026 年 10 月 3 日)对所有 user agent 禁止 /shop/view/product/,并对一份点名列表中的爬虫封禁整个站点,其中包括 GPTBot、ClaudeBot、CCBot、PerplexityBot 和 Bytespider。它没有提到 /shop/pdp/,也没有 Sitemap 行。我们从测试机读取不到 shop.tiktok.com/robots.txt,因为它返回的是 HTML 挑战页。这是个陷阱:Python 的 urllib.robotparser 会把 HTML 页面解析为“没有规则”,也就是“全部允许”。下面的代码只信任纯文本的 200 响应。
条款比 robots.txt 更严格。美国服务条款(与 TikTok USDS Joint Venture LLC 签订,最后更新于 2026 年 7 月 15 日)禁止“出于任何目的,使用任何自动化系统或软件从平台”抓取或提取数据,“经书面批准的除外”。EEA 条款的表述大致相同。这些条款能否约束未登录的访客取决于司法管辖区,但它们是优先选择官方和授权渠道的有力理由。
设计规则及其理由
- 从已知 URL 列表开始,而不是爬取。调研问题针对的是具体商品或抽样集合。爬取搜索页和类目页正是负载和服务条款风险累积的地方,而且 TikTok 的 robots.txt 本来就禁止
/search?路径。 - 每次运行只用一个市场,并固定下来。对出口 IP 做国家定向,并把浏览器的 locale 和时区设成一致,让一批数据中的每条观测都来自同一个市场。
- 保持低速率。每个市场每 20–40 秒一个页面,用来对几百件商品做每日价格快照已经绰绰有余。
- 按(商品 ID、市场、日期)缓存。如果今天的英国观测已经有了,就不要再抓。
- 把挑战当作停止信号。记录下来,等待,并降低速率。不要通过轮换 IP 来绕过同一个封锁。
- 不要登录。只采集未登录访客能看到的内容。登录状态下采集会把自动化与账号及其条款绑定,还会触及个人数据。
一个谨慎的 Playwright Python 示例
下面的脚本在 Chromium 中通过国家定向代理逐个打开商品 URL。它先检查真正的 robots.txt,遇到非 200 响应或 “Security Check” 标题就停止,并提取 JSON-LD 商品数据、几个 meta 标签,以及一份内嵌 JSON 块清单供你查看。它没有使用任何 TikTok 专用的选择器,因为我们无法验证它们;任何缺失的字段都返回 None。代理设置遵循 Playwright 文档中的 proxy 选项。我们用 Playwright 1.55 在本地测试页面上验证了提取和 robots 逻辑,但没有在真实的 TikTok Shop 页面上运行过。
import json
import random
import time
import urllib.robotparser
from urllib.parse import urlsplit
from playwright.sync_api import sync_playwright
# 国家在代理用户名中指定;locale/时区要与之匹配。
PROXY = {
"server": "http://gate.proxyhat.com:8080",
"username": "USERNAME-country-GB",
"password": "PASSWORD",
}
UA_NAME = "acme-price-research" # 在 robots.txt 分组中用来匹配的标识
# 你已有的商品 URL(来自你自己的目录匹配、合作方
# 导出或人工调研),而不是通过爬取搜索页发现的 URL。
URLS = [
"https://shop.tiktok.com/...", # 粘贴浏览器中显示的真实商品 URL
]
def robots_verdict(context, url):
"""Return True/False from a real robots.txt, or None if we couldn't get one."""
parts = urlsplit(url)
resp = context.request.get(f"{parts.scheme}://{parts.netloc}/robots.txt")
ctype = resp.headers.get("content-type", "")
# 在 urllib.robotparser 中,HTML 挑战页会被解析为“没有规则”(= 全部允许),
# 所以只信任纯文本的 200 响应。
if resp.status != 200 or "text/plain" not in ctype:
return None
rp = urllib.robotparser.RobotFileParser()
rp.parse(resp.text().splitlines())
return rp.can_fetch(UA_NAME, url)
def _types(node):
t = node.get("@type", [])
return t if isinstance(t, list) else [t]
def extract(page):
"""Read what the page itself publishes: JSON-LD, Open Graph/product meta,
and an inventory of embedded JSON blobs for a human to inspect."""
jsonld = []
for raw in page.locator('script[type="application/ld+json"]').all_text_contents():
try:
data = json.loads(raw)
except json.JSONDecodeError:
continue
nodes = data if isinstance(data, list) else data.get("@graph", [data])
jsonld.extend(n for n in nodes if isinstance(n, dict))
meta = {}
for prop in ("og:title", "og:url", "product:price:amount", "product:price:currency"):
loc = page.locator(f'meta[property="{prop}"]')
if loc.count():
meta[prop] = loc.first.get_attribute("content")
blobs = page.evaluate(
"""() => [...document.querySelectorAll('script[type="application/json"]')]
.map(s => ({id: s.id || null, chars: s.textContent.length}))"""
)
product = next((n for n in jsonld if "Product" in _types(n)), None)
record = {"name": None, "price": None, "currency": None,
"availability": None, "rating": None, "review_count": None}
if product:
record["name"] = product.get("name")
offers = product.get("offers") or {}
if isinstance(offers, list):
offers = offers[0] if offers else {}
record["price"] = offers.get("price") or offers.get("lowPrice")
record["currency"] = offers.get("priceCurrency")
record["availability"] = offers.get("availability")
agg = product.get("aggregateRating") or {}
record["rating"] = agg.get("ratingValue")
record["review_count"] = agg.get("reviewCount") or agg.get("ratingCount")
return {"record": record, "meta": meta, "json_blobs": blobs,
"has_jsonld_product": product is not None}
def main():
results = []
with sync_playwright() as p:
browser = p.chromium.launch(proxy=PROXY)
context = browser.new_context(locale="en-GB", timezone_id="Europe/London")
page = context.new_page()
for url in URLS:
verdict = robots_verdict(context, url)
if verdict is not True:
results.append({"url": url, "status": "skipped", "robots": verdict})
continue
resp = page.goto(url, wait_until="domcontentloaded", timeout=45_000)
status = resp.status if resp else None
if status != 200 or page.title().strip().lower() == "security check":
# 不要尝试破解。记录下来,放慢速度,继续下一个。
results.append({"url": url, "status": "challenge_or_error", "http": status})
time.sleep(120)
continue
data = extract(page)
data.update({"url": url, "final_url": page.url, "status": "ok",
"fetched_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"proxy_country": "GB"})
results.append(data)
time.sleep(random.uniform(20, 40)) # 一次一个页面,慢慢来
browser.close()
print(json.dumps(results, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
当 has_jsonld_product 返回 false 时,查看 json_blobs。在普通浏览器中打开一件商品,找到包含价格的那个 JSON 块,为你需要的两三个键写一个简短、明确的映射,再加一个测试,在这些键位置变化时明确报错。关于 JavaScript 方面的更全面介绍,参见采集重度依赖 JavaScript 的网站。
数据质量陷阱
大多数 TikTok Shop 数据集出问题是在分析阶段,而不是采集阶段:跨市场比较价格却没有归一化,把促销价当成划线价,把四舍五入的销量当成销售额。
- 地区与币种。每一行都保存市场、币种和出口国家。按观测当天的汇率换算币种,永远不要跨市场求平均价格。
- 限时抢购与促销。短期促销可能让显示价格变动几个小时。每天在同一时间做快照,同时记录显示价格和划线价,以免把一次促销误判为永久降价。
- 销量取整。显示的数字常为缩写,即便页面数据中是精确整数,那也是平台给出的数字,而不是经审计的数据。使用随时间的差值,并预期它呈阶梯状而不是平滑曲线。
- 按市场划分的商品目录。某件商品在一个市场缺失并不代表缺货,可能只是那里不卖。把“该市场不可售”与“缺货”分开记录。
同样的问题在我们介绍过的其他电商平台上也会出现。Shein 价格情报指南讨论了区域店面,Temu 商品数据指南讨论了内嵌 JSON,Shopify 指南则讨论了有文档的公开商品接口,与没有此类接口的平台形成了有益的对照。
法律与伦理边界
公开的商品列表是这个数据集中风险最低的部分,达人则是风险最高的部分。商品价格是商业信息,但当达人的账号名、主页和联盟活动关联到可识别的个人时,它们就属于 GDPR 下的个人数据。为欧盟或英国的项目采集这些数据,需要合法依据、数据最小化和保存期限,即便主页是公开的也一样。能聚合就聚合,例如“被超过 50 位达人推广的商品”,而不是为个人建立档案。
在此之上还有前面引用的 TikTok 条款,以及商标问题:如实描述 TikTok Shop,不要把你的数据集或工具包装成与其有关联。如果调研是商业性且持续进行的,官方或授权渠道几乎总是更持久的选择。公开页面采集器最适合官方渠道覆盖不到的、范围窄、量小的核查,比如确认英国买家今天实际看到的是什么。
这种方法解决不了什么
一个克制的公开页面采集器给不了你完整的类目覆盖、达人归因、直播销量或仅限应用内的内容。它过不了拦截页,也不应该去尝试。几个月下来,它还会随着前端改版而失效。如果你需要上述任何一项,缺口在于数据授权,而不是工程。至于剩下部分的网络层面,我们的网页采集应用场景页面和地区列表列出了你可以路由经过的国家。






