基于 Python 的 Cdiscount 商品详情 API 调用与 JSON 核心字段解析(含多规格 SKU 提取)

Cdiscount 是法国头部电商平台,其开放 API(需通过官方开发者平台授权)提供商品详情、库存、定价等核心数据,返回格式为 JSON。本文聚焦「合规调用商品详情 API + 解析核心字段 + 提取多规格 SKU」,给出完整 Python 实现方案,兼顾实用性与容错性。

一、核心前提(合规与授权)

  1. 授权准备
    • 在 Cdiscount 开发者平台注册账号,创建应用,获取 Client IDClient Secret
    • 通过 OAuth 2.0 协议获取 Access Token(有效期通常 1 小时),所有 API 请求需携带 Token。
  2. API 基础信息
    • 商品详情 API 端点:https://api.cdiscount.com/OpenApi/json/GetProduct
    • 请求方式:POST(需传入商品 ID / 参考 SKU);
    • 频率限制:默认 100 次 / 分钟,超量触发 429 错误。

二、完整实现代码(含多规格 SKU 提取)

1. 环境依赖

bash

运行

复制代码
pip install requests  # 发送HTTP请求
pip install python-dotenv  # 可选,存储敏感信息(避免硬编码)

2. 核心代码(含注释)

python

运行

复制代码
import requests
import json
import time
from typing import Dict, List, Optional
import logging

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

# 敏感信息(建议通过.env文件读取,避免硬编码)
CLIENT_ID = "你的Client ID"
CLIENT_SECRET = "你的Client Secret"
TOKEN_URL = "https://api.cdiscount.com/OpenApi/json/AccessToken"  # Token获取地址
PRODUCT_API_URL = "https://api.cdiscount.com/OpenApi/json/GetProduct"  # 商品详情API

# 超时与限流配置
TIMEOUT = 15  # 请求超时时间(秒)
RATE_LIMIT_DELAY = 0.6  # 调用间隔(避免超限,100次/分钟=0.6秒/次)
RETRY_TIMES = 3  # 异常重试次数

# -------------------------- 通用工具函数 --------------------------
def get_access_token() -> Optional[str]:
    """获取Cdiscount Access Token(OAuth 2.0授权)"""
    payload = {
        "parameters": {
            "Login": CLIENT_ID,
            "Password": CLIENT_SECRET
        }
    }
    for retry in range(RETRY_TIMES):
        try:
            response = requests.post(
                TOKEN_URL,
                data=json.dumps(payload),
                headers={"Content-Type": "application/json"},
                timeout=TIMEOUT
            )
            response.raise_for_status()  # 触发HTTP错误(如401、500)
            token_data = response.json()
            
            # 校验Token响应(Cdiscount返回格式特殊,需检查Success字段)
            if token_data.get("Success") and token_data.get("Token"):
                logger.info("Token获取成功,有效期1小时")
                return token_data["Token"]
            else:
                error_msg = token_data.get("ErrorMessage", "未知错误")
                logger.error(f"Token获取失败:{error_msg}")
                return None
        except Exception as e:
            logger.error(f"Token获取异常(重试{retry+1}/{RETRY_TIMES}):{str(e)}")
            if retry < RETRY_TIMES - 1:
                time.sleep(2 ** retry)  # 指数退避重试
    return None

def safe_extract_field(data: Dict, field_path: str, default: any = None) -> any:
    """
    安全提取JSON嵌套字段(处理字段缺失/类型不匹配)
    :param data: 解析后的JSON字典
    :param field_path: 字段路径,如"Product.SellerName"
    :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 fetch_cdiscount_product(product_id: str, token: str) -> Optional[Dict]:
    """
    调用Cdiscount商品详情API,解析核心字段(含多规格SKU)
    :param product_id: Cdiscount商品ID(如"123456789")
    :param token: 有效Access Token
    :return: 解析后的商品数据字典
    """
    # 构造API请求参数(Cdiscount要求固定格式)
    payload = {
        "Token": token,
        "Parameters": {
            "ProductIdList": [product_id],  # 支持传入多个商品ID(最多20个)
            "WithProductAttribute": True,   # 启用属性(多规格依赖)
            "WithProductB2BPrice": False,   # 关闭B2B价格(仅需零售价格)
            "WithProductEan": True,         # 启用EAN码
            "WithProductMedia": True,       # 启用商品图片
            "WithProductStock": True        # 启用库存信息
        }
    }

    # 发送API请求(带重试)
    for retry in range(RETRY_TIMES):
        time.sleep(RATE_LIMIT_DELAY)  # 限流控制
        try:
            logger.info(f"开始调用商品详情API,商品ID:{product_id}")
            response = requests.post(
                PRODUCT_API_URL,
                data=json.dumps(payload),
                headers={"Content-Type": "application/json"},
                timeout=TIMEOUT
            )
            response.raise_for_status()
            raw_json = response.json()

            # 校验API响应是否成功
            if not raw_json.get("Success"):
                error_msg = raw_json.get("ErrorMessage", "API响应失败")
                logger.error(f"商品 {product_id} API响应失败:{error_msg}")
                return None

            # 提取商品列表(支持批量查询,此处取第一个)
            products = raw_json.get("Products", [])
            if not products:
                logger.warning(f"商品 {product_id} 无返回数据")
                return None
            product = products[0]

            # -------------------------- 解析核心字段 --------------------------
            parsed_product = {
                # 基础信息
                "商品ID": safe_extract_field(product, "Id", "未知ID"),
                "商品标题": safe_extract_field(product, "Name", "无标题"),
                "卖家名称": safe_extract_field(product, "SellerName", "未知卖家"),
                "品牌": safe_extract_field(product, "Brand", "未知品牌"),
                "类目ID": safe_extract_field(product, "CategoryId", 0),
                "类目名称": safe_extract_field(product, "CategoryName", "未知类目"),
                "主图URL": safe_extract_field(product, "MainImageUrl", ""),
                "平均评分": safe_extract_field(product, "ReviewAverageNote", 0.0),
                "评论数": safe_extract_field(product, "ReviewCount", 0),
                
                # 价格与库存(默认取第一个SKU的基础价格)
                "基础售价": safe_extract_field(product, "Price", 0.0),
                "基础库存": safe_extract_field(product, "Stock", 0),
                "库存状态": "有货" if safe_extract_field(product, "Stock", 0) > 0 else "无货",
                
                # 多规格SKU列表(核心提取)
                "多规格SKU": []
            }

            # -------------------------- 提取多规格SKU --------------------------
            # Cdiscount多规格存储在"Attributes"和"Skus"字段中
            skus = safe_extract_field(product, "Skus", [])
            attributes = safe_extract_field(product, "Attributes", [])

            # 解析规格属性(如颜色、尺寸)
            attr_map = {}
            for attr in attributes:
                attr_code = safe_extract_field(attr, "Code")
                attr_name = safe_extract_field(attr, "Name")
                if attr_code and attr_name:
                    attr_map[attr_code] = attr_name

            # 解析每个SKU的详细信息
            for sku in skus:
                try:
                    sku_data = {
                        "SKU ID": safe_extract_field(sku, "Id", "未知SKU"),
                        "SKU参考码": safe_extract_field(sku, "SellerProductId", ""),
                        "售价": safe_extract_field(sku, "Price", 0.0),
                        "库存": safe_extract_field(sku, "Stock", 0),
                        "EAN码": safe_extract_field(sku, "Ean", ""),
                        "规格属性": {}
                    }

                    # 提取SKU对应的规格值(如颜色=红色、尺寸=M)
                    sku_attributes = safe_extract_field(sku, "Attributes", [])
                    for sku_attr in sku_attributes:
                        attr_code = safe_extract_field(sku_attr, "Code")
                        attr_value = safe_extract_field(sku_attr, "Value")
                        if attr_code in attr_map and attr_value:
                            sku_data["规格属性"][attr_map[attr_code]] = attr_value

                    parsed_product["多规格SKU"].append(sku_data)
                except Exception as e:
                    logger.warning(f"SKU解析失败:{str(e)},跳过该SKU")
                    continue

            logger.info(f"商品 {product_id} 解析完成,共提取 {len(parsed_product['多规格SKU'])} 个SKU")
            return parsed_product

        except requests.exceptions.Timeout:
            logger.error(f"商品 {product_id} 请求超时(重试{retry+1}/{RETRY_TIMES})")
        except requests.exceptions.ConnectionError:
            logger.error(f"商品 {product_id} 连接失败(重试{retry+1}/{RETRY_TIMES})")
        except Exception as e:
            logger.error(f"商品 {product_id} 解析异常(重试{retry+1}/{RETRY_TIMES}):{str(e)}")
        
        if retry < RETRY_TIMES - 1:
            time.sleep(2 ** retry)  # 重试延迟

    logger.error(f"商品 {product_id} 调用/解析失败(重试{RETRY_TIMES}次后)")
    return None

# -------------------------- 主函数(测试调用) --------------------------
if __name__ == "__main__":
    # 1. 获取Token
    access_token = get_access_token()
    if not access_token:
        logger.critical("Token获取失败,终止程序")
        exit(1)

    # 2. 调用商品详情API(替换为目标商品ID)
    target_product_id = "123456789"  # Cdiscount商品ID
    product_data = fetch_cdiscount_product(target_product_id, access_token)

    # 3. 打印解析结果
    if product_data:
        print("\n===== Cdiscount商品详情解析结果 =====")
        print(f"商品ID:{product_data['商品ID']}")
        print(f"标题:{product_data['商品标题']}")
        print(f"品牌:{product_data['品牌']}")
        print(f"基础售价:{product_data['基础售价']} €")
        print(f"库存状态:{product_data['库存状态']}")
        print(f"平均评分:{product_data['平均评分']}({product_data['评论数']}条评论)")
        
        print("\n===== 多规格SKU详情 =====")
        for idx, sku in enumerate(product_data["多规格SKU"], 1):
            print(f"\nSKU {idx}:")
            print(f"  SKU ID:{sku['SKU ID']}")
            print(f"  参考码:{sku['SKU参考码']}")
            print(f"  售价:{sku['售价']} €")
            print(f"  库存:{sku['库存']}")
            print(f"  规格属性:{sku['规格属性']}")

三、关键解析逻辑说明

1. Cdiscount JSON 响应结构(核心部分)

Cdiscount 商品详情 API 返回的 JSON 结构层级较深,核心字段路径如下:

json

复制代码
{
  "Success": true,
  "Products": [
    {
      "Id": "123456789",          // 商品ID
      "Name": "无线蓝牙耳机",      // 商品标题
      "Brand": "SoundCore",       // 品牌
      "Price": 79.99,             // 基础价格(欧元)
      "Stock": 156,               // 基础库存
      "MainImageUrl": "https://...", // 主图
      "ReviewAverageNote": 4.7,   // 平均评分
      "ReviewCount": 328,         // 评论数
      "Attributes": [             // 规格属性定义(如颜色、尺寸)
        {"Code": "COLOR", "Name": "Couleur"},
        {"Code": "SIZE", "Name": "Taille"}
      ],
      "Skus": [                   // 多规格SKU列表
        {
          "Id": "SKU123",
          "SellerProductId": "BT-BLK-M",
          "Price": 79.99,
          "Stock": 89,
          "Ean": "3560070621564",
          "Attributes": [
            {"Code": "COLOR", "Value": "Noir"},
            {"Code": "SIZE", "Value": "M"}
          ]
        }
      ]
    }
  ]
}

2. 多规格 SKU 提取核心逻辑

  1. 属性映射 :先解析Attributes字段,将规格编码(如COLOR)映射为易读名称(如Couleur/ 颜色);
  2. SKU 遍历 :循环Skus数组,提取每个 SKU 的 ID、价格、库存、EAN 码;
  3. 规格值匹配 :将 SKU 的Attributes与全局属性映射匹配,得到「颜色 = 黑色、尺寸 = M」等易读规格;
  4. 容错处理:单个 SKU 解析失败时跳过,不影响其他 SKU 提取。

四、避坑与优化建议

1. 常见问题解决

  • Token 失效:Token 有效期 1 小时,需在代码中捕获 401 错误,自动重新获取 Token;
  • 字段缺失 :部分商品无 SKU(单规格),Skus字段为空,需默认返回空列表;
  • 价格单位:Cdiscount 默认返回欧元(€),需根据业务需求转换汇率;
  • 编码问题:商品标题含法语特殊字符(如 é、è),需确保 JSON 解析时使用 UTF-8 编码。

2. 性能优化

  • 批量查询:API 支持单次传入最多 20 个商品 ID,批量调用可减少请求次数;
  • 数据缓存:将解析后的商品数据缓存到本地 / Redis,避免重复调用 API;
  • 异步调用 :高并发场景下,使用aiohttp替代requests实现异步请求。

3. 合规要求

  • 数据用途:仅可用于授权范围内的业务(如店铺运营、价格监控),禁止转售数据;
  • 频率控制:严格遵守 100 次 / 分钟的限流规则,超量会导致账号临时封禁;
  • 隐私保护:不泄露卖家名称、SKU 参考码等敏感信息,解析日志需脱敏。

五、总结

Python 调用 Cdiscount 商品详情 API 的核心流程为:「获取 Token → 构造合规请求 → 解析 JSON 基础字段 → 提取多规格 SKU → 容错处理」。

本文方案覆盖了字段缺失、网络异常、SKU 解析失败等常见问题,解析逻辑适配 Cdiscount 的 JSON 结构特点,可直接用于商品数据采集、价格监控、库存管理等合法业务场景。重点关注 Token 有效期和限流规则,即可稳定采集数据。

相关推荐
想回家的一天14 小时前
ECONNREFUSED ::1:8000 前端代理问题
开发语言
cike_y14 小时前
Mybatis之解析配置优化
java·开发语言·tomcat·mybatis·安全开发
Jay_Franklin16 小时前
SRIM通过python计算dap
开发语言·python
是一个Bug16 小时前
Java基础50道经典面试题(四)
java·windows·python
Slow菜鸟16 小时前
Java基础架构设计(三)| 通用响应与异常处理(分布式应用通用方案)
java·开发语言
吴佳浩16 小时前
Python入门指南(七) - YOLO检测API进阶实战
人工智能·后端·python
消失的旧时光-194316 小时前
401 自动刷新 Token 的完整架构设计(Dio 实战版)
开发语言·前端·javascript
wadesir16 小时前
Rust中的条件变量详解(使用Condvar的wait方法实现线程同步)
开发语言·算法·rust
tap.AI16 小时前
RAG系列(二)数据准备与向量索引
开发语言·人工智能
阿蒙Amon16 小时前
C#每日面试题-重写和重载的区别
开发语言·c#