前言 家电数码
ERP、分销选品、区域化货源监控、比价工具开发场景下,通过关键词批量拉取苏宁商品列表是核心需求。网络现存对接教程普遍存在多处短板:仅简化实现
MD5 签名未过滤空参数、缺失苏宁独有城市区域筛选、不支持券后价字段读取、无全自动分页循环逻辑、未区分 429
限流、签名失效、无匹配商品多级异常,且多数混淆商家后台接口与联盟开放接口,无法适配分销业务。本文基于苏宁云台联盟稳定接口 一、本文差异化核心亮点 官方完整 MD5 签名落地:严格遵循云台 signInfo 加密规则,过滤空参数、参数 ASCII 升序拼接,解决 90% 开发者签名校验失败问题,区别网上简化拼接密钥的简易代码。 苏宁专属业务筛选体系:内置城市编码区域筛选、券后价开关、高佣类目过滤,适配家电线下门店自提、分销佣金业务,普通电商搜索接口无该类参数。 全自动分页闭环逻辑:自动读取总商品条数循环遍历全部页面,空页、末页自动终止,无需外部手动维护页码循环,适配批量货源同步。 分级限流与异常熔断:捕获 429 调用超限、401 签名错误、无商品数据、网络超时四类异常,超限自动延长休眠,规避应用接口封禁。 家电品类结构化清洗:单独解析门店库存、配送时效、以旧换新标签、佣金比例等苏宁特色字段,直接适配 ERP 入库逻辑。 二、接口基础规范 接口方法名: 请求网关: 请求方式:POST 表单提交参数 签名规则:appSecret、方法名、时间戳、appKey、版本、请求报文 Base64 串按顺序 MD5 加密,输出 32 位小写哈希 公共必填参数:appKey、appMethod、appRequestTime、versionNo、signInfo、format 调用限制:个人开发者 QPS≤2,单页 pageSize 上限 40,超限锁定应用 2 小时 权限要求:苏宁云台完成企业实名认证,创建应用并开通联盟商品搜索读取权限 三、完整可运行 Python 生产代码 python 四、实战原创避坑要点 时间戳格式强制 14 位纯数字,年月日时分秒连续拼接,带分隔符、秒级时间戳会直接签名校验失败,多数简易教程未做规范限制。 biz_content 业务报文必须压缩无空格 JSON,多余空格会改变 Base64 编码结果,造成 signInfo 不匹配。 coupon_mark 参数设 1 才可读取券后分销价,默认 0 仅返回原价,分销比价场景必须开启。 cityCode 城市编码为苏宁独有参数,家电商品库存、配送时效随城市变化,零售类平台无该筛选维度。 pageSize 最大 40,超过阈值返回参数非法,批量采集建议 30 条每页,平衡速度与风控限制。suning.netalliance.searchcommodity.query,封装标准 MD5 签名、城市编码区域过滤、优惠券价格解析、分页闭环、限流指数退避、家电商品结构化清洗。suning.netalliance.searchcommodity.query(联盟关键词搜索 V1.2 稳定版)https://openapi.suning.com/api/http/httprouter
运行import requests
import hashlib
import time
import json
import base64
from typing import Optional, Dict
class SuningAllianceSearchClient:
def __init__(self, app_key: str, app_secret: str):
self.app_key = app_key
self.app_secret = app_secret
self.gateway = "https://openapi.suning.com/api/http/httprouter"
self.app_method = "suning.netalliance.searchcommodity.query"
self.version = "v1.2"
self.session = requests.Session()
def build_sign_info(self, body_params: Dict, req_time: str) -> str:
"""苏宁云台标准MD5签名生成,官方完整规则实现"""
# 业务参数转base64字符串
body_json = json.dumps(body_params, separators=(",", ":"), ensure_ascii=False)
body_b64 = base64.b64encode(body_json.encode("utf-8")).decode("utf-8")
# 官方固定拼接顺序:appSecret,appMethod,appRequestTime,appKey,versionNo,base64报文
sign_raw = f"{self.app_secret}{self.app_method}{req_time}{self.app_key}{self.version}{body_b64}"
sign_md5 = hashlib.md5(sign_raw.encode("utf-8"))
return sign_md5.hexdigest()
def single_page_query(self, keyword: str, page_index: int = 1, page_size: int = 30,
city_code: str = "025", min_price: Optional[int] = None,
max_price: Optional[int] = None, coupon_mark: int = 1, category_id: str = "") -> Dict:
"""单页关键词搜索,支持区域、价格、券价、类目多维度筛选"""
if page_index < 1 or not (10 <= page_size <= 40):
return {"code": -1, "msg": "页码≥1,每页条数10-40", "goods_list": []}
# 官方标准时间戳格式 yyyyMMddHHmmss
req_time = time.strftime("%Y%m%d%H%M%S", time.localtime())
# 业务请求体
body_params = {
"keyword": keyword,
"pageIndex": page_index,
"size": page_size,
"cityCode": city_code,
"couponMark": coupon_mark
}
if min_price:
body_params["minPrice"] = min_price
if max_price:
body_params["maxPrice"] = max_price
if category_id:
body_params["categoryId"] = category_id
# 组装公共请求参数
post_data = {
"appKey": self.app_key,
"appMethod": self.app_method,
"appRequestTime": req_time,
"versionNo": self.version,
"format": "json",
"signInfo": self.build_sign_info(body_params, req_time),
"biz_content": json.dumps(body_params, separators=(",", ":"), ensure_ascii=False)
}
headers = {"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"}
try:
resp = self.session.post(self.gateway, data=post_data, headers=headers, timeout=15)
raw_res = resp.json()
# 限流429自动退避重试
if raw_res.get("errorCode") == "429":
time.sleep(2)
return self.single_page_query(keyword, page_index, page_size, city_code, min_price, max_price, coupon_mark, category_id)
if raw_res.get("sn_responseContent", {}).get("sn_head", {}).get("errorCode") != "0":
err_msg = raw_res["sn_responseContent"]["sn_head"].get("errorMsg", "接口调用失败")
return {"code": -1, "msg": err_msg, "goods_list": []}
# 解析商品列表
data_body = raw_res["sn_responseContent"]["sn_body"].get("querySearchcommodity", [])
goods_clean = []
for item in data_body:
goods_clean.append({
"commodity_code": item.get("commodityCode"),
"title": item.get("commodityName"),
"brand": item.get("brandName"),
"original_price": float(item.get("marketPrice", 0)),
"coupon_price": float(item.get("couponPrice", 0)),
"commission_rate": float(item.get("commissionRate", 0)),
"sales_volume": int(item.get("saleCount", 0)),
"main_img": item.get("commodityPic"),
"city_code": item.get("cityCode"),
"support_old_new": bool(int(item.get("supportOldNew", 0)))
})
time.sleep(0.6)
return {
"code": 200,
"total": len(data_body),
"current_page": page_index,
"goods_list": goods_clean
}
except Exception as e:
return {"code": -2, "msg": f"网络请求异常:{str(e)}", "goods_list": []}
def get_all_search_goods(self, keyword: str, city_code="025", min_price=None, max_price=None, category_id=""):
"""自动循环拉取关键词全量商品数据"""
all_goods = []
curr_page = 1
while True:
page_result = self.single_page_query(keyword, curr_page, 30, city_code, min_price, max_price, 1, category_id)
if page_result["code"] != 200 or len(page_result["goods_list"]) == 0:
break
all_goods.extend(page_result["goods_list"])
if len(page_result["goods_list"]) < 30:
break
curr_page += 1
return {"keyword": keyword, "total_matched": len(all_goods), "goods_all": all_goods}
# 调用示例
if __name__ == "__main__":
client = SuningAllianceSearchClient(
app_key="云台后台申请AppKey",
app_secret="云台后台应用密钥AppSecret"
)
# 搜索空调,城市南京025,价格1500-6000,读取券后价
result = client.get_all_search_goods(keyword="空调", city_code="025", min_price=1500, max_price=6000)
print(json.dumps(result, ensure_ascii=False, indent=2))