今年上半年,我在给一家做南亚市场的跨境卖家搭数据系统,第一个硬需求就是:把Daraz店铺的商品详情数据稳定地拉回内部业务库。团队里几个同学的第一反应都出奇一致——写爬虫,模拟浏览器登录,遍历商品详情页。我当时就把这个方案拦下了。倒不是说爬虫一定不能用,而是对Daraz这种体量、这种技术背景的平台来说,页面级爬虫的维护成本会指数级上升,早晚把你拖垮。更合理、也更体面的路子,是走Daraz开放平台的官方API。
这篇文章就把我这次完整接入Daraz API获取商品详情数据的经验摊开来讲,包括为什么弃爬虫选API、接入前要做哪些准备、HMAC-SHA256签名机制是怎么回事、商品详情接口怎么一步步调通,以及我在这个过程中踩过的坑。如果你正准备对接Daraz、Lazada这类阿里系电商开放平台,或者只是想了解电商平台商品数据的标准获取姿势,这篇应该能帮你少走不少弯路。
1. 为什么最终选择官方API:爬虫方案在Daraz上的真实处境
1.1 直接抓页面为什么行不通
我承认,爬虫在一开始确实很诱人——打开商品页,F12看接口,把JSON拉下来,十几行代码就见效。但这个"见效"通常只能维持几天。等到我们真要在Daraz上长期、稳定、大批量地同步商品数据时,问题就全暴露了。
首先,Daraz的商品页是典型的动态渲染页面,商品信息、库存、价格、SKU规格全部由前端脚本请求异步接口再渲染到页面上。你直接requests.get拿到的HTML里,根本找不到真实价格和库存,拿到的一堆JS变量名和混淆过的加载参数。想绕过这个,就得先搞懂它的前端加载链路,用无头浏览器模拟整个渲染过程,速度慢不说,内存占用也大,跑几十个页面就够呛。
其次是风控。Daraz在这块玩得很深,请求频率稍高就会触发验证码,紧接着就是IP层面的临时封禁。我们的采集任务跑到几百上千个商品时,就开始频繁遇到"当前访问量过大"的提示页。后来只能把采集频率压得很低,一个晚上都跑不完几千个SKU,数据时效性完全没法保证。
第三个问题其实是压垮我们的最后一根稻草:平台条款。Daraz的服务条款里明确禁止绕过平台正常机制抓取数据。作为一家正规运营的跨境公司,我不可能拿整个店铺去赌这个风险。一旦被平台判定为恶意采集,轻则限流,重则封店,这对业务来说是毁灭性的。
1.2 官方API的能力边界与适用场景
既然爬虫走不通,那就看官方API能给什么。Daraz开放平台(Open Platform)本质上是给卖家、第三方ERP服务商、代运营团队提供的标准数据通道。你通过认证后,可以拉取自己店铺内的商品列表、商品详情、订单、物流、库存等核心数据。
商品详情这块,官方API能拿到的字段非常完整,基本覆盖了商品页上展示的全部核心信息:
| 数据类别 | 具体字段 |
|---|---|
| 基础信息 | item_id、seller_sku、name、description、category_id、brand |
| 价格信息 | price、special_price、建议零售价 |
| 库存信息 | quantity、状态 |
| 多媒体 | 主图、副图、SKU图 |
| 规格变体 | 变体ID、变体名称、变体SKU、变体价格、变体库存 |
| 状态信息 | active/inactive、创建时间、更新时间 |
但是也要说清楚边界。官方API拿不到的东西同样很多:比如你不能用它去批量抓取别人家店铺的商品数据,接口权限是按卖家维度划定的;用户评论、买家ID这类数据,普通应用默认也拿不到,需要另外申请特定权限。如果你是想做全网商品情报分析、比价抓取,那官方API不适合你,那是另一个领域的事情,本文不展开。
所以,官方API适合谁?适合手里有Daraz店铺、需要把自有商品数据同步到ERP、WMS、数据报表、多平台铺货系统里的卖家和技术服务商。我的这次项目正是这种典型场景:把店铺里几百个SKU的商品信息做成一份实时可查的内部数据源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 接入前的基础设施:应用创建与三个关键凭证
2.1 注册开发者账号与创建应用
Daraz开放平台是挂在卖家中心体系下的,流程不复杂,但有几个细节容易被忽略。你在Daraz的卖家后台找到开发者选项,进入开放平台后可以看到创建应用的入口。创建应用时需要填写应用名称、类型(自用/第三方服务商)、以及回调地址。
这里要提醒一句:回调地址建议一开始就填真实的生产环境地址,哪怕你还在本地开发阶段,也先用内网穿透或者测试域名顶上,而不是随便填个localhost。 因为OAuth授权流程里,应用身份和回调地址是绑定的,后续新增回调地址在某些版本的后台里要重新审核,等你开发完再改,会白白浪费审核时间。
创建完成后,你会拿到一组核心凭证,这组凭证决定了你之后所有请求能否通过验证:
- App Key:相当于你的应用ID,所有请求里都会带它,让平台知道"你是谁"。
- App Secret:签名密钥,用于计算签名,绝对不能泄露到前端或者公开仓库。
- Access Token:授权后的访问令牌,代表"某个卖家账号允许这个应用访问它的数据"。
三者关系可以类比一张门禁卡:App Key是卡上的编号,App Secret是卡内的加密芯片,Access Token则是这张卡开通了哪些门禁的授权记录。三者配合,平台才能既识别应用身份,又确认用户授权,还要验证请求确实来自持卡人本人。
2.2 Access Token的获取与刷新机制
Access Token不是你在后台手动复制的,而是需要通过OAuth 2.0的授权码流程换取。大致流程是:
- 在应用后台配置应用的授权回调地址。
- 构造授权URL,引导卖家账号所有者点击授权。
- 授权成功后,平台会带着一个临时授权码(code)重定向到你的回调地址。
- 后端用这个code加上App Key和App Secret,请求token接口,换回Access Token和Refresh Token。
这里最容易犯的错是:把Access Token当成长期有效的东西,写死到配置文件里,一用就是几个月。实际上Access Token的有效期通常只有24小时左右,过期后就得用Refresh Token去换新的Access Token。Refresh Token的有效期会长很多,一般是几个月到一年,但也存在失效的情况。所以正确的工程做法是:封装一个token管理器,在请求返回token失效错误时,自动用Refresh Token刷新并重放请求,而不是让业务流程中断。
我当时的实现大致是这样一个缓存逻辑:
python复制import time
import requests
class DarazTokenManager:
def __init__(self, app_key, app_secret, refresh_token):
self.app_key = app_key
self.app_secret = app_secret
self.refresh_token = refresh_token
self.access_token = None
self.expires_at = 0
def get_access_token(self):
if self.access_token and time.time() < self.expires_at - 60:
return self.access_token
# 用refresh_token换取新token
resp = requests.post(
"https://api.daraz.com/auth/token/refresh",
data={
"app_key": self.app_key,
"app_secret": self.app_secret,
"refresh_token": self.refresh_token,
"grant_type": "refresh_token",
},
).json()
self.access_token = resp["access_token"]
self.expires_at = time.time() + int(resp["expires_in"])
return self.access_token
这个管理器每次往外拿token时先判断是否快过期,快过期就自动刷新,尽量让上层业务感知不到token的存在。
2.3 环境选择:沙箱与生产的隔离
Daraz开放平台区分沙箱环境和生产环境,刚创建的应用默认只能调沙箱。沙箱环境里返回的是模拟数据,用来联调签名、跑通流程已经完全够用。等应用审核通过后,才会有生产环境调用权限。
我的建议是:前期所有开发调试一律在沙箱做,签名逻辑、参数拼装、响应解析这些代码全部先在沙箱验证,确认无误后再把环境地址切换到生产。 这样可以把对真实数据的误操作风险降到最低,尤其是后面涉及商品更新、库存修改这类写操作时,沙箱的价值会体现得更明显。
3. 签名认证原理:HMAC-SHA256背后的设计逻辑
3.1 为什么需要签名而不是直接带密码
你可能会有疑问:请求里已经带了App Key和Access Token,为什么还要额外做一次签名?这不是多此一举吗?
App Key是公开的,相当于用户名。Access Token虽然不公开,但它在网络中传输,理论上存在被截获的可能。如果只靠这两个东西就能调接口,那一旦Token泄露,攻击者就能拿它随便操作店铺数据。签名机制的意义在于:请求里每一个参数都被密钥(App Secret)"锁"过一遍,任何人只要在传输过程中动了任何一个参数,接收方重新计算签名时就会发现对不上,直接拒绝请求。
你可以把签名想象成"参数内容的指纹"。参数集合是原文,App Secret是生成指纹的盐。原文里任何一个字符发生变化,指纹就会变得完全不同。这样既能防止参数在传输中被篡改,也能让平台确认请求的发起者确实掌握了App Secret这个核心机密。
3.2 签名与请求的完整构造过程
Daraz的签名规则沿用了阿里系开放平台经典的那套逻辑,核心步骤可以归纳为三步:
第一步:整理全部参数。 参与签名的参数包括公共参数和业务参数,公共参数有app_key、timestamp、sign_method、access_token、version、format等,业务参数就是像action、item_id这种,你实际要调用的接口是什么,就把它的参数全部纳入签名集合。
第二步:排序并拼接字符串。 将所有参数按key的ASCII码升序排序,然后用key=value的形式拼接起来,参数之间用&连接。排序这一步非常关键,因为只有约定统一的排序规则,双方才能算出同样的签名。
第三步:计算签名。 以App Secret作为密钥,对上一步得到的字符串做HMAC-SHA256计算,结果转成大写十六进制字符串,作为signature参数拼进请求里。
我在项目里写了一个通用的签名函数,长这样:
python复制import hashlib
import hmac
from urllib.parse import quote
def generate_signature(params: dict, app_secret: str) -> str:
# 1. 剔除sign和signature本身,只保留参与签名的参数
sorted_keys = sorted(params.keys())
# 2. 按key=value拼接,注意value要做URL编码
parts = []
for key in sorted_keys:
if key.lower() in ("sign", "signature"):
continue
value = str(params[key])
parts.append(f"{key}={quote(value, safe='')}")
string_to_sign = "&".join(parts)
# 3. HMAC-SHA256计算,十六进制大写
digest = hmac.new(
app_secret.encode("utf-8"),
string_to_sign.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return digest.upper()
这里我想单独说一个特别容易被坑的地方:value的URL编码。 商品名称、描述里有大量非ASCII字符,比如孟加拉语、法语的变音符,这些字符在拼进待签名串之前必须做URL编码,并且是把所有除了字母数字之外的字符都转成%XX的形式。如果你漏了这步,或者编码函数选的规则不对,前后端算出来的签名永远不一致,就会出现后续我们要说的"签名验证失败"错误。
3.3 时间戳参数为什么这么重要
公共参数里有个timestamp,很多第一次接触的人容易忽略它,以为只是个附带信息。其实时间戳在签名机制中承担了"防重放攻击"的职责。平台接到请求后,会比较当前时间和请求里的timestamp,如果相差太大(一般超过5分钟或10分钟),就直接拒绝。
这意味着,你生成签名时的timestamp和实际发起请求时携带的timestamp必须是同一个值,不能先算完签名再重新填一次时间。否则服务器看到的参数集合变了,算出来的签名自然对不上。同时,本地时钟偏差问题也不容忽视。如果你的服务器时间与NTP标准时间偏差过大——我见过有虚拟机跑了几个月没做时间同步,误差超过10分钟——那么你的请求即使签名完全正确,也可能被当成长途重放攻击拒绝。所以在正式部署前,确认一下服务器的时间同步服务是否正常,这个小细节能帮你省下一整天的排查时间。
4. 商品详情接口的调用实战:Python完整实现
4.1 获取商品列表:先拿到商品底账
Daraz开放平台调商品数据,接口粒度分两层:外层是获取商品列表,内层是获取单个商品的完整详情。大多数情况下,你不会直接对着一个不存在的item_id去查询,而是先拉列表,拿到店铺里所有商品的item_id和seller_sku,再逐个取详情。
我第一次调通的商品列表接口,完整请求是这样构造的:
python复制import requests
import time
def get_product_list(access_token, app_key, app_secret, offset=0, limit=20):
params = {
"app_key": app_key,
"timestamp": int(time.time()),
"sign_method": "sha256",
"access_token": access_token,
"version": "1.0",
"format": "json",
"action": "GetProducts",
"offset": offset,
"limit": limit,
}
signature = generate_signature(params, app_secret)
params["signature"] = signature
resp = requests.post(
"https://api.daraz.com",
data=params,
timeout=10,
)
resp.raise_for_status()
return resp.json()
响应体长什么样呢?我简化一下关键结构:
json复制{
"code": "0",
"request_id": "b8c6f4b3-1234-4f2b-9c8d-abcdef123456",
"result": {
"products": [
{
"item_id": "123456789",
"seller_sku": "SKU-BLACK-M",
"name": "Men Running Shoes Size 42"
}
],
"total_products": 214
}
}
注意这里的total_products字段,它是分页的总条数。当初我天真地以为一次能全量拉完,结果发现列表接口单次最多返回20条。要拉全部,就得按offset分页循环,或者后续用更新时间的增量参数去优化,这个我们放到最后一章细说。
4.2 查询单个商品详情的请求实现
拿到item_id后,再看单个商品详情的动作。这个接口一般叫GetProductItem,业务参数里带上item_id即可:
python复制def get_product_item(access_token, app_key, app_secret, item_id):
params = {
"app_key": app_key,
"timestamp": int(time.time()),
"sign_method": "sha256",
"access_token": access_token,
"version": "1.0",
"format": "json",
"action": "GetProductItem",
"item_id": str(item_id),
}
signature = generate_signature(params, app_secret)
params["signature"] = signature
resp = requests.post("https://api.daraz.com", data=params, timeout=10)
resp.raise_for_status()
data = resp.json()
if data.get("code") != "0":
raise RuntimeError(f"API error: {data.get('code')} {data.get('message')}")
return data["result"]["item"]
返回的item结构比列表接口完整得多,我贴一个精简后的实际字段结构:
json复制{
"item_id": "123456789",
"seller_sku": "SKU-BLACK-M",
"name": "Men Running Shoes Size 42",
"description": "Breathable mesh upper...",
"category_id": "12345",
"price": 49.99,
"special_price": 39.99,
"quantity": 120,
"images": [
"https://daraz-blog-image.s3.amazonaws.com/img1.jpg"
],
"variations": [
{
"variation_id": "987654321",
"name": "Black / 42",
"seller_sku": "SKU-BLACK-42",
"price": 49.99,
"quantity": 40,
"images": ["https://daraz-blog-image.s3.amazonaws.com/variation1.jpg"],
"status": "active"
}
],
"attributes": {
"brand": "Nike",
"color": "Black",
"material": "Mesh"
},
"status": "active",
"created_at": "2024-01-15 10:00:00",
"updated_at": "2024-03-20 18:30:00"
}
4.3 响应字段深度解读与类型转换要点
拿到这个JSON之后,处理上有几个关键点值得展开。
第一个是字段类型。 item_id和variation_id虽然是数字,但在JSON里经常是字符串。你在数据库里做主键关联时,最好统一转成字符串再存,避免后续和Excel导出的商品编码做关联时出现类型错位。price字段则是浮点数,浮点数在Python里会有精度问题,比如49.99存成49.99000000001,所以建议转成Decimal或者直接以分为单位存整数。
第二个是variations的处理。 一个商品如果有多个规格,主层级的price和quantity仍然是基础值,但真正作用于库存同步和订单匹配的是variation层级的seller_sku和quantity。如果你的业务只关注主商品维度,那直接用主字段就行;但如果你要做库存精确同步,就必须展开到variation粒度。我在设计表结构时,是把variations单独拆成一张子表的,原因就在这。
第三个是attributes的解析。 不同类目的attributes字段结构差异很大,鞋服类有color、size、material,家电类可能就有voltage、warranty。这些字段是动态的,设计数据模型时不要硬编码字段名,建议用JSON类型直接存储,或者做成键值对子表。
5. 踩坑实录:四个高频错误与完整排查链路
5.1 签名验证失败:从报错倒推参数编码问题
我们项目上线第一周,报错日志里出现最多的就是签名验证失败。一开始我以为是App Secret配错了,检查了好几遍,也没发现问题。后来单独写了个脚本,把所有参数打印出来和官方文档的示例手工比对,才发现问题出在商品描述里的换行符和特殊符号上。
详情里有一句英文描述中间带了&符号,这个符号在URL编码后变成%26,但我在拼接待签名串时用的quote函数没指定safe参数,导致某些字符被保留原样输出,而平台端用的是严格编码。两边算出来的stringToSign不一致,签名自然对不上。
后来我养成了一个习惯:签名函数写完先不要急着调接口,先用一组固定的输入跑一遍,把stringToSign和最终的signature打印出来,和官方文档的示例结果做对照。 如果示例能对得上,再去联调真实接口,这样就能把编码问题隔离在最外层。
5.2 400 Invalid Schema:请求体与接口契约不匹配
有段时间,我们批量同步时频繁收到类似400 Invalid schema的错误,响应体里有个schema描述,指出某个字段格式非法。当时的直觉是数据类型不对,但仔细排查后发现问题往往更隐蔽。
比如GetProductItem的item_id参数,API契约要求的是字符串,我们前面一直用str()转字符串,没问题。但有一次在构建入参时,我们直接复用了从列表接口里拿到的数值,没做转换,导致签名编码时value变成1234,而平台端期望的是"1234"。一字之差,签名校验就不通过。
再一个典型案例是时间字段。Daraz API的部分参数要求yyyy-MM-dd HH:mm:ss这种带空格的格式,而我们一开始传的是ISO标准的2024-03-20T18:30:00,照样报schema错误。排查这类问题最快的方式是:把请求的原始body存到日志里,一条一条和文档中的字段样例比对,而不是盯着错误码猜。
我把常见的400类原因整理成了一个小表,给大家参考:
| 现象 | 根因 | 处理方式 |
|---|---|---|
| code=400 invalid schema | 参数类型与契约不符 | 转成契约要求的字符串格式 |
| code=400 invalid timestamp | 时间戳格式不对 | 统一用yyyy-MM-dd HH:mm:ss |
| code=400 missing required | 缺了必填业务参数 | 对照文档补全公共参数 |
| code=400 signature invalid | 参数与签名不匹配 | 排序、编码、拼接三步重查 |
5.3 时区与日期格式:商品时间字段的隐形坑
商品详情的created_at和updated_at,平台返回的是UTC时间,但我们的业务库用的是本地时间。一开始没做转换,直接当成本地时间入库,结果导致第二天生成的对账报表里,更新时间比实际晚了8个小时,产生了不少"今天没更新"的误判。
解决方案很粗暴,但在业务上很有效:在解析响应时,统一把所有时间字段转成UTC内部存储,展示层再根据用户所在时区渲染。 数据库层面存UTC,等于全局只有一个时间基准,所有跨境团队看到的数据就都对齐了。如果你就是单店铺单地区使用,转成当地标准时间也完全可以,但务必在代码里显式声明时区转换,而不是依赖服务器默认时区。
5.4 调用频率超限:退避重试的正确姿势
Daraz API对单应用的调用频率有限制,具体数值和应用等级绑定,大概是每分钟几十次到几百次的量级。我们第一次全量同步时,直接循环了几百个商品,结果跑到一半就被限流了,返回的响应里带了频率限制的错误码。
当时我的处理比较粗糙,就是简单sleep。后来才发现正确的做法是指数退避 + 抖动:第一次失败等1秒,第二次4秒,第三次9秒,最多等几轮之后还是失败就把任务挂起并告警。同时,全量同步任务尽量安排在业务低峰期,平时只做增量同步,才能把调用量控制在安全范围内。
这里有一个高频易错点:不要对同样的item_id做重复同步。 全量拉完列表后,已经是完整数据了,后面只需要根据updated_at筛选出有变更的商品去更新,就能大幅降低接口调用量。
6. 数据落地与增量同步:从接口到业务系统的最后一公里
6.1 商品数据表结构设计建议
从API拿到数据只是一个中间状态,真正落地还是要设计一套适合自己业务的表结构。我这次用的是PostgreSQL,主表加变体子表的设计,核心DDL大概长这样:
sql复制CREATE TABLE products (
item_id VARCHAR(64) PRIMARY KEY,
seller_sku VARCHAR(128),
name TEXT,
description TEXT,
category_id VARCHAR(32),
price NUMERIC(12,2),
special_price NUMERIC(12,2),
main_images JSONB,
attributes JSONB,
status VARCHAR(32),
raw_data JSONB,
fetched_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE product_variations (
id BIGSERIAL PRIMARY KEY,
item_id VARCHAR(64) REFERENCES products(item_id),
variation_id VARCHAR(64),
seller_sku VARCHAR(128),
price NUMERIC(12,2),
quantity INT,
images JSONB,
status VARCHAR(32)
);
两个设计点我觉得很重要:一是保留raw_data字段存储API返回的原始JSON,出了问题可以直接对照,不用重新拉接口;二是变体表单独拆出,因为后续订单同步需要精确到variation_level的SKU。
6.2 增量同步与任务调度的要点
全量同步在SKU数量少的时候没问题,但一旦商品数量涨到几千上万个,每次全部重拉就是灾难。增量同步的核心思路是:记录上次同步的时间点,用API的更新时间参数只拉取这个时间点之后有变更的商品。
Daraz的接口一般支持按update_time范围筛选,具体参数名以文档为准。我在方案里设计的是一个基于last_sync_at字段的循环任务:
- 启动任务后,从状态表读取last_sync_at,如果不存在就执行全量同步。
- 用last_sync_at作为起点,请求增量商品列表。
- 对返回的商品逐个查详情,更新到products表。
- 同步完成后,把本次任务的执行时间更新到last_sync_at。
这个任务用Celery的beat调度,每15分钟跑一次。商品数据本身不是强实时要求,15分钟的延迟足够满足运营和供应链的查询需求。
6.3 数据校验与异常告警的实践
接口同步最怕的不是报错,而是静默丢数据:请求返回成功,但某些字段为空,导致业务侧拿到残缺数据却不自知。我在校验层的做法是,每次同步完成后对比几个关键指标:API返回的total_products、本次新增数量、本次更新数量、实际写入数量。如果四者对不上,立即告警。
告警渠道接的是钉钉机器人,核心就一条消息:任务名、同步范围、成功数、失败数、错误样本链接。这样白天跑批出了问题,运营同事第一时间就能收到通知,不用等业务报表出来才发现数据滞后。
另外,我建议给每个同步任务都做个简单的运行历史表,记录每次任务的启动时间、结束时间、耗时、结果状态。时间久了,这张表就是优化数据管线的第一手依据,哪些任务慢、哪些任务频繁失败,一目了然。
最后说一个我自己的习惯:Daraz开放平台的字段和接口版本迭代不算慢,每次项目上线前,我都会把API返回的原始数据字段和官方文档重新比对一遍,因为字段名一旦在平台侧更新,硬编码的映射逻辑就会静默失效。不管代码写得多么封装良好,最终可靠的基准还是文档本身。接入类似的电商开放平台,把认证、限流、增量同步、异常观察这几件事想清楚,整个数据链路就不会出大乱子。
