构建京东商品数据采集服务:API 接口调用与数据落地方案

在电商数据分析、竞品监控和市场调研等场景中,商品数据采集是基础且关键的环节。本文将详细介绍如何构建一套京东商品数据采集服务,涵盖 API 接口调用、数据解析、存储落地及服务化封装的完整流程,并提供可复用的代码实现。

一、方案设计思路

京东商品数据采集服务需解决三个核心问题:数据获取渠道、数据处理机制和存储方案。本方案采用以下架构设计:

  1. 数据来源:通过京东 API 或第三方数据服务接口获取商品数据
  2. 处理层:实现数据清洗、格式转换和字段映射
  3. 存储层:采用 MySQL 存储结构化数据,Redis 缓存热门商品信息
  4. 服务层:封装为可调用的 API 服务,支持定时任务和手动触发

架构优势:兼顾数据实时性与存储效率,同时通过服务化设计提高复用性。

二、前置准备

2.1 开发环境

  • 编程语言:Python 3.8+
  • 依赖库:requests(HTTP 请求)、pymysql(MySQL 连接)、redis-py(Redis 操作)、schedule(定时任务)
  • 数据库:MySQL 8.0、Redis 6.0

2.2 接口准备

京东需申请开发者账号获取api_keyapi_secret。若使用第三方服务,需获取对应 API 密钥。

三、核心功能实现

3.1 API 接口调用模块

京东 API 调用需进行签名验证,以下是通用调用封装:

import requests
import time
import hashlib
import json

class JDAPI:
    def __init__(self, app_key, app_secret, api_url="https://api.jd.com/routerjson"):
        self.app_key = app_key
        self.app_secret = app_secret
        self.api_url = api_url

    def _generate_sign(self, params):
        """生成签名"""
        sorted_params = sorted(params.items(), key=lambda x: x[0])
        sign_str = self.app_secret + ''.join([f"{k}{v}" for k, v in sorted_params]) + self.app_secret
        return hashlib.md5(sign_str.encode()).hexdigest().upper()

    def call(self, method, params=None):
        """调用京东API"""
        if not params:
            params = {}
            
        # 公共参数
        public_params = {
            "app_key": self.app_key,
            "method": method,
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
            "format": "json",
            "v": "2.0",
            "sign_method": "md5"
        }
        
        # 合并参数
        all_params = {**public_params,** params}
        all_params["sign"] = self._generate_sign(all_params)
        
        try:
            response = requests.post(self.api_url, data=all_params, timeout=10)
            result = json.loads(response.text)
            if "error_response" in result:
                print(f"API调用错误: {result['error_response']}")
                return None
            return result
        except Exception as e:
            print(f"请求异常: {str(e)}")
            return None

    def get_product_detail(self, sku_id):
        """获取商品详情"""
        return self.call(
            method="jd.union.open.goods.detail.query",
            params={"skuId": sku_id}
        )

    def search_products(self, keyword, page=1, page_size=20):
        """搜索商品"""
        return self.call(
            method="jd.union.open.goods.search.query",
            params={
                "keyword": keyword,
                "pageIndex": page,
                "pageSize": page_size
            }
        )

3.2 数据处理模块

对 API 返回的原始数据进行清洗和转换,提取关键字段:

class DataProcessor:
    @staticmethod
    def process_product_data(raw_data):
        """处理商品详情数据"""
        if not raw_data or "jd_union_open_goods_detail_query_response" not in raw_data:
            return None
            
        result = raw_data["jd_union_open_goods_detail_query_response"]["result"]
        if not result:
            return None
            
        data = json.loads(result)["data"][0]
        
        # 提取关键字段
        processed = {
            "sku_id": data.get("skuId"),
            "name": data.get("name"),
            "price": data.get("price"),
            "original_price": data.get("originalPrice"),
            "brand_name": data.get("brandName"),
            "category": data.get("categoryInfo", {}).get("cateName"),
            "shop_name": data.get("shopInfo", {}).get("shopName"),
            "comment_count": data.get("commentCount"),
            "good_rate": data.get("goodRate"),
            "image_url": data.get("imageInfo", {}).get("imageList", [{}])[0].get("url"),
            "create_time": time.strftime("%Y-%m-%d %H:%M:%S"),
            "update_time": time.strftime("%Y-%m-%d %H:%M:%S")
        }
        
        return processed

    @staticmethod
    def process_search_data(raw_data):
        """处理搜索结果数据"""
        if not raw_data or "jd_union_open_goods_search_query_response" not in raw_data:
            return []
            
        result = raw_data["jd_union_open_goods_search_query_response"]["result"]
        if not result:
            return []
            
        data_list = json.loads(result)["data"]
        return [DataProcessor.process_product_data({"jd_union_open_goods_detail_query_response": {"result": json.dumps({"data": [item]})}}) 
                for item in data_list]

3.3 数据存储模块

实现 MySQL 存储和 Redis 缓存功能:

import pymysql
from redis import Redis

class DataStorage:
    def __init__(self, mysql_config, redis_config):
        # 初始化MySQL连接
        self.mysql_conn = pymysql.connect(
            host=mysql_config["host"],
            user=mysql_config["user"],
            password=mysql_config["password"],
            database=mysql_config["db"],
            charset="utf8mb4"
        )
        self.mysql_cursor = self.mysql_conn.cursor(pymysql.cursors.DictCursor)
        
        # 初始化Redis连接
        self.redis = Redis(
            host=redis_config["host"],
            port=redis_config["port"],
            password=redis_config.get("password"),
            db=redis_config.get("db", 0)
        )
        
        # 创建商品表
        self._create_product_table()

    def _create_product_table(self):
        """创建商品表"""
        sql = """
        CREATE TABLE IF NOT EXISTS jd_products (
            id INT AUTO_INCREMENT PRIMARY KEY,
            sku_id BIGINT NOT NULL UNIQUE,
            name VARCHAR(255) NOT NULL,
            price DECIMAL(10,2),
            original_price DECIMAL(10,2),
            brand_name VARCHAR(100),
            category VARCHAR(100),
            shop_name VARCHAR(100),
            comment_count INT,
            good_rate DECIMAL(5,2),
            image_url VARCHAR(512),
            create_time DATETIME,
            update_time DATETIME,
            INDEX idx_sku (sku_id),
            INDEX idx_category (category)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
        """
        self.mysql_cursor.execute(sql)
        self.mysql_conn.commit()

    def save_product(self, product_data):
        """保存商品数据到MySQL"""
        if not product_data or "sku_id" not in product_data:
            return False
            
        try:
            # 先查询是否存在
            self.mysql_cursor.execute("SELECT id FROM jd_products WHERE sku_id = %s", (product_data["sku_id"],))
            if self.mysql_cursor.fetchone():
                # 更新数据
                update_fields = [f"{k} = %s" for k in product_data if k not in ["sku_id", "create_time"]]
                sql = f"UPDATE jd_products SET {', '.join(update_fields)} WHERE sku_id = %s"
                values = [product_data[k] for k in product_data if k not in ["sku_id", "create_time"]] + [product_data["sku_id"]]
            else:
                # 插入新数据
                fields = ", ".join(product_data.keys())
                placeholders = ", ".join(["%s"] * len(product_data))
                sql = f"INSERT INTO jd_products ({fields}) VALUES ({placeholders})"
                values = list(product_data.values())
                
            self.mysql_cursor.execute(sql, values)
            self.mysql_conn.commit()
            
            # 缓存到Redis(过期时间2小时)
            self.redis.setex(
                f"jd_product:{product_data['sku_id']}",
                7200,
                json.dumps(product_data, ensure_ascii=False)
            )
            return True
        except Exception as e:
            self.mysql_conn.rollback()
            print(f"存储失败: {str(e)}")
            return False

    def batch_save_products(self, products):
        """批量保存商品数据"""
        success_count = 0
        for product in products:
            if self.save_product(product):
                success_count += 1
        return success_count

    def close(self):
        """关闭连接"""
        self.mysql_cursor.close()
        self.mysql_conn.close()
        self.redis.close()

3.4 服务封装与调度

将上述模块整合为完整服务,并支持定时任务:

import schedule
import time

class JDDataCollectionService:
    def __init__(self, app_key, app_secret, mysql_config, redis_config):
        self.api = JDAPI(app_key, app_secret)
        self.processor = DataProcessor()
        self.storage = DataStorage(mysql_config, redis_config)

    def collect_product_by_sku(self, sku_id):
        """通过SKU采集商品数据"""
        raw_data = self.api.get_product_detail(sku_id)
        if not raw_data:
            return False
        product_data = self.processor.process_product_data(raw_data)
        return self.storage.save_product(product_data)

    def collect_products_by_keyword(self, keyword, pages=1):
        """通过关键词采集商品数据"""
        total = 0
        for page in range(1, pages + 1):
            raw_data = self.api.search_products(keyword, page)
            if not raw_data:
                continue
            products = self.processor.process_search_data(raw_data)
            count = self.storage.batch_save_products(products)
            total += count
            print(f"第{page}页采集完成,成功保存{count}条数据")
            time.sleep(2)  # 避免请求过于频繁
        return total

    def schedule_collection(self, keyword, pages=1, interval=24):
        """定时采集任务"""
        def job():
            print(f"开始定时采集:{keyword}")
            self.collect_products_by_keyword(keyword, pages)
            print(f"定时采集完成:{keyword}")

        # 每interval小时执行一次
        schedule.every(interval).hours.do(job)
        print(f"已设置定时任务:每{interval}小时采集一次关键词'{keyword}'")
        
        # 启动调度
        while True:
            schedule.run_pending()
            time.sleep(60)

    def close(self):
        """关闭服务"""
        self.storage.close()

# 使用示例
if __name__ == "__main__":
    # 配置信息(实际使用时建议通过环境变量或配置文件加载)
    APP_KEY = "your_jd_app_key"
    APP_SECRET = "your_jd_app_secret"
    MYSQL_CONFIG = {
        "host": "localhost",
        "user": "root",
        "password": "password",
        "db": "jd_data"
    }
    REDIS_CONFIG = {
        "host": "localhost",
        "port": 6379,
        "db": 0
    }

    # 初始化服务
    service = JDDataCollectionService(APP_KEY, APP_SECRET, MYSQL_CONFIG, REDIS_CONFIG)
    
    # 1. 采集单个商品
    service.collect_product_by_sku("100012345678")
    
    # 2. 关键词搜索采集(前3页)
    service.collect_products_by_keyword("笔记本电脑", pages=3)
    
    # 3. 启动定时任务(每12小时采集一次手机数据)
    # service.schedule_collection("手机", pages=2, interval=12)
    
    # 关闭服务
    service.close()

四、扩展与优化建议

  1. 反爬策略:增加请求间隔随机化、代理 IP 池,避免触发接口频率限制
  2. 分布式部署:对于大规模采集需求,可采用 Celery 分布式任务队列
  3. 监控告警:添加采集成功率监控和异常告警机制(邮件 / 钉钉)
  4. 数据增量更新:通过比对商品更新时间,实现增量采集而非全量覆盖
  5. 字段扩展:根据业务需求补充采集字段(如促销信息、库存状态等)

五、总结

本文构建的京东商品数据采集服务实现了从 API 调用、数据处理到存储落地的完整流程,通过模块化设计保证了代码的可维护性和可扩展性。实际应用中,需根据京东的接口规范和限流策略调整请求参数,同时做好数据合规性处理,确保采集行为符合平台规定和相关法律法规。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值