1. Scrapy框架高级功能解析
1.1 分布式爬虫架构设计
Scrapy-Redis是构建分布式爬虫系统的核心组件,它通过Redis实现任务队列共享和去重机制。在实际部署中,我们通常采用主从架构:
- Master节点:负责URL种子管理和任务分发
- Worker节点:执行实际爬取任务
- Redis服务器:作为消息中间件存储请求队列和去重集合
这种架构下,每个Worker节点都可以独立运行,通过Redis获取待抓取URL。我们使用Bloom Filter算法优化去重性能,将内存占用降低80%以上。以下是典型配置示例:
python复制# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_URL = 'redis://:password@127.0.0.1:6379'
注意:生产环境务必设置Redis密码并启用持久化,避免任务丢失
1.2 动态请求处理进阶
对于需要处理动态渲染页面的场景,我们通常采用以下三种方案:
- 中间件集成Selenium:通过Downloader Middleware注入浏览器实例
python复制class SeleniumMiddleware:
def process_request(self, request, spider):
if request.meta.get('selenium'):
driver = webdriver.Chrome()
driver.get(request.url)
html = driver.page_source
driver.quit()
return HtmlResponse(url=request.url, body=html, encoding='utf-8')
- Splash服务集成:使用Docker部署Splash服务处理JavaScript渲染
python复制SPLASH_URL = 'http://localhost:8050'
DOWNLOADER_MIDDLEWARES = {
'scrapy_splash.SplashCookiesMiddleware': 723,
'scrapy_splash.SplashMiddleware': 725,
}
- API逆向工程:通过Chrome DevTools分析XHR请求,直接调用数据接口
1.3 数据管道优化策略
百万级数据存储需要考虑写入性能和故障恢复能力。我们采用多级管道设计:
- 内存缓冲:使用Item Buffer暂存数据,批量写入
- 异步写入:配合Twisted的异步IO特性
- 断点续传:通过Redis记录最后处理位置
python复制class MySQLPipeline:
def __init__(self):
self.buffer = []
self.batch_size = 1000
def process_item(self, item, spider):
self.buffer.append(item)
if len(self.buffer) >= self.batch_size:
self._flush_buffer()
return item
def _flush_buffer(self):
try:
# 批量插入逻辑
pass
except Exception as e:
spider.logger.error(f"Batch insert failed: {e}")
2. 性能调优实战
2.1 并发参数精细控制
Scrapy的并发参数需要根据目标网站承受能力和服务器配置动态调整:
python复制# settings.py
CONCURRENT_REQUESTS = 32 # 总并发数
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 单域名并发
DOWNLOAD_DELAY = 0.25 # 下载延迟
AUTOTHROTTLE_ENABLED = True # 自动限速
经验值:4核8G服务器建议并发数在16-64之间,需通过ab测试确定最优值
2.2 智能限速算法
自定义AutoThrottle扩展实现动态限速:
python复制class SmartThrottle(AutoThrottle):
def _adjust_delay(self, slot, latency, response):
# 根据响应时间动态调整延迟
if response.status == 429:
return slot.delay * 2
elif latency < 0.2:
return max(slot.delay * 0.9, 0.1)
else:
return slot.delay * 1.1
2.3 内存优化技巧
处理海量数据时需特别注意内存管理:
- 使用
__slots__减少Item内存占用
python复制class ProductItem(scrapy.Item):
__slots__ = ['name', 'price']
name = scrapy.Field()
price = scrapy.Field()
- 定期清理解析树
python复制def parse(self, response):
l = response.xpath('//div[@class="product"]')
for product in l:
yield ProductItem(name=product.xpath('./h2/text()').get())
del l # 显式释放内存
- 启用内存监控中间件
python复制class MemoryMonitor:
def process_spider_output(self, response, result, spider):
import psutil
mem = psutil.virtual_memory()
spider.logger.info(f"Memory usage: {mem.percent}%")
return result
3. 分布式系统部署方案
3.1 Docker集群部署
使用Docker Compose编排服务:
yaml复制version: '3'
services:
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
master:
build: .
command: scrapy crawl example -L INFO
depends_on:
- redis
environment:
- ROLE=master
worker:
image: scrapy-worker
command: scrapy crawl example -L INFO
deploy:
replicas: 10
depends_on:
- redis
environment:
- ROLE=worker
volumes:
redis_data:
3.2 负载均衡策略
实现自定义调度策略:
python复制class PriorityScheduler(Scheduler):
def enqueue_request(self, request):
if request.meta.get('priority'):
self.queue.push(request, -request.meta['priority'])
else:
self.queue.push(request)
3.3 分布式锁实现
使用Redis实现任务锁:
python复制import redis
from contextlib import contextmanager
r = redis.StrictRedis()
@contextmanager
def redis_lock(lock_name, timeout=30):
identifier = str(uuid.uuid4())
end = time.time() + timeout
while time.time() < end:
if r.setnx(lock_name, identifier):
r.expire(lock_name, timeout)
try:
yield
finally:
if r.get(lock_name) == identifier:
r.delete(lock_name)
return
time.sleep(0.001)
raise Exception("Could not acquire lock")
4. 异常处理与监控
4.1 智能重试机制
python复制RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 408]
RETRY_PRIORITY_ADJUST = 1
class CustomRetryMiddleware(RetryMiddleware):
def process_response(self, request, response, spider):
if response.status in self.RETRY_HTTP_CODES:
reason = 'HTTP %s' % response.status
return self._retry(request, reason, spider) or response
return response
4.2 Prometheus监控集成
python复制from prometheus_client import start_http_server, Counter
class StatsExporter:
def __init__(self, port=8000):
self.items_scraped = Counter('scrapy_items_scraped', 'Items scraped')
start_http_server(port)
def process_item(self, item, spider):
self.items_scraped.inc()
return item
4.3 日志集中管理
使用ELK Stack收集分布式日志:
python复制LOGGING = {
'version': 1,
'handlers': {
'logstash': {
'class': 'logstash.TCPLogstashHandler',
'host': 'logstash.example.com',
'port': 5959,
'version': 1,
}
},
'loggers': {
'scrapy': {
'handlers': ['logstash'],
'level': 'DEBUG',
}
}
}
5. 实战:百万级电商爬虫案例
5.1 架构设计
我们构建的电商爬虫系统包含以下组件:
- URL调度中心:管理商品列表页队列
- 详情页爬虫集群:50个Worker节点
- 评论采集专用节点:处理动态加载的评论
- 图像下载专用节点:异步下载商品图片
- 数据清洗服务:实时处理原始数据
5.2 性能指标
| 指标 | 数值 |
|---|---|
| 日均处理量 | 120万商品 |
| 峰值QPS | 350请求/秒 |
| 数据完整性 | 99.8% |
| 平均延迟 | 1.2秒 |
| 故障恢复时间 | <5分钟 |
5.3 关键代码实现
商品详情解析器:
python复制def parse_product(self, response):
loader = ItemLoader(item=ProductItem(), response=response)
loader.add_xpath('name', '//h1[@id="title"]/text()')
loader.add_xpath('price', '//span[@id="price"]/text()',
re='[\d,.]+')
loader.add_value('url', response.url)
# 异步加载评论
yield scrapy.Request(
self.review_api.format(product_id=response.meta['product_id']),
callback=self.parse_reviews,
meta={'loader': loader}
)
def parse_reviews(self, response):
loader = response.meta['loader']
data = json.loads(response.text)
loader.add_value('review_count', data['count'])
yield loader.load_item()
5.4 反爬虫对抗策略
- IP轮换:使用代理中间件
python复制class ProxyMiddleware:
def process_request(self, request, spider):
request.meta['proxy'] = self.get_proxy()
def get_proxy(self):
# 从代理池获取IP
return random.choice(self.proxies)
- 请求指纹混淆
python复制class FakerMiddleware:
def process_request(self, request, spider):
request.headers['User-Agent'] = fake.user_agent()
request.headers['Accept-Language'] = 'en-US,en;q=0.9'
- 验证码自动识别
python复制class CaptchaSolver:
def process_response(self, request, response, spider):
if 'captcha' in response.url:
img = response.xpath('//img[@id="captcha"]/@src').get()
solution = self.solve_captcha(img)
return scrapy.FormRequest.from_response(
response,
formdata={'captcha': solution},
callback=self.after_captcha
)
return response
