VVIC(搜款网,聚焦服装批发的 B2B 平台)的商品详情数据(如档口货源、实时库存、批发价、同款对比等)是服装采购、供应链管理的核心依据。由于平台无公开官方 API,开发者需通过页面解析实现商品详情(item_get)的获取。本文系统讲解接口逻辑、技术实现、服装批发场景适配及反爬应对,帮助构建稳定的 VVIC 商品详情获取系统。
一、接口基础认知(核心功能与场景)
二、对接前置准备(环境与 URL 结构)
三、接口调用流程(基于页面解析)
四、代码实现示例(Python)
import requests
import time
import random
import re
import json
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
from typing import Dict, List
class VVICItemApi:
def __init__(self, proxy_pool: List[str] = None, cookie: str = ""):
self.base_url = "https://www.vvic.com/goods/{goods_id}.html"
self.stock_api = "https://www.vvic.com/ajax/goods/stock" # 库存接口
self.similar_api = "https://www.vvic.com/ajax/goods/similar" # 同款接口
self.ua = UserAgent()
self.proxy_pool = proxy_pool # 代理池列表
self.cookie = cookie # 登录态Cookie(必填,否则价格/库存不全)
def _get_headers(self) -> Dict[str, str]:
"""生成随机请求头"""
headers = {
"User-Agent": self.ua.random,
"Referer": "https://www.vvic.com/category/",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"X-Requested-With": "XMLHttpRequest" # 动态接口需携带
}
if self.cookie:
headers["Cookie"] = self.cookie
return headers
def _get_proxy(self) -> Dict[str, str]:
"""随机获取代理"""
if self.proxy_pool and len(self.proxy_pool) > 0:
proxy = random.choice(self.proxy_pool)
return {"http": proxy, "https": proxy}
return None
def _clean_price(self, price_str: str) -> float:
"""清洗价格(提取数字,支持“58元/件”“55元(10件起)”)"""
if not price_str:
return 0.0
price_num = re.search(r"\d+\.?\d*", price_str)
return float(price_num.group()) if price_num else 0.0
def _parse_static_data(self, html: str) -> Dict:
"""解析主页面静态数据"""
soup = BeautifulSoup(html, "lxml")
# 提取颜色选项(名称+图片)
colors = []
for color_item in soup.select("div.color-list .color-item"):
color_name = color_item.get("data-color") or color_item.get("title", "").strip()
color_img = color_item.select_one("img")?.get("src") or ""
if color_name:
colors.append({"name": color_name, "img": color_img})
# 提取尺码选项
sizes = [size.get("data-size") for size in soup.select("div.size-list .size-item") if size.get("data-size")]
# 提取起批规则
wholesale_rule = soup.select_one("div.batch-rule")?.text.strip() or ""
batch_rule = ""
batch_price_str = soup.select_one("div.price-batch")?.text or ""
if "(" in batch_price_str and ")" in batch_price_str:
batch_rule = re.search(r"(.*?)", batch_price_str).group().strip("()")
return {
"title": soup.select_one("h1.goods-title")?.text.strip() or "",
"images": {
"main": [img.get("src") for img in soup.select("div.slider-content img") if img.get("src")],
"detail": [img.get("src") for img in soup.select("div.detail-img img") if img.get("src")],
"tag": [img.get("src") for img in soup.select("div.tag-img img") if img.get("src")]
},
"price": {
"wholesale_str": soup.select_one("div.price-wholesale")?.text.strip() or "",
"batch_str": batch_price_str.strip() or "",
"wholesale": self._clean_price(soup.select_one("div.price-wholesale")?.text or ""),
"batch_price": self._clean_price(batch_price_str or ""),
"wholesale_rule": wholesale_rule,
"batch_rule": batch_rule
},
"specs": {
"colors": colors,
"sizes": sizes
},
"shop": {
"name": soup.select_one("div.shop-info .name")?.text.strip() or "",
"address": soup.select_one("div.shop-info .address")?.text.strip() or "",
"contact": soup.select_one("div.shop-info .contact")?.text.strip() or ""
},
"supply_chain": {
"delivery_time": soup.select_one("div.delivery-time")?.text.strip() or "",
"batch_rule": wholesale_rule
}
}
def _fetch_dynamic_data(self, goods_id: str, headers: Dict[str, str], proxy: Dict[str, str]) -> Dict:
"""调用动态接口获取库存和同款商品"""
dynamic_data = {
"stock": [], # 颜色+尺码库存
"total_stock": 0, # 总库存
"similar_goods": [] # 同款商品ID列表
}
try:
# 1. 获取库存
stock_params = {"goods_id": goods_id}
stock_resp = requests.get(
self.stock_api,
params=stock_params,
headers=headers,
proxies=proxy,
timeout=10
)
stock_data = stock_resp.json()
if stock_data.get("code") == 0 and "data" in stock_data:
dynamic_data["stock"] = stock_data["data"].get("stock", [])
dynamic_data["total_stock"] = stock_data["data"].get("total", 0)
# 2. 获取同款商品
similar_params = {"goods_id": goods_id}
similar_resp = requests.get(
self.similar_api,
params=similar_params,
headers=headers,
proxies=proxy,
timeout=10
)
similar_data = similar_resp.json()
if similar_data.get("code") == 0 and "data" in similar_data:
# 提取同款商品ID
dynamic_data["similar_goods"] = [
item.get("goods_id") for item in similar_data["data"] if item.get("goods_id")
]
except Exception as e:
print(f"动态数据获取失败: {str(e)}")
return dynamic_data
def _merge_specs_and_stock(self, static_colors: List[Dict], static_sizes: List[str], stock_list: List[Dict]) -> List[Dict]:
"""合并颜色、尺码与库存(生成二维规格表)"""
# 按颜色分组库存
color_stock_map = {}
for stock in stock_list:
color = stock["color"]
if color not in color_stock_map:
color_stock_map[color] = {}
color_stock_map[color][stock["size"]] = stock["num"]
# 合并每个颜色的尺码库存
merged_specs = []
for color in static_colors:
color_name = color["name"]
color_img = color["img"]
# 关联静态尺码列表(保证顺序)
size_stock = {}
for size in static_sizes:
size_stock[size] = color_stock_map.get(color_name, {}).get(size, 0)
# 生成尺码详情
sizes = [
{
"size": size,
"stock": size_stock[size],
"available": size_stock[size] > 0
} for size in static_sizes
]
merged_specs.append({
"color": color_name,
"color_img": color_img,
"sizes": sizes
})
return merged_specs
def item_get(self, goods_id: str, timeout: int = 10) -> Dict:
"""
获取VVIC商品详情
:param goods_id: 商品ID(如1234567)
:param timeout: 超时时间
:return: 标准化商品数据
"""
try:
if not self.cookie:
return {"success": False, "error_msg": "请提供登录态Cookie(否则无法获取完整数据)"}
# 1. 主页面请求
url = self.base_url.format(goods_id=goods_id)
headers = self._get_headers()
proxy = self._get_proxy()
# 随机延迟,避免反爬
time.sleep(random.uniform(2, 4))
response = requests.get(
url=url,
headers=headers,
proxies=proxy,
timeout=timeout
)
response.raise_for_status()
main_html = response.text
# 2. 解析主页面数据
static_data = self._parse_static_data(main_html)
if not static_data["title"]:
return {"success": False, "error_msg": "商品不存在或已下架"}
# 3. 获取动态数据(库存、同款)
dynamic_data = self._fetch_dynamic_data(goods_id, headers, proxy)
# 4. 合并规格与库存
merged_specs = self._merge_specs_and_stock(
static_data["specs"]["colors"],
static_data["specs"]["sizes"],
dynamic_data["stock"]
)
# 5. 整合结果
result = {
"success": True,
"data": {
"goods_id": goods_id,** static_data,
"specs": merged_specs,
"total_stock": dynamic_data["total_stock"],
"supply_chain": {
**static_data["supply_chain"],
"similar_goods": dynamic_data["similar_goods"]
},
"url": url,
"update_time": time.strftime("%Y-%m-%d %H:%M:%S")
}
}
return result
except requests.exceptions.HTTPError as e:
if "403" in str(e):
return {"success": False, "error_msg": "触发反爬,建议更换代理或Cookie", "code": 403}
if "401" in str(e):
return {"success": False, "error_msg": "Cookie无效,请重新登录获取", "code": 401}
return {"success": False, "error_msg": f"HTTP错误: {str(e)}", "code": response.status_code}
except Exception as e:
return {"success": False, "error_msg": f"获取失败: {str(e)}", "code": -1}
# 使用示例
if __name__ == "__main__":
# 代理池(替换为有效代理)
PROXIES = [
"http://123.45.67.89:8888",
"http://98.76.54.32:8080"
]
# 登录态Cookie(必填,从浏览器获取)
COOKIE = "userid=xxx; PHPSESSID=xxx; token=xxx"
# 初始化API客户端
api = VVICItemApi(proxy_pool=PROXIES, cookie=COOKIE)
# 获取商品详情(示例goods_id)
goods_id = "1234567" # 替换为实际商品ID
result = api.item_get(goods_id)
if result["success"]:
data = result["data"]
print(f"商品标题: {data['title']}")
print(f"价格信息: 批发价{data['price']['wholesale_str']} | 打包价{data['price']['batch_str']}")
print(f"起批规则: {data['price']['wholesale_rule']} | 发货时效: {data['supply_chain']['delivery_time']}")
print(f"档口信息: {data['shop']['name']} | 地址: {data['shop']['address']} | 联系方式: {data['shop']['contact']}")
print(f"规格与库存(总库存{data['total_stock']}件):")
for spec in data['specs'][:3]: # 前3个颜色
print(f" 颜色: {spec['color']} | 颜色图: {spec['color_img'][:50]}")
for size in spec['sizes'][:3]: # 前3个尺码
print(f" 尺码{size['size']}: 库存{size['stock']}件 | {'可购' if size['available'] else '无货'}")
print(f"同款商品ID: {data['supply_chain']['similar_goods'][:3]}") # 前3个同款
print(f"详情页: {data['url']}")
else:
print(f"获取失败: {result['error_msg']}(错误码: {result.get('code')})")