前言 服装批发
ERP、货源选品、跨境铺货、档口价格监控场景中,VVIC
关键词搜索是货源采集核心入口。网上现有教程普遍存在多处短板:签名逻辑简化缺失空值过滤、仅实现单页查询无全量分页、缺少服装行业专属筛选字段、无
429 限流重试机制、未结构化解析批发核心数据(起批量、产业带、混批价),且大量混淆第三方中转 API 与 VVIC 原生开放接口。本文基于
VVIC 官方 V2.1 原生搜索接口 一、本文差异化核心亮点 原生官方接口适配:摒弃第三方中转 API,基于 VVIC V2.1 正式版规范开发,参数、签名完全贴合官方文档。 服装批发专属筛选:内置价格区间、最小起订量、产业带市场、新品 / 爆款筛选,适配服装采购真实业务。 严谨 MD5 签名封装:过滤空参数、中文 URL 编码、13 位毫秒时间戳,解决 90% 开发者签名校验失败问题。 全量智能分页闭环:自动读取总页数循环拉取,空页自动终止,无需手动维护页码循环逻辑。 风控分级保护:捕获 429 限流、500 服务异常、403 鉴权失败,区分错误类型并自动休眠重试,规避 AppKey 封禁。 二、接口基础规范 接口路径: 请求方式:POST JSON(禁止 GET,易触发风控拦截) 鉴权规则:AppKey+13 位毫秒 timestamp+MD5 大写签名 调用限制:基础权限 30 次 / 分钟,单页 page_size 最大 50,IP+AppKey 双重限流,超限返回 code=429 权限要求:VVIC 开放平台企业认证,开通商品搜索读取权限 三、完整可运行 Python 生产代码 python 四、实战原创避坑要点 时间戳必须 13 位毫秒,秒级时间戳会直接签名不通过,全网多数简易教程遗漏该规范。 中文关键词必须 URL 编码,签名函数内置转义,直接传入中文会导致校验失败。 page_size 上限 50,超过 50 会返回参数非法,批量采集建议 30 条每页平衡速度与风控。 sort 排序参数区分:sales 销量、price_asc 低价、price_desc 高价、new 新品,服装选品优先销量排序。 market 产业带编码必填数字,如十三行、沙河对应专属编码,不传则返回全市场混合商品。/api/search/item/keyword,封装标准 MD5 大写签名、产业带 / 起批量多维度筛选、自动分页闭环、限流指数退避、批发数据清洗降噪,全程仅使用平台官方开放 API,无爬虫逆向。https://api.vvic.com/api/search/item/keyword(生产环境)
运行import requests
import hashlib
import time
import json
from urllib.parse import quote
from typing import Dict, List, Optional
class VvicKeywordSearchClient:
def __init__(self, app_key: str, app_secret: str):
self.app_key = app_key
self.app_secret = app_secret
self.api_url = "https://api.vvic.com/api/search/item/keyword"
self.session = requests.Session()
def generate_sign(self, params: Dict) -> str:
"""VVIC V2.1标准MD5签名,过滤空值+中文编码+ASCII升序"""
filter_params = {k: v for k, v in params.items() if v is not None and str(v).strip() != ""}
sorted_kv = sorted(filter_params.items(), key=lambda x: x[0])
sign_parts = []
for k, v in sorted_kv:
val_encode = quote(str(v), encoding="utf-8")
sign_parts.append(f"{k}={val_encode}")
sign_str = "&".join(sign_parts) + self.app_secret
md5_raw = hashlib.md5(sign_str.encode("utf-8"))
return md5_raw.hexdigest().upper()
def single_page_query(self, keyword: str, page: int = 1, page_size: int = 30,
min_price: Optional[int] = None, max_price: Optional[int] = None,
min_buy: Optional[int] = None, market_code: Optional[str] = None,
sort: str = "") -> Dict:
"""单页关键词搜索,支持批发多条件筛选"""
timestamp = str(int(time.time() * 1000))
base_params = {
"app_key": self.app_key,
"timestamp": timestamp,
"version": "2.1",
"keyword": keyword,
"page": page,
"page_size": min(page_size, 50),
"sort": sort
}
# 服装批发筛选参数
if min_price:
base_params["price_min"] = min_price
if max_price:
base_params["price_max"] = max_price
if min_buy:
base_params["min_buy"] = min_buy
if market_code:
base_params["market"] = market_code
base_params["sign"] = self.generate_sign(base_params)
headers = {"Content-Type": "application/json;charset=utf-8"}
try:
resp = self.session.post(self.api_url, json=base_params, headers=headers, timeout=15)
raw_data = resp.json()
# 限流429指数退避重试
if raw_data.get("code") == 429:
time.sleep(2)
return self.single_page_query(keyword, page, page_size, min_price, max_price, min_buy, market_code, sort)
if raw_data.get("code") != 200:
return {"code": -1, "msg": raw_data.get("msg", "接口调用失败"), "data": []}
res_body = raw_data.get("data", {})
item_list = res_body.get("item_list", [])
product_clean = []
# 批发商品结构化清洗
for item in item_list:
product_clean.append({
"item_id": item.get("item_id"),
"title": item.get("title"),
"market_name": item.get("market_name"),
"wholesale_price": item.get("price"),
"min_order": item.get("min_buy"),
"stock_num": item.get("stock"),
"sales_volume": item.get("sales"),
"main_img": item.get("img_url"),
"shop_name": item.get("shop_name"),
"update_time": item.get("update_time")
})
time.sleep(0.4)
return {
"code": 200,
"total": res_body.get("total", 0),
"total_page": res_body.get("total_page", 0),
"current_page": page,
"product_list": product_clean
}
except Exception as e:
return {"code": -2, "msg": f"网络异常:{str(e)}", "data": []}
def get_all_search_goods(self, keyword: str, min_price=None, max_price=None, min_buy=None, market_code=None, sort="sales") -> Dict:
"""一键拉取关键词全部商品,自动循环分页"""
all_product = []
page = 1
while True:
page_res = self.single_page_query(keyword, page, 30, min_price, max_price, min_buy, market_code, sort)
if page_res["code"] != 200 or len(page_res["product_list"]) == 0:
break
all_product.extend(page_res["product_list"])
if page >= page_res["total_page"]:
break
page += 1
return {"keyword": keyword, "total_matched": len(all_product), "goods_all": all_product}
# 调用示例
if __name__ == "__main__":
client = VvicKeywordSearchClient(
app_key="开发者后台申请的AppKey",
app_secret="开发者后台AppSecret密钥"
)
# 搜索连衣裙,价格30-120,起批2件,按销量排序
result = client.get_all_search_goods(keyword="连衣裙", min_price=30, max_price=120, min_buy=2)
print(json.dumps(result, ensure_ascii=False, indent=2))