唯品会(聚焦品牌折扣的 B2C 电商平台)的商品详情数据(如折扣价、品牌信息、规格库存、促销活动等)对价格监控、竞品分析、消费趋势跟踪等场景具有重要价值。由于平台无公开官方 API,开发者需通过页面解析或第三方服务实现商品详情(item_get)的获取。本文系统讲解接口逻辑、技术实现、促销场景适配及反爬应对,帮助构建稳定的唯品会商品详情获取系统。
一、接口基础认知(核心功能与场景)
二、对接前置准备(环境与 URL 结构)
三、接口调用流程(基于页面解析与动态接口)
四、代码实现示例(Python)
import requests
import time
import random
import re
import json
import hashlib
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
from typing import Dict, List
class VipItemApi:
def __init__(self, proxy_pool: List[str] = None, cookie: str = ""):
self.base_url = "https://detail.vip.com/detail-{product_id}.html"
self.api_url = "https://mapi.vip.com/vips-mobile/rest/product/detail/info"
self.ua = UserAgent()
self.proxy_pool = proxy_pool # 代理池列表
self.cookie = cookie # 登录态Cookie
# 接口固定参数(从JS中提取,可能随平台更新)
self.app_key = "12574478" # 示例值,需实际提取
self.secret = "44982319a4d47a74563504601123456" # 示例值,需实际提取
def _get_headers(self) -> Dict[str, str]:
"""生成随机请求头"""
headers = {
"User-Agent": self.ua.random,
"Referer": "https://category.vip.com/",
"Accept": "application/json, text/plain, */*",
"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 _generate_sign(self, product_id: str, timestamp: int) -> str:
"""生成签名参数sign"""
params = {
"app_name": "vipshop",
"format": "json",
"productId": product_id,
"timestamp": timestamp
}
# 按key排序并拼接参数
params_str = "&".join([f"{k}={v}" for k, v in sorted(params.items(), key=lambda x: x[0])])
# 拼接签名字符串(格式:app_key + params_str + secret)
sign_str = f"{self.app_key}{params_str}{self.secret}"
return hashlib.md5(sign_str.encode()).hexdigest()
def _clean_price(self, price_str: str) -> float:
"""清洗价格(去除¥、逗号等)"""
if not price_str:
return 0.0
price_str = re.sub(r"[^\d.]", "", price_str)
return float(price_str) if price_str else 0.0
def _parse_static_data(self, html: str) -> Dict:
"""解析主页面静态数据"""
soup = BeautifulSoup(html, "lxml")
# 提取颜色选项
colors = []
for color_item in soup.select("div.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-item") if size.get("data-size")]
return {
"title": soup.select_one("h1.product-title")?.text.strip() or "",
"images": {
"main": [img.get("src") for img in soup.select("div.swiper-wrapper img") if img.get("src")],
"detail": [img.get("src") for img in soup.select("div.detail-img img") if img.get("src")]
},
"price": {
"discount_str": soup.select_one("div.discount-price")?.text.strip() or "",
"original_str": soup.select_one("div.original-price")?.text.strip() or "",
"discount_rate": soup.select_one("div.discount-tag")?.text.strip() or ""
},
"brand": {
"name": soup.select_one("div.brand-name")?.text.strip() or "",
"level": soup.select_one("div.brand-level")?.text.strip() or ""
},
"promotion": {
"tags": [tag.text.strip() for tag in soup.select("div.promotion-tag")]
},
"specs": {
"colors": colors,
"sizes": sizes
}
}
def _fetch_api_data(self, product_id: str, headers: Dict[str, str], proxy: Dict[str, str]) -> Dict:
"""调用动态API接口获取核心数据"""
api_data = {
"price": {},
"stock": {},
"sales": {},
"promotion": {}
}
try:
timestamp = int(time.time() * 1000)
sign = self._generate_sign(product_id, timestamp)
params = {
"productId": product_id,
"app_name": "vipshop",
"format": "json",
"sign": sign,
"timestamp": timestamp
}
response = requests.get(
self.api_url,
params=params,
headers=headers,
proxies=proxy,
timeout=10
)
data = response.json()
if data.get("code") == 0 and "data" in data:
result = data["data"]
# 价格信息
api_data["price"] = {
"salePrice": result.get("price", {}).get("salePrice", 0),
"marketPrice": result.get("price", {}).get("marketPrice", 0),
"vipPrice": result.get("price", {}).get("vipPrice", 0)
}
# 库存信息
api_data["stock"] = {
"skuMap": result.get("stock", {}).get("skuMap", {}), # {skuId: 库存}
"skuInfo": result.get("skuInfo", []) # [{skuId, color, size}, ...]
}
# 销量信息
api_data["sales"] = {
"salesCount": result.get("sales", {}).get("salesCount", 0),
"commentCount": result.get("sales", {}).get("commentCount", 0),
"goodRate": result.get("sales", {}).get("goodRate", 0)
}
# 促销信息
api_data["promotion"]["activities"] = result.get("promotion", {}).get("activities", [])
except Exception as e:
print(f"API数据获取失败: {str(e)}")
return api_data
def _merge_specs_and_stock(self, static_colors: List[Dict], static_sizes: List[str], sku_info: List[Dict], sku_map: Dict) -> List[Dict]:
"""合并规格(颜色+尺码)与库存"""
# 构建skuId到颜色+尺码的映射
sku_detail = {}
for sku in sku_info:
sku_id = sku.get("skuId")
if sku_id:
sku_detail[sku_id] = {
"color": sku.get("color", ""),
"size": sku.get("size", ""),
"stock": sku_map.get(sku_id, 0)
}
# 按颜色分组
color_stock = {}
for sku_id, detail in sku_detail.items():
color = detail["color"]
if color not in color_stock:
color_stock[color] = []
color_stock[color].append({
"size": detail["size"],
"stock": detail["stock"],
"available": detail["stock"] > 0
})
# 合并静态颜色列表(保证顺序)
merged_specs = []
for color in static_colors:
color_name = color["name"]
color_img = color["img"]
sizes = color_stock.get(color_name, [])
# 补充静态尺码中未在库存中出现的项(标记为0库存)
for size in static_sizes:
if not any(s["size"] == size for s in sizes):
sizes.append({
"size": size,
"stock": 0,
"available": False
})
merged_specs.append({
"color": color_name,
"color_img": color_img,
"sizes": sizes
})
return merged_specs
def item_get(self, product_id: str, timeout: int = 10) -> Dict:
"""
获取唯品会商品详情
:param product_id: 商品ID(如12345678)
:param timeout: 超时时间
:return: 标准化商品数据
"""
try:
# 1. 主页面请求
url = self.base_url.format(product_id=product_id)
headers = self._get_headers()
proxy = self._get_proxy()
# 随机延迟,避免反爬
time.sleep(random.uniform(3, 5))
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. 调用动态API获取核心数据
api_data = self._fetch_api_data(product_id, headers, proxy)
# 4. 合并规格与库存
merged_specs = self._merge_specs_and_stock(
static_data["specs"]["colors"],
static_data["specs"]["sizes"],
api_data["stock"]["skuInfo"],
api_data["stock"]["skuMap"]
)
# 5. 整合结果
result = {
"success": True,
"data": {
"product_id": product_id,** static_data,
"price": {
**static_data["price"],** api_data["price"]
},
"specs": merged_specs,
"sales": api_data["sales"],
"promotion": {
**static_data["promotion"],** api_data["promotion"]
},
"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 = "vip_access_token=xxx; userid=xxx; vip_guid=xxx"
# 初始化API客户端
api = VipItemApi(proxy_pool=PROXIES, cookie=COOKIE)
# 获取商品详情(示例product_id)
product_id = "12345678" # 替换为实际商品ID
result = api.item_get(product_id)
if result["success"]:
data = result["data"]
print(f"商品标题: {data['title']}")
print(f"价格信息: 折扣价¥{data['price']['salePrice']} | 原价¥{data['price']['marketPrice']} | {data['price']['discount_rate']}")
if data['price']['vipPrice'] > 0:
print(f"会员专享价: ¥{data['price']['vipPrice']}")
print(f"销量评价: 已售{data['sales']['salesCount']}件 | 评价{data['sales']['commentCount']}条 | 好评率{data['sales']['goodRate']}%")
print(f"促销活动: {', '.join(data['promotion']['tags'] + data['promotion']['activities'])}")
print(f"品牌信息: {data['brand']['name']} | {data['brand']['level']}")
print(f"规格与库存:")
for spec in data['specs'][:2]: # 前2个颜色
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"详情页: {data['url']}")
else:
print(f"获取失败: {result['error_msg']}(错误码: {result.get('code')})")