在当今的电商数据分析领域,高效且合规地获取商品数据至关重要。京东(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}")
合规性考虑
- API 使用规范:严格遵守API 使用条款,不超过请求频率限制
- 数据用途:确保采集的数据仅用于合法用途,不侵犯用户隐私和平台权益
- 身份认证:始终使用合法申请的 API 密钥进行访问
- 请求频率控制:通过设置合理的延迟控制请求频率,避免给服务器造成过大负担
总结与扩展
本文介绍的 Scrapy 集成 JD API 方案,实现了高效且合规的商品数据采集。该方案的优势在于:
- 合规性高,符合平台规范
- 稳定性好,API 接口相对稳定,减少维护成本
- 可扩展性强,可以根据需要扩展采集的字段和范围
未来可以从以下方面进行扩展:
- 集成代理池,进一步提高采集稳定性
- 实现分布式爬取,提高大规模数据采集效率
- 添加数据清洗和分析模块,实现从采集到分析的完整流程
- 集成数据库存储,支持更复杂的查询和分析需求
通过这种方案,企业和开发者可以在合法合规的前提下,高效获取电商平台数据,为商业决策提供支持。