使用 Scrapy 框架定制爬虫中间件接入淘宝 API 采集商品数据

一、引言

在电商数据分析、市场调研等领域,获取淘宝平台上的商品数据是一项常见需求。淘宝提供了 API 接口,允许开发者通过授权的方式获取商品信息。本文将介绍如何使用 Scrapy 框架定制爬虫中间件,实现对淘宝 API 的接入,从而高效地采集商品数据。

二、淘宝 API 概述

淘宝提供了丰富的 API 接口,涵盖商品、交易、用户、营销等多个领域。对于商品数据采集,常用的 API 包括:

  • taobao.search:搜索商品列表
  • taobao.item.get:获取单个商品详情
  • taobao.cats.get:获取商品分类
  • taobao.props.get:获取商品属性

使用淘宝 API 需要先注册账号并获取 **ApiKey**和 ApiSecret。同时,API 调用有频率限制,需要合理控制请求速度。

三、Scrapy 框架与中间件
复制代码
import logging
import time
import random
import hashlib
import hmac
import json
from urllib.parse import urlencode
import requests
from scrapy import signals
from scrapy.exceptions import NotConfigured, IgnoreRequest

class TaobaoAPIMiddleware:
    """淘宝API爬虫中间件,处理API请求和响应"""
    
    def __init__(self, app_key, app_secret, api_url, rate_limit, retry_times):
        self.app_key = app_key
        self.app_secret = app_secret
        self.api_url = api_url
        self.rate_limit = rate_limit
        self.retry_times = retry_times
        self.request_count = 0
        self.last_reset_time = time.time()
        self.logger = logging.getLogger(__name__)

    @classmethod
    def from_crawler(cls, crawler):
        """从配置中获取中间件设置"""
        app_key = crawler.settings.get('TAOBAO_APP_KEY')
        app_secret = crawler.settings.get('TAOBAO_APP_SECRET')
        api_url = crawler.settings.get('TAOBAO_API_URL', 'https://eco.taobao.com/router/rest')
        rate_limit = crawler.settings.getint('TAOBAO_RATE_LIMIT', 500)  # 默认500次/小时
        retry_times = crawler.settings.getint('TAOBAO_RETRY_TIMES', 3)  # 默认重试3次

        if not app_key or not app_secret:
            raise NotConfigured("淘宝API配置缺失:TAOBAO_APP_KEY 和 TAOBAO_APP_SECRET")

        middleware = cls(app_key, app_secret, api_url, rate_limit, retry_times)
        crawler.signals.connect(middleware.spider_closed, signal=signals.spider_closed)
        return middleware

    def process_request(self, request, spider):
        """处理API请求,生成签名并发送"""
        # 检查是否为淘宝API请求
        if not request.meta.get('taobao_api', False):
            return None

        # 检查速率限制
        self._check_rate_limit()

        # 构建API请求参数
        method = request.meta.get('taobao_method')
        if not method:
            self.logger.error("淘宝API请求缺少方法名:taobao_method")
            raise IgnoreRequest

        params = self._build_common_params()
        params.update({
            'method': method,
            **request.meta.get('taobao_params', {})
        })

        # 生成签名
        params['sign'] = self._generate_sign(params)

        try:
            # 发送API请求
            response = requests.post(
                self.api_url,
                data=params,
                timeout=30
            )
            response.raise_for_status()
            
            # 解析JSON响应
            result = response.json()
            request.meta['api_result'] = result
            
            # 检查API返回状态
            if not self._check_api_success(result):
                error_code = result.get('error_response', {}).get('code')
                error_msg = result.get('error_response', {}).get('msg')
                self.logger.warning(f"淘宝API返回错误: {error_code} - {error_msg}")
                
                # 如果是限流错误,暂停一段时间
                if error_code in ('isp.over-quota', 'isp.access-control'):
                    self.logger.warning("API请求达到限流阈值,暂停60秒")
                    time.sleep(60)
                    
                # 重试机制
                retry_times = request.meta.get('taobao_retry_times', 0)
                if retry_times < self.retry_times:
                    self.logger.info(f"准备重试API请求,当前重试次数: {retry_times + 1}")
                    new_request = request.copy()
                    new_request.dont_filter = True
                    new_request.meta['taobao_retry_times'] = retry_times + 1
                    # 随机延迟后重试
                    time.sleep(random.uniform(1, 3))
                    return new_request
                else:
                    self.logger.error("API请求重试次数已达上限")
                    raise IgnoreRequest

        except requests.exceptions.RequestException as e:
            self.logger.error(f"API请求发生异常: {str(e)}")
            # 异常情况下的重试逻辑
            retry_times = request.meta.get('taobao_retry_times', 0)
            if retry_times < self.retry_times:
                self.logger.info(f"准备重试API请求,当前重试次数: {retry_times + 1}")
                new_request = request.copy()
                new_request.dont_filter = True
                new_request.meta['taobao_retry_times'] = retry_times + 1
                # 指数退避策略
                time.sleep(2 ** retry_times + random.random())
                return new_request
            else:
                self.logger.error("API请求重试次数已达上限")
                raise IgnoreRequest

        # 处理正常响应
        return None

    def _build_common_params(self):
        """构建API公共参数"""
        return {
            'app_key': self.app_key,
            'format': 'json',
            'v': '2.0',
            'sign_method': 'hmac',
            'timestamp': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
        }

    def _generate_sign(self, params):
        """生成API请求签名"""
        # 排序参数
        sorted_params = sorted(params.items(), key=lambda x: x[0])
        # 拼接参数
        string_to_sign = self.app_secret
        for k, v in sorted_params:
            string_to_sign += f"{k}{v}"
        string_to_sign += self.app_secret
        
        # HMAC-SHA1加密
        sign = hmac.new(
            self.app_secret.encode('utf-8'),
            string_to_sign.encode('utf-8'),
            hashlib.sha1
        ).hexdigest().upper()
        
        return sign

    def _check_api_success(self, result):
        """检查API响应是否成功"""
        if 'error_response' in result:
            return False
        # 根据具体API调整检查逻辑
        method_name = result.get('method')
        if method_name:
            # 例如:taobao.items.list.get 返回值检查
            if method_name == 'taobao.items.list.get' and 'items_list_response' in result:
                return True
            # 其他API方法的检查逻辑...
        return True

    def _check_rate_limit(self):
        """检查API请求速率限制"""
        current_time = time.time()
        # 如果已经过了1小时,重置计数器
        if current_time - self.last_reset_time > 3600:
            self.request_count = 0
            self.last_reset_time = current_time
        
        # 检查是否超过速率限制
        if self.request_count >= self.rate_limit:
            wait_time = 3600 - (current_time - self.last_reset_time)
            self.logger.warning(f"达到API速率限制,等待{wait_time:.2f}秒")
            time.sleep(wait_time)
            # 重置计数器
            self.request_count = 0
            self.last_reset_time = current_time
        
        # 增加请求计数
        self.request_count += 1

    def spider_closed(self, spider):
        """爬虫关闭时的清理工作"""
        self.logger.info("淘宝API中间件: 爬虫已关闭")    

Scrapy 是一个为了爬取网站数据、提取结构性数据而编写的应用框架。它可以应用在数据挖掘、信息处理或存储历史数据等一系列的程序中。

Scrapy 的架构中,中间件是一个很重要的组件,分为下载中间件 (Downloader Middleware) 和爬虫中间件 (Spider Middleware)。下载中间件可以拦截请求和响应,进行预处理;爬虫中间件则可以处理爬虫的输入 (响应) 和输出 (请求)。

在接入淘宝 API 的场景中,我们可以定制下载中间件,专门处理 API 请求的签名生成、速率控制、错误重试等逻辑,使爬虫代码更加简洁和可维护。

四、定制淘宝 API 中间件

下面是一个完整的淘宝 API 中间件实现,它负责处理 API 请求的签名生成、速率控制和错误重试:

中间件代码见上面 doubaocanvas 中的 taobao_middleware.py

这个中间件具有以下功能:

  1. 参数处理:自动构建 API 请求的公共参数,并根据不同 API 方法添加特定参数
  2. 签名生成:按照淘宝 API 的要求,生成 HMAC-SHA1 签名
  3. 速率控制:根据 API 的 QPS 限制,自动控制请求频率,避免被限流
  4. 错误处理:捕获网络异常和 API 返回的错误,实现智能重试
  5. 数据解析:解析 API 返回的 JSON 数据,并传递给爬虫处理
五、创建淘宝商品爬虫

有了中间件之后,我们可以创建一个简单的爬虫来使用这个中间件:

爬虫代码见上面 doubaocanvas 中的 taobao_spider.py

这个爬虫的工作流程是:

  1. 初始化多个搜索关键词,每个关键词生成多页请求
  2. 通过中间件发送 API 请求获取商品列表
  3. 解析 API 返回的商品数据,提取需要的字段
  4. 生成 Item 对象传递给管道处理
六、数据模型与处理管道

为了存储爬取到的商品数据,我们需要定义数据模型和处理管道:

数据模型和管道代码见上面 doubaocanvas 中的 items.py 和 pipelines.py

这里我们使用 MongoDB 作为数据存储,管道会将商品数据去重并存储到 MongoDB 中。你也可以根据需要扩展管道,添加数据清洗、验证或导出到其他存储系统的功能。

七、项目配置

最后,我们需要在 Scrapy 项目的设置文件中配置中间件和其他参数:

配置代码见上面 doubaocanvas 中的 settings.py

在配置文件中,需要填入你的淘宝 API 的 ApiKey 和 ApiSecret,以及 MongoDB 的连接信息。同时,可以根据 API 的访问权限和性能要求,调整请求并发数、下载延迟等参数。

八、使用与优化建议
  1. 运行爬虫 :在项目根目录下执行命令 scrapy crawl taobao 即可启动爬虫

  2. API 权限:不同的 API 需要不同的权限,需要申请相应的权限

  3. 速率控制:中间件已经实现了基本的速率控制,但不同 API 的 QPS 限制不同,需要根据实际情况调整

  4. 数据量控制:淘宝 API 通常对单次请求返回的数据量有限制,可以通过分页获取更多数据

  5. 异常处理:中间件已经包含了基本的异常处理和重试机制,但在实际使用中可能需要根据具体情况调整

  6. 数据存储:MongoDB 适合存储结构灵活的商品数据,也可以根据需要改用其他数据库

通过以上步骤,你可以建立一个高效、稳定的淘宝商品数据采集系统。定制的中间件使 API 请求处理逻辑与爬虫业务逻辑分离,提高了代码的可维护性和复用性。