|
数据类别 |
包含字段 |
|
基础信息 |
商品 ID(num_iid)、标题、主图 URL、卖家昵称、店铺 ID、商品类目 |
|
价格信息 |
基础售价、优惠价、价格单位、是否支持优惠券 |
|
库存与规格 |
总库存、SKU 列表(含 SKU ID、规格名称、SKU 价格、SKU 库存)、规格属性组 |
|
物流与服务 |
发货地、运费模板、是否包邮、售后服务类型 |
|
详情内容 |
商品详情页 HTML、卖点描述、包装清单 |
import hashlibimport timefrom urllib.parse import urlencodeclass TaobaoSignUtil:"""淘宝开放平台签名工具类"""def __init__(self, app_key, app_secret):self.app_key = app_keyself.app_secret = app_secret# 公共参数(所有接口通用)self.common_params = {"app_key": self.app_key,"format": "json", # 响应格式:json/xml,推荐json"v": "2.0", # API版本,固定2.0"sign_method": "md5", # 签名方式,固定md5"timestamp": "" # 时间戳,需实时生成(格式:YYYYMMDDHHMMSS)}def generate_timestamp(self):"""生成符合要求的时间戳(YYYYMMDDHHMMSS)"""return time.strftime("%Y%m%d%H%M%S", time.localtime())def generate_sign(self, params):"""生成签名:param params: 接口私有参数(如num_iid):return: 签名后的字符串与完整请求参数"""# 1. 合并公共参数与私有参数all_params = self.common_params.copy()all_params.update(params)# 2. 补充实时时间戳all_params["timestamp"] = self.generate_timestamp()# 3. 按参数名ASCII升序排序sorted_params = sorted(all_params.items(), key=lambda x: x[0])# 4. 拼接参数字符串(key=value)param_str = "&".join([f"{k}={v}" for k, v in sorted_params])# 5. 拼接AppSecret并MD5加密(32位大写)sign_str = param_str + self.app_secretsign = hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper()# 6. 添加签名到参数中all_params["sign"] = signreturn all_paramsimport requestsimport timefrom threading import Lockclass TaobaoItemClient:"""淘宝商品详情接口请求客户端"""def __init__(self, app_key, app_secret, timeout=10, max_retries=2, qps=10):self.sign_util = TaobaoSignUtil(app_key, app_secret)self.timeout = timeout # 请求超时时间(秒)self.max_retries = max_retries # 最大重试次数self.qps = qps # 每秒最大请求数self.last_request_time = 0self.request_lock = Lock() # 线程锁控制QPSdef _control_qps(self):"""QPS限流,避免超限被封禁"""with self.request_lock:current_time = time.time()# 计算最小请求间隔(秒)min_interval = 1.0 / self.qpsif current_time - self.last_request_time < min_interval:# 休眠至满足间隔sleep_time = min_interval - (current_time - self.last_request_time)time.sleep(sleep_time)self.last_request_time = current_timedef get_item_detail(self, num_iid, fields=None):"""获取商品详情:param num_iid: 商品ID(淘宝商品num_iid,如678901234567):param fields: 需返回的字段列表(默认返回所有核心字段):return: 结构化商品详情数据(字典)"""# 1. 构造接口私有参数params = {"num_iid": num_iid}# 可选:指定返回字段(减少数据传输量)if fields and isinstance(fields, list):params["fields"] = ",".join(fields)# 2. 生成签名与完整参数request_params = self.sign_util.generate_sign(params)# 3. QPS限流self._control_qps()# 4. 发送请求(带重试机制)api_url = "https://gw.api.taobao.com/router/rest" # 淘宝开放平台网关地址retry_count = 0while retry_count < self.max_retries:try:response = requests.get(api_url,params=request_params,headers={"User-Agent": "TaobaoItemClient/1.0"},timeout=self.timeout)response.raise_for_status() # 捕获4xx/5xx错误result = response.json()# 5. 处理返回结果(判断是否成功)if "error_response" in result:error_msg = result["error_response"]["msg"]error_code = result["error_response"]["code"]print(f"请求失败({error_code}):{error_msg}")# 若为签名错误,直接返回(重试无效)if error_code in [15, 16]: # 15=签名错误,16=参数错误return Noneretry_count += 1print(f"第{retry_count}次重试...")time.sleep(1) # 重试前休眠1秒else:# 返回商品详情数据(接口返回在"item_get_response"中)return result["item_get_response"]["item"]except requests.exceptions.RequestException as e:print(f"请求异常:{str(e)}")retry_count += 1print(f"第{retry_count}次重试...")time.sleep(1)print(f"超过最大重试次数({self.max_retries}次),请求失败")return Noneimport refrom datetime import datetimeclass TaobaoItemParser:"""淘宝商品详情数据解析工具类"""@staticmethoddef parse_base_info(item_data):"""解析商品基础信息"""if not item_data:return None# 价格处理:分转元(淘宝返回价格单位为分)price = float(item_data.get("price", 0)) / 100.0promo_price = float(item_data.get("promo_price", 0)) / 100.0 if item_data.get("promo_price") else pricereturn {"num_iid": item_data.get("num_iid", ""), # 商品ID"title": item_data.get("title", ""), # 商品标题"seller_nick": item_data.get("nick", ""), # 卖家昵称"shop_id": item_data.get("shop_id", ""), # 店铺ID"category_id": item_data.get("cid", ""), # 商品类目ID"main_image": item_data.get("pic_url", ""),# 主图URL"price": round(price, 2), # 基础售价(元)"promo_price": round(promo_price, 2), # 优惠价(元)"create_time": item_data.get("created", ""), # 商品创建时间"update_time": item_data.get("modified", ""),# 商品更新时间"sales_count": int(item_data.get("sales", 0)) # 销量}@staticmethoddef parse_sku(item_data):"""解析SKU信息(含规格与库存)"""sku_list = item_data.get("sku", [])if not sku_list:return {"has_sku": False, "sku_list": []}parsed_skus = []# 解析规格属性组(如颜色、尺寸)props_name = item_data.get("props_name", "") # 格式:"1627207:3232483:颜色:红色;20509:28315:尺寸:M"props_map = {}if props_name:props_parts = props_name.split(";")for part in props_parts:if ":" in part:parts = part.split(":")if len(parts) >= 4:prop_id = parts[0] + ":" + parts[1] # 规格ID(如1627207:3232483)prop_name = parts[3] # 规格名称(如红色)props_map[prop_id] = prop_name# 解析每个SKUfor sku in sku_list:# 处理SKU规格(如{"1627207":"3232483","20509":"28315"} → 颜色:红色,尺寸:M)sku_props = sku.get("props", {})sku_spec = []for prop_id, prop_val_id in sku_props.items():full_prop_id = f"{prop_id}:{prop_val_id}"if full_prop_id in props_map:sku_spec.append(props_map[full_prop_id])else:sku_spec.append(f"{prop_id}:{prop_val_id}")# SKU价格处理(分转元)sku_price = float(sku.get("price", 0)) / 100.0sku_stock = int(sku.get("stock", 0))parsed_skus.append({"sku_id": sku.get("sku_id", ""), # SKU唯一ID"spec": " | ".join(sku_spec), # 规格描述(如红色 | M)"price": round(sku_price, 2), # SKU售价(元)"stock": sku_stock, # SKU库存"image": sku.get("spec_img", ""), # SKU图片URL"sales_count": int(sku.get("sales", 0))# SKU销量})return {"has_sku": len(parsed_skus) > 0,"sku_list": parsed_skus,"total_stock": sum([sku["stock"] for sku in parsed_skus]) # 总库存}@staticmethoddef parse_logistics(item_data):"""解析物流与服务信息"""return {"location": item_data.get("location", ""), # 发货地(如浙江杭州)"is_free_shipping": item_data.get("freight_payer", "") == "seller", # 是否包邮"shipping_fee": float(item_data.get("shipping_fee", 0)) / 100.0, # 运费(元)"service_type": item_data.get("service_type", ""), # 服务类型(如七天无理由)"delivery_time": item_data.get("delivery_time", "") # 发货时间承诺}@staticmethoddef parse_detail(item_data):"""解析商品详情内容(处理HTML与卖点)"""# 提取详情页纯文本(去除HTML标签)detail_html = item_data.get("desc", "")detail_text = re.sub(r"<.*?>", "", detail_html) if detail_html else ""# 解析卖点(如["包邮","七天无理由"])sell_points = item_data.get("sell_point", "").split("|") if item_data.get("sell_point") else []return {"detail_html": detail_html, # 详情页HTML"detail_text": detail_text[:500] + "..." if len(detail_text) > 500 else detail_text, # 简化纯文本"sell_points": [p.strip() for p in sell_points if p.strip()], # 卖点列表"package_list": item_data.get("package_list", "") # 包装清单}@staticmethoddef parse_full_item(item_data):"""解析完整商品信息(整合所有维度)"""if not item_data:return Nonereturn {"base_info": TaobaoItemParser.parse_base_info(item_data),"sku_info": TaobaoItemParser.parse_sku(item_data),"logistics_info": TaobaoItemParser.parse_logistics(item_data),"detail_info": TaobaoItemParser.parse_detail(item_data),"parse_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 解析时间}def single_item_demo():# 1. 替换为自己的淘宝开放平台凭证(从开放平台获取)APP_KEY = "your_app_key"APP_SECRET = "your_app_secret"# 目标商品ID(示例:淘宝商品num_iid,可从商品详情页URL获取)TARGET_NUM_IID = "678901234567"# 2. 初始化客户端与解析器item_client = TaobaoItemClient(APP_KEY, APP_SECRET, timeout=10, max_retries=2)item_parser = TaobaoItemParser()try:# 3. 调用接口获取原始数据print("=== 开始获取商品详情 ===")raw_item_data = item_client.get_item_detail(TARGET_NUM_IID)if not raw_item_data:print("获取商品详情失败")return# 4. 解析商品数据print("=== 开始解析商品数据 ===")full_item_info = item_parser.parse_full_item(raw_item_data)# 5. 输出结果(关键信息)print("\n=== 商品核心信息 ===")base_info = full_item_info["base_info"]print(f"商品ID:{base_info['num_iid']}")print(f"标题:{base_info['title']}")print(f"售价:¥{base_info['price']}(优惠价:¥{base_info['promo_price']})")print(f"卖家:{base_info['seller_nick']}(店铺ID:{base_info['shop_id']})")print(f"销量:{base_info['sales_count']}件")print(f"发货地:{full_item_info['logistics_info']['location']}")print(f"是否包邮:{'是' if full_item_info['logistics_info']['is_free_shipping'] else '否'}")# 输出SKU信息(若有)sku_info = full_item_info["sku_info"]if sku_info["has_sku"]:print(f"\n=== SKU信息(共{len(sku_info['sku_list'])}个规格) ===")for i, sku in enumerate(sku_info["sku_list"], 1):print(f"{i}. 规格:{sku['spec']} | 价格:¥{sku['price']} | 库存:{sku['stock']}件")except Exception as e:print(f"操作失败:{str(e)}")if __name__ == "__main__":single_item_demo()import csvimport osfrom datetime import datetimedef batch_item_demo():# 1. 配置信息APP_KEY = "your_app_key"APP_SECRET = "your_app_secret"# 批量商品ID列表(从文件读取或手动输入)BATCH_NUM_IIDS = ["678901234567", "678901234568", "678901234569"]# 导出CSV路径OUTPUT_DIR = "./taobao_item_data"os.makedirs(OUTPUT_DIR, exist_ok=True)OUTPUT_CSV = os.path.join(OUTPUT_DIR, f"taobao_batch_items_{datetime.now().strftime('%Y%m%d%H%M%S')}.csv")# 2. 初始化客户端与解析器item_client = TaobaoItemClient(APP_KEY, APP_SECRET, qps=10) # 按权限设置QPSitem_parser = TaobaoItemParser()# 3. 批量获取与解析batch_result = []print(f"=== 开始批量获取(共{len(BATCH_NUM_IIDS)}个商品) ===")for num_iid in BATCH_NUM_IIDS:try:print(f"正在处理商品ID:{num_iid}")raw_data = item_client.get_item_detail(num_iid)if not raw_data:print(f"商品{num_iid}获取失败,跳过")continueparsed_data = item_parser.parse_full_item(raw_data)batch_result.append(parsed_data)except Exception as e:print(f"商品{num_iid}处理异常:{str(e)},跳过")continue# 4. 导出CSVif not batch_result:print("无有效商品数据,无需导出")return# 定义CSV表头csv_headers = ["商品ID", "标题", "卖家昵称", "店铺ID", "主图URL","基础售价(元)", "优惠价(元)", "销量", "发货地", "是否包邮","总库存", "解析时间", "卖点"]with open(OUTPUT_CSV, "w", encoding="utf-8-sig", newline="") as f:writer = csv.writer(f)writer.writerow(csv_headers)for item in batch_result:base = item["base_info"]logistics = item["logistics_info"]sku = item["sku_info"]sell_points = "|".join(item["detail_info"]["sell_points"])writer.writerow([base["num_iid"],base["title"],base["seller_nick"],base["shop_id"],base["main_image"],base["price"],base["promo_price"],base["sales_count"],logistics["location"],"是" if logistics["is_free_shipping"] else "否",sku["total_stock"],item["parse_time"],sell_points])print(f"\n=== 批量处理完成 ===")print(f"成功获取:{len(batch_result)}/{len(BATCH_NUM_IIDS)}个商品")print(f"数据已导出至:{OUTPUT_CSV}")if __name__ == "__main__":batch_item_demo()|
问题原因 |
现象描述 |
解决方案 |
|
参数排序错误 |
返回invalid-sign,错误码 15 |
严格按参数名 ASCII 升序排序,可使用sorted(params.items(), key=lambda x: x[0]) |
|
时间戳偏差过大 |
返回invalid-timestamp,错误码 16 |
确保时间戳格式为YYYYMMDDHHMMSS,且与淘宝服务器时间差不超过 10 分钟 |
|
AppSecret 泄露或错误 |
签名始终失败,换账号后正常 |
重新生成 AppSecret,删除代码中硬编码的密钥,通过环境变量或配置文件读取 |
def incremental_get_items(client, parser, last_update_time, num_iids):"""增量获取商品:仅更新有变化的商品"""incremental_result = []for num_iid in num_iids:raw_data = client.get_item_detail(num_iid)if not raw_data:continueitem_update_time = raw_data.get("modified", "")if item_update_time > last_update_time:parsed = parser.parse_full_item(raw_data)incremental_result.append(parsed)return incremental_result
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70047598/viewspace-3096273/ ,如需转载,请注明出处,否则将追究法律责任。