普通视图

发现新文章,点击刷新页面。
昨天 — 2025年9月8日首页

1688 item_get_app 接口深度分析及 Python 实现

2025年9月8日 11:27

1688 平台的 item_get_app 接口是获取商品原始详情数据的核心接口,专门针对移动端应用场景设计。与普通的 item_get 接口相比,它返回的数据结构更贴近 1688 APP 端展示的原始格式,包含更丰富的 B2B 场景特有字段,如批发价格、起订量、供应商信息等,对采购决策和供应链分析具有重要价值。

一、接口核心特性分析

  1. 接口功能与定位
  • 核心功能:获取 1688 商品的原始详情数据,包括商品基础信息、价格体系(含批发价)、规格参数、供应商信息、交易数据等

  • 数据特点

    • 与 1688 APP 端数据结构一致,保留移动端特有的展示字段
    • 包含 B2B 特有的批发价格、起订量、混批规则等信息
    • 提供供应商详细信息,如经营年限、交易量、认证资质等
    • 包含实时库存、发货地、物流模板等供应链关键数据
  • 应用场景

    • 采购决策支持系统
    • 供应商评估与筛选
    • 市场价格监测
    • 竞品分析与比较
    • 供应链优化与风险控制
  1. 认证机制 1688 开放平台采用 appkey + access_token 的认证方式:
  • 开发者在 1688 开放平台注册应用,获取 appkey 和 appsecret
  • 通过 appkey 和 appsecret 获取 access_token(通常有效期为 24 小时)
  • 每次接口调用需携带有效 access_token
  • item_get_app 属于高级接口,需要单独申请权限,企业账号权限更高
  1. 核心参数与响应结构

请求参数

参数名 类型 是否必填 说明
offer_id String 商品 ID(1688 中称为 offer_id)
access_token String 访问令牌
member_id String 采购商 ID,用于获取个性化价格
province String 省份名称,用于获取区域化价格和物流信息
fields String 需要返回的字段,默认返回全部字段

响应核心字段

  • 商品基础信息:商品 ID、名称、标题、详情描述、类目信息
  • 价格体系:批发价、零售价、阶梯价格、混批规则、折扣信息
  • 规格参数:SKU 规格、属性组合、起订量、库存
  • 供应商信息:供应商 ID、名称、经营年限、交易量、认证信息
  • 物流信息:发货地、运费模板、预计发货时间
  • 交易数据:30 天成交量、买家数、重复采购率
  • 多媒体信息:主图、详情图、视频、规格图

二、Python 脚本实现

以下是调用 1688 item_get_app 接口的完整 Python 实现,包含令牌获取、接口调用、数据解析及 B2B 场景特有分析功能: import requests import time import json import logging import re from typing import Dict, Optional, List from requests.exceptions import RequestException

配置日志 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" )

class Alibaba1688ItemGetAppAPI: def init(self, appkey: str, appsecret: str): """ 初始化1688商品详情API客户端 :param appkey: 1688开放平台appkey :param appsecret: 1688开放平台appsecret """ self.appkey = appkey self.appsecret = appsecret self.base_url = "gw.open.1688.com/openapi" self.access_token = None self.token_expires_at = 0 # token过期时间戳 self.session = requests.Session() self.session.headers.update({ "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "AlibabaApp/9.1.0 (iPhone; iOS 14.4; Scale/2.00)" })

def _get_access_token(self) -> Optional[str]:
    """获取访问令牌"""
    # 检查token是否有效
    if self.access_token and self.token_expires_at > time.time() + 60:
        return self.access_token
        
    logging.info("获取新的access_token")
    params = {
        "method": "alibaba.oauth2.getToken",
        "client_id": self.appkey,
        "client_secret": self.appsecret,
        "grant_type": "client_credentials",
        "format": "json"
    }
    
    try:
        response = self.session.get(f"{self.base_url}/gateway.do", params=params, timeout=10)
        response.raise_for_status()
        result = response.json()
        
        if "error_response" in result:
            logging.error(f"获取access_token失败: {result['error_response']['msg']} (错误码: {result['error_response']['code']})")
            return None
            
        self.access_token = result["access_token"]
        self.token_expires_at = time.time() + result.get("expires_in", 86400)  # 默认为24小时
        return self.access_token
            
    except RequestException as e:
        logging.error(f"获取access_token请求异常: {str(e)}")
        return None

def get_item_raw_data(self, 
                     offer_id: str, 
                     member_id: Optional[str] = None,
                     province: Optional[str] = None,
                     fields: Optional[str] = None) -> Optional[Dict]:
    """
    获取商品原始详情数据
    :param offer_id: 商品ID(1688中称为offer_id)
    :param member_id: 采购商ID
    :param province: 省份名称
    :param fields: 需要返回的字段
    :return: 商品原始数据
    """
    # 获取有效的access_token
    if not self._get_access_token():
        return None
        
    params = {
        "method": "alibaba.item.get.app",
        "client_id": self.appkey,
        "access_token": self.access_token,
        "offer_id": offer_id,
        "format": "json",
        "v": "1.0",
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
    }
    
    # 可选参数
    if member_id:
        params["member_id"] = member_id
    if province:
        params["province"] = province
    if fields:
        params["fields"] = fields
        
    try:
        response = self.session.get(f"{self.base_url}/gateway.do", params=params, timeout=20)
        response.raise_for_status()
        result = response.json()
        
        if "error_response" in result:
            logging.error(f"获取商品数据失败: {result['error_response']['msg']} (错误码: {result['error_response']['code']})")
            return None
            
        item_response = result.get("alibaba_item_get_app_response", {})
        item_data = item_response.get("result", {})
        
        if not item_data:
            logging.warning("未获取到商品数据")
            return None
            
        # 格式化原始数据
        return self._process_raw_data(item_data)
        
    except RequestException as e:
        logging.error(f"获取商品数据请求异常: {str(e)}")
        return None
    except json.JSONDecodeError:
        logging.error(f"商品数据响应解析失败: {response.text[:200]}...")
        return None

def _process_raw_data(self, raw_data: Dict) -> Dict:
    """处理原始数据,提取关键信息并格式化"""
    # 基础信息提取
    base_info = {
        "offer_id": raw_data.get("offer_id"),
        "title": raw_data.get("title"),
        "sub_title": raw_data.get("sub_title"),
        "detail_url": raw_data.get("detail_url"),
        "category": {
            "cid": raw_data.get("category_id"),
            "name": raw_data.get("category_name"),
            "level1_cid": raw_data.get("level1_category_id"),
            "level1_name": raw_data.get("level1_category_name"),
            "level2_cid": raw_data.get("level2_category_id"),
            "level2_name": raw_data.get("level2_category_name")
        },
        "tags": raw_data.get("tags", "").split(","),
        "creation_time": raw_data.get("creation_time"),
        "modify_time": raw_data.get("modify_time")
    }
    
    # 价格信息提取(B2B特有的阶梯价格体系)
    price_info = {
        "retail_price": self._safe_float(raw_data.get("retail_price")),  # 零售价
        "wholesale_prices": self._format_wholesale_prices(raw_data.get("wholesale_price_list", [])),  # 批发价列表
        "mix_batch": {  # 混批规则
            "support": raw_data.get("support_mix_batch", False),
            "min_amount": self._safe_float(raw_data.get("mix_batch_min_amount")),
            "min_quantity": self._safe_int(raw_data.get("mix_batch_min_quantity"))
        },
        "currency": raw_data.get("currency", "CNY"),
        "price_unit": raw_data.get("price_unit", "个")
    }
    
    # 库存与规格信息提取
    sku_info = {
        "total_sku": self._safe_int(raw_data.get("total_sku")),
        "skus": self._format_skus(raw_data.get("skus", [])),
        "specs": self._format_specs(raw_data.get("specs", [])),
        "total_stock": self._safe_int(raw_data.get("total_stock")),
        "sales_count": self._safe_int(raw_data.get("sales_count_30d")),  # 30天销量
        "unit": raw_data.get("unit", "个")
    }
    
    # 供应商信息提取(B2B核心信息)
    supplier_info = {
        "supplier_id": raw_data.get("supplier_id"),
        "supplier_name": raw_data.get("supplier_name"),
        "company_name": raw_data.get("company_name"),
        "years": self._safe_int(raw_data.get("operating_years")),  # 经营年限
        "main_products": raw_data.get("main_products", "").split(";"),
        "location": raw_data.get("location"),
        "transaction": {
            "turnover": self._safe_float(raw_data.get("annual_turnover")),  # 年交易额
            "rating": self._safe_float(raw_data.get("supplier_rating")),  # 供应商评分
            "repeat_rate": self._safe_float(raw_data.get("repeat_purchase_rate")),  # 重复采购率
            "buyer_count": self._safe_int(raw_data.get("buyer_count_30d"))  # 30天买家数
        },
        "authentication": {
            "real_name": raw_data.get("real_name_authentication", False),
            "company": raw_data.get("company_authentication", False),
            "gold_supplier": raw_data.get("is_gold_supplier", False),  # 是否金牌供应商
            "assessed_supplier": raw_data.get("is_assessed_supplier", False)  # 是否实力商家
        },
        "contact": {
            "phone": raw_data.get("contact_phone"),
            "online_status": raw_data.get("online_status")
        }
    }
    
    # 物流信息提取
    logistics_info = {
        "delivery_location": raw_data.get("delivery_location"),  # 发货地
        "freight_template": raw_data.get("freight_template_name"),
        "delivery_time": raw_data.get("delivery_time"),  # 发货时间
        "transport_modes": raw_data.get("transport_modes", "").split(","),  # 运输方式
        "min_delivery_days": self._safe_int(raw_data.get("min_delivery_days")),
        "max_delivery_days": self._safe_int(raw_data.get("max_delivery_days"))
    }
    
    # 多媒体信息提取
    media_info = {
        "main_images": raw_data.get("main_image_list", []),
        "detail_images": raw_data.get("detail_image_list", []),
        "video_url": raw_data.get("video_url"),
        "video_cover": raw_data.get("video_cover")
    }
    
    # 详情描述提取
    description = {
        "detail": self._clean_html(raw_data.get("detail", "")),
        "short_description": raw_data.get("short_description")
    }
    
    return {
        "base_info": base_info,
        "price_info": price_info,
        "sku_info": sku_info,
        "supplier_info": supplier_info,
        "logistics_info": logistics_info,
        "media_info": media_info,
        "description": description,
        "raw_data": raw_data  # 保留原始数据
    }

def _safe_float(self, value) -> float:
    """安全转换为float"""
    try:
        return float(value) if value is not None else 0.0
    except (ValueError, TypeError):
        return 0.0

def _safe_int(self, value) -> int:
    """安全转换为int"""
    try:
        return int(value) if value is not None else 0
    except (ValueError, TypeError):
        return 0

def _format_wholesale_prices(self, prices: List[Dict]) -> List[Dict]:
    """格式化批发价格列表(阶梯价格)"""
    formatted = []
    for price in prices:
        formatted.append({
            "min_quantity": self._safe_int(price.get("min_quantity")),
            "max_quantity": self._safe_int(price.get("max_quantity")),
            "price": self._safe_float(price.get("price")),
            "discount": self._safe_float(price.get("discount"))  # 折扣
        })
    # 按起订量排序
    return sorted(formatted, key=lambda x: x["min_quantity"])

def _format_skus(self, skus: List[Dict]) -> List[Dict]:
    """格式化SKU列表"""
    formatted = []
    for sku in skus:
        # 处理SKU的阶梯价格
        sku_prices = []
        if sku.get("price_list"):
            for p in sku.get("price_list"):
                sku_prices.append({
                    "min_quantity": self._safe_int(p.get("min_quantity")),
                    "price": self._safe_float(p.get("price"))
                })
        
        formatted.append({
            "sku_id": sku.get("sku_id"),
            "specs": sku.get("specs", []),  # 规格组合
            "price": self._safe_float(sku.get("price")),
            "wholesale_prices": sku_prices,  # SKU级别的阶梯价格
            "stock": self._safe_int(sku.get("stock")),
            "sales_count": self._safe_int(sku.get("sales_count")),
            "image_url": sku.get("image_url")
        })
    return formatted

def _format_specs(self, specs: List[Dict]) -> List[Dict]:
    """格式化规格参数列表"""
    formatted = []
    for spec in specs:
        formatted.append({
            "name": spec.get("name"),
            "values": [
                {
                    "name": val.get("name"),
                    "image_url": val.get("image_url"),
                    "spec_id": val.get("spec_id")
                } for val in spec.get("values", [])
            ]
        })
    return formatted

def _clean_html(self, html: str) -> str:
    """清理HTML内容,提取纯文本"""
    if not html:
        return ""
    # 去除HTML标签
    clean = re.sub(r'<.*?>', ' ', html)
    # 去除多余空格和换行
    clean = re.sub(r'\s+', ' ', clean).strip()
    return clean

def analyze_b2b_value(self, item_data: Dict) -> Dict:
    """分析商品的B2B采购价值,生成评估报告"""
    if not item_data:
        return {}
        
    # 价格优势评分(0-10分)
    price_advantage = 0
    if item_data["price_info"]["wholesale_prices"]:
        # 基于批发价与零售价的差异计算
        retail_price = item_data["price_info"]["retail_price"]
        lowest_wholesale = item_data["price_info"]["wholesale_prices"][0]["price"]
        
        if retail_price > 0 and lowest_wholesale > 0:
            discount_rate = (retail_price - lowest_wholesale) / retail_price
            price_advantage = min(10, round(discount_rate * 10))
    
    # 供应商可靠性评分(0-10分)
    supplier_score = 0
    supplier = item_data["supplier_info"]
    if supplier["authentication"]["company"]:
        supplier_score += 3
    if supplier["authentication"]["gold_supplier"]:
        supplier_score += 2
    if supplier["authentication"]["assessed_supplier"]:
        supplier_score += 2
    if supplier["years"] >= 3:
        supplier_score += 2
    if supplier["transaction"]["repeat_rate"] > 30:
        supplier_score += 1
    
    # 采购灵活性评分(0-10分)
    flexibility_score = 0
    if item_data["price_info"]["mix_batch"]["support"]:
        flexibility_score += 5
    # 基于起订量评估
    min_order = item_data["price_info"]["mix_batch"]["min_quantity"] or float('inf')
    if min_order <= 5:
        flexibility_score += 5
    elif min_order <= 20:
        flexibility_score += 3
    elif min_order <= 100:
        flexibility_score += 1
    
    # 物流评分(0-10分)
    logistics_score = 0
    logistics = item_data["logistics_info"]
    if logistics["min_delivery_days"] <= 2:
        logistics_score += 5
    elif logistics["min_delivery_days"] <= 5:
        logistics_score += 3
    if len(logistics["transport_modes"]) >= 2:
        logistics_score += 3
    if logistics["freight_template"]:
        logistics_score += 2
    
    # 综合评分
    overall_score = round((price_advantage + supplier_score + flexibility_score + logistics_score) / 4, 1)
    
    return {
        "overall_score": overall_score,
        "price_advantage": {
            "score": price_advantage,
            "description": f"批发价最低为零售价的{round(item_data['price_info']['wholesale_prices'][0]['price']/item_data['price_info']['retail_price']*100, 1)}%" if item_data["price_info"]["retail_price"] > 0 else ""
        },
        "supplier_reliability": {
            "score": supplier_score,
            "description": f"{supplier['years']}年经营,重复采购率{supplier['transaction']['repeat_rate']}%"
        },
        "purchase_flexibility": {
            "score": flexibility_score,
            "description": f"{'支持' if item_data['price_info']['mix_batch']['support'] else '不支持'}混批,最低起订量{item_data['price_info']['mix_batch']['min_quantity'] or '未知'}"
        },
        "logistics_capability": {
            "score": logistics_score,
            "description": f"发货地{logistics['delivery_location']}{logistics['min_delivery_days']}-{logistics['max_delivery_days']}天送达"
        },
        "risk_assessment": {
            "high_risk": overall_score < 3,
            "medium_risk": 3 <= overall_score < 6,
            "low_risk": overall_score >= 6
        }
    }

示例调用 if name == "main": # 替换为实际的appkey和appsecret(从1688开放平台获取) APPKEY = "your_appkey" APPSECRET = "your_appsecret" # 替换为目标商品offer_id OFFER_ID = "61234567890"

# 初始化API客户端
api = Alibaba1688ItemGetAppAPI(APPKEY, APPSECRET)

# 获取商品原始数据
item_data = api.get_item_raw_data(
    offer_id=OFFER_ID,
    # member_id="your_member_id",  # 可选,采购商ID
    # province="浙江省",  # 可选,省份
    # fields="offer_id,title,price,stock"  # 可选,指定需要的字段
)

if item_data:
    print(f"=== 1688商品详情 (offer_id: {OFFER_ID}) ===")
    print(f"商品名称: {item_data['base_info']['title']}")
    print(f"类目: {item_data['base_info']['category']['level1_name']} > {item_data['base_info']['category']['level2_name']}")
    print(f"供应商: {item_data['supplier_info']['supplier_name']} ({item_data['supplier_info']['company_name']})")
    print(f"经营年限: {item_data['supplier_info']['years']}年")
    print(f"30天销量: {item_data['sku_info']['sales_count']}件")
    print(f"30天买家数: {item_data['supplier_info']['transaction']['buyer_count']}")
    print(f"重复采购率: {item_data['supplier_info']['transaction']['repeat_rate']}%")
    
    # 价格信息
    print("\n价格信息:")
    print(f"  零售价: {item_data['price_info']['retail_price']}元/{item_data['price_info']['price_unit']}")
    print("  批发价:")
    for i, wholesale in enumerate(item_data['price_info']['wholesale_prices'], 1):
        max_qty = f"-{wholesale['max_quantity']}" if wholesale['max_quantity'] else "+"
        print(f"    {wholesale['min_quantity']}{max_qty}件: {wholesale['price']}元/{item_data['price_info']['price_unit']}")
    
    # 混批信息
    print("\n采购规则:")
    mix_batch = item_data['price_info']['mix_batch']
    print(f"  混批: {'支持' if mix_batch['support'] else '不支持'}")
    if mix_batch['support']:
        print(f"    最低金额: {mix_batch['min_amount']}元")
        print(f"    最低数量: {mix_batch['min_quantity']}件")
    
    # 物流信息
    print("\n物流信息:")
    print(f"  发货地: {item_data['logistics_info']['delivery_location']}")
    print(f"  预计发货时间: {item_data['logistics_info']['delivery_time']}")
    print(f"  运输方式: {', '.join(item_data['logistics_info']['transport_modes'])}")
    print(f"  预计送达时间: {item_data['logistics_info']['min_delivery_days']}-{item_data['logistics_info']['max_delivery_days']}天")
    
    # 规格信息
    if item_data['sku_info']['skus']:
        print("\n规格信息:")
        for i, sku in enumerate(item_data['sku_info']['skus'][:3], 1):
            specs = ", ".join([f"{s['name']}:{s['value']}" for s in sku['specs']]) if sku['specs'] else "标准"
            print(f"  {i}. {specs}: {sku['price']}元, 库存{sku['stock']}件")
    
    # B2B价值分析
    b2b_analysis = api.analyze_b2b_value(item_data)
    print("\n=== B2B采购价值评估 ===")
    print(f"综合评分: {b2b_analysis['overall_score']}/10分")
    print(f"价格优势: {b2b_analysis['price_advantage']['score']}/10分 {b2b_analysis['price_advantage']['description']}")
    print(f"供应商可靠性: {b2b_analysis['supplier_reliability']['score']}/10分 {b2b_analysis['supplier_reliability']['description']}")
    print(f"采购灵活性: {b2b_analysis['purchase_flexibility']['score']}/10分 {b2b_analysis['purchase_flexibility']['description']}")
    print(f"物流能力: {b2b_analysis['logistics_capability']['score']}/10分 {b2b_analysis['logistics_capability']['description']}")
    print(f"风险评估: {'高风险' if b2b_analysis['risk_assessment']['high_risk'] else '中等风险' if b2b_analysis['risk_assessment']['medium_risk'] else '低风险'}")

三、接口调用注意事项

  1. 调用限制与规范
  • QPS 限制:1688 开放平台对商品详情接口的 QPS 限制通常为 5-10 次 / 秒
  • 权限差异:企业认证账号比个人账号能获取更详细的供应商数据和价格信息
  • 数据缓存:建议缓存获取的数据(缓存时间 30-60 分钟),减少重复调用
  • 地区差异:不同地区可能有不同的物流政策和价格,需正确设置 province 参数
  • 字段权限:部分敏感字段(如供应商联系方式)需要额外申请权限
  1. 常见错误及解决方案
错误码 说明 解决方案
401 未授权或 token 无效 重新获取 access_token,检查权限是否正确
403 权限不足 升级账号类型,申请商品详情接口的完整权限
404 商品不存在 确认 offer_id 是否正确,1688 商品 ID 通常为 10-12 位数字
429 调用频率超限 降低调用频率,实现请求限流
500 服务器内部错误 实现重试机制,最多 3 次,间隔指数退避
110 参数错误 检查 offer_id 格式是否正确
  1. 数据解析要点
  • 价格体系:1688 采用阶梯价格体系,需特别处理不同采购量对应的价格
  • 供应商评估:重点关注经营年限、重复采购率、认证资质等供应商指标
  • 库存状态:部分商品显示的是总库存,部分显示 SKU 级库存,需区分处理
  • 混批规则:混批支持情况和具体规则对采购决策至关重要
  • 物流信息:发货地和物流模板直接影响采购成本和到货时间

四、应用场景与扩展建议 典型应用场景

  • 智能采购系统:基于商品详情数据和供应商评估实现自动化采购决策
  • 供应商管理平台:整合商品数据和供应商信息,构建供应商评估体系
  • 价格监测工具:实时监控商品价格变化,把握最佳采购时机
  • 竞品分析系统:对比同类商品的价格、规格、供应商等信息
  • 供应链优化工具:基于物流信息和供应商分布优化采购布局 扩展建议
  • 实现价格趋势分析:定期获取价格数据,分析价格波动规律
  • 开发供应商匹配系统:基于采购需求自动匹配最合适的供应商
  • 构建采购风险评估模型:结合供应商信息和商品数据评估采购风险
  • 实现多地区价格对比:分析不同地区的价格和物流成本差异
  • 开发批量采购计算器:根据阶梯价格计算最优采购量和总成本
  • 构建商品相似度匹配:基于规格参数和描述实现同类商品自动匹配 通过合理使用 1688 item_get_app 接口,开发者可以构建针对 B2B 场景的商品分析和采购决策系统,充分利用 1688 平台的批发特性,为企业采购提供数据支持。使用时需特别关注 B2B 场景特有的阶梯价格、起订量、供应商评估等维度,以获取更有价值的分析结果
昨天以前首页

京东 item_review 接口深度分析及 Python 实现

2025年9月6日 17:55

京东的 item_review 接口是用于获取商品用户评论数据的专业接口,能够获取京东平台上指定商品的用户评价、评分、晒单、追评等详细信息。这些评论数据对于商品口碑分析、用户需求挖掘、服务质量评估等场景具有重要价值,是电商运营和产品改进的重要数据来源。

一、接口核心特性分析

  1. 接口功能与定位
  • 核心功能:获取京东商品的用户评论数据,包括评价内容、评分、评价时间、用户信息、晒单图片等

  • 数据维度

    • 基础评价:评价 ID、商品 ID、用户昵称、评价时间、评价内容
    • 评分数据:商品评分、包装评分、物流评分等多维度评分
    • 多媒体内容:晒单图片、视频
    • 追评信息:追加评价内容、追评时间
    • 互动数据:有用数、回复数、点赞数
  • 应用场景

    • 商品口碑监控与舆情分析
    • 用户需求与痛点挖掘
    • 售后服务质量评估
    • 竞品评价对比分析
    • 产品改进建议提取
  1. 认证机制

京东开放平台采用 appkey + access_token 的认证方式:

  • 开发者在京东开放平台注册应用,获取 appkey 和 appsecret
  • 通过 appkey 和 appsecret 获取 access_token(通常有效期为 24 小时)
  • 每次接口调用需在请求参数中携带有效 access_token
  • 评论接口属于中等敏感数据接口,需要申请相应权限
  1. 核心参数与响应结构 请求参数
参数名 类型 是否必填 说明
sku_id String 商品 SKU ID
access_token String 访问令牌
page Integer 页码,默认 1
page_size Integer 每页条数,默认 10,最大 50
sort_type Integer 排序方式:0 - 推荐,1 - 时间,2 - 评分高,3 - 评分低
score Integer 评分筛选:0 - 全部,1 - 好评,2 - 中评,3 - 差评
has_image Integer 是否有图:0 - 全部,1 - 有图

响应核心字段

  • 分页信息:总评论数、总页数、当前页码

  • 评价列表:每条评价包含

    • 评价基本信息:评价 ID、用户信息、评价时间
    • 评分信息:各维度评分
    • 评价内容:文本内容、标签
    • 多媒体:晒单图片 URL 列表
    • 追评:内容与时间
    • 互动数据:有用数、回复数 二、Python 脚本实现

以下是调用京东 item_review 接口的完整 Python 实现,包含令牌获取、接口调用、数据解析及情感分析功能: import requests import time import json import logging import re from typing import Dict, Optional, List from requests.exceptions import RequestException from snownlp import SnowNLP # 用于情感分析,需安装:pip install snownlp

配置日志 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" )

class JDItemReviewAPI: def init(self, appkey: str, appsecret: str): """ 初始化京东评论API客户端 :param appkey: 京东开放平台appkey :param appsecret: 京东开放平台appsecret """ self.appkey = appkey self.appsecret = appsecret self.base_url = "api.jd.com" self.access_token = None self.token_expires_at = 0 # token过期时间戳 self.session = requests.Session() self.session.headers.update({ "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" })

def _get_access_token(self) -> Optional[str]:
    """获取访问令牌"""
    # 检查token是否有效
    if self.access_token and self.token_expires_at > time.time() + 60:
        return self.access_token
        
    logging.info("获取新的access_token")
    url = f"{self.base_url}/oauth2/token"
    
    params = {
        "grant_type": "client_credentials",
        "appkey": self.appkey,
        "appsecret": self.appsecret
    }
    
    try:
        response = self.session.get(url, params=params, timeout=10)
        response.raise_for_status()
        result = response.json()
        
        if "access_token" in result:
            self.access_token = result["access_token"]
            self.token_expires_at = time.time() + result.get("expires_in", 86400)  # 默认为24小时
            return self.access_token
        else:
            logging.error(f"获取access_token失败: {result.get('error_description', '未知错误')}")
            return None
            
    except RequestException as e:
        logging.error(f"获取access_token请求异常: {str(e)}")
        return None

def get_item_reviews(self, 
                    sku_id: str, 
                    page: int = 1, 
                    page_size: int = 10,
                    sort_type: int = 0,
                    score: int = 0,
                    has_image: int = 0) -> Optional[Dict]:
    """
    获取商品评论
    :param sku_id: 商品SKU ID
    :param page: 页码
    :param page_size: 每页条数
    :param sort_type: 排序方式:0-推荐,1-时间,2-评分高,3-评分低
    :param score: 评分筛选:0-全部,1-好评,2-中评,3-差评
    :param has_image: 是否有图:0-全部,1-有图
    :return: 评论数据
    """
    # 验证参数
    valid_sort_types = [0, 1, 2, 3]
    if sort_type not in valid_sort_types:
        logging.error(f"无效的排序方式: {sort_type},支持: {valid_sort_types}")
        return None
        
    valid_scores = [0, 1, 2, 3]
    if score not in valid_scores:
        logging.error(f"无效的评分筛选: {score},支持: {valid_scores}")
        return None
        
    if page_size < 1 or page_size > 50:
        logging.error(f"每页条数必须在1-50之间,当前为: {page_size}")
        return None
        
    # 获取有效的access_token
    if not self._get_access_token():
        return None
        
    url = f"{self.base_url}/item/review"
    
    # 构建请求参数
    params = {
        "sku_id": sku_id,
        "access_token": self.access_token,
        "page": page,
        "page_size": page_size,
        "sort_type": sort_type,
        "score": score,
        "has_image": has_image,
        "timestamp": int(time.time() * 1000)
    }
    
    try:
        response = self.session.get(url, params=params, timeout=15)
        response.raise_for_status()
        result = response.json()
        
        # 检查响应状态
        if result.get("code") == 200:
            # 格式化评论数据
            return self._format_review_data(result.get("data", {}))
        else:
            logging.error(f"获取商品评论失败: {result.get('message', '未知错误')} (错误码: {result.get('code')})")
            return None
            
    except RequestException as e:
        logging.error(f"获取商品评论请求异常: {str(e)}")
        return None
    except json.JSONDecodeError:
        logging.error(f"评论响应解析失败: {response.text[:200]}...")
        return None

def _format_review_data(self, review_data: Dict) -> Dict:
    """格式化评论数据"""
    # 分页信息
    pagination = {
        "total_reviews": int(review_data.get("totalCount", 0)),
        "total_pages": (int(review_data.get("totalCount", 0)) + int(review_data.get("pageSize", 10)) - 1) // int(review_data.get("pageSize", 10)),
        "current_page": int(review_data.get("page", 1)),
        "page_size": int(review_data.get("pageSize", 10))
    }
    
    # 格式化评论列表
    reviews = []
    for review in review_data.get("comments", []):
        # 处理评价内容(去除HTML标签)
        content = self._clean_text(review.get("content", ""))
        
        # 情感分析(0-1之间,越接近1越积极)
        sentiment_score = self._analyze_sentiment(content)
        sentiment = "positive" if sentiment_score > 0.6 else "negative" if sentiment_score < 0.4 else "neutral"
        
        # 处理评价图片
        images = []
        if review.get("images"):
            images = [img.get("url") for img in review.get("images") if img.get("url")]
        
        # 处理追评
        append_comment = None
        if review.get("afterUserComment") and review["afterUserComment"].get("hAfterUserComment"):
            append_content = self._clean_text(review["afterUserComment"]["hAfterUserComment"].get("content", ""))
            if append_content:
                append_comment = {
                    "content": append_content,
                    "created": review["afterUserComment"]["hAfterUserComment"].get("creationTime")
                }
        
        # 处理标签
        tags = []
        if review.get("tagList"):
            tags = [tag.get("name") for tag in review.get("tagList") if tag.get("name")]
        
        reviews.append({
            "review_id": review.get("id"),
            "user": {
                "nickname": review.get("nickname"),
                "level_name": review.get("userLevelName")
            },
            "rating": {
                "score": int(review.get("score", 0)),  # 总评分
                "product_score": int(review.get("productScore", 0)),  # 商品评分
                "packing_score": int(review.get("packingScore", 0)),  # 包装评分
                "logistics_score": int(review.get("logisticsScore", 0))  # 物流评分
            },
            "content": content,
            "created_time": review.get("creationTime"),
            "images": images,
            "append_comment": append_comment,
            "useful_vote_count": int(review.get("usefulVoteCount", 0)),  # 有用数
            "reply_count": int(review.get("replyCount", 0)),  # 回复数
            "tags": tags,
            "sentiment": {
                "score": round(sentiment_score, 4),
                "label": sentiment
            },
            "is_vip": review.get("isVip", False),  # 是否VIP用户
            "order_info": {
                "product_color": review.get("productColor"),
                "product_size": review.get("productSize"),
                "buy_time": review.get("buyTime")
            }
        })
    
    return {
        "pagination": pagination,
        "reviews": reviews,
        "raw_data": review_data  # 保留原始数据
    }

def _clean_text(self, text: str) -> str:
    """清理文本,去除HTML标签和特殊字符"""
    if not text:
        return ""
    # 去除HTML标签
    clean = re.sub(r'<.*?>', '', text)
    # 去除多余空格和换行
    clean = re.sub(r'\s+', ' ', clean).strip()
    # 去除特殊字符
    clean = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,.?!,。?!]', ' ', clean)
    return clean

def _analyze_sentiment(self, text: str) -> float:
    """使用SnowNLP进行情感分析"""
    if not text:
        return 0.5  # 中性
    try:
        return SnowNLP(text).sentiments
    except:
        return 0.5  # 分析失败时返回中性

def get_all_reviews(self, sku_id: str, max_pages: int = 10, score: int = 0, has_image: int = 0) -> List[Dict]:
    """
    获取多页评论数据
    :param sku_id: 商品SKU ID
    :param max_pages: 最大页数限制
    :param score: 评分筛选
    :param has_image: 是否有图筛选
    :return: 所有评论列表
    """
    all_reviews = []
    page = 1
    
    while page <= max_pages:
        logging.info(f"获取第 {page} 页评论")
        result = self.get_item_reviews(
            sku_id=sku_id,
            page=page,
            page_size=50,  # 使用最大页大小减少请求次数
            sort_type=1,  # 按时间排序
            score=score,
            has_image=has_image
        )
        
        if not result or not result["reviews"]:
            break
            
        all_reviews.extend(result["reviews"])
        
        # 检查是否已到最后一页
        if page >= result["pagination"]["total_pages"]:
            break
            
        page += 1
        # 控制请求频率,遵守京东API的QPS限制
        time.sleep(2)
        
    return all_reviews

def analyze_reviews(self, reviews: List[Dict]) -> Dict:
    """分析评论数据,生成统计报告"""
    if not reviews:
        return {}
        
    total = len(reviews)
    sentiment_counts = {"positive": 0, "neutral": 0, "negative": 0}
    rating_stats = {
        "score": [],
        "product_score": [],
        "packing_score": [],
        "logistics_score": []
    }
    tag_counts = {}
    has_image_count = 0
    vip_count = 0
    append_comment_count = 0
    useful_vote_total = 0
    
    # 提取评论中的关键词(简单实现)
    keywords = {}
    positive_keywords = ["好", "不错", "满意", "推荐", "快", "值", "优秀", "棒"]
    negative_keywords = ["差", "慢", "不好", "失望", "问题", "破损", "糟糕", "后悔"]
    
    # 统计基础数据
    for review in reviews:
        # 情感统计
        sentiment = review["sentiment"]["label"]
        sentiment_counts[sentiment] += 1
        
        # 评分统计
        for key in rating_stats:
            if key in review["rating"]:
                rating_stats[key].append(review["rating"][key])
        
        # 标签统计
        for tag in review["tags"]:
            if tag:
                tag_counts[tag] = tag_counts.get(tag, 0) + 1
        
        # 有图评价统计
        if review["images"]:
            has_image_count += 1
            
        # VIP用户统计
        if review["is_vip"]:
            vip_count += 1
            
        # 追评统计
        if review["append_comment"]:
            append_comment_count += 1
            
        # 有用数统计
        useful_vote_total += review["useful_vote_count"]
        
        # 关键词统计
        content = review["content"].lower()
        for kw in positive_keywords:
            if kw in content:
                keywords[kw] = keywords.get(kw, 0) + 1
        for kw in negative_keywords:
            if kw in content:
                keywords[kw] = keywords.get(kw, 0) + 1
    
    # 计算平均评分
    avg_ratings = {}
    for key, values in rating_stats.items():
        if values:
            avg_ratings[key] = round(sum(values) / len(values), 1)
        else:
            avg_ratings[key] = 0
    
    # 获取热门标签(前10)
    top_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)[:10]
    
    # 获取热门关键词(前10)
    top_keywords = sorted(keywords.items(), key=lambda x: x[1], reverse=True)[:10]
    
    return {
        "total_reviews": total,
        "sentiment_distribution": {
            "count": sentiment_counts,
            "percentage": {
                k: round(v / total * 100, 1) for k, v in sentiment_counts.items()
            }
        },
        "average_rating": avg_ratings,
        "image_review_ratio": round(has_image_count / total * 100, 1) if total > 0 else 0,
        "vip_review_ratio": round(vip_count / total * 100, 1) if total > 0 else 0,
        "append_comment_ratio": round(append_comment_count / total * 100, 1) if total > 0 else 0,
        "avg_useful_votes": round(useful_vote_total / total, 1) if total > 0 else 0,
        "top_tags": top_tags,
        "top_keywords": top_keywords
    }

示例调用 if name == "main": # 替换为实际的appkey和appsecret(从京东开放平台获取) APPKEY = "your_appkey" APPSECRET = "your_appsecret" # 替换为目标商品SKU ID SKU_ID = "100012345678"

# 初始化API客户端
api = JDItemReviewAPI(APPKEY, APPSECRET)

# 方式1:获取单页评论
# review_result = api.get_item_reviews(
#     sku_id=SKU_ID,
#     page=1,
#     page_size=10,
#     sort_type=1,  # 按时间排序
#     score=0,  # 全部评分
#     has_image=0  # 全部评论
# )

# 方式2:获取多页评论
review_result = api.get_all_reviews(
    sku_id=SKU_ID,
    max_pages=3,
    score=0,  # 全部评分
    has_image=0  # 全部评论
)

if isinstance(review_result, dict) and "reviews" in review_result:
    print(f"共获取到 {review_result['pagination']['total_reviews']} 条评论")
    print(f"当前第 {review_result['pagination']['current_page']}/{review_result['pagination']['total_pages']} 页\n")
    
    # 打印前3条评论
    for i, review in enumerate(review_result["reviews"][:3], 1):
        print(f"{i}. 用户: {review['user']['nickname']} ({'VIP' if review['is_vip'] else '普通用户'})")
        print(f"   评分: {review['rating']['score']}分 (商品: {review['rating']['product_score']}, 包装: {review['rating']['packing_score']}, 物流: {review['rating']['logistics_score']})")
        print(f"   时间: {review['created_time']}")
        print(f"   内容: {review['content'][:100]}{'...' if len(review['content'])>100 else ''}")
        print(f"   情感: {review['sentiment']['label']} (得分: {review['sentiment']['score']})")
        print(f"   有用数: {review['useful_vote_count']}")
        if review['images']:
            print(f"   图片数: {len(review['images'])}")
        if review['tags']:
            print(f"   标签: {', '.join(review['tags'])}")
        if review['append_comment']:
            print(f"   追评: {review['append_comment']['content'][:50]}{'...' if len(review['append_comment']['content'])>50 else ''}")
        print("-" * 100)
        
    # 分析评论
    analysis = api.analyze_reviews(review_result["reviews"])
    print("\n=== 评论分析报告 ===")
    print(f"总评论数: {analysis['total_reviews']}")
    print(f"情感分布: 正面 {analysis['sentiment_distribution']['percentage']['positive']}%, 中性 {analysis['sentiment_distribution']['percentage']['neutral']}%, 负面 {analysis['sentiment_distribution']['percentage']['negative']}%")
    print(f"平均评分: 总评分 {analysis['average_rating']['score']}, 商品 {analysis['average_rating']['product_score']}, 包装 {analysis['average_rating']['packing_score']}, 物流 {analysis['average_rating']['logistics_score']}")
    print(f"有图评价占比: {analysis['image_review_ratio']}%")
    print(f"VIP评价占比: {analysis['vip_review_ratio']}%")
    print("热门标签:")
    for tag, count in analysis['top_tags']:
        print(f"  {tag}: {count}次")
    print("热门关键词:")
    for kw, count in analysis['top_keywords']:
        print(f"  {kw}: {count}次")
        
elif isinstance(review_result, list):
    # 处理多页评论结果
    print(f"共获取到 {len(review_result)} 条评论")
    
    # 分析评论
    analysis = api.analyze_reviews(review_result)
    print("\n=== 评论分析报告 ===")
    print(f"总评论数: {analysis['total_reviews']}")
    print(f"情感分布: 正面 {analysis['sentiment_distribution']['percentage']['positive']}%, 中性 {analysis['sentiment_distribution']['percentage']['neutral']}%, 负面 {analysis['sentiment_distribution']['percentage']['negative']}%")

三、接口调用注意事项

  1. 调用限制与规范
  • QPS 限制:京东开放平台对评论接口的 QPS 限制通常为 5-10 次 / 秒
  • 数据权限:评论接口需要申请相应权限,部分高级字段可能需要额外审批
  • 分页限制:最多可获取前 100 页评论数据(约 5000 条)
  • 调用频率:批量获取时建议设置 2-3 秒间隔,避免触发频率限制
  • 合规使用:评论数据受用户隐私保护,不得泄露用户个人信息
  1. 常见错误及解决方案 | 错误码 | 说明 | 解决方案 | | ----- | ------------- | -------------------------- | | 401 | 未授权或 token 无效 | 重新获取 access_token,检查权限是否正确 | | 403 | 权限不足 | 申请评论接口的访问权限 | | 404 | 商品不存在或无评论 | 确认 sku_id 是否正确,该商品可能没有评论 | | 429 | 调用频率超限 | 降低调用频率,实现请求限流 | | 500 | 服务器内部错误 | 实现重试机制,最多 3 次,间隔指数退避 | | 10001 | 参数错误 | 检查参数格式和取值范围是否正确 |

  2. 数据解析要点

  • 文本清洗:评价内容可能包含 HTML 标签、特殊符号等,需要预处理
  • 评分体系:京东评分通常为 1-5 分,5 分为最高分
  • 时间格式:评价时间可能为时间戳或字符串,需统一转换为标准格式
  • 图片 URL:部分图片 URL 需要处理才能直接访问
  • 多维度评分:区分商品、包装、物流等不同维度的评分 四、应用场景与扩展建议 典型应用场景
  • 商品口碑监控系统:实时监控商品评价变化,及时发现负面评价
  • 用户需求分析工具:从评论中提取用户对产品功能、特性的需求
  • 服务质量评估平台:分析包装和物流评分,评估供应链服务质量
  • 竞品评价对比系统:对比同类商品的评价数据,找出优势与不足
  • 产品改进建议提取:从评论中挖掘产品改进的具体建议 扩展建议
  • 实现评论关键词提取:使用 TF-IDF 或 TextRank 算法提取核心评价点
  • 构建情感趋势分析:追踪评论情感随时间的变化趋势
  • 开发负面评价预警:当负面评价比例超过阈值时触发警报
  • 实现评论分类模型:基于内容自动分类评论(如质量、价格、服务等)
  • 构建多维度评分看板:直观展示商品、包装、物流等不同维度的评分变化
  • 开发评论可视化系统:生成情感分布饼图、关键词云图、评分趋势图等 通过合理使用京东 item_review 接口,开发者可以构建全面的商品评价分析系统,为电商运营决策、产品改进和用户服务优化提供数据支持。使用时需严格遵守京东开放平台的使用规范和数据保护条款,确保合法合规地获取和使用评论数据

京东 item_get_app 接口深度分析及 Python 实现

2025年9月6日 10:53

京东的 item_get_app 接口是用于获取商品原始详情数据的核心接口,与普通的 item_get 接口相比,它返回的数据更贴近京东 APP 端展示的原始结构,包含更丰富的字段和细节信息。这些原始数据对于深度电商分析、竞品监控、价格追踪等场景具有重要价值。

一、接口核心特性分析

  1. 接口功能与定位
  • 核心功能:获取京东商品的原始详情数据,包括商品基础信息、价格体系、库存状态、规格参数、促销活动等完整字段

  • 数据特点

    • 与京东 APP 端数据结构一致,保留原始字段名和层级关系
    • 包含普通接口不返回的内部标识和计算参数
    • 实时性高,反映当前商品的最新状态
    • 字段丰富,包含算法推荐、权重参数等内部信息
  • 应用场景

    • 深度电商数据分析系统
    • 商品全生命周期监控
    • 价格变动实时追踪
    • 竞品全方位对比分析
    • 电商大数据挖掘与研究
  1. 认证机制

京东开放平台采用 appkey + access_token 的认证方式:

  • 开发者在京东开放平台注册应用,获取 appkey 和 appsecret
  • 通过 appkey 和 appsecret 获取 access_token(通常有效期为 24 小时)
  • 每次接口调用需在请求参数中携带有效 access_token
  • item_get_app 属于高级接口,需要单独申请权限
  1. 核心参数与响应结构

请求参数

参数名 类型 是否必填 说明
sku_id String 商品 SKU ID,京东商品的唯一标识
access_token String 访问令牌
platform String 平台类型,如 "android"、"ios",默认 "android"
fields String 需要返回的字段,默认返回全部字段
area String 地区编码,用于获取区域化价格和库存

响应核心字段

  • 商品基础信息:SKU ID、商品名称、品牌信息、分类信息等
  • 价格信息:基准价、促销价、会员价、活动价、价格计算规则等
  • 库存信息:实时库存、区域库存、库存状态、限购信息等
  • 规格参数:SKU 规格、属性组合、规格图片等
  • 促销信息:活动列表、优惠券、满减规则、赠品信息等
  • 服务信息:售后政策、配送服务、安装服务等
  • 多媒体信息:图片、视频、3D 模型等资源链接

二、Python 脚本实现

以下是调用京东 item_get_app 接口的完整 Python 实现,包含令牌获取、接口调用、数据解析等功能: import requests import time import json import logging import hashlib from typing import Dict, Optional, List from requests.exceptions import RequestException

配置日志

logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" )

class JDItemGetAppAPI: def init(self, appkey: str, appsecret: str): """ 初始化京东商品详情原数据API客户端 :param appkey: 京东开放平台appkey :param appsecret: 京东开放平台appsecret """ self.appkey = appkey self.appsecret = appsecret self.base_url = "api.jd.com" self.access_token = None self.token_expires_at = 0 # token过期时间戳 self.session = requests.Session() self.session.headers.update({ "Content-Type": "application/json", "User-Agent": "JDApp;android;9.5.4;android 10;scale/3.0" # 模拟京东APP请求头 })

def _get_access_token(self) -> Optional[str]:
    """获取访问令牌"""
    # 检查token是否有效
    if self.access_token and self.token_expires_at > time.time() + 60:
        return self.access_token
        
    logging.info("获取新的access_token")
    url = f"{self.base_url}/oauth2/token"
    
    params = {
        "grant_type": "client_credentials",
        "appkey": self.appkey,
        "appsecret": self.appsecret
    }
    
    try:
        response = self.session.get(url, params=params, timeout=10)
        response.raise_for_status()
        result = response.json()
        
        if "access_token" in result:
            self.access_token = result["access_token"]
            self.token_expires_at = time.time() + result.get("expires_in", 86400)  # 默认为24小时
            return self.access_token
        else:
            logging.error(f"获取access_token失败: {result.get('error_description', '未知错误')}")
            return None
            
    except RequestException as e:
        logging.error(f"获取access_token请求异常: {str(e)}")
        return None

def get_item_raw_data(self, 
                     sku_id: str, 
                     platform: str = "android",
                     area: str = "1_72_2799_0",  # 默认地区编码:北京
                     fields: Optional[str] = None) -> Optional[Dict]:
    """
    获取商品原始详情数据
    :param sku_id: 商品SKU ID
    :param platform: 平台类型
    :param area: 地区编码
    :param fields: 需要返回的字段
    :return: 商品原始数据
    """
    # 验证参数
    valid_platforms = ["android", "ios"]
    if platform not in valid_platforms:
        logging.error(f"无效的平台类型: {platform},支持: {valid_platforms}")
        return None
        
    # 获取有效的access_token
    if not self._get_access_token():
        return None
        
    url = f"{self.base_url}/item/get_app"
    
    # 构建请求参数
    params = {
        "sku_id": sku_id,
        "access_token": self.access_token,
        "platform": platform,
        "area": area,
        "timestamp": int(time.time() * 1000)  # 时间戳,毫秒级
    }
    
    # 添加字段筛选
    if fields:
        params["fields"] = fields
        
    # 生成签名(部分接口需要)
    params["sign"] = self._generate_sign(params)
        
    try:
        response = self.session.get(url, params=params, timeout=20)
        response.raise_for_status()
        result = response.json()
        
        # 检查响应状态
        if result.get("code") == 200:
            # 格式化原始数据
            return self._process_raw_data(result.get("data", {}))
        else:
            logging.error(f"获取商品数据失败: {result.get('message', '未知错误')} (错误码: {result.get('code')})")
            return Non          
    except RequestException as e:
        logging.error(f"获取商品数据请求异常: {str(e)}")
        return None
    except json.JSONDecodeError:
        logging.error(f"商品数据响应解析失败: {response.text[:200]}...")
        return None

def _generate_sign(self, params: Dict) -> str:
    """生成签名(部分京东接口需要)"""
    # 按参数名排序
    sorted_params = sorted(params.items(), key=lambda x: x[0])
    # 拼接参数
    sign_str = self.appsecret
    for k, v in sorted_params:
        if k != "sign":
            sign_str += f"{k}{v}"
    sign_str += self.appsecret
    # SHA256加密
    return hashlib.sha256(sign_str.encode()).hexdigest().upper()

def _process_raw_data(self, raw_data: Dict) -> Dict:
    """处理原始数据,提取关键信息并格式化"""
    # 基础信息提取
    base_info = {
        "sku_id": raw_data.get("skuId"),
        "spu_id": raw_data.get("spuId"),
        "item_id": raw_data.get("itemId"),
        "title": raw_data.get("skuName"),
        "sub_title": raw_data.get("skuSubName"),
        "brand": {
            "id": raw_data.get("brandId"),
            "name": raw_data.get("brandName")
        },
        "category": {
            "cid1": raw_data.get("cid1"),
            "cid2": raw_data.get("cid2"),
            "cid3": raw_data.get("cid3"),
            "name1": raw_data.get("cateName1"),
            "name2": raw_data.get("cateName2"),
            "name3": raw_data.get("cateName3")
        },
        "url": f"https://item.jd.com/{raw_data.get('skuId')}.html" if raw_data.get('skuId') else None
    }
    
    # 价格信息提取
    price_info = {
        "jd_price": self._safe_float(raw_data.get("jdPrice", {}).get("p")),  # 京东价
        "market_price": self._safe_float(raw_data.get("marketPrice", {}).get("p")),  # 市场价
        "vip_price": self._safe_float(raw_data.get("vipPrice", {}).get("p")),  # 会员价
        "plus_price": self._safe_float(raw_data.get("plusPrice", {}).get("p")),  # PLUS会员价
        "promotion_price": self._safe_float(raw_data.get("promotionPrice", {}).get("p")),  # 促销价
        "price_history": raw_data.get("priceHistory"),  # 价格历史
        "price_tags": raw_data.get("priceTags", [])  # 价格标签
    }
    
    # 库存信息提取
    stock_info = {
        "stock_num": self._safe_int(raw_data.get("stock", {}).get("stockNum")),  # 库存数量
        "stock_state": raw_data.get("stock", {}).get("stockState"),  # 库存状态
        "stock_state_name": raw_data.get("stock", {}).get("stockStateName"),  # 库存状态名称
        "limit_buy": self._safe_int(raw_data.get("limitBuy")),  # 限购数量
        "area_stock": raw_data.get("areaStock")  # 区域库存信息
    }
    
    # 规格信息提取
    sku_info = {
        "total_sku": self._safe_int(raw_data.get("totalSku")),  # 总规格数量
        "sku_list": self._format_sku_list(raw_data.get("skuList", [])),  # 规格列表
        "spec_list": self._format_spec_list(raw_data.get("specList", []))  # 规格参数
    }
    
    # 促销信息提取
    promotion_info = {
        "promotions": raw_data.get("promotion", {}).get("promotionList", []),  # 促销活动列表
        "coupons": raw_data.get("couponList", []),  # 优惠券列表
        "gift_list": raw_data.get("giftList", []),  # 赠品列表
        "seckill_info": raw_data.get("seckillInfo")  # 秒杀信息
    }
    
    # 图片信息提取
    image_info = {
        "main_images": self._extract_images(raw_data.get("mainImgList", [])),  # 主图列表
        "detail_images": self._extract_images(raw_data.get("detailImgList", [])),  # 详情图列表
        "video_url": raw_data.get("videoInfo", {}).get("videoUrl")  # 视频URL
    }
    
    # 服务信息提取
    service_info = {
        "after_sale_service": raw_data.get("afterSaleService"),  # 售后服务
        "delivery_service": raw_data.get("deliveryService"),  # 配送服务
        "install_service": raw_data.get("installService"),  # 安装服务
        "warranty": raw_data.get("warranty")  # 保修信息
    }
    
    return {
        "base_info": base_info,
        "price_info": price_info,
        "stock_info": stock_info,
        "sku_info": sku_info,
        "promotion_info": promotion_info,
        "image_info": image_info,
        "service_info": service_info,
        "raw_data": raw_data  # 保留原始数据
    }

def _safe_float(self, value) -> float:
    """安全转换为float"""
    try:
        return float(value) if value is not None else 0.0
    except (ValueError, TypeError):
        return 0.0

def _safe_int(self, value) -> int:
    """安全转换为int"""
    try:
        return int(value) if value is not None else 0
    except (ValueError, TypeError):
        return 0

def _format_sku_list(self, sku_list: List[Dict]) -> List[Dict]:
    """格式化SKU列表"""
    formatted = []
    for sku in sku_list:
        formatted.append({
            "sku_id": sku.get("skuId"),
            "name": sku.get("name"),
            "price": self._safe_float(sku.get("jdPrice", {}).get("p")),
            "stock_num": self._safe_int(sku.get("stockNum")),
            "specs": sku.get("specs"),
            "image_url": sku.get("imgUrl"),
            "status": sku.get("status")
        })
    return formatted

def _format_spec_list(self, spec_list: List[Dict]) -> List[Dict]:
    """格式化规格参数列表"""
    formatted = []
    for spec in spec_list:
        formatted.append({
            "name": spec.get("name"),
            "values": [
                {
                    "name": val.get("name"),
                    "image_url": val.get("imgUrl"),
                    "selected": val.get("selected", False)
                } for val in spec.get("valueList", [])
            ]
        })
    return formatted

def _extract_images(self, image_list: List[Dict]) -> List[str]:
    """提取图片URL列表"""
    images = []
    for img in image_list:
        if isinstance(img, dict):
            url = img.get("url")
            if url:
                images.append(url)
        elif isinstance(img, str):
            images.append(img)
    return images

def compare_prices(self, current_data: Dict, previous_data: Dict) -> Dict:
    """比较两个时间点的价格差异"""
    if not current_data or not previous_data:
        return {}
        
    price_changes = {}
    price_types = ["jd_price", "market_price", "vip_price", "plus_price", "promotion_price"]
    
    for price_type in price_types:
        current = current_data["price_info"][price_type]
        previous = previous_data["price_info"][price_type]
        
        if current != previous:
            change = current - previous
            change_percent = (change / previous) * 100 if previous != 0 else 0
            price_changes[price_type] = {
                "current": current,
                "previous": previous,
                "change": round(change, 2),
                "change_percent": round(change_percent, 2)
            }
    
    return {
        "has_change": len(price_changes) > 0,
        "changes": price_changes,
        "compared_at": time.strftime("%Y-%m-%d %H:%M:%S")
    }

示例调用 if name == "main": # 替换为实际的appkey和appsecret(从京东开放平台获取) APPKEY = "your_appkey" APPSECRET = "your_appsecret" # 替换为目标商品SKU ID SKU_ID = "100012345678"

# 初始化API客户端
api = JDItemGetAppAPI(APPKEY, APPSECRET)

# 获取商品原始数据
item_data = api.get_item_raw_data(
    sku_id=SKU_ID,
    platform="android",
    area="1_72_2799_0"  # 北京地区编码
    # fields="skuId,skuName,jdPrice,stock"  # 可选,指定需要的字段
)

if item_data:
    print(f"=== 京东商品详情 (SKU: {SKU_ID}) ===")
    print(f"商品名称: {item_data['base_info']['title']}")
    print(f"品牌: {item_data['base_info']['brand']['name']}")
    print(f"分类: {item_data['base_info']['category']['name1']} > {item_data['base_info']['category']['name2']} > {item_data['base_info']['category']['name3']}")
    print(f"京东价: {item_data['price_info']['jd_price']}元")
    
    if item_data['price_info']['promotion_price'] and item_data['price_info']['promotion_price'] < item_data['price_info']['jd_price']:
        print(f"促销价: {item_data['price_info']['promotion_price']}元")
        
    if item_data['price_info']['vip_price'] and item_data['price_info']['vip_price'] < item_data['price_info']['jd_price']:
        print(f"会员价: {item_data['price_info']['vip_price']}元")
        
    print(f"库存状态: {item_data['stock_info']['stock_state_name']}")
    print(f"规格数量: {item_data['sku_info']['total_sku']}")
    print(f"主图数量: {len(item_data['image_info']['main_images'])}")
    print(f"促销活动数量: {len(item_data['promotion_info']['promotions'])}")
    
    # 打印前3个规格信息
    if item_data['sku_info']['sku_list']:
        print("\n规格信息:")
        for i, sku in enumerate(item_data['sku_info']['sku_list'][:3], 1):
            print(f"  {i}. {sku['name']}: {sku['price']}元 (库存: {sku['stock_num']})")
    
    # 打印主要促销活动
    if item_data['promotion_info']['promotions']:
        print("\n促销活动:")
        for i, promo in enumerate(item_data['promotion_info']['promotions'][:3], 1):
            print(f"  {i}. {promo.get('title')}")

三、接口调用注意事项

  1. 调用限制与规范
  • 权限要求item_get_app 属于高级接口,需要向京东开放平台单独申请权限
  • QPS 限制:该接口 QPS 限制通常较低(1-5 次 / 秒),需严格控制调用频率
  • 数据缓存:建议缓存获取的数据(缓存时间 30-60 分钟),减少重复调用
  • 地区编码:不同地区可能有不同的价格和库存,需正确设置 area 参数
  • 字段筛选:不需要全部字段时,通过 fields 参数指定所需字段,提高响应速度
  1. 常见错误及解决方案
错误码 说明 解决方案
401 未授权或 token 无效 重新获取 access_token,检查权限是否正确
403 权限不足 申请item_get_app接口的访问权限
404 商品不存在 确认 sku_id 是否正确有效
429 调用频率超限 降低调用频率,实现请求限流
500 服务器内部错误 实现重试机制,最多 3 次,间隔指数退避
10002 参数错误 检查地区编码等参数是否正确
  1. 数据解析要点
  • 价格字段:价格相关字段可能以字符串形式返回,需转换为数值类型
  • 库存状态:库存状态有多种编码,需结合状态名称理解实际含义
  • 规格结构:规格数据结构复杂,需递归解析多层规格组合
  • 促销信息:促销活动有多种类型,需分别处理不同的促销规则
  • 图片 URL:部分图片 URL 需要拼接域名才能访问

四、应用场景与扩展建议

典型应用场景

  • 商品全信息监控系统:实时监控商品所有属性的变化
  • 价格趋势分析工具:追踪商品价格历史,预测价格变化
  • 竞品深度分析平台:全方位对比分析竞品的各项参数和策略
  • 库存预警系统:基于原始库存数据设置预警阈值
  • 电商大数据分析平台:挖掘商品数据中的关联关系和规律

扩展建议

  • 实现价格变化监控:定期获取数据,对比价格变化并记录
  • 开发库存趋势分析:跟踪库存数量变化,预测库存耗尽时间
  • 构建商品特征向量:将原始数据转换为特征向量,用于机器学习
  • 实现多地区数据对比:获取不同地区的价格和库存,分析区域差异
  • 开发数据导出功能:支持将原始数据和解析后的数据导出为 CSV/Excel
  • 构建商品相似度计算:基于原始属性计算商品间的相似度 通过合理使用京东 item_get_app 接口,开发者可以获取最全面的商品原始数据,为深度电商分析和决策支持提供有力保障。使用时需遵守京东开放平台的相关规定,确保数据的合法使用

京东 item_video 接口深度 深度分析及 Python 实现

2025年9月6日 10:25

京东的 item_video 接口是用于获取商品相关视频资源的专业接口,能够获取商品主视频、细节展示视频、使用教程视频等多种类型的视频资源。这些视频内容对于商品展示、用户体验提升、竞品分析等场景具有重要价值,是电商内容分析中不可或缺的一部分。

一、接口核心特性分析

  1. 接口功能与定位
  • 核心功能:获取京东商品的相关视频资源,包括视频 URL、时长、分辨率、封面图等信息

  • 视频类型

    • 主视频:商品主展示视频,通常在商品详情页顶部展示
    • 细节视频:展示商品细节特征的视频片段
    • 场景视频:商品使用场景展示视频
    • 对比视频:与同类商品的对比视频(部分商品有)
  • 应用场景

    • 商品内容聚合平台
    • 竞品视频内容分析
    • 多平台商品展示系统
    • 电商内容营销研究
  1. 认证机制

京东开放平台采用 appkey + access_token 的认证方式:

  • 开发者在京东开放平台注册应用,获取 appkey 和 appsecret
  • 使用 appkey 和 appsecret 获取 access_token(有有效期限制)
  • 每次接口调用时,在请求参数中携带 access_token 进行身份验证
  1. 核心参数与响应结构

请求参数

参数名 类型 是否必填 说明
sku_id String 商品 SKU ID,京东商品的唯一标识
access_token String 访问令牌
video_type String 视频类型筛选,如 "main"(主视频)、"detail"(细节视频)等
need_all Boolean 是否返回所有视频,默认 false 只返回主视频

响应核心字段

  • 视频列表:每个视频包含

    • 基础信息:视频 ID、标题、描述、类型
    • 资源信息:视频 URL(可能包含多种清晰度)、封面图 URL
    • 规格信息:时长、分辨率、大小、格式
    • 额外信息:上传时间、播放次数 二、Python 脚本实现

以下是调用京东 item_video 接口的完整 Python 实现,包含令牌获取、接口调用、数据解析等功能: import requests import time import json import logging import re from typing import Dict, Optional, List from requests.exceptions import RequestException

配置日志 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" )

class JDItemVideoAPI: def init(self, appkey: str, appsecret: str): """ 初始化京东商品视频API客户端 :param appkey: 京东开放平台appkey :param appsecret: 京东开放平台appsecret """ self.appkey = appkey self.appsecret = appsecret self.base_url = "api.jd.com" self.access_token = None self.token_expires_at = 0 # token过期时间戳 self.session = requests.Session() self.session.headers.update({ "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" })

def _get_access_token(self) -> Optional[str]:
    """获取访问令牌"""
    # 检查token是否有效
    if self.access_token and self.token_expires_at > time.time() + 60:
        return self.access_token
        
    logging.info("获取新的access_token")
    url = f"{self.base_url}/oauth2/token"
    
    params = {
        "grant_type": "client_credentials",
        "appkey": self.appkey,
        "appsecret": self.appsecret
    }
    
    try:
        response = self.session.get(url, params=params, timeout=10)
        response.raise_for_status()
        result = response.json()
        
        if "access_token" in result:
            self.access_token = result["access_token"]
            self.token_expires_at = time.time() + result.get("expires_in", 86400)  # 默认为24小时
            return self.access_token
        else:
            logging.error(f"获取access_token失败: {result.get('error_description', '未知错误')}")
            return None
            
    except RequestException as e:
        logging.error(f"获取access_token请求异常: {str(e)}")
        return None

def get_item_videos(self, 
                   sku_id: str, 
                   video_type: Optional[str] = None,
                   need_all: bool = False) -> Optional[Dict]:
    """
    获取商品视频
    :param sku_id: 商品SKU ID
    :param video_type: 视频类型筛选
    :param need_all: 是否返回所有视频
    :return: 视频数据
    """
    # 验证参数
    valid_types = ["main", "detail", "scene", "compare"]
    if video_type and video_type not in valid_types:
        logging.error(f"无效的视频类型: {video_type},支持: {valid_types}")
        return None
        
    # 获取有效的access_token
    if not self._get_access_token():
        return None
        
    url = f"{self.base_url}/item/video"
    
    # 构建请求参数
    params = {
        "sku_id": sku_id,
        "access_token": self.access_token,
        "need_all": "true" if need_all else "false"
    }
    
    # 添加视频类型筛选
    if video_type:
        params["video_type"] = video_type
        
    try:
        response = self.session.get(url, params=params, timeout=15)
        response.raise_for_status()
        result = response.json()
        
        # 检查响应状态
        if result.get("code") == 200:
            # 格式化视频数据
            return self._format_video_data(result.get("data", {}))
        else:
            logging.error(f"获取商品视频失败: {result.get('message', '未知错误')} (错误码: {result.get('code')})")
            return None
            
    except RequestException as e:
        logging.error(f"获取商品视频请求异常: {str(e)}")
        return None
    except json.JSONDecodeError:
        logging.error(f"商品视频响应解析失败: {response.text[:200]}...")
        return None

def _format_video_data(self, video_data: Dict) -> Dict:
    """格式化视频数据"""
    # 基础商品信息
    item_info = {
        "sku_id": video_data.get("sku_id"),
        "item_id": video_data.get("item_id"),
        "title": video_data.get("title"),
        "total_videos": int(video_data.get("total_count", 0))
    }
    
    # 格式化视频列表
    videos = []
    for video in video_data.get("videos", []):
        # 处理视频URL(可能有多种清晰度)
        video_urls = {}
        if video.get("url_list"):
            for url_info in video.get("url_list"):
                quality = url_info.get("quality", "unknown")
                video_urls[quality] = url_info.get("url")
        
        # 提取视频时长(转换为秒)
        duration_seconds = self._parse_duration(video.get("duration", ""))
        
        videos.append({
            "video_id": video.get("video_id"),
            "title": video.get("title"),
            "description": video.get("description"),
            "type": video.get("type"),
            "type_name": self._get_video_type_name(video.get("type")),
            "urls": video_urls,
            "cover_url": video.get("cover_url"),
            "duration": {
                "original": video.get("duration"),
                "seconds": duration_seconds
            },
            "resolution": video.get("resolution"),
            "size": video.get("size"),  # 视频大小,单位字节
            "format": video.get("format"),
            "play_count": int(video.get("play_count", 0)),
            "upload_time": video.get("upload_time")
        })
    
    # 按视频类型分组
    videos_by_type = {}
    for video in videos:
        type_name = video["type_name"]
        if type_name not in videos_by_type:
            videos_by_type[type_name] = []
        videos_by_type[type_name].append(video)
    
    return {
        "item_info": item_info,
        "videos": videos,
        "videos_by_type": videos_by_type,
        "raw_data": video_data  # 保留原始数据
    }

def _parse_duration(self, duration_str: str) -> int:
    """解析视频时长字符串为秒数"""
    if not duration_str:
        return 0
        
    # 处理格式如 "00:01:23" 或 "01:23"
    try:
        parts = list(map(int, duration_str.split(':')))
        if len(parts) == 3:  # 时:分:秒
            return parts[0] * 3600 + parts[1] * 60 + parts[2]
        elif len(parts) == 2:  # 分:秒
            return parts[0] * 60 + parts[1]
        elif len(parts) == 1:  # 秒
            return parts[0]
    except:
        logging.warning(f"无法解析时长: {duration_str}")
        
    return 0

def _get_video_type_name(self, type_code: str) -> str:
    """将视频类型代码转换为名称"""
    type_map = {
        "main": "主视频",
        "detail": "细节视频",
        "scene": "场景视频",
        "compare": "对比视频",
        "other": "其他视频"
    }
    return type_map.get(type_code, type_code or "未知类型")

def download_video_cover(self, video_info: Dict, save_dir: str = "./covers/") -> bool:
    """
    下载视频封面图
    :param video_info: 视频信息字典
    :param save_dir: 保存目录
    :return: 是否下载成功
    """
    import os
    from urllib.parse import urlparse
    
    if not video_info.get("cover_url"):
        logging.warning("没有封面图URL")
        return False
        
    # 创建保存目录
    os.makedirs(save_dir, exist_ok=True)
    
    # 生成文件名
    url_path = urlparse(video_info["cover_url"]).path
    ext = os.path.splitext(url_path)[1] or ".jpg"
    filename = f"{video_info['video_id']}{ext}"
    save_path = os.path.join(save_dir, filename)
    
    try:
        response = self.session.get(video_info["cover_url"], timeout=15, stream=True)
        response.raise_for_status()
        
        with open(save_path, 'wb') as f:
            for chunk in response.iter_content(chunk_size=1024):
                if chunk:
                    f.write(chunk)
        
        logging.info(f"封面图已保存至: {save_path}")
        return True
    except Exception as e:
        logging.error(f"下载封面图失败: {str(e)}")
        return False

示例调用 if name == "main": # 替换为实际的appkey和appsecret(从京东开放平台获取) APPKEY = "your_appkey" APPSECRET = "your_appsecret" # 替换为目标商品SKU ID SKU_ID = "100012345678"

# 初始化API客户端
api = JDItemVideoAPI(APPKEY, APPSECRET)

# 获取商品视频
video_result = api.get_item_videos(
    sku_id=SKU_ID,
    # video_type="main",  # 可选,指定视频类型
    need_all=True  # 获取所有视频
)

if video_result:
    print(f"=== 商品视频信息 (SKU: {SKU_ID}) ===")
    print(f"商品标题: {video_result['item_info']['title']}")
    print(f"视频总数: {video_result['item_info']['total_videos']}")
    print(f"视频类型分布: {', '.join([f'{k}: {len(v)}个' for k, v in video_result['videos_by_type'].items()])}\n")
    
    # 打印所有视频信息
    for i, video in enumerate(video_result["videos"], 1):
        print(f"{i}. {video['type_name']}: {video['title']}")
        print(f"   视频ID: {video['video_id']}")
        print(f"   时长: {video['duration']['original']} ({video['duration']['seconds']}秒)")
        print(f"   分辨率: {video['resolution'] or '未知'}")
        print(f"   播放次数: {video['play_count']}")
        print(f"   上传时间: {video['upload_time'] or '未知'}")
        print(f"   可用清晰度: {', '.join(video['urls'].keys())}")
        print(f"   封面图: {'有' if video['cover_url'] else '无'}")
        
        # 下载第一个视频的封面图
        if i == 1:
            api.download_video_cover(video)
            
        print("-" * 100)

三、接口调用注意事项

  1. 调用限制与规范
  • QPS 限制:京东开放平台对视频接口有 QPS 限制,通常为 5-10 次 / 秒
  • 视频权限:部分视频可能受版权保护,仅允许在特定场景下使用
  • 带宽考虑:视频文件较大,批量获取时需考虑带宽和存储成本
  • URL 时效:视频 URL 可能有时效性(通常 24 小时),需及时使用或缓存
  • 合规使用:获取的视频资源需遵守京东平台规定,不得用于非法用途
  1. 常见错误及解决方案
错误码 说明 解决方案
401 未授权或 token 无效 重新获取 access_token
403 权限不足 检查应用是否已申请视频接口权限
404 商品不存在或无视频 确认 sku_id 是否正确,该商品可能没有视频
429 调用频率超限 降低调用频率,实现请求限流
500 服务器内部错误 稍后重试,或联系京东技术支持
10006 视频资源不存在 该商品没有对应类型的视频
  1. 数据解析要点
  • 视频 URL 处理:同一视频可能有多个清晰度版本,需根据需求选择
  • 时长转换:将原始时长字符串转换为秒数便于比较和筛选
  • 视频类型识别:不同类型视频有不同用途,需正确分类
  • 封面图下载:封面图是视频内容的重要预览,建议同步获取
  • 异常处理:部分视频可能缺少某些字段(如时长、分辨率),需做容错处理

四、应用场景与扩展建议 典型应用场景

  • 商品内容聚合平台:整合多平台商品视频,提供丰富的商品展示
  • 竞品视频分析系统:分析竞争对手的商品视频内容策略
  • 电商内容质量评估:评估商品视频的完整性和质量
  • 视频推荐系统:基于视频内容为用户推荐相关商品 扩展建议
  • 实现视频下载功能:支持按清晰度下载视频(需遵守版权规定)
  • 视频内容分析:结合 AI 技术提取视频中的商品特征和关键词
  • 视频质量评估:自动评估视频分辨率、时长等质量指标
  • 多平台视频对比:对比同一商品在不同平台的视频展示策略
  • 视频内容检索:构建视频内容索引,支持按内容搜索相关商品视频 通过合理使用京东 item_video 接口,开发者可以获取丰富的商品视频资源,为商品展示、内容分析和用户体验提升提供有力支持。使用时需遵守京东开放平台的相关规定和版权要求,确保视频资源的合法使用

Trae实现的前端路线象限图

2025年9月4日 19:11

前言

你是否还在找前端学习图,来看看我整理的这份图吧,前端学习路线图,从入门到进阶,一网打尽!再也不用担心找不到前端学习路线了!

我决定使用象限图来展示前端学习路线图,因为象限图可以更好地展示前端学习路线的广度和深度。那么怎么做这个象限图呢?那当然是使用Trae,看看Trae的象限图效果吧!

向Trae提问

image-20250904190409616

前端学习路线图

象限图分为四部分,分别是:

  • 现在·高价值:React + Hooks、TypeScript、工程化(Vite/ESBuild)、性能与稳定性、监控与埋点、数据可视化、可访问性/国际化。理由:对求职即时增益最大,也是大中型前端项目的通用基座。
  • 现在·低价值:IE Hack、jQuery 细节、老旧脚手架的深度技能。理由:生态已淡出主流,不建议投入大量时间。
  • 未来·高价值:WebGPU、WASM/Rust、边缘渲染/SSR、前端侧 AI、CRDT/实时协作、微前端2.0/模块联邦、PWA。理由:面向前端能力边界和业务形态演进,适合中长期投入。
  • 未来·低价值:小众 CSS 魔法、重复性样式搬砖。理由:投入产出比低,适合点到为止。

悬浮上去即可看到每一点的具体内容 image-20250904190217788image-20250904190934132

image-20250904190939850

Trae关键代码解析

  • 高 DPI 适配(一次性设置坐标变换,消除模糊)
const dpr = Math.max(1, Math.floor
(window.devicePixelRatio || 1));
canvas.width = rect.width * dpr; 
canvas.height = rect.height * dpr;
canvas.style.width = rect.width+'px'; 
canvas.style.height = rect.height
+'px';
ctx.setTransform(dpr,0,0,dpr,0,0); // 
HiDPI 一次性缩放
  • 归一化坐标到像素映射(统一入口,确保布局与交互一致)
const pad = {l:80, r:60, t:60, b:64};
function map(p){
  const w = canvas.clientWidth, h = 
  canvas.clientHeight;
  const x0 = pad.l, y0 = pad.t, cw = 
  w - pad.l - pad.r, ch = h - pad.t - 
  pad.b;
  return {x: x0 + p.x * cw, y: y0 + p.
  y * ch, x0, y0, cw, ch, w, h};
}
  • 绘制网格、中线与象限标题(一次 path,减少状态切换)
const {x0,y0,cw,ch} = map({x:0,y:0});
ctx.fillStyle = '#161b3d'; ctx.
fillRect(x0,y0,cw,ch);
ctx.strokeStyle = '#8fa3ff33'; ctx.
lineWidth = 1;
ctx.strokeRect(x0+.5,y0+.5,cw-1,ch-1);
ctx.beginPath();
ctx.moveTo(x0+cw/2+.5, y0); ctx.lineTo
(x0+cw/2+.5, y0+ch);
ctx.moveTo(x0, y0+ch/2+.5); ctx.lineTo
(x0+cw, y0+ch/2+.5);
ctx.stroke();
  • 技能点数据与绘制(归一化数据驱动,可随时增删)
const skills = [  {name:'React + Hooks', x:.86, y:.  10, color:'var(--nowHi)'},  {name:'WebGPU', x:.88, y:.74,   color:'var(--futureHi)'},  {name:'jQuery 细节', x:.22, y:.10,   color:'var(--low)'},  // ...];
for(const p of skills){ const {x,y} = 
map(p); drawPoint(x,y,p.color); }
  • 悬停命中与气泡提示(简单半径检测,轻量无依赖)
canvas.addEventListener('mousemove', 
(e)=>{
  const rect = canvas.
  getBoundingClientRect();
  const mx = e.clientX - rect.left, 
  my = e.clientY - rect.top;
  let found = -1;
  for(let i=0;i<skills.length;i++){
    const {x0,y0,cw,ch} = map({x:0,
    y:0});
    const cx = x0 + skills[i].x*cw, 
    cy = y0 + skills[i].y*ch;
    if((mx-cx)*(mx-cx)+(my-cy)*
    (my-cy) <= 64){ found=i
    break; } // 命中半径 8
  }
  if(found>=0){ showTip(mx,my,skills
  [found]); } else { hideTip(); }
});

为什么Trae要用 Canvas 而非 DOM/SVG

  • 自绘坐标体系,在 HiDPI 下保持锐利;大量点/实时交互时更稳定高效。
  • 渲染逻辑集中、无额外布局计算,易于扩展(拖拽、缩放、区域选择都可按统一坐标处理)。

这个象限的路线图,如何定制你的专属路线

  • 直接在 skills 数组中增删或移动点(修改 x/y 即可),颜色规范 nowHi/futureHi/low 已预设。
  • 可为每个点扩展字段如 “学习资源链接、目标周数、前置技能”,在 showTip 中拼接展示。

总结

象限图是极好的可视化工具,但需注意:

  1. 每个象限的划分标准,需结合个人情况。
  2. 每个象限的技能点,需结合个人情况。

这次Trae的表现可圈可点,但是也有一些不足的地方,比如:Trae绘制象限图,内容不是适合每个人,需要根据自己的实际情况来定制。

快来试试Trae的象限图,看看你适合哪个象限,然后说出的想法,让Trae定制你的专属路线吧!

Trae实现动漫人物眼睛跟随鼠标移动的登录页面

2025年9月3日 18:50

前言

你是否看到一个登录页面,它的背景是一个充满活力的动漫人物,她的眼睛会跟随你的鼠标移动,仿佛在注视着你。

这个页面不仅美观,而且富有互动性,让人眼前一亮。那么,这个页面是如何实现的呢?让我们一起来探讨一下。

在这个看似简单的登录页面背后,隐藏着前端技术的精妙运用。通过纯CSS和原生JavaScript,我们创造了一个具有生命力的动漫人物,她的眼睛能够实时追踪鼠标移动,为冰冷的登录界面注入了灵魂。

我们给Trae 简单的提示,看看Trae能不能实现这个效果。 等待几分钟之后,Trae 生成的效果

动漫任务向右下角看的效果 image-20250903183916722 动漫任务向左下角看的效果 image-20250903183922320 动漫任务向上看的效果 image-20250903183929032

Trae核心代码解读

鼠标跟随动画的实现原理非常简单,就是通过监听鼠标移动事件,获取鼠标的坐标,然后根据坐标计算出眼睛的位置,最后将眼睛的位置应用到页面上。

眼睛跟随鼠标的核心算法,主要是使用了三角函数来计算角度和距离,然后根据角度和距离来计算瞳孔的位置。

document.addEventListener('mousemove', (e) => {
    const eyes = document.querySelectorAll('.eye');

    eyes.forEach(eye => {
        const pupil = eye.querySelector('.pupil');
        const eyeRect = eye.getBoundingClientRect();
        
        // 计算眼睛中心点坐标
        const eyeCenterX = eyeRect.left + eyeRect.width / 2;
        const eyeCenterY = eyeRect.top + eyeRect.height / 2;
        
        // 使用三角函数计算角度和距离
        const angle = Math.atan2(e.clientY - eyeCenterY, e.clientX - eyeCenterX);
        const maxDistance = 8; // 限制瞳孔移动范围
        const distance = Math.min(maxDistance, Math.hypot(e.clientX - eyeCenterX, e.clientY - eyeCenterY) / 5);
        
        // 计算瞳孔新位置
        const pupilX = Math.cos(angle) * distance;
        const pupilY = Math.sin(angle) * distance;
        
        // 应用平滑变换
        pupil.style.transform = `translate(calc(-50% + ${pupilX}px), calc(-50% + ${pupilY}px))`;
    });
});

毛玻璃效果,主要是使用了CSS的backdrop-filter属性来实现。让登录表单的背景模糊,从而让表单上面的动漫人物更加生动形象。

.login-container {
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(10px);
    border-radius: 20px;
    box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
    border: 1px solid rgba(255, 255, 255, 0.18);
}

表单提交处理,这里Trae只是简单的模拟登录成功,实际项目中需要根据项目的需求来处理表单提交,比如发送请求到后端,验证用户信息,等等。

document.querySelector('form').addEventListener('submit', (e) => {
    e.preventDefault();
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;

    if (username && password) {
        // 模拟登录成功
        alert('登录成功!欢迎 ' + username);
    } else {
        alert('请填写完整信息');
    }
});

实现这个效果的意义

传统的登录页面往往冰冷机械,而Trae AI创造的动漫人物登录界面,标志着人机交互从功能性向情感化的重大转变。当用户看到动漫人物的眼睛随着鼠标移动而转动时,会产生一种被关注和理解的感觉,这种微妙的心理效应能够显著提升用户体验。

总结

通过结合CSS动画、JavaScript事件处理和毛玻璃效果,我们实现了一个具有动态交互和视觉吸引力的动漫人物登录界面。这不仅增加了页面的趣味性,也提升了用户的参与度和满意度。

Trae的表现也是值得称赞的,他通过精心设计的动画和视觉效果,成功地将动漫人物与登录功能结合,创造出了一种全新的用户体验,虽然个别网站已经实现了,但是作为前端无需在自己找代码,或者自己手搓,Trae AI 已经帮我们实现了,我们只需要简单的替换或者是修改,就可以把这个效果融入到我们的项目里面,快点来试试Trae,帮你实现这个眼睛跟随鼠标移动的效果吧。

❌
❌