淘宝商品详情 API 接口逆向与 SDK 封装实践 (Python/Java/Go)

105 阅读10分钟

一、背景与目标

在电商数据分析、商品比价、价格监控等应用场景中,获取淘宝商品详情是一个常见需求。然而,淘宝并没有公开提供全面的商品详情 API,因此需要通过逆向工程的方式来获取这些数据。本文将介绍如何逆向淘宝商品详情 API 接口,并使用 Python、Java 和 Go 三种语言封装 SDK,方便开发者调用。

二、API 接口逆向分析

2.1 分析工具准备

逆向 API 接口需要使用以下工具:

  • 浏览器开发者工具(Chrome DevTools)
  • HTTP 抓包工具(Fiddler、Charles 等)
  • 代理工具(Proxifier、SwitchyOmega 等)
  • 代码编辑器(VS Code、PyCharm 等)

2.2 接口发现与分析

通过浏览器访问淘宝商品详情页,使用开发者工具捕获网络请求,分析发现以下关键接口:

  1. 商品基本信息接口h5api.m.taobao.com/h5/mtop.tao…
  2. 商品 SKU 信息接口:h5api.m.taobao.com/h5/mtop.tao…
  3. 商品价格信息接口:h5api.m.taobao.com/h5/mtop.tao…

2.3 请求参数与签名机制

分析发现,淘宝 API 请求需要以下参数:

  • apiKey:应用标识

  • t:时间戳

  • sign:签名

  • data:业务参数 JSON

签名机制是逆向的关键,经过分析,签名生成规则如下:

sign = md5(appSecret + 参数名1 + 参数值1 + ... + 参数名n + 参数值n + appSecret)

其中,参数需要按参数名升序排列。

2.4 Cookie 与防爬机制

淘宝 API 对请求头和 Cookie 有严格要求,逆向过程中需要注意:

  1. 维护有效的cookies,特别是_m_h5_tk和_m_h5_tk_enc
  2. 伪造合理的请求头,包括 User-Agent、Referer 等
  3. 处理验证码和频率限制

三、SDK 封装实现

3.1 Python SDK 实现

以下是 Python 版本的 SDK 实现,包含了商品详情获取和卖家信息获取功能:

import requests import time import hashlib import json import random import string from urllib.parse import urlencode class TaobaoAPI: def init(self, app_key, app_secret): self.app_key = app_key self.app_secret = app_secret self.base_url = "eco.taobao.com/router/rest" self.session = requests.Session() self.session.headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Accept": "application/json, text/plain, /", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" } def generate_sign(self, params): """生成API签名""" sorted_params = sorted(params.items(), key=lambda x: x[0]) sign_str = self.app_secret for k, v in sorted_params: sign_str += f"{k}{v}" sign_str += self.app_secret return hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper() def get_common_params(self, method): """获取通用请求参数""" timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=16)) params = { "app_key": self.app_key, "method": method, "format": "json", "v": "2.0", "sign_method": "md5", "timestamp": timestamp, "partner_id": "top-apitools", "nonce": nonce } return params def get_item_detail(self, num_iid): """获取商品详情""" method = "taobao.item_get" params = self.get_common_params(method) params.update({ "num_iid": num_iid, "fields": "num_iid,title,price,pic_url,detail_url,seller_id,props_name,item_img,skus,props" }) params["sign"] = self.generate_sign(params) try: response = self.session.post(self.base_url, data=params, timeout=10) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"请求异常: {e}") return {"error": str(e)} def get_seller_info(self, seller_id): """获取卖家信息""" method = "taobao.seller_get" params = self.get_common_params(method) params.update({ "seller_id": seller_id, "fields": "user_id,nick,sex,location,created,last_visit,type,alipay_account,item_num_id" }) params["sign"] = self.generate_sign(params) try: response = self.session.post(self.base_url, data=params, timeout=10) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"请求异常: {e}") return {"error": str(e)} def parse_item_detail(self, item_data): """解析商品详情数据""" if "error_response" in item_data: return {"error": item_data["error_response"]["sub_msg"]} item = item_data.get("item_get_response", {}).get("item", {}) if not item: return {"error": "未获取到商品数据"} parsed_data = { "商品ID": item.get("num_iid"), "标题": item.get("title"), "价格": item.get("price"), "主图": item.get("pic_url"), "详情页": item.get("detail_url"), "卖家ID": item.get("seller_id"), "属性": {} } # 解析商品属性 props_name = item.get("props_name", "") if props_name: for prop in props_name.split(";"): if ":" in prop and "|" in prop: _, pid, name_value = prop.split(":", 2) name, value = name_value.split("|", 1) parsed_data["属性"][name] = value # 解析SKU信息 skus = item.get("skus", {}).get("sku", []) if skus: parsed_data["SKU"] = [] for sku in skus: sku_info = { "SKU ID": sku.get("sku_id"), "属性": sku.get("properties_name"), "价格": sku.get("price"), "库存": sku.get("quantity") } parsed_data["SKU"].append(sku_info) return parsed_data # 使用示例 if name == "main": APP_KEY = "your_app_key" APP_SECRET = "your_app_secret" api = TaobaoAPI(APP_KEY, APP_SECRET) # 获取商品详情 item_id = "623456789012" # 示例商品ID item_detail = api.get_item_detail(item_id) print("原始商品详情数据:", json.dumps(item_detail, ensure_ascii=False, indent=2)) # 解析商品详情 parsed_item = api.parse_item_detail(item_detail) print("解析后的商品数据:", json.dumps(parsed_item, ensure_ascii=False, indent=2)) # 获取卖家信息 if "seller_id" in parsed_item: seller_info = api.get_seller_info(parsed_item["seller_id"]) print("卖家信息:", json.dumps(seller_info, ensure_ascii=False, indent=2))

3.2 Java SDK 实现

以下是 Java 版本的 SDK 实现:

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.; public class TaobaoAPIClient { private final String appKey; private final String appSecret; private final String baseUrl = "eco.taobao.com/router/rest"; private final Map<String, String> defaultHeaders; public TaobaoAPIClient(String appKey, String appSecret) { this.appKey = appKey; this.appSecret = appSecret; // 设置默认请求头 defaultHeaders = new HashMap<>(); defaultHeaders.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); defaultHeaders.put("Accept", "application/json, text/plain, /"); defaultHeaders.put("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); defaultHeaders.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); } /** * 生成API签名 / public String generateSign(Map<String, String> params) { // 排序参数 List<Map.Entry<String, String>> sortedParams = new ArrayList<>(params.entrySet()); sortedParams.sort(Map.Entry.comparingByKey()); // 构建签名原串 StringBuilder signBuilder = new StringBuilder(appSecret); for (Map.Entry<String, String> entry : sortedParams) { signBuilder.append(entry.getKey()).append(entry.getValue()); } signBuilder.append(appSecret); // 计算MD5 try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(signBuilder.toString().getBytes(StandardCharsets.UTF_8)); StringBuilder hexString = new StringBuilder(); for (byte b : digest) { String hex = String.format("%02x", b); hexString.append(hex); } return hexString.toString().toUpperCase(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("生成签名失败", e); } } / * 获取通用请求参数 / public Map<String, String> getCommonParams(String method) { Map<String, String> params = new HashMap<>(); params.put("app_key", appKey); params.put("method", method); params.put("format", "json"); params.put("v", "2.0"); params.put("sign_method", "md5"); // 设置时间戳 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); params.put("timestamp", dateFormat.format(new Date())); // 设置随机数 params.put("partner_id", "top-apitools"); params.put("nonce", generateRandomString(16)); return params; } /* * 生成随机字符串 / private String generateRandomString(int length) { String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; StringBuilder sb = new StringBuilder(length); Random random = new Random(); for (int i = 0; i < length; i++) { sb.append(characters.charAt(random.nextInt(characters.length()))); } return sb.toString(); } /* * 发送HTTP POST请求 / public String sendPostRequest(Map<String, String> params) throws IOException { // 添加签名 params.put("sign", generateSign(params)); // 构建请求参数 StringBuilder postData = new StringBuilder(); for (Map.Entry<String, String> param : params.entrySet()) { if (postData.length() != 0) { postData.append('&'); } postData.append(param.getKey()); postData.append('='); postData.append(java.net.URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8)); } byte[] postDataBytes = postData.toString().getBytes(StandardCharsets.UTF_8); // 发送请求 URL url = new URL(baseUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setConnectTimeout(10000); conn.setReadTimeout(10000); // 设置请求头 for (Map.Entry<String, String> header : defaultHeaders.entrySet()) { conn.setRequestProperty(header.getKey(), header.getValue()); } conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); conn.setDoOutput(true); try (OutputStream os = conn.getOutputStream()) { os.write(postDataBytes); } // 读取响应 int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { try (BufferedReader br = new BufferedReader( new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { StringBuilder response = new StringBuilder(); String responseLine; while ((responseLine = br.readLine()) != null) { response.append(responseLine.trim()); } return response.toString(); } } else { try (BufferedReader br = new BufferedReader( new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8))) { StringBuilder response = new StringBuilder(); String responseLine; while ((responseLine = br.readLine()) != null) { response.append(responseLine.trim()); } throw new IOException("HTTP请求失败,状态码: " + responseCode + ", 响应: " + response); } } } /* * 获取商品详情 / public String getItemDetail(String numIid) throws IOException { String method = "taobao.item_get"; Map<String, String> params = getCommonParams(method); params.put("num_iid", numIid); params.put("fields", "num_iid,title,price,pic_url,detail_url,seller_id,props_name,item_img,skus,props"); return sendPostRequest(params); } /* * 获取卖家信息 */ public String getSellerInfo(String sellerId) throws IOException { String method = "taobao.seller_get"; Map<String, String> params = getCommonParams(method); params.put("seller_id", sellerId); params.put("fields", "user_id,nick,sex,location,created,last_visit,type,alipay_account,item_num_id"); return sendPostRequest(params); } public static void main(String[] args) { String appKey = "your_app_key"; String appSecret = "your_app_secret"; TaobaoAPIClient client = new TaobaoAPIClient(appKey, appSecret); try { // 获取商品详情 String itemId = "623456789012"; // 示例商品ID String itemDetail = client.getItemDetail(itemId); System.out.println("原始商品详情数据: " + itemDetail); // 这里可以添加JSON解析代码来处理返回数据 // 示例:从商品详情中提取卖家ID并获取卖家信息 // 注意:实际应用中需要正确解析JSON数据 String sellerId = "12345678"; // 示例卖家ID String sellerInfo = client.getSellerInfo(sellerId); System.out.println("卖家信息: " + sellerInfo); } catch (IOException e) { e.printStackTrace(); } } }

3.3 Go SDK 实现

以下是 Go 版本的 SDK 实现:

package main import ( "crypto/md5" "encoding/json" "fmt" "io" "math/rand" "net/http" "net/url" "sort" "strings" "time" ) const ( baseURL = "eco.taobao.com/router/rest" ) // TaobaoAPI 淘宝API客户端 type TaobaoAPI struct { AppKey string AppSecret string Client *http.Client } // NewTaobaoAPI 创建新的淘宝API客户端 func NewTaobaoAPI(appKey, appSecret string) *TaobaoAPI { return &TaobaoAPI{ AppKey: appKey, AppSecret: appSecret, Client: &http.Client{ Timeout: 10 * time.Second, }, } } // generateSign 生成API签名 func (t *TaobaoAPI) generateSign(params map[string]string) string { // 排序参数 keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) // 构建签名原串 signStr := t.AppSecret for _, k := range keys { signStr += k + params[k] } signStr += t.AppSecret // 计算MD5 h := md5.New() h.Write([]byte(signStr)) return strings.ToUpper(fmt.Sprintf("%x", h.Sum(nil))) } // getCommonParams 获取通用请求参数 func (t *TaobaoAPI) getCommonParams(method string) map[string]string { params := map[string]string{ "app_key": t.AppKey, "method": method, "format": "json", "v": "2.0", "sign_method": "md5", "timestamp": time.Now().Format("2006-01-02 15:04:05"), "partner_id": "top-apitools", "nonce": randomString(16), } return params } // randomString 生成随机字符串 func randomString(length int) string { rand.Seed(time.Now().UnixNano()) chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") var b strings.Builder for i := 0; i < length; i++ { b.WriteRune(chars[rand.Intn(len(chars))]) } return b.String() } // sendRequest 发送HTTP请求 func (t *TaobaoAPI) sendRequest(params map[string]string) (map[string]interface{}, error) { // 添加签名 params["sign"] = t.generateSign(params) // 构建请求参数 data := url.Values{} for k, v := range params { data.Add(k, v) } // 发送POST请求 resp, err := t.Client.PostForm(baseURL, data) if err != nil { return nil, err } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } // 解析JSON var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析JSON失败: %v, 响应内容: %s", err, string(body)) } return result, nil } // ItemDetail 商品详情结构 type ItemDetail struct { NumIID string json:"num_iid" Title string json:"title" Price string json:"price" PicURL string json:"pic_url" DetailURL string json:"detail_url" SellerID string json:"seller_id" PropsName string json:"props_name" ItemImg []map[string]string json:"item_img" Skus map[string][]Sku json:"skus" Props []map[string]string json:"props" } // Sku SKU信息结构 type Sku struct { SkuID string json:"sku_id" PropertiesName string json:"properties_name" Price string json:"price" Quantity int json:"quantity" IsPromotion bool json:"is_promotion" PromotionPrice string json:"promotion_price" PromotionType string json:"promotion_type" PromotionId string json:"promotion_id" PromotionStart string json:"promotion_start" PromotionEnd string json:"promotion_end" IsPreSale bool json:"is_pre_sale" PreSaleDeposit string json:"pre_sale_deposit" PreSaleBalance string json:"pre_sale_balance" PreSaleStartTime string json:"pre_sale_start_time" PreSaleEndTime string json:"pre_sale_end_time" } // GetItemDetail 获取商品详情 func (t *TaobaoAPI) GetItemDetail(numIid string) (*ItemDetail, error) { method := "taobao.item_get" params := t.getCommonParams(method) params["num_iid"] = numIid params["fields"] = "num_iid,title,price,pic_url,detail_url,seller_id,props_name,item_img,skus,props" result, err := t.sendRequest(params) if err != nil { return nil, err } // 检查是否有错误 if errorResp, exists := result["error_response"]; exists { errorMap := errorResp.(map[string]interface{}) return nil, fmt.Errorf("API错误: %v", errorMap) } // 解析商品详情 itemGetResponse, exists := result["item_get_response"] if !exists { return nil, fmt.Errorf("未找到商品详情数据") } itemMap, ok := itemGetResponse.(map[string]interface{})["item"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("解析商品详情失败") } // 转换为JSON字符串再解析到结构体 itemJSON, err := json.Marshal(itemMap) if err != nil { return nil, err } var itemDetail ItemDetail if err := json.Unmarshal(itemJSON, &itemDetail); err != nil { return nil, err } return &itemDetail, nil } // SellerInfo 卖家信息结构 type SellerInfo struct { UserID string json:"user_id" Nick string json:"nick" Sex string json:"sex" Location string json:"location" Created string json:"created" LastVisit string json:"last_visit" Type string json:"type" AlipayAccount string json:"alipay_account" ItemNumID string json:"item_num_id" } // GetSellerInfo 获取卖家信息 func (t *TaobaoAPI) GetSellerInfo(sellerID string) (*SellerInfo, error) { method := "taobao.seller_get" params := t.getCommonParams(method) params["seller_id"] = sellerID params["fields"] = "user_id,nick,sex,location,created,last_visit,type,alipay_account,item_num_id" result, err := t.sendRequest(params) if err != nil { return nil, err } // 检查是否有错误 if errorResp, exists := result["error_response"]; exists { errorMap := errorResp.(map[string]interface{}) return nil, fmt.Errorf("API错误: %v", errorMap) } // 解析卖家信息 sellerGetResponse, exists := result["seller_get_response"] if !exists { return nil, fmt.Errorf("未找到卖家信息数据") } sellerMap, ok := sellerGetResponse.(map[string]interface{})["seller"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("解析卖家信息失败") } // 转换为JSON字符串再解析到结构体 sellerJSON, err := json.Marshal(sellerMap) if err != nil { return nil, err } var sellerInfo SellerInfo if err := json.Unmarshal(sellerJSON, &sellerInfo); err != nil { return nil, err } return &sellerInfo, nil } func main() { appKey := "your_app_key" appSecret := "your_app_secret" api := NewTaobaoAPI(appKey, appSecret) // 获取商品详情 itemID := "623456789012" // 示例商品ID itemDetail, err := api.GetItemDetail(itemID) if err != nil { fmt.Printf("获取商品详情失败: %v\n", err) return } // 打印商品详情 itemJSON, _ := json.MarshalIndent(itemDetail, "", " ") fmt.Printf("商品详情: %s\n", itemJSON) // 获取卖家信息 if itemDetail.SellerID != "" { sellerInfo, err := api.GetSellerInfo(itemDetail.SellerID) if err != nil { fmt.Printf("获取卖家信息失败: %v\n", err) return } // 打印卖家信息 sellerJSON, _ := json.MarshalIndent(sellerInfo, "", " ") fmt.Printf("卖家信息: %s\n", sellerJSON) } }

四、SDK 使用示例

以下是使用 Python SDK 的示例代码:

if name == "main": APP_KEY = "your_app_key" APP_SECRET = "your_app_secret" api = TaobaoAPI(APP_KEY, APP_SECRET) # 获取商品详情 item_id = "623456789012" # 示例商品ID item_detail = api.get_item_detail(item_id) print("原始商品详情数据:", json.dumps(item_detail, ensure_ascii=False, indent=2)) # 解析商品详情 parsed_item = api.parse_item_detail(item_detail) print("解析后的商品数据:", json.dumps(parsed_item, ensure_ascii=False, indent=2)) # 获取卖家信息 if "seller_id" in parsed_item: seller_info = api.get_seller_info(parsed_item["seller_id"]) print("卖家信息:", json.dumps(seller_info, ensure_ascii=False, indent=2))

五、注意事项与常见问题

  1. API 权限问题:部分淘宝 API 需要申请相应权限才能使用,请确保你的应用已获得授权。
  2. 频率限制:淘宝 API 有请求频率限制,建议合理控制请求频率,避免被封禁。
  3. 签名验证:签名生成规则可能会随时间变化,如遇到签名错误,需要重新分析签名算法。
  4. Cookie 维护:部分 API 需要有效的 Cookie 才能正常工作,需要定期更新 Cookie。
  5. 数据解析:淘宝返回的数据结构可能会变化,需要做好异常处理和版本兼容。

六、总结

通过逆向工程和 SDK 封装,我们可以方便地获取淘宝商品详情数据。本文提供了 Python、Java 和 Go 三种语言的实现方案,开发者可以根据自己的需求选择合适的版本。在实际应用中,需要注意遵守淘宝的使用条款,合理使用 API,避免对其系统造成不必要的负担。