Python 实现苏宁商品详情 API 调用:请求构造、响应解析与异常处理

苏宁开放平台(苏宁开发者中心)的商品详情 API 是获取商品基础信息、价格、库存、规格等核心数据的核心接口,其调用需遵循「签名验证 + 固定请求格式」规范,且需处理网络异常、签名错误、字段缺失等问题。本文基于 Python 实现全流程调用方案,覆盖请求构造、响应解析、异常处理全链路,确保调用稳定合规。

一、核心前提(苏宁开放平台准备)

1. 授权与密钥获取

  • 注册苏宁开放平台开发者账号,创建应用并审核通过;
  • 获取核心参数:appKey(应用密钥)、appSecret(应用秘钥)、accessToken(若需用户授权,公共接口可省略);
  • 确认商品详情 API 端点:苏宁商品详情 API(公共版)端点为 https://open.suning.com/api/http/sopRequest(需按接口文档确认最新地址)。

2. 核心规则

  • 请求方式:POST/GET(苏宁 API 多为 POST,需携带签名);
  • 签名规则:基于appKey+appSecret+ 请求参数 + 时间戳生成 MD5/SHA256 签名(苏宁默认 MD5);
  • 响应格式:JSON,包含code(状态码)、msg(提示信息)、body(核心数据);
  • 限流规则:默认 100 次 / 分钟,超限返回 429 错误。

二、完整实现代码

1. 环境依赖

bash

运行

pip install requests  # HTTP请求
pip install hashlib   # 签名生成(Python内置,无需额外安装)
pip install time      # 时间戳生成(内置)

2. 核心代码(含请求构造、解析、异常处理)

python

运行

import requests
import hashlib
import time
import json
from typing import Dict, Optional, Any
import logging

# -------------------------- 基础配置与日志 --------------------------
# 日志配置(记录调用日志与异常)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[logging.FileHandler("suning_api.log"), logging.StreamHandler()]
)
logger = logging.getLogger(__name__)

# 苏宁API核心参数(替换为你的真实信息)
APP_KEY = "你的appKey"
APP_SECRET = "你的appSecret"
API_URL = "https://open.suning.com/api/http/sopRequest"  # 商品详情API端点
TIMEOUT = 15  # 请求超时时间(秒)
RETRY_TIMES = 3  # 异常重试次数
RATE_LIMIT_DELAY = 0.6  # 限流延迟(100次/分钟=0.6秒/次)

# -------------------------- 签名生成工具函数 --------------------------
def generate_sign(params: Dict, app_secret: str) -> str:
    """
    生成苏宁API签名(按苏宁规则:参数排序+拼接秘钥+MD5加密)
    :param params: 请求参数(不含sign)
    :param app_secret: 应用秘钥appSecret
    :return: 签名字符串
    """
    # 1. 按参数名ASCII升序排序
    sorted_params = sorted(params.items(), key=lambda x: x[0])
    # 2. 拼接参数为 "key1=value1key2=value2" 格式
    param_str = "".join([f"{k}{v}" for k, v in sorted_params])
    # 3. 首尾拼接appSecret
    sign_str = f"{app_secret}{param_str}{app_secret}"
    # 4. MD5加密并转大写
    md5 = hashlib.md5()
    md5.update(sign_str.encode("utf-8"))
    sign = md5.hexdigest().upper()
    return sign

# -------------------------- 安全字段提取函数 --------------------------
def safe_extract_field(data: Dict, field_path: str, default: Any = None) -> Any:
    """
    安全提取嵌套JSON字段(处理字段缺失/类型不匹配)
    :param data: 解析后的JSON字典
    :param field_path: 字段路径,如"body.productInfo.productName"
    :param default: 字段缺失时的默认值
    :return: 提取的字段值
    """
    keys = field_path.split(".")
    value = data
    for key in keys:
        if isinstance(value, dict) and key in value:
            value = value[key]
        else:
            logger.debug(f"字段 {field_path} 缺失,返回默认值:{default}")
            return default
    return value

# -------------------------- 苏宁API请求构造与调用 --------------------------
def build_suning_request_params(product_id: str) -> Dict:
    """
    构造苏宁商品详情API请求参数(含签名)
    :param product_id: 苏宁商品ID(如"123456789")
    :return: 完整请求参数(含签名)
    """
    # 基础参数(苏宁API必传)
    params = {
        "appKey": APP_KEY,
        "timestamp": str(int(time.time() * 1000)),  # 毫秒级时间戳
        "format": "json",  # 响应格式
        "version": "v1.0",  # API版本
        "method": "suning.custom.product.get",  # 商品详情API方法名(需确认最新方法名)
        "productCode": product_id,  # 商品ID参数(按接口文档调整参数名)
        "signMethod": "md5"  # 签名方式
    }
    # 生成签名并添加到参数
    params["sign"] = generate_sign(params, APP_SECRET)
    return params

def call_suning_product_api(product_id: str) -> Optional[Dict]:
    """
    调用苏宁商品详情API,处理重试、限流、异常
    :param product_id: 苏宁商品ID
    :return: 解析后的商品数据字典(失败返回None)
    """
    # 限流延迟(避免超限)
    time.sleep(RATE_LIMIT_DELAY)
    
    for retry in range(RETRY_TIMES):
        try:
            # 1. 构造请求参数(含签名)
            request_params = build_suning_request_params(product_id)
            logger.info(f"开始调用苏宁API,商品ID:{product_id},重试次数:{retry+1}")
            
            # 2. 发送POST请求(苏宁API多为POST,部分为GET,需按文档调整)
            response = requests.post(
                API_URL,
                data=request_params,
                timeout=TIMEOUT,
                headers={"Content-Type": "application/x-www-form-urlencoded"}
            )
            # 触发HTTP错误(如404、500、429)
            response.raise_for_status()
            
            # 3. 解析响应JSON
            response_json = response.json()
            logger.debug(f"API响应:{json.dumps(response_json, ensure_ascii=False)[:500]}...")
            
            # 4. 校验业务状态码
            code = safe_extract_field(response_json, "code", "")
            msg = safe_extract_field(response_json, "msg", "未知错误")
            
            if code == "0000":  # 苏宁API成功状态码(以文档为准)
                logger.info(f"商品 {product_id} API调用成功")
                return response_json
            else:
                logger.error(f"商品 {product_id} 业务错误:{code} - {msg}")
                return None
            
        except requests.exceptions.Timeout:
            logger.error(f"商品 {product_id} 请求超时(重试{retry+1}/{RETRY_TIMES})")
            if retry < RETRY_TIMES - 1:
                time.sleep(2 ** retry)  # 指数退避重试
            continue
        
        except requests.exceptions.ConnectionError:
            logger.error(f"商品 {product_id} 连接失败(重试{retry+1}/{RETRY_TIMES})")
            if retry < RETRY_TIMES - 1:
                time.sleep(2 ** retry)
            continue
        
        except requests.exceptions.HTTPError as e:
            # 处理特定HTTP状态码
            if response.status_code == 429:
                logger.warning(f"商品 {product_id} 限流(429),休眠60秒重试")
                time.sleep(60)
                continue
            elif response.status_code == 401:
                logger.error(f"商品 {product_id} 签名错误/权限不足(401):{e}")
                return None  # 签名错误无需重试
            elif response.status_code == 404:
                logger.error(f"商品 {product_id} 不存在(404)")
                return None
            else:
                logger.error(f"商品 {product_id} HTTP错误:{e}")
                if retry < RETRY_TIMES - 1:
                    time.sleep(2 ** retry)
                continue
        
        except json.JSONDecodeError:
            logger.error(f"商品 {product_id} 响应JSON解析失败:{response.text[:200]}...")
            return None
        
        except Exception as e:
            logger.error(f"商品 {product_id} 未知异常(重试{retry+1}/{RETRY_TIMES}):{str(e)}", exc_info=True)
            if retry < RETRY_TIMES - 1:
                time.sleep(2 ** retry)
            continue
    
    logger.error(f"商品 {product_id} 调用失败(重试{RETRY_TIMES}次后)")
    return None

# -------------------------- 商品数据解析(核心字段提取) --------------------------
def parse_suning_product_data(raw_data: Dict) -> Dict:
    """
    解析苏宁商品详情API响应,提取核心字段
    :param raw_data: API返回的原始JSON字典
    :return: 结构化的商品数据
    """
    parsed_data = {
        # 基础信息
        "商品ID": safe_extract_field(raw_data, "body.productInfo.productCode", "未知ID"),
        "商品名称": safe_extract_field(raw_data, "body.productInfo.productName", "无标题"),
        "品牌": safe_extract_field(raw_data, "body.productInfo.brandName", "未知品牌"),
        "类目ID": safe_extract_field(raw_data, "body.productInfo.categoryCode", 0),
        "类目名称": safe_extract_field(raw_data, "body.productInfo.categoryName", "未知类目"),
        
        # 价格信息
        "售价(元)": safe_extract_field(raw_data, "body.priceInfo.salePrice", 0.0),
        "原价(元)": safe_extract_field(raw_data, "body.priceInfo.originalPrice", 0.0),
        "会员价(元)": safe_extract_field(raw_data, "body.priceInfo.memberPrice", 0.0),
        
        # 库存信息
        "库存数量": safe_extract_field(raw_data, "body.stockInfo.stockCount", 0),
        "库存状态": safe_extract_field(raw_data, "body.stockInfo.stockStatus", "未知"),
        
        # 规格信息
        "主图URL": safe_extract_field(raw_data, "body.productInfo.mainImageUrl", ""),
        "规格列表": safe_extract_field(raw_data, "body.skuInfo.skuList", [])
    }
    
    # 解析规格列表(格式化SKU信息)
    sku_list = []
    for sku in parsed_data["规格列表"]:
        if isinstance(sku, dict):
            sku_info = {
                "SKU ID": safe_extract_field(sku, "skuCode", ""),
                "规格属性": safe_extract_field(sku, "skuAttr", ""),
                "SKU售价": safe_extract_field(sku, "salePrice", 0.0),
                "SKU库存": safe_extract_field(sku, "stockCount", 0)
            }
            sku_list.append(sku_info)
    parsed_data["规格列表"] = sku_list
    
    logger.info(f"商品 {parsed_data['商品ID']} 解析完成")
    return parsed_data

# -------------------------- 主函数(测试调用) --------------------------
if __name__ == "__main__":
    # 目标商品ID(替换为真实苏宁商品ID)
    target_product_id = "123456789"
    
    # 1. 调用API获取原始数据
    raw_api_data = call_suning_product_api(target_product_id)
    if not raw_api_data:
        logger.critical("API调用失败,终止解析")
        exit(1)
    
    # 2. 解析核心字段
    product_data = parse_suning_product_data(raw_api_data)
    
    # 3. 打印解析结果
    print("\n===== 苏宁商品详情解析结果 =====")
    print(f"商品ID:{product_data['商品ID']}")
    print(f"商品名称:{product_data['商品名称']}")
    print(f"品牌:{product_data['品牌']}")
    print(f"售价:{product_data['售价(元)']} 元(原价:{product_data['原价(元)']} 元)")
    print(f"库存:{product_data['库存数量']} 件(状态:{product_data['库存状态']})")
    print(f"主图URL:{product_data['主图URL']}")
    
    print("\n===== 规格SKU列表 =====")
    for idx, sku in enumerate(product_data["规格列表"], 1):
        print(f"\nSKU {idx}:")
        print(f"  SKU ID:{sku['SKU ID']}")
        print(f"  规格属性:{sku['规格属性']}")
        print(f"  售价:{sku['SKU售价']} 元")
        print(f"  库存:{sku['SKU库存']} 件")

三、核心流程拆解

1. 请求构造(签名是核心)

苏宁 API 的核心风控是签名验证,步骤如下:

  1. 构造基础参数(appKey、时间戳、API 方法名、商品 ID 等);
  2. 按参数名 ASCII 升序排序(如appKeyformatmethod);
  3. 拼接参数为key1=value1key2=value2格式;
  4. 首尾拼接appSecret,生成 MD5 并转大写,得到sign
  5. sign加入参数,作为最终请求参数。

注意:参数名 / 值不可遗漏,时间戳需为毫秒级,否则签名验证失败(返回 401 错误)。

2. 响应解析(结构化提取核心字段)

苏宁商品详情 API 响应结构示例:

json

{
  "code": "0000",
  "msg": "success",
  "body": {
    "productInfo": {
      "productCode": "123456789",
      "productName": "苏宁自营 小米14 5G手机",
      "brandName": "小米",
      "categoryCode": "10001",
      "mainImageUrl": "https://img10.suning.cn/..."
    },
    "priceInfo": {
      "salePrice": 3999.0,
      "originalPrice": 4299.0,
      "memberPrice": 3949.0
    },
    "stockInfo": {
      "stockCount": 500,
      "stockStatus": "有货"
    },
    "skuInfo": {
      "skuList": [
        {"skuCode": "SKU123", "skuAttr": "黑色+12GB+256GB", "salePrice": 3999.0, "stockCount": 200}
      ]
    }
  }
}

通过safe_extract_field函数逐层提取字段,避免因字段缺失导致KeyError中断程序。

3. 异常处理(全链路容错)

异常类型 处理策略
签名错误(401) 校验appKey/appSecret是否正确,参数排序 / 拼接是否符合规则,无需重试
限流(429) 休眠 60 秒后重试,或降低请求频率(如 0.8 秒 / 次)
超时 / 连接失败 指数退避重试(1s→2s→4s),最多 3 次
商品不存在(404) 记录无效商品 ID,直接返回 None
JSON 解析失败 记录原始响应片段,返回 None
业务错误(code≠0000) 打印错误码 + 提示信息,返回 None

四、避坑指南与优化建议

1. 常见坑点解决

  • 签名验证失败
    • 检查参数名是否与文档一致(如productCode vs productId);
    • 时间戳需为毫秒级(int(time.time()*1000)),而非秒级;
    • 拼接参数时不可加多余符号(如 &、=),严格按key=value拼接。
  • 字段缺失:部分商品无会员价 / 规格,需设置默认值(如 0.0、空列表);
  • 限流触发:严格遵守 100 次 / 分钟规则,批量调用时添加RATE_LIMIT_DELAY
  • 响应编码:苏宁 API 响应为 UTF-8,无需额外转码,避免乱码。

2. 性能 / 稳定性优化

  • 批量调用优化:将商品 ID 按批次拆分,每批调用间隔 0.6 秒,避免超限;
  • Token 缓存(如需):若 API 需accessToken,缓存 Token(有效期 2 小时),避免重复获取;
  • 断点续采:记录已调用的商品 ID,程序中断后从断点继续;
  • 日志脱敏:日志中隐藏appSecret/accessToken,仅保留商品 ID、错误码等非敏感信息;
  • 异步调用(进阶):使用aiohttp替代requests,实现异步批量调用,提升采集效率。

3. 合规要求

  • 仅采集自有业务所需数据,禁止滥用 / 转售;
  • 遵守苏宁开放平台的《开发者协议》,不高频恶意调用;
  • 数据留存不超过业务必需周期,定期清理过期数据。

五、总结

Python 调用苏宁商品详情 API 的核心是「合规构造请求(签名)+ 结构化解析响应 + 全链路异常容错」:

  1. 签名是基础:严格按苏宁规则生成签名,避免 401 错误;
  2. 解析是核心:通过安全字段提取函数,处理字段缺失 / 类型不匹配;
  3. 异常是保障:针对不同异常场景(限流、超时、业务错误)制定差异化处理策略,确保调用稳定。

该方案可直接适配单商品调用、批量采集等场景,兼顾合规性与稳定性,适合电商数据监控、库存同步等业务需求。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐