Scrapy 集成 JD API:一种高效且合规的商品数据采集方案

发布于:2025-09-13 ⋅ 阅读:(20) ⋅ 点赞:(0)

在当今的电商数据分析领域,高效且合规地获取商品数据至关重要。京东(JD)作为中国领先的电商平台,其商品数据具有极高的商业价值。本文将介绍如何将 Scrapy 框架与 JD API 集成,实现一种高效且合规的商品数据采集方案。

方案背景与优势

传统的网页爬虫存在诸多问题,包括:

  • 容易触发网站反爬机制,导致 IP 被封
  • 网页结构变化频繁,需要频繁维护爬虫
  • 可能存在法律合规风险
  • 数据提取效率低,尤其是对于 JavaScript 动态加载的内容

相比之下,使用API 结合 Scrapy 框架的方案具有以下优势:

  • 合规性高,符合平台的数据使用规范
  • 数据结构稳定,减少维护成本
  • 采集效率高,支持批量获取
  • 数据完整性好,可获取 API 开放的所有字段

准备工作

1. 申请 JD API 访问权限

首先需要注册创建申请相应的 API 权限。具体步骤请参考api文档。

2. 环境搭建

确保已安装必要的依赖:

pip install scrapy requests python-jose

实现方案

1. 项目结构

plaintext

jd_spider/
├── jd_spider/
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders/
│       ├── __init__.py
│       └── jd_product_spider.py
└── scrapy.cfg

2. 核心实现

首先定义数据模型:

import scrapy

class JdProductItem(scrapy.Item):
    # 商品基本信息
    product_id = scrapy.Field()  # 商品ID
    name = scrapy.Field()  # 商品名称
    price = scrapy.Field()  # 商品价格
    original_price = scrapy.Field()  # 原价
    brand = scrapy.Field()  # 品牌
    
    # 库存与销量
    stock = scrapy.Field()  # 库存
    sales_count = scrapy.Field()  # 销量
    
    # 分类信息
    category = scrapy.Field()  # 分类
    category_id = scrapy.Field()  # 分类ID
    
    # 其他信息
    shop_name = scrapy.Field()  # 店铺名称
    shop_id = scrapy.Field()  # 店铺ID
    comments_count = scrapy.Field()  # 评论数
    score = scrapy.Field()  # 评分
    url = scrapy.Field()  # 商品链接
    crawl_time = scrapy.Field()  # 爬取时间

接下来实现 API 请求中间件,处理认证和请求头:

import time
import hashlib
from scrapy import signals
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
import random

class JdApiAuthMiddleware:
    """
    京东API认证中间件,负责处理API请求的签名和认证
    """
    def __init__(self, app_key, app_secret):
        self.app_key = app_key
        self.app_secret = app_secret
        
    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            app_key=crawler.settings.get('JD_APP_KEY'),
            app_secret=crawler.settings.get('JD_APP_SECRET')
        )
    
    def process_request(self, request, spider):
        # 只处理JD API的请求
        if 'api.jd.com' in request.url:
            # 生成时间戳
            timestamp = str(int(time.time() * 1000))
            
            # 获取请求参数
            params = request.meta.get('params', {})
            
            # 添加必要参数
            params['app_key'] = self.app_key
            params['timestamp'] = timestamp
            params['format'] = 'json'
            params['v'] = '1.0'
            params['sign_method'] = 'md5'
            
            # 生成签名
            sign = self.generate_sign(params)
            params['sign'] = sign
            
            # 更新请求URL
            request._set_url(self.build_url(request.url, params))
    
    def generate_sign(self, params):
        """生成签名"""
        # 按照参数名ASCII升序排序
        sorted_params = sorted(params.items(), key=lambda x: x[0])
        
        # 拼接参数
        sign_str = self.app_secret
        for k, v in sorted_params:
            sign_str += f"{k}{v}"
        sign_str += self.app_secret
        
        # MD5加密并转为大写
        return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
    
    def build_url(self, base_url, params):
        """构建完整的URL"""
        params_str = '&'.join([f"{k}={v}" for k, v in params.items()])
        return f"{base_url}?{params_str}"


class RandomUserAgentMiddleware(UserAgentMiddleware):
    """随机User-Agent中间件"""
    def __init__(self, user_agent_list):
        self.user_agent_list = user_agent_list
        
    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            user_agent_list=crawler.settings.get('USER_AGENT_LIST')
        )
    
    def process_request(self, request, spider):
        # 随机选择一个User-Agent
        user_agent = random.choice(self.user_agent_list)
        if user_agent:
            request.headers.setdefault('User-Agent', user_agent)

然后实现核心爬虫:

import json
import time
from datetime import datetime
import scrapy
from jd_spider.items import JdProductItem

class JdProductSpider(scrapy.Spider):
    name = 'jd_product'
    allowed_domains = ['api.jd.com']
    
    # JD API基础URL
    base_api_url = 'https://api.jd.com/routerjson'
    
    # 要爬取的商品分类ID列表
    category_ids = [1316, 919, 652, 12259]  # 示例分类ID
    
    def start_requests(self):
        """开始请求,遍历分类ID"""
        for category_id in self.category_ids:
            # 每个分类从第一页开始
            yield self.build_category_request(category_id, 1)
    
    def build_category_request(self, category_id, page):
        """构建分类商品列表请求"""
        params = {
            'method': 'jingdong.category.goods.get',
            'category_id': category_id,
            'page': page,
            'page_size': self.settings.get('PAGE_SIZE', 20)
        }
        
        return scrapy.Request(
            url=self.base_api_url,
            meta={
                'params': params,
                'category_id': category_id,
                'page': page
            },
            callback=self.parse_category
        )
    
    def parse_category(self, response):
        """解析分类商品列表响应"""
        try:
            result = json.loads(response.text)
            
            # 检查API调用是否成功
            if 'error_response' in result:
                self.logger.error(f"API调用错误: {result['error_response']}")
                return
            
            # 提取商品列表数据
            category_data = result.get('jingdong_category_goods_get_response', {})
            result_data = category_data.get('result', {})
            products = result_data.get('data', [])
            total_count = result_data.get('total_count', 0)
            page = response.meta['page']
            page_size = self.settings.get('PAGE_SIZE', 20)
            category_id = response.meta['category_id']
            
            self.logger.info(f"分类 {category_id} 第 {page} 页,获取到 {len(products)} 个商品,总计 {total_count} 个商品")
            
            # 处理每个商品
            for product in products:
                # 构建商品详情请求
                yield self.build_product_detail_request(product['sku_id'])
            
            # 分页处理
            if page * page_size < total_count:
                # 控制请求频率,避免触发API限制
                time.sleep(self.settings.get('API_REQUEST_DELAY', 1))
                # 发送下一页请求
                yield self.build_category_request(category_id, page + 1)
                
        except Exception as e:
            self.logger.error(f"解析分类响应出错: {str(e)},响应内容: {response.text}")
    
    def build_product_detail_request(self, product_id):
        """构建商品详情请求"""
        params = {
            'method': 'jingdong.product.detail.get',
            'sku_id': product_id
        }
        
        return scrapy.Request(
            url=self.base_api_url,
            meta={'params': params, 'product_id': product_id},
            callback=self.parse_product_detail
        )
    
    def parse_product_detail(self, response):
        """解析商品详情响应"""
        try:
            result = json.loads(response.text)
            
            # 检查API调用是否成功
            if 'error_response' in result:
                self.logger.error(f"商品 {response.meta['product_id']} API调用错误: {result['error_response']}")
                return
            
            # 提取商品详情数据
            product_data = result.get('jingdong_product_detail_get_response', {}).get('result', {})
            
            # 创建商品Item
            item = JdProductItem()
            
            # 填充商品信息
            item['product_id'] = product_data.get('sku_id')
            item['name'] = product_data.get('name')
            item['price'] = product_data.get('price')
            item['original_price'] = product_data.get('market_price')
            item['brand'] = product_data.get('brand_name')
            item['stock'] = product_data.get('stock_num')
            item['sales_count'] = product_data.get('sales_count')
            item['category'] = product_data.get('category_name')
            item['category_id'] = product_data.get('category_id')
            item['shop_name'] = product_data.get('shop_name')
            item['shop_id'] = product_data.get('shop_id')
            item['comments_count'] = product_data.get('comment_count')
            item['score'] = product_data.get('score')
            item['url'] = f"https://item.jd.com/{product_data.get('sku_id')}.html"
            item['crawl_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            
            yield item
            
        except Exception as e:
            self.logger.error(f"解析商品 {response.meta['product_id']} 详情出错: {str(e)},响应内容: {response.text}")

最后配置项目设置:

# JD API 配置
JD_APP_KEY = 'your_app_key'  # 替换为你的app key
JD_APP_SECRET = 'your_app_secret'  # 替换为你的app secret

# 爬虫设置
BOT_NAME = 'jd_spider'
SPIDER_MODULES = ['jd_spider.spiders']
NEWSPIDER_MODULE = 'jd_spider.spiders'

# 并发设置
CONCURRENT_REQUESTS = 4  # 并发请求数
CONCURRENT_REQUESTS_PER_DOMAIN = 2  # 每个域名的并发请求数

# 下载延迟,控制请求频率
DOWNLOAD_DELAY = 2
API_REQUEST_DELAY = 1  # API请求之间的延迟

# 每页商品数量
PAGE_SIZE = 20

# 用户代理列表
USER_AGENT_LIST = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/89.0',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15'
]

# 下载中间件配置
DOWNLOADER_MIDDLEWARES = {
    'jd_spider.middlewares.JdApiAuthMiddleware': 543,
    'jd_spider.middlewares.RandomUserAgentMiddleware': 544,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,  # 禁用默认的UserAgent中间件
}

# 管道配置,可配置保存到数据库或文件
ITEM_PIPELINES = {
    'jd_spider.pipelines.JdSpiderPipeline': 300,
    # 如需保存到CSV,取消下面的注释
    # 'scrapy.pipelines.files.CsvItemExporter': 400,
}

# 日志设置
LOG_LEVEL = 'INFO'
LOG_FILE = 'jd_spider.log'

# 遵守robots.txt规则
ROBOTSTXT_OBEY = True

# 自动限速设置
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0

数据存储

实现一个简单的管道,将数据保存到 JSON 文件:

import json
import os
from datetime import datetime

class JdSpiderPipeline:
    def __init__(self):
        # 创建数据存储目录
        self.data_dir = 'data'
        if not os.path.exists(self.data_dir):
            os.makedirs(self.data_dir)
        
        # 按日期创建数据文件
        date_str = datetime.now().strftime('%Y%m%d')
        self.filename = os.path.join(self.data_dir, f'jd_products_{date_str}.json')
        
        # 打开文件
        self.file = open(self.filename, 'a', encoding='utf-8')
    
    def process_item(self, item, spider):
        """处理每个商品数据"""
        # 将Item转换为字典并写入文件
        line = json.dumps(dict(item), ensure_ascii=False) + '\n'
        self.file.write(line)
        return item
    
    def close_spider(self, spider):
        """爬虫关闭时关闭文件"""
        self.file.close()
        spider.logger.info(f"数据已保存到 {self.filename}")

合规性考虑

  1. API 使用规范:严格遵守API 使用条款,不超过请求频率限制
  2. 数据用途:确保采集的数据仅用于合法用途,不侵犯用户隐私和平台权益
  3. 身份认证:始终使用合法申请的 API 密钥进行访问
  4. 请求频率控制:通过设置合理的延迟控制请求频率,避免给服务器造成过大负担

总结与扩展

本文介绍的 Scrapy 集成 JD API 方案,实现了高效且合规的商品数据采集。该方案的优势在于:

  1. 合规性高,符合平台规范
  2. 稳定性好,API 接口相对稳定,减少维护成本
  3. 可扩展性强,可以根据需要扩展采集的字段和范围

未来可以从以下方面进行扩展:

  1. 集成代理池,进一步提高采集稳定性
  2. 实现分布式爬取,提高大规模数据采集效率
  3. 添加数据清洗和分析模块,实现从采集到分析的完整流程
  4. 集成数据库存储,支持更复杂的查询和分析需求

通过这种方案,企业和开发者可以在合法合规的前提下,高效获取电商平台数据,为商业决策提供支持。