1. 项目概述:为什么需要分布式爬取亚马逊榜单?
做数据抓取的朋友,尤其是盯上亚马逊这类大型电商平台的,估计都经历过单机爬虫的“阵痛期”。当你兴致勃勃地写了个Scrapy爬虫,准备把亚马逊Best Sellers排行榜(我们简称Best榜)的数据一网打尽时,最初可能很顺利。但随着抓取深度的增加——比如不仅要榜单列表,还要每个商品详情页的评论、价格历史、库存状态——问题就来了。IP被封、请求速率限制、海量URL管理混乱、一台机器跑几天几夜也抓不完…… 这就像一个人想搬空一个巨型仓库,效率低下且容易累垮。
这时候,“分布式爬虫”就成了必然选择。而scrapy-redis这个组件,正是将Scrapy这个强大的爬虫框架,与Redis这个高性能的内存数据库连接起来的桥梁,让多个爬虫实例可以协同工作。简单说,它解决了两个核心问题:统一的任务调度(去重与分发)和统一的数据存储。所有爬虫从同一个Redis队列里领取任务(URL),并将抓取到的数据推送到同一个Redis管道,再由一个统一的进程进行后续处理(如存入数据库)。这样,你可以轻松地增加爬虫节点(机器)来提升抓取速度,并且单个节点的失效不会导致任务丢失。
抓取亚马逊Best榜,恰恰是分布式爬虫一个非常典型且高价值的应用场景。这个榜单更新频繁,品类繁多,商品数量巨大,且页面结构复杂,反爬措施严格。通过分布式架构,我们不仅能以更快的速度获取全量数据,还能更稳定地应对IP封锁等问题,实现7x24小时不间断的监控。接下来,我就结合自己搭建这套系统的实际经验,从设计思路到避坑细节,完整地拆解一遍。
2. 核心架构与组件选型解析
在动手写代码之前,理清架构是关键。一个基于scrapy-redis的分布式爬虫系统,通常由以下几部分组成:
2.1 核心组件职责分工
Redis服务器:系统的“大脑”和“中枢神经”。它主要负责:
- 请求队列:存储所有待抓取的URL(Scrapy中的Request对象)。
scrapy-redis默认使用PriorityQueue(优先级队列),确保任务有序执行。 - 请求去重指纹集合:利用Redis的Set类型,存储所有已调度URL的指纹(通常是SHA1哈希),实现高效的全局去重,避免多个爬虫节点重复抓取同一页面。
- 数据管道:临时存储爬虫抓取到的Item数据,等待后续的持久化处理。
- 统计信息:存储爬虫运行状态,如已抓取数量、剩余请求数等。
- 请求队列:存储所有待抓取的URL(Scrapy中的Request对象)。
多个Scrapy爬虫节点(Worker):系统的“四肢”。这些是运行在不同机器或进程上的、一模一样的Scrapy爬虫项目。它们不再拥有自己的本地任务队列,而是:
- 从共享的Redis请求队列中获取下一个要抓取的URL。
- 执行网页下载、解析。
- 将解析出的新URL(比如“下一页”或商品详情链接)的Request对象,放回共享的Redis请求队列。
- 将解析出的数据Item,推送到共享的Redis数据管道。
数据后处理进程(可选):系统的“消化系统”。这是一个独立的进程(可以用Scrapy的
Item Pipeline扩展,也可以单独写脚本),持续监听Redis中的数据管道,将Item取出并进行后续操作,如清洗、验证、存入MySQL/MongoDB等数据库。
2.2 为什么是scrapy-redis而不是其他?
分布式爬虫方案有很多,比如纯手工用消息队列(RabbitMQ, Kafka)自己封装调度器。但scrapy-redis有不可替代的优势:
- 与Scrapy无缝集成:它通过重写Scrapy的
Scheduler(调度器)和DupeFilter(去重过滤器),几乎零成本地将一个普通Scrapy项目改造为分布式。你绝大部分的爬虫代码(Spider, Item, Pipeline)都不需要改变。 - 开箱即用的健壮性:它已经处理了分布式环境下的很多细节,比如连接重试、空队列等待、数据序列化等。自己从头实现一套,容易在边界条件上出错。
- 社区成熟,资料丰富:作为最经典的Scrapy分布式方案,遇到问题很容易找到解决方案和社区讨论。
关于Redis版本的选择:建议使用Redis 5.0及以上版本。它更稳定,功能也更完善。对于这个项目,我们主要用到其基础的数据结构功能,无需特殊模块。
3. 环境准备与项目初始化
3.1 基础环境搭建
假设我们使用两台Linux服务器(可以是云主机)作为爬虫节点,一台作为Redis服务器(也可以与其中一台爬虫节点共用,但生产环境建议分离)。
在Redis服务器上:
# 安装Redis sudo apt-get update sudo apt-get install redis-server -y # 编辑配置文件,允许远程连接并设置密码(强烈建议) sudo vim /etc/redis/redis.conf需要修改的关键配置:
bind 0.0.0.0 # 监听所有网络接口,允许远程连接 protected-mode no # 关闭保护模式(在配置了bind和密码后更安全) requirepass your_strong_password_here # 设置一个强密码 daemonize yes # 以守护进程运行# 重启Redis使配置生效 sudo systemctl restart redis-server # 检查是否运行并监听6379端口 sudo netstat -tlnp | grep 6379在所有爬虫节点上:
# 安装Python3、pip及虚拟环境工具 sudo apt-get install python3 python3-pip -y pip3 install virtualenv # 创建项目目录并进入 mkdir -p ~/projects/amazon_distributed_spider cd ~/projects/amazon_distributed_spider # 创建虚拟环境并激活 virtualenv venv source venv/bin/activate # 安装核心依赖 pip install scrapy scrapy-redis redis注意:
scrapy-redis库的活跃度需要关注。如果遇到安装或兼容性问题,可以指定稍旧但稳定的版本,如pip install scrapy-redis==0.7.2。同时,根据亚马逊页面的实际情况,你可能还需要安装用于解析动态内容的库,如selenium或playwright,但初期建议先尝试用scrapy直接抓取,因为Best榜页面大多是静态或服务端渲染的。
3.2 创建Scrapy项目并集成scrapy-redis
# 在虚拟环境中,创建标准的Scrapy项目 scrapy startproject amazon_best cd amazon_best接下来是改造项目的核心步骤。我们不需要修改爬虫逻辑,但要修改配置文件settings.py和创建基于RedisSpider的爬虫文件。
修改amazon_best/settings.py:
# 启用scrapy-redis调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 启用scrapy-redis去重过滤器 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 指定Redis服务器连接信息 REDIS_HOST = '你的Redis服务器IP地址' # 例如 '192.168.1.100' REDIS_PORT = 6379 REDIS_PASSWORD = 'your_strong_password_here' # 与redis.conf中设置的一致 # 是否在爬虫关闭时保留Redis中的请求队列和去重集合。 # 设为True时,暂停爬虫后,下次启动会接着上次的进度爬。非常适合长期运行的监控爬虫。 SCHEDULER_PERSIST = True # 使用优先级队列进行调度(默认) SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue' # 将爬取到的Item暂存到Redis中,供后续处理 ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 300, } # 非常重要:设置一个唯一的项目名,用于在Redis中生成key的前缀 # 这允许多个不同的爬虫项目使用同一个Redis服务器而互不干扰 REDIS_PARAMS = { 'password': REDIS_PASSWORD, } REDIS_KEY_PREFIX = 'amazon_best:spider' # 调整Scrapy其他设置以适应分布式和反爬 CONCURRENT_REQUESTS = 32 # 全局并发请求数,可根据机器和网络调整 DOWNLOAD_DELAY = 1.5 # 下载延迟,礼貌爬取,避免被封。分布式下每个节点都应遵守。 ROBOTSTXT_OBEY = False # 亚马逊的robots.txt通常禁止爬虫,我们需要谨慎地忽略它 USER_AGENT = '你的自定义User-Agent字符串' # 务必设置一个真实的浏览器UA DOWNLOADER_MIDDLEWARES = { # 可以在这里添加代理中间件、重试中间件等 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 90, } RETRY_TIMES = 5 # 失败重试次数4. 爬虫(Spider)逻辑设计与实现
这是项目的核心业务逻辑。我们的目标是抓取亚马逊某个站点(如 amazon.com)的Best Sellers排行榜。
4.1 分析目标页面结构
以https://www.amazon.com/Best-Sellers/zgbs为例。通常,Best榜页面是分页的,并且有清晰的品类树状结构。我们的爬虫策略可以采用“广度优先”:
- 入口:从总榜页面或几个顶级品类页面开始。
- 解析列表页:提取当前页面上所有商品的ASIN(亚马逊标准识别号)和详情页链接,并生成商品详情页的抓取任务。
- 翻页:找到“下一页”的链接,将其作为新的请求放回队列。
- 解析详情页:从商品详情页提取我们需要的具体信息,如标题、价格、评分、评论数、卖家信息等。
- 品类挖掘:同时,从页面侧边栏或底部提取子品类的链接,作为新的列表页抓取起点,从而实现全品类覆盖。
4.2 编写RedisSpider
在amazon_best/spiders/目录下创建amazon_best_spider.py。
import scrapy from scrapy_redis.spiders import RedisSpider from amazon_best.items import AmazonBestItem # 需要先定义Item from urllib.parse import urljoin, urlparse import re class AmazonBestSpider(RedisSpider): """ 继承自RedisSpider。 关键区别:它不会从`start_urls`读取初始URL,而是从Redis列表中读取。 初始URL需要通过`redis-cli`手动推送到Redis队列,或者通过`start_urls`属性配合`next_requests`方法实现批量初始化。 """ name = 'amazon_best' # 爬虫名称,必须唯一 redis_key = 'amazon_best:start_urls' # 用于从Redis读取起始URL的Key # 如果你有一批确定的起始URL,也可以在这里定义,爬虫启动时会自动将它们推送到Redis队列。 # 但更灵活的方式是通过外部脚本或redis-cli推送。 # start_urls = ['https://www.amazon.com/Best-Sellers/zgbs'] def parse(self, response): """ 解析Best Sellers列表页(如 https://www.amazon.com/Best-Sellers/zgbs) """ # 1. 提取当前页面的商品卡片 product_cards = response.css('#zg-ordered-list li.zg-item-immersion') self.logger.info(f'Parsing list page: {response.url}, found {len(product_cards)} products.') for card in product_cards: # 提取ASIN和详情页链接 # 亚马逊的链接结构可能变化,需要根据实际情况调整选择器 relative_url = card.css('a.a-link-normal::attr(href)').get() if relative_url: product_url = urljoin('https://www.amazon.com', relative_url) # 从URL中提取ASIN,商品详情页URL通常包含`/dp/ASIN/`或`/gp/product/ASIN/` asin_match = re.search(r'/dp/([A-Z0-9]{10})', product_url) or re.search(r'/gp/product/([A-Z0-9]{10})', product_url) asin = asin_match.group(1) if asin_match else None if asin: # 构造一个请求去抓取商品详情页,回调函数为`parse_product` # 注意:这里使用`meta`传递ASIN等信息,避免在回调中再次解析 yield scrapy.Request( url=product_url, callback=self.parse_product, meta={'asin': asin, 'category': response.meta.get('category', 'Unknown')}, # 可以在这里添加代理、优先级等参数 priority=10, # 商品详情页优先级可以设高一些 ) # 2. 处理翻页:查找“下一页”按钮 next_page_url = response.css('li.a-last a::attr(href)').get() if next_page_url: next_page_full_url = urljoin(response.url, next_page_url) # 将下一页的请求放回Redis队列,优先级可以设低一些 yield scrapy.Request( url=next_page_full_url, callback=self.parse, # 回调自己,继续解析列表页 priority=1, meta={'category': response.meta.get('category', 'Unknown')} ) # 3. 提取子品类链接,扩展抓取范围 sub_category_links = response.css('#zg_browseRoot ul a::attr(href)').getall() for link in sub_category_links: full_link = urljoin(response.url, link) # 可以在这里对链接进行去重判断,或者依赖Redis的全局去重 # 生成新的列表页请求 category_name = link.split('/')[-1] # 简单提取品类名 yield scrapy.Request( url=full_link, callback=self.parse, meta={'category': category_name}, priority=5 ) def parse_product(self, response): """ 解析商品详情页 """ item = AmazonBestItem() item['asin'] = response.meta['asin'] item['url'] = response.url item['category'] = response.meta['category'] # 使用CSS选择器或XPath提取数据,这里需要根据亚马逊实际页面结构调整 # 示例选择器,可能已过时,务必自行检查更新 item['title'] = response.css('#productTitle::text').get('').strip() price_whole = response.css('.a-price-whole::text').get('').strip().replace(',', '') price_fraction = response.css('.a-price-fraction::text').get('') item['price'] = f"{price_whole}.{price_fraction}" if price_whole else None item['rating'] = response.css('#acrPopover::attr(title)').get('').split()[0] # 如 “4.5 out of 5 stars” item['review_count'] = response.css('#acrCustomerReviewText::text').get('').split()[0].replace(',', '') item['seller'] = response.css('#merchantInfo .a-size-small::text').get('') item['rank'] = response.css('#productDetails_detailBullets_sections1 tr:contains("Best Sellers Rank") td::text').get('') item['crawl_time'] = datetime.datetime.utcnow().isoformat() # 记录抓取时间 # 处理可能缺失的字段 for key in item.fields: item.setdefault(key, None) self.logger.debug(f'Parsed product: {item["asin"]} - {item["title"][:50]}...') yield item # 这个Item会被scrapy-redis的RedisPipeline推送到Redis # 可选:进一步抓取评论页 # reviews_url = response.css('a[data-hook="see-all-reviews-link"]::attr(href)').get() # if reviews_url: # yield scrapy.Request(url=urljoin(response.url, reviews_url), # callback=self.parse_reviews, # meta={'asin': item['asin']})4.3 定义数据模型(Item)
在amazon_best/items.py中定义我们要抓取的数据结构。
import scrapy class AmazonBestItem(scrapy.Item): # 商品唯一标识 asin = scrapy.Field() # 商品URL url = scrapy.Field() # 所属品类 category = scrapy.Field() # 商品标题 title = scrapy.Field() # 当前价格 price = scrapy.Field() # 评分(星级) rating = scrapy.Field() # 评论数量 review_count = scrapy.Field() # 卖家信息 seller = scrapy.Field() # 销售排名 rank = scrapy.Field() # 抓取时间戳 crawl_time = scrapy.Field() # 可以继续添加其他字段,如图片URL、描述、规格等5. 分布式部署与运行实战
5.1 启动Redis并注入种子URL
首先,确保Redis服务器正在运行。然后,我们需要将初始的爬虫入口URL推送到Redis队列中。可以在任意一台能连接Redis的机器上操作。
方法一:使用redis-cli命令行(简单测试)
redis-cli -h your_redis_host -p 6379 -a your_password # 进入交互界面后,使用LPUSH命令将URL推送到爬虫定义的`redis_key`中 LPUSH amazon_best:start_urls "https://www.amazon.com/Best-Sellers/zgbs" LPUSH amazon_best:start_urls "https://www.amazon.com/Best-Sellers-Electronics/zgbs/electronics" # 可以推送多个起始URL方法二:使用Python脚本(推荐,便于管理)创建一个seed_urls.py脚本:
import redis redis_client = redis.Redis( host='your_redis_host', port=6379, password='your_password', decode_responses=True # 返回字符串而不是bytes ) start_urls = [ 'https://www.amazon.com/Best-Sellers/zgbs', 'https://www.amazon.com/Best-Sellers-Electronics/zgbs/electronics', 'https://www.amazon.com/Best-Sellers-Home-Kitchen/zgbs/home-garden', # ... 添加更多品类入口 ] key = 'amazon_best:start_urls' for url in start_urls: redis_client.lpush(key, url) print(f'Pushed: {url}') print(f'Total {len(start_urls)} seed URLs pushed to Redis key: {key}')5.2 在多台机器上启动爬虫节点
在第一台爬虫服务器上:
cd ~/projects/amazon_distributed_spider/amazon_best source venv/bin/activate # 使用 `scrapy crawl` 命令启动爬虫,`-s` 参数可以覆盖settings.py中的设置(如果需要) scrapy crawl amazon_best -s LOG_FILE=node1.log在第二台爬虫服务器上,执行完全相同的命令(确保项目代码和虚拟环境一致):
cd ~/projects/amazon_distributed_spider/amazon_best source venv/bin/activate scrapy crawl amazon_best -s LOG_FILE=node2.log现在,两个爬虫节点都会连接到同一个Redis服务器,从amazon_best:requests队列中争抢任务(URL),并将抓取到的Item放入amazon_best:items队列。你可以通过查看日志文件node1.log和node2.log来观察它们的抓取进度。
5.3 监控爬虫状态
查看Redis中的关键信息:
redis-cli -h your_redis_host -a your_password # 查看待抓取请求队列长度(通常这个key是 `爬虫名:requests`) LLEN amazon_best:spider:requests # 查看已抓取Item队列长度(key是 `爬虫名:items`) LLEN amazon_best:spider:items # 查看去重集合的大小(已调度过的唯一请求数) SCARD amazon_best:spider:dupefilter通过Scrapy内置的Telnet控制台(可选):在settings.py中启用TELNETCONSOLE_ENABLED = True,然后可以在爬虫运行时,通过telnet localhost 6023连接,使用est()命令查看引擎状态。
6. 数据持久化与后处理
爬虫节点只负责抓取和解析,并将Item推送到Redis。我们需要另一个独立的进程来消费这些Item,并将其保存到数据库(如MySQL、MongoDB、CSV文件等)。
6.1 编写数据消费脚本
创建一个item_consumer.py脚本,运行在任意一台能连接Redis的机器上。
import redis import json import pymysql # 以MySQL为例,也可以使用pymongo, sqlite3等 from datetime import datetime import time import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class ItemConsumer: def __init__(self, redis_host, redis_port, redis_password, db_config): self.redis_client = redis.Redis( host=redis_host, port=redis_port, password=redis_password, decode_responses=True ) self.items_key = 'amazon_best:spider:items' # 与settings.py中的前缀对应 # 初始化数据库连接 self.db_connection = pymysql.connect(**db_config) self.cursor = self.db_connection.cursor() self._create_table_if_not_exists() def _create_table_if_not_exists(self): create_table_sql = """ CREATE TABLE IF NOT EXISTS amazon_best_sellers ( id INT AUTO_INCREMENT PRIMARY KEY, asin VARCHAR(20) NOT NULL UNIQUE, url TEXT, category VARCHAR(255), title TEXT, price DECIMAL(10, 2), rating FLOAT, review_count INT, seller VARCHAR(255), `rank` TEXT, crawl_time DATETIME, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_asin (asin), INDEX idx_category (category), INDEX idx_crawl_time (crawl_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """ try: self.cursor.execute(create_table_sql) self.db_connection.commit() logger.info("Table checked/created successfully.") except Exception as e: logger.error(f"Failed to create table: {e}") self.db_connection.rollback() def process_item(self, item_dict): """处理单个Item,插入数据库""" # 数据清洗和转换 try: price = float(item_dict.get('price', 0)) if item_dict.get('price') else None rating = float(item_dict.get('rating', 0).split()[0]) if item_dict.get('rating') else None review_count = int(item_dict.get('review_count', '0').replace(',', '')) if item_dict.get('review_count') else 0 crawl_time = datetime.fromisoformat(item_dict.get('crawl_time')) if item_dict.get('crawl_time') else datetime.utcnow() except (ValueError, AttributeError) as e: logger.warning(f"Data conversion error for ASIN {item_dict.get('asin')}: {e}. Using defaults.") price = rating = None review_count = 0 crawl_time = datetime.utcnow() insert_sql = """ INSERT INTO amazon_best_sellers (asin, url, category, title, price, rating, review_count, seller, `rank`, crawl_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE url = VALUES(url), category = VALUES(category), title = VALUES(title), price = VALUES(price), rating = VALUES(rating), review_count = VALUES(review_count), seller = VALUES(seller), `rank` = VALUES(`rank`), crawl_time = VALUES(crawl_time) """ try: self.cursor.execute(insert_sql, ( item_dict.get('asin'), item_dict.get('url'), item_dict.get('category'), item_dict.get('title'), price, rating, review_count, item_dict.get('seller'), item_dict.get('rank'), crawl_time )) self.db_connection.commit() logger.info(f"Item saved/updated: ASIN {item_dict.get('asin')}") return True except pymysql.Error as e: logger.error(f"Failed to insert item {item_dict.get('asin')}: {e}") self.db_connection.rollback() return False def run(self): """主循环,持续从Redis中取出并处理Item""" logger.info(f"Starting item consumer, listening on Redis key: {self.items_key}") while True: try: # BRPOP是阻塞式弹出,如果队列为空则等待。0表示无限等待。 # 返回一个元组 (key, value) _, item_json = self.redis_client.brpop(self.items_key, timeout=0) if item_json: item_dict = json.loads(item_json) self.process_item(item_dict) except redis.exceptions.ConnectionError as e: logger.error(f"Redis connection error: {e}. Reconnecting in 10 seconds...") time.sleep(10) # 可以在这里添加重连逻辑 except json.JSONDecodeError as e: logger.error(f"Failed to decode JSON from Redis: {e}, data: {item_json[:100]}...") except KeyboardInterrupt: logger.info("Consumer stopped by user.") break except Exception as e: logger.error(f"Unexpected error: {e}") time.sleep(5) self.cursor.close() self.db_connection.close() logger.info("Database connection closed.") if __name__ == '__main__': # 配置信息 REDIS_CONFIG = { 'redis_host': 'your_redis_host', 'redis_port': 6379, 'redis_password': 'your_password', } DB_CONFIG = { 'host': 'your_mysql_host', 'user': 'your_username', 'password': 'your_db_password', 'database': 'spider_data', 'charset': 'utf8mb4', } consumer = ItemConsumer(**REDIS_CONFIG, db_config=DB_CONFIG) consumer.run()6.2 运行消费进程
# 在数据库服务器或另一台机器上运行 cd ~/projects/amazon_distributed_spider source venv/bin/activate python item_consumer.py这个进程会一直运行,监听Redis中的Item队列,并存入MySQL。你可以同时运行多个消费进程来提高处理速度,但要注意数据库写入的并发控制。
7. 高级调优与反爬策略
分布式爬虫搭建起来只是第一步,要让它在亚马逊这种网站上稳定、高效、长期地运行,还需要一系列调优和反爬措施。
7.1 请求管理与去重深度优化
自定义去重规则:
scrapy-redis默认使用请求的指纹(URL + method + body)去重。但对于亚马逊,同一个商品可能有多个URL(如带不同查询参数)。你可能需要重写dupefilter,例如只根据ASIN去重。# 在settings.py中指定自定义的去重类 DUPEFILTER_CLASS = 'amazon_best.dupefilters.ASINDupeFilter'然后在项目中创建
dupefilters.py:from scrapy_redis.dupefilter import RFPDupeFilter from urllib.parse import urlparse, parse_qs import re class ASINDupeFilter(RFPDupeFilter): def request_fingerprint(self, request): # 尝试从URL或meta中提取ASIN作为去重依据 asin = request.meta.get('asin') if not asin: # 从URL中解析ASIN path = urlparse(request.url).path asin_match = re.search(r'/dp/([A-Z0-9]{10})', path) or re.search(r'/gp/product/([A-Z0-9]{10})', path) asin = asin_match.group(1) if asin_match else None # 如果找到了ASIN,就用ASIN生成指纹;否则回退到默认的URL指纹 if asin: return self._fingerprint(asin.encode('utf-8')) return super().request_fingerprint(request)请求优先级管理:在爬虫中,通过
Request(priority=)参数可以控制抓取顺序。通常,详情页(高价值)优先级高于列表页翻页(低价值)。
7.2 应对反爬机制
亚马逊的反爬非常严格,分布式爬虫虽然能分摊请求压力,但仍需谨慎。
- User-Agent轮换:在
settings.py的DOWNLOADER_MIDDLEWARES中启用并配置UserAgentMiddleware,使用一个庞大的UA池。 - IP代理池:这是分布式爬虫应对封IP的核心。你需要一个可靠的代理IP服务,并在中间件中实现代理轮换逻辑。
在# middlewares.py import random class RandomProxyMiddleware: def __init__(self, proxy_list): self.proxy_list = proxy_list @classmethod def from_crawler(cls, crawler): # 从配置文件或外部API加载代理列表 proxy_list = [...] return cls(proxy_list) def process_request(self, request, spider): proxy = random.choice(self.proxy_list) request.meta['proxy'] = proxysettings.py中启用它,并设置较高的优先级。 - 请求速率控制:
DOWNLOAD_DELAY和CONCURRENT_REQUESTS_PER_DOMAIN是关键。即使使用代理,对单一域名的请求频率也不能太高。建议将DOWNLOAD_DELAY设置在1-3秒,并根据代理IP数量调整并发数。 - 处理验证码和重定向:编写中间件检测响应内容是否包含验证码页面或意外重定向,并触发相应的处理逻辑(如更换代理、暂停任务、发送警报)。
7.3 系统监控与告警
- 日志集中管理:将各节点的日志通过
scrapy的LOG_STDOUT和LOG_FILE设置输出到文件,并可以使用像ELK(Elasticsearch, Logstash, Kibana)这样的工具进行集中收集和查看。 - 关键指标监控:
- Redis队列长度(
LLEN):如果持续快速增长,说明消费跟不上生产;如果快速减少到0,可能爬虫停止了或没新任务了。 - 爬虫节点日志中的抓取速度(items/min)和错误率。
- 数据库写入速度。
- Redis队列长度(
- 简易告警:可以写一个定时脚本,检查Redis队列长度或爬虫日志中特定错误关键词,一旦异常就发送邮件或钉钉消息。
8. 常见问题与故障排查实录
在实际部署和运行中,你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方案。
8.1 爬虫节点不抓取任务
现象:启动了爬虫,Redis里也有start_urls,但爬虫日志一直显示DEBUG: Crawled (200)很少或者没有新请求。
排查步骤:
- 检查Redis连接:在爬虫节点的Python交互环境中,尝试连接Redis,看密码、主机、端口是否正确。
- 检查队列Key:确认爬虫
redis_key设置(amazon_best:start_urls)和你推送种子URL的Key是否完全一致(包括前缀)。用redis-cli的KEYS amazon_best:*命令查看所有相关Key。 - 检查调度器状态:
scrapy-redis的待抓取队列Key是爬虫名:requests(如amazon_best:spider:requests)。用LLEN查看其长度。如果种子URL被消费了,但此队列为空,可能是爬虫解析后没有生成新的Request,或者生成的Request因为去重被过滤了。检查爬虫的parse方法逻辑和去重集合大小(SCARD amazon_best:spider:dupefilter)。 - 查看爬虫日志级别:启动时加上
-L DEBUG参数,查看更详细的调度日志。
8.2 数据重复或丢失
现象:数据库里出现了完全相同的记录,或者有些商品明明看到了抓取日志,但数据库里没有。
原因与解决:
- 重复:
- 去重失效:检查自定义的
DupeFilter逻辑是否正确。确保ASIN提取逻辑覆盖所有商品URL模式。 - 爬虫重启导致:如果
SCHEDULER_PERSIST = False,爬虫重启后会清空去重集合,导致重新抓取。对于长期任务,务必设为True。
- 去重失效:检查自定义的
- 丢失:
- Item Pipeline处理失败:检查
item_consumer.py脚本的日志。可能是数据格式错误导致插入数据库失败,脚本应记录错误而不是静默丢弃。 - Redis内存不足:如果Item堆积太多,Redis可能触发淘汰策略或崩溃。监控Redis内存使用情况,确保消费进程能跟上爬取速度。可以增加消费进程数量。
- 网络波动:爬虫节点与Redis之间、消费进程与Redis/数据库之间的网络不稳定,可能导致数据在传输中丢失。增加重试机制和更完善的错误处理。
- Item Pipeline处理失败:检查
8.3 爬虫被封锁(403 Forbidden, 503 Service Unavailable)
现象:大量请求返回403/503状态码,或者返回的是验证码页面。
应对策略:
- 立即降速:大幅增加
DOWNLOAD_DELAY,减少CONCURRENT_REQUESTS。 - 更换代理IP:如果用了代理池,确保当前代理IP是有效的、未被亚马逊封禁的住宅或数据中心IP。立即切换到新的IP段。
- 检查请求头:确保
User-Agent,Accept-Language,Accept-Encoding等头部看起来像一个真实浏览器。可以考虑使用scrapy-fake-useragent库。 - 模拟浏览器行为(最后手段):对于特别顽固的页面,可以考虑在中间件中集成
selenium或playwright。但这会极大降低抓取速度,应仅用于最关键的数据,并严格控制使用频率。最好将其作为备用解析方案,当常规请求多次失败后再触发。
8.4 性能瓶颈分析
当爬虫速度达不到预期时,按以下顺序排查:
| 可能瓶颈点 | 检查方法 | 优化建议 |
|---|---|---|
| 网络延迟 | 在爬虫节点ping Redis服务器和亚马逊。 | 将爬虫节点部署在离目标网站和Redis服务器网络延迟低的区域(如同云厂商同区域)。 |
| Redis性能 | 使用redis-cli --stat或INFO命令查看Redis的CPU、内存、连接数。 | 升级Redis配置,使用连接池,避免频繁创建连接。对于超大规模队列,考虑Redis集群。 |
| 爬虫解析效率 | 分析爬虫日志,计算parse方法执行时间。 | 优化CSS选择器/XPath,避免复杂的字符串处理。将耗时的清洗操作移到后处理消费端。 |
| 数据库写入 | 监控消费进程的日志和数据库CPU/IO。 | 优化数据库表结构、添加索引。消费端采用批量插入(如每100条插入一次)而非逐条插入。 |
| 反爬限制 | 观察请求错误率和响应时间。 | 这是最主要的限速因素。优化代理池质量,实施更精细的请求间隔和并发控制策略。 |
搭建并维护一个针对亚马逊这类大型网站的分布式爬虫,是一个持续迭代和对抗的过程。这套基于scrapy-redis的架构提供了坚实的基础。核心在于理解其“中心调度,分布式执行”的思想,并围绕稳定性、可扩展性和反爬应对这三个目标进行不断优化。从种子URL注入,到多节点爬取,再到数据消费入库,每一个环节都需要根据实际情况进行监控和调整。记住,礼貌爬取、尊重robots.txt(在商业伦理和法律允许的范围内),并准备好随时调整你的策略,因为你的对手——网站的反爬系统——也一直在进化。