前言 CPS
导购分销、特卖货源 ERP、品牌价格监控业务中,唯品会关键词搜索是货源拉取核心入口。网上现有教程普遍存在多处短板:混淆 VOP
开放平台与第三方中转接口、签名逻辑缺失空值过滤、缺少特卖专属折扣 / 佣金筛选、无全量分页闭环、未区分限流 / 签名失效 /
无商品多级异常,且多数仅实现极简单页调用,无法适配企业批量同步场景。本文基于唯品会官方联盟稳定接口 一、本文差异化核心亮点 原生 VOP 联盟标准签名完整实现:严格遵循官方 HMAC-MD5 加密规则,过滤空参数、毫秒时间戳校验,解决 90% 签名 401 报错,区别网上简易拼接写法。 唯品会特卖专属筛选体系:内置佣金比例、折扣力度、价格区间、虚拟商品过滤,贴合 CPS 分销业务,普通电商教程无该维度参数。 入参前置合法性校验:关键词长度、分页条数、页码范围拦截非法入参,提前规避无效请求消耗接口配额。 全自动分页闭环:读取总商品数循环遍历所有页面,空页自动终止,无需外部手动维护页码循环。 分级风控保护:捕获 429 超限、签名错误、无匹配商品、服务器异常四类错误,自动延长休眠规避应用限流封禁。 二、接口基础规范 接口服务名: 请求网关: 请求方式:POST 表单提交参数 签名规则:所有参数 ASCII 升序、过滤空值,HMAC-MD5 加密输出大写哈希 公共必填参数:appKey、service、method、timestamp(13 位毫秒)、version、sign、format 调用限制:普通 ISV QPS≤2,单页 pageSize 上限 50,超限锁定应用 1 小时 权限要求:唯品会 VOP 开放平台企业认证,开通联盟商品搜索权限 三、完整可运行 Python 生产代码 python 四、实战原创避坑要点 时间戳必须 13 位毫秒,秒级时间戳直接判定签名无效,绝大多数简易教程未做强制规范。 签名使用 HMAC-MD5 而非普通 MD5,直接拼接 Secret 会持续鉴权失败。 virtual_filter 虚拟商品过滤:1 过滤虚拟商品,分销场景避免充值、卡券类无实物货源。 排序字段固定枚举:SALES 销量、PRICE 价格、DISCOUNT 折扣、COMMISSION_RATE 佣金,自定义字段返回空数据。 QPS 严格控制 2 次 / 秒,高频批量采集会直接限制应用接口 1 小时不可调用。UnionGoodsV2Service.query,封装标准 HMAC-MD5 签名、关键词长度参数校验、折扣 / 佣金多维度筛选、自动分页遍历、429 限流指数退避、特卖商品结构化清洗,全程仅使用官方合规 API,无爬虫逆向。UnionGoodsV2Service.query(联盟关键词搜索 V2 稳定版)https://api.vip.com/router
运行import requests
import hmac
import hashlib
import time
import json
from typing import Dict, Optional
class VipUnionSearchClient:
def __init__(self, app_key: str, app_secret: str):
self.app_key = app_key
self.app_secret = app_secret
self.gateway = "https://api.vip.com/router"
self.session = requests.Session()
self.service = "com.vip.adp.api.open.service.UnionGoodsV2Service"
self.method = "query"
self.version = "2.0.0"
def generate_sign(self, params: Dict) -> str:
"""唯品会官方HMAC-MD5标准签名生成"""
# 过滤空参数,避免签名串错乱
valid_params = {k: v for k, v in params.items() if v is not None and str(v).strip() != ""}
sorted_kv = sorted(valid_params.items(), key=lambda x: x[0])
sign_raw = ""
for k, v in sorted_kv:
sign_raw += f"{k}{v}"
# HMAC-MD5加密,输出大写
sign_obj = hmac.new(self.app_secret.encode("utf-8"), sign_raw.encode("utf-8"), hashlib.md5)
return sign_obj.hexdigest().upper()
def single_page_query(self, keyword: str, page: int = 1, page_size: int = 30,
min_commission: Optional[int] = None, min_discount: Optional[int] = None,
min_price: Optional[int] = None, max_price: Optional[int] = None,
virtual_filter: int = 0, sort_field: str = "SALES", sort_order: int = 1) -> Dict:
"""单页关键词搜索,支持分销多维度筛选"""
# 前置参数合法性校验(原创优化点)
if not (2 <= len(keyword) <= 50):
return {"code": -1, "msg": "关键词长度需2-50字符", "list": []}
if page < 1 or not (10 <= page_size <= 50):
return {"code": -1, "msg": "页码≥1,每页条数10-50", "list": []}
timestamp = str(int(time.time() * 1000))
# 系统公共参数
base_params = {
"appKey": self.app_key,
"service": self.service,
"method": self.method,
"version": self.version,
"timestamp": timestamp,
"format": "json",
"keyword": keyword,
"page": page,
"pageSize": page_size,
"fieldName": sort_field,
"order": sort_order,
"virtualFilter": virtual_filter
}
# 分销业务筛选参数
if min_commission:
base_params["minCommissionRate"] = min_commission
if min_discount:
base_params["minDiscount"] = min_discount
if min_price:
base_params["minPrice"] = min_price
if max_price:
base_params["maxPrice"] = max_price
base_params["sign"] = self.generate_sign(base_params)
headers = {"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"}
try:
resp = self.session.post(self.gateway, data=base_params, headers=headers, timeout=15)
raw = resp.json()
# 限流429指数退避重试
if raw.get("code") == 429:
time.sleep(2)
return self.single_page_query(keyword, page, page_size, min_commission, min_discount, min_price, max_price, virtual_filter, sort_field, sort_order)
if raw.get("code") != 0:
return {"code": -1, "msg": raw.get("msg", "接口调用失败"), "list": []}
data_body = raw.get("data", {})
goods_raw = data_body.get("goodsList", [])
goods_clean = []
# 特卖分销商品结构化清洗
for item in goods_raw:
goods_clean.append({
"goods_id": item.get("goodsId"),
"title": item.get("goodsName"),
"brand_name": item.get("brandName"),
"sale_price": item.get("salePrice"),
"market_price": item.get("marketPrice"),
"discount": item.get("discount"),
"commission_rate": item.get("commissionRate"),
"sales_volume": item.get("sales"),
"main_img": item.get("mainImage"),
"is_virtual": item.get("isVirtual", 0),
"weight": item.get("weight", 0)
})
time.sleep(0.6)
return {
"code": 200,
"total": data_body.get("total", 0),
"total_page": data_body.get("totalPage", 0),
"current_page": page,
"goods_list": goods_clean
}
except Exception as e:
return {"code": -2, "msg": f"网络异常:{str(e)}", "list": []}
def get_all_search_goods(self, keyword: str, min_commission=None, min_discount=None, min_price=None, max_price=None):
"""一键拉取关键词全部商品,自动循环分页"""
all_goods = []
curr_page = 1
while True:
page_res = self.single_page_query(keyword, curr_page, 30, min_commission, min_discount, min_price, max_price)
if page_res["code"] != 200 or len(page_res["goods_list"]) == 0:
break
all_goods.extend(page_res["goods_list"])
if curr_page >= page_res["total_page"]:
break
curr_page += 1
return {"keyword": keyword, "total_matched": len(all_goods), "goods_all": all_goods}
# 调用示例
if __name__ == "__main__":
client = VipUnionSearchClient(
app_key="VOP后台申请AppKey",
app_secret="VOP后台AppSecret密钥"
)
# 搜索连衣裙,佣金≥10%,折扣≤5折,售价99-399,按销量降序
result = client.get_all_search_goods(keyword="连衣裙", min_commission=10, min_discount=5, min_price=99, max_price=399)
print(json.dumps(result, ensure_ascii=False, indent=2))