1. Scrapy框架概述与爬虫基础
Scrapy是一个为爬取网站数据、提取结构性数据而设计的应用框架,它最初是为了页面抓取所设计的,也可以用于获取API返回的数据。作为Python生态中最成熟的爬虫框架之一,Scrapy通过其异步处理机制能够高效地完成大规模数据采集任务。
我第一次接触Scrapy是在2015年参与一个电商价格监控项目,当时需要实时追踪50多个电商平台的商品价格波动。尝试了各种爬虫方案后,最终选择Scrapy作为核心框架,主要看中其以下几个特点:
- 内置的异步处理引擎:基于Twisted实现的事件驱动架构,可以同时发送数百个请求而不阻塞
- 完善的中间件系统:可以灵活插入各种处理逻辑(如代理切换、请求重试)
- 可扩展的架构设计:每个组件都可以被自定义实现替换
- 内置的Selector支持:同时支持XPath和CSS选择器提取数据
1.1 爬虫工作流程解析
一个典型的Scrapy爬虫工作流程包含以下核心环节:
- 引擎(Engine):控制所有组件之间的数据流
- 调度器(Scheduler):接收引擎发来的请求并排队
- 下载器(Downloader):获取网页内容并返回给引擎
- 爬虫(Spiders):解析响应并提取数据或新请求
- 项目管道(Item Pipeline):处理爬取到的数据(清洗、验证、存储)
python复制# 典型Scrapy项目结构
myproject/
scrapy.cfg # 部署配置文件
myproject/ # 项目Python模块
__init__.py
items.py # 项目项定义文件
middlewares.py # 中间件文件
pipelines.py # 项目管道文件
settings.py # 项目设置文件
spiders/ # 爬虫目录
__init__.py
example.py # 爬虫实现文件
提示:新手常见误区是直接在spider中处理所有逻辑,实际上应该遵循Scrapy的组件化设计原则,将不同功能分散到对应的组件中实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与项目创建
2.1 安装与配置
推荐使用Python 3.7+环境,通过pip安装Scrapy:
bash复制pip install scrapy
创建新项目:
bash复制scrapy startproject myproject
cd myproject
scrapy genspider example example.com
2.2 核心配置文件解析
settings.py是Scrapy项目的控制中心,几个关键配置项:
python复制# 并发相关设置
CONCURRENT_REQUESTS = 16 # 默认并发请求数
DOWNLOAD_DELAY = 0.25 # 下载延迟(秒)
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 单域名并发限制
# 中间件启用设置
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.MyDownloaderMiddleware': 543,
}
# 管道设置
ITEM_PIPELINES = {
'myproject.pipelines.MyPipeline': 300,
}
# 用户代理设置
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
2.3 调试技巧
使用Scrapy shell可以快速测试选择器:
bash复制scrapy shell "http://example.com"
在shell中可以直接使用response对象:
python复制response.css('title::text').get() # 获取页面标题
response.xpath('//h1/text()').get() # XPath选择器
3. 爬虫开发实战
3.1 定义数据模型
在items.py中定义要爬取的数据结构:
python复制import scrapy
class ProductItem(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
description = scrapy.Field()
stock = scrapy.Field()
last_updated = scrapy.Field()
3.2 编写爬虫核心逻辑
一个完整的电商产品爬虫示例:
python复制import scrapy
from myproject.items import ProductItem
from urllib.parse import urljoin
class ProductSpider(scrapy.Spider):
name = "products"
allowed_domains = ["example.com"]
start_urls = ["https://example.com/category"]
def parse(self, response):
# 提取产品列表页链接
products = response.css('div.product-item')
for product in products:
item = ProductItem()
item['name'] = product.css('h2::text').get()
item['price'] = product.css('.price::text').get()
detail_url = product.css('a::attr(href)').get()
yield response.follow(detail_url,
self.parse_detail,
meta={'item': item})
# 处理分页
next_page = response.css('a.next-page::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
def parse_detail(self, response):
item = response.meta['item']
item['description'] = response.css('.description::text').get()
item['stock'] = response.css('.stock::text').get()
yield item
3.3 处理动态内容
对于JavaScript渲染的页面,可以结合Selenium或Playwright:
python复制from scrapy_playwright.handler import PerBrowserContextHandler
class DynamicSpider(scrapy.Spider):
name = "dynamic"
def start_requests(self):
yield scrapy.Request(
url="https://dynamic-site.com",
meta={
"playwright": True,
"playwright_page_methods": [
PageMethod("wait_for_selector", "div.loaded-content")
]
}
)
def parse(self, response):
# 现在可以解析动态加载的内容
yield {
"content": response.css("div.loaded-content::text").get()
}
4. 数据处理与存储
4.1 数据清洗管道
在pipelines.py中实现数据清洗逻辑:
python复制from itemadapter import ItemAdapter
from datetime import datetime
class CleanPricePipeline:
def process_item(self, item, spider):
adapter = ItemAdapter(item)
# 价格清洗
if adapter.get('price'):
price = adapter['price']
price = price.replace('$', '').strip()
adapter['price'] = float(price)
# 日期标准化
if adapter.get('last_updated'):
adapter['last_updated'] = datetime.strptime(
adapter['last_updated'],
'%Y-%m-%d %H:%M:%S'
)
return item
4.2 多种存储方式
存储到MongoDB
python复制import pymongo
class MongoPipeline:
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.db[spider.name].insert_one(ItemAdapter(item).asdict())
return item
存储到MySQL
python复制import mysql.connector
class MySQLPipeline:
def __init__(self, host, database, user, password):
self.host = host
self.database = database
self.user = user
self.password = password
@classmethod
def from_crawler(cls, crawler):
return cls(
host=crawler.settings.get('MYSQL_HOST'),
database=crawler.settings.get('MYSQL_DATABASE'),
user=crawler.settings.get('MYSQL_USER'),
password=crawler.settings.get('MYSQL_PASSWORD')
)
def open_spider(self, spider):
self.conn = mysql.connector.connect(
host=self.host,
user=self.user,
password=self.password,
database=self.database
)
self.cursor = self.conn.cursor()
# 创建表
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
price DECIMAL(10,2),
description TEXT,
stock INT,
last_updated DATETIME
)
""")
def close_spider(self, spider):
self.conn.close()
def process_item(self, item, spider):
self.cursor.execute("""
INSERT INTO products (name, price, description, stock, last_updated)
VALUES (%s, %s, %s, %s, %s)
""", (
item['name'],
item['price'],
item['description'],
item['stock'],
item['last_updated']
))
self.conn.commit()
return item
5. 高级技巧与反爬对抗
5.1 请求头与代理配置
在middlewares.py中实现随机User-Agent和代理:
python复制import random
from scrapy import signals
class RandomUserAgentMiddleware:
def __init__(self, user_agents):
self.user_agents = user_agents
@classmethod
def from_crawler(cls, crawler):
return cls(
user_agents=crawler.settings.get('USER_AGENT_LIST')
)
def process_request(self, request, spider):
request.headers['User-Agent'] = random.choice(self.user_agents)
class ProxyMiddleware:
def process_request(self, request, spider):
request.meta['proxy'] = "http://your-proxy-server:port"
5.2 处理验证码
使用第三方服务自动识别验证码:
python复制import requests
from PIL import Image
from io import BytesIO
class CaptchaSolver:
def solve_captcha(self, image_url):
response = requests.get(image_url)
img = Image.open(BytesIO(response.content))
# 这里调用第三方验证码识别API
# 示例使用2captcha服务
api_key = "YOUR_API_KEY"
url = "http://2captcha.com/in.php"
# 发送验证码图片
files = {'file': ('captcha.jpg', response.content)}
data = {'key': api_key, 'method': 'post'}
response = requests.post(url, files=files, data=data)
if response.text.startswith('OK|'):
captcha_id = response.text.split('|')[1]
# 获取识别结果
result_url = f"http://2captcha.com/res.php?key={api_key}&action=get&id={captcha_id}"
for _ in range(10): # 最多尝试10次
response = requests.get(result_url)
if response.text == 'CAPCHA_NOT_READY':
time.sleep(5)
elif response.text.startswith('OK|'):
return response.text.split('|')[1]
return None
5.3 分布式爬虫实现
使用Scrapy-Redis实现分布式爬虫:
- 安装依赖:
bash复制pip install scrapy-redis
- 修改settings.py:
python复制SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_URL = 'redis://your-redis-server:6379'
# 可选:爬虫结束后保留去重集合
SCHEDULER_PERSIST = True
- 修改爬虫继承RedisSpider:
python复制from scrapy_redis.spiders import RedisSpider
class MyDistributedSpider(RedisSpider):
name = 'distributed_spider'
redis_key = 'spider:start_urls'
def parse(self, response):
# 解析逻辑...
6. 性能优化与监控
6.1 性能调优技巧
- 调整并发参数:
python复制# settings.py
CONCURRENT_REQUESTS = 100 # 总并发数
CONCURRENT_REQUESTS_PER_DOMAIN = 20 # 单域名并发
DOWNLOAD_DELAY = 0 # 下载延迟
- 启用HTTP缓存(开发阶段):
python复制HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 0 # 永不过期
HTTPCACHE_DIR = 'httpcache'
- 使用更快的DNS解析:
python复制DNSCACHE_ENABLED = True
DNS_TIMEOUT = 60
6.2 监控与日志
- 扩展日志收集:
python复制LOG_FILE = 'scrapy.log'
LOG_LEVEL = 'INFO'
LOG_STDOUT = True
- 集成Sentry监控:
bash复制pip install scrapy-sentry
python复制# settings.py
SENTRY_DSN = 'your-sentry-dsn'
EXTENSIONS = {
'scrapy_sentry.extensions.Errors': 10,
}
- 自定义统计收集:
python复制class CustomStatsExtension:
def __init__(self, stats):
self.stats = stats
@classmethod
def from_crawler(cls, crawler):
ext = cls(crawler.stats)
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
return ext
def spider_closed(self, spider, reason):
# 发送统计到外部系统
stats_data = {
'spider': spider.name,
'items': self.stats.get_value('item_scraped_count', 0),
'finish_reason': reason,
'duration': self.stats.get_value('elapsed_time_seconds', 0)
}
requests.post('https://your-monitoring-system.com/stats', json=stats_data)
7. 项目部署与调度
7.1 使用Scrapyd部署
- 安装Scrapyd:
bash复制pip install scrapyd
- 启动服务:
bash复制scrapyd
- 部署项目:
bash复制scrapyd-deploy default -p myproject
- 调度爬虫运行:
bash复制curl http://localhost:6800/schedule.json -d project=myproject -d spider=example
7.2 定时任务配置
使用APScheduler实现复杂调度:
python复制from apscheduler.schedulers.twisted import TwistedScheduler
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings
scheduler = TwistedScheduler()
process = CrawlerProcess(get_project_settings())
@scheduler.scheduled_job('cron', hour=3, minute=30)
def run_spider():
process.crawl('example')
process.start()
scheduler.start()
7.3 容器化部署
Dockerfile示例:
dockerfile复制FROM python:3.8
RUN pip install scrapy scrapyd scrapy-redis
WORKDIR /app
COPY . /app
EXPOSE 6800
CMD ["scrapyd", "--pidfile="]
docker-compose.yml配置:
yaml复制version: '3'
services:
scrapyd:
build: .
ports:
- "6800:6800"
volumes:
- ./:/app
redis:
image: redis:alpine
ports:
- "6379:6379"
8. 实战案例:电商价格监控系统
8.1 系统架构设计
code复制用户界面层 (Web Dashboard)
↑
API服务层 (Flask/Django)
↑
数据存储层 (MySQL + Redis)
↑
爬虫集群 (Scrapy + Scrapyd)
↑
代理服务层 (代理池)
8.2 核心实现代码
价格监控爬虫
python复制class PriceMonitorSpider(scrapy.Spider):
name = "price_monitor"
def __init__(self, products=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.products = products or []
def start_requests(self):
for product in self.products:
yield scrapy.Request(
url=product['url'],
callback=self.parse_product,
meta={'product_id': product['id']}
)
def parse_product(self, response):
yield {
'product_id': response.meta['product_id'],
'price': response.css('.price::text').get(),
'timestamp': datetime.now().isoformat(),
'availability': bool(response.css('.in-stock'))
}
价格异常检测管道
python复制class PriceAlertPipeline:
def __init__(self, threshold=0.2):
self.threshold = threshold
def process_item(self, item, spider):
# 获取历史价格
history = get_price_history(item['product_id'])
if history:
last_price = history[-1]['price']
current_price = float(item['price'])
# 计算价格变化百分比
change = abs(current_price - last_price) / last_price
if change > self.threshold:
send_alert_email(
product_id=item['product_id'],
old_price=last_price,
new_price=current_price,
change=change
)
return item
数据可视化API
python复制from flask import Flask, jsonify
import mysql.connector
app = Flask(__name__)
@app.route('/api/price-history/<product_id>')
def price_history(product_id):
conn = mysql.connector.connect(
host="localhost",
user="user",
password="password",
database="price_monitor"
)
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT price, timestamp
FROM price_history
WHERE product_id = %s
ORDER BY timestamp DESC
LIMIT 30
""", (product_id,))
data = cursor.fetchall()
conn.close()
return jsonify(data)
9. 法律合规与道德考量
9.1 robots.txt遵守
Scrapy默认遵守robots.txt规则(可通过设置修改):
python复制ROBOTSTXT_OBEY = True # 默认True,建议保持
9.2 合理爬取策略
- 设置合理的下载延迟:
python复制DOWNLOAD_DELAY = 2.5 # 2.5秒间隔
- 限制并发请求数:
python复制CONCURRENT_REQUESTS_PER_DOMAIN = 4
- 设置爬取深度限制:
python复制DEPTH_LIMIT = 3
9.3 数据使用规范
- 仅爬取公开可用数据
- 不爬取个人隐私信息
- 遵守网站服务条款
- 对数据进行匿名化处理
- 设置数据访问权限控制
10. 常见问题与解决方案
10.1 请求被封锁
现象:收到403响应或验证码
解决方案:
- 轮换User-Agent
- 使用代理IP池
- 降低请求频率
- 模拟浏览器行为(通过Playwright/Selenium)
10.2 数据解析失败
现象:选择器无法匹配内容
排查步骤:
- 检查页面是否动态加载
- 验证选择器是否正确
- 查看响应内容是否包含预期数据
- 检查是否有反爬机制干扰
10.3 性能瓶颈
优化方向:
- 调整并发参数(CONCURRENT_REQUESTS)
- 启用缓存(HTTPCACHE)
- 优化选择器(避免复杂XPath)
- 使用更快的解析器(lxml代替html.parser)
10.4 内存泄漏
诊断方法:
- 使用muppy检查内存对象
- 分析爬虫运行时的内存增长
- 检查是否有未关闭的资源
常见原因:
- 未正确关闭数据库连接
- 大对象未被及时释放
- 中间件中累积状态
11. 扩展学习与资源推荐
11.1 进阶学习路径
-
Scrapy源码研究:
- 引擎核心流程
- 调度器实现原理
- 下载器中间件机制
-
相关技术扩展:
- 分布式爬虫(Scrapy-Redis)
- 动态页面渲染(Playwright/Selenium集成)
- 机器学习在爬虫中的应用(自动识别页面结构)
-
性能优化专题:
- 异步IO深入理解
- 网络请求优化
- 解析效率提升
11.2 推荐资源
官方文档:
实用工具:
- ProxyBroker(代理管理)
- Frontera(大规模爬取框架)
- Splash(JavaScript渲染服务)
开源项目参考:
- scrapy-cluster
- Gerapy(Scrapy管理面板)
- scrapy-djangoitem
在实际项目中,我发现最难的不是技术实现,而是如何长期稳定地运行爬虫系统。曾经有一个电商爬虫运行了三个月后突然失效,排查发现是网站改版后CSS选择器全部失效。这让我意识到必须建立完善的监控和告警机制,现在我会为所有生产爬虫添加以下检查点:
- 每日抓取量监控
- 数据字段完整性检查
- 响应时间异常检测
- 自动化的选择器测试套件
另一个经验是,不要过度依赖单一选择器策略。对于关键数据字段,我会同时配置XPath和CSS两种选择器,并在管道中进行校验,确保至少有一种能正确提取数据。这种冗余设计虽然增加了开发成本,但大大提高了系统的健壮性。
