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

在当今的电商数据分析领域,高效且合规地获取商品数据至关重要。京东(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. 集成数据库存储,支持更复杂的查询和分析需求

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

相关推荐
美狐美颜sdk12 分钟前
直播APP开发技术架构解析:从音视频流到第三方美颜SDK接入的完整方案
大数据·人工智能·音视频·美颜sdk·美颜api
NiceCloud喜云17 分钟前
腾讯云国际版云数据库选型:MySQL、Redis 怎么按业务架构来判断
数据库·mysql·腾讯云
ew4521826 分钟前
MySQL JOIN 关联语法、差异、标准用法、经典坑点汇总
数据库·mysql
Capricorn198835 分钟前
知芽研究问题看板无法推进?从孵化区到笔记的机制排查指南
大数据·论文阅读·人工智能·笔记·学习方法·论文笔记
凡科网小帆1 小时前
2026小程序商城平台哪家强,小程序商城平台选型怎么做
大数据·小程序·小程序商城平台
Rain的Java大神之路1 小时前
线上接口负载满了如何解决
java·数据库·redis·后端·缓存·面试·架构
Nturmoils1 小时前
给运营导个 CSV,在 ksql 里用 \copy 就够了
数据库
睿本云1 小时前
从“有数据”到“会用数据”,一次标签体系的升级能带来的改变
大数据·人工智能
牛奶咖啡131 小时前
大数据Hadoop运维应用实践——Hadoop平台常见问题与故障汇总、系统调优、网络规划与硬件存储选型
大数据·hadoop·hadoop集群操作系统调优·hadoop集群运维问题汇总·hadoop集群硬件规划·大数据平台网络规划·大数据平台硬件存储选型
ciqingloveless1 小时前
Oracle打开数据库加密
数据库·oracle