1. 项目概述
作为一名长期使用Scrapy框架的数据爬取工程师,我深知一个规范的Scrapy项目目录结构对于项目可维护性和团队协作的重要性。很多新手在使用Scrapy时,往往只关注爬虫逻辑本身,而忽略了项目结构的规范性,导致后期维护困难、扩展性差等问题。本文将基于我多年实战经验,详细解析Scrapy项目的标准目录结构及各文件的核心配置。
Scrapy框架之所以强大,很大程度上得益于其清晰的项目架构设计。一个标准的Scrapy项目包含约10个核心文件和目录,每个都有其特定的职责和最佳实践。理解这些文件的作用和相互关系,不仅能帮助你更好地组织代码,还能在遇到问题时快速定位和解决。
提示:本文所有示例基于Scrapy 2.5+版本,部分配置在早期版本中可能略有不同。
2. 项目创建与基础结构
2.1 项目初始化
让我们从一个全新的Scrapy项目开始。打开终端,执行以下命令:
bash复制# 创建项目(项目名以news_crawler为例)
scrapy startproject news_crawler
# 进入项目目录
cd news_crawler
# 创建第一个爬虫(以爬取news.example.com为例)
scrapy genspider news_spider news.example.com
这个简单的三步操作会生成一个完整的Scrapy项目骨架。值得注意的是,项目名称(news_crawler)和爬虫名称(news_spider)应该具有描述性,避免使用通用名称如"project1"或"spider1"。
2.2 标准目录结构解析
创建完成后,项目的目录结构如下:
code复制news_crawler/ # 项目根目录
├── scrapy.cfg # 项目部署/全局配置文件
├── news_crawler/ # 项目核心代码目录(与项目名同名)
│ ├── __init__.py # Python包标识文件
│ ├── items.py # 数据模型定义
│ ├── middlewares.py # 中间件实现
│ ├── pipelines.py # 数据处理管道
│ ├── settings.py # 项目核心配置
│ └── spiders/ # 爬虫脚本存放目录
│ ├── __init__.py # Python包标识
│ └── news_spider.py # 生成的爬虫脚本
├── logs/ # (推荐)日志存储目录
└── data/ # (推荐)爬取数据输出目录
这个结构看似简单,但每个文件和目录都有其特定的用途和最佳实践。下面我将逐一深入解析。
3. 核心文件详解
3.1 scrapy.cfg:项目全局配置
scrapy.cfg是Scrapy项目的入口配置文件,主要包含两类配置:
ini复制[settings]
# 必须配置:指定项目核心配置文件的Python路径
default = news_crawler.settings
[deploy]
# 可选配置:部署到Scrapyd服务器的设置
url = http://localhost:6800/
project = news_crawler
在实际项目中,我通常会添加以下额外配置:
ini复制[settings]
default = news_crawler.settings
test = news_crawler.test_settings # 测试环境配置
[deploy:production]
url = http://scrapyd.example.com:6800/
project = news_crawler_prod
version = GIT_COMMIT_HASH # 使用Git提交哈希作为版本号
[deploy:staging]
url = http://staging-scrapyd.example.com:6800/
project = news_crawler_stage
这种多环境配置方案可以让团队在不同阶段(开发、测试、生产)使用不同的设置,避免环境冲突。
3.2 items.py:数据模型定义
items.py定义了爬取数据的结构化模型。良好的Item设计能显著提高代码的可读性和可维护性。
python复制import scrapy
from itemloaders.processors import TakeFirst, MapCompose
from w3lib.html import remove_tags
def clean_text(text):
"""清理文本中的多余空格和换行"""
return ' '.join(text.strip().split())
class NewsItem(scrapy.Item):
# 基础字段
title = scrapy.Field(
input_processor=MapCompose(remove_tags, clean_text),
output_processor=TakeFirst(),
required=True
)
url = scrapy.Field(
output_processor=TakeFirst(),
required=True
)
publish_time = scrapy.Field(
input_processor=MapCompose(remove_tags, clean_text),
output_processor=TakeFirst()
)
# 内容字段
content = scrapy.Field(
input_processor=MapCompose(remove_tags, clean_text),
output_processor=''.join
)
# 元数据
source = scrapy.Field(
output_processor=TakeFirst(),
default='unknown'
)
crawl_time = scrapy.Field(
output_processor=TakeFirst()
)
在这个示例中,我使用了Item Loaders的处理器(processor)来规范化数据处理流程。MapCompose用于对输入值进行一系列处理(如移除HTML标签、清理空白字符),TakeFirst则从处理结果中提取第一个值。
3.3 settings.py:核心配置详解
settings.py是Scrapy项目最重要的配置文件。以下是我在项目中常用的配置模板:
python复制# 基础设置
BOT_NAME = 'news_crawler'
SPIDER_MODULES = ['news_crawler.spiders']
NEWSPIDER_MODULE = 'news_crawler.spiders'
ROBOTSTXT_OBEY = False # 通常设置为False以爬取更多页面
# 并发设置
CONCURRENT_REQUESTS = 32 # 默认16,可根据目标网站承受能力调整
DOWNLOAD_DELAY = 0.5 # 请求间隔,防止被封
CONCURRENT_REQUESTS_PER_DOMAIN = 8
# 缓存和重试
HTTPCACHE_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408]
# 中间件和管道
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'news_crawler.middlewares.RandomUserAgentMiddleware': 400,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 500,
}
ITEM_PIPELINES = {
'news_crawler.pipelines.DataValidationPipeline': 100,
'news_crawler.pipelines.MongoDBPipeline': 300,
}
# 日志设置
LOG_LEVEL = 'INFO'
LOG_FILE = 'logs/news_crawler.log'
LOG_FORMAT = '%(asctime)s [%(name)s] %(levelname)s: %(message)s'
LOG_DATEFORMAT = '%Y-%m-%d %H:%M:%S'
# 自定义设置
MONGO_URI = 'mongodb://localhost:27017'
MONGO_DATABASE = 'news_db'
在实际项目中,我会根据需求添加更多配置,如:
- 代理设置
- 自动限速扩展
- 布隆过滤器去重
- 自定义扩展
3.4 middlewares.py:中间件开发实践
中间件是Scrapy最强大的扩展机制之一。以下是一个实用的随机User-Agent中间件示例:
python复制import random
from scrapy import signals
class RandomUserAgentMiddleware:
"""随机User-Agent中间件"""
def __init__(self, user_agents):
self.user_agents = user_agents
@classmethod
def from_crawler(cls, crawler):
# 从settings.py读取USER_AGENTS配置
user_agents = crawler.settings.get('USER_AGENTS', [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...'
])
return cls(user_agents)
def process_request(self, request, spider):
# 为每个请求随机分配User-Agent
request.headers['User-Agent'] = random.choice(self.user_agents)
return None
另一个实用的例子是代理中间件:
python复制class ProxyMiddleware:
"""代理中间件"""
def process_request(self, request, spider):
if not hasattr(spider, 'use_proxy') or not spider.use_proxy:
return None
proxy = self.get_proxy() # 从代理池获取代理
request.meta['proxy'] = f"http://{proxy.ip}:{proxy.port}"
return None
3.5 pipelines.py:数据处理管道
管道用于处理爬取到的Item数据。以下是几个典型管道示例:
python复制import pymongo
from scrapy.exceptions import DropItem
class DataValidationPipeline:
"""数据验证管道"""
def process_item(self, item, spider):
# 验证必填字段
if not item.get('title') or not item.get('url'):
raise DropItem("Missing required fields")
# 验证URL格式
if not item['url'].startswith(('http://', 'https://')):
raise DropItem(f"Invalid URL: {item['url']}")
return item
class MongoDBPipeline:
"""MongoDB存储管道"""
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):
collection_name = item.__class__.__name__.lower()
self.db[collection_name].insert_one(dict(item))
return item
4. 爬虫开发最佳实践
4.1 基础爬虫结构
一个典型的Scrapy爬虫包含以下核心部分:
python复制import scrapy
from news_crawler.items import NewsItem
from datetime import datetime
class NewsSpider(scrapy.Spider):
name = 'news_spider'
allowed_domains = ['news.example.com']
start_urls = ['https://news.example.com/latest']
custom_settings = {
'CONCURRENT_REQUESTS': 8,
'DOWNLOAD_DELAY': 1.0,
}
def parse(self, response):
"""解析列表页"""
for article in response.css('div.article-list > div.article'):
url = article.css('a.title::attr(href)').get()
yield response.follow(url, self.parse_article)
# 翻页逻辑
next_page = response.css('a.next-page::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
def parse_article(self, response):
"""解析详情页"""
item = NewsItem()
item['title'] = response.css('h1.headline::text').get()
item['url'] = response.url
item['publish_time'] = response.css('time.published::attr(datetime)').get()
item['content'] = ''.join(response.css('div.article-body p::text').getall())
item['source'] = self.name
item['crawl_time'] = datetime.utcnow().isoformat()
yield item
4.2 高级爬虫技巧
- 请求元数据:通过
request.meta传递额外信息
python复制def parse(self, response):
for category in ['politics', 'technology', 'sports']:
url = f'https://news.example.com/{category}'
yield scrapy.Request(
url,
callback=self.parse_category,
meta={'category': category}
)
def parse_category(self, response):
category = response.meta['category']
# 使用category信息处理响应
- 动态配置:通过Spider参数定制爬取行为
bash复制scrapy crawl news_spider -a category=technology -a pages=5
python复制class NewsSpider(scrapy.Spider):
def __init__(self, category=None, pages=1, *args, **kwargs):
super().__init__(*args, **kwargs)
self.category = category
self.max_pages = int(pages)
- 增量爬取:使用
scrapy-deltafetch等扩展实现增量爬取
python复制custom_settings = {
'DELTAFETCH_ENABLED': True,
'DELTAFETCH_DIR': '/path/to/deltafetch/storage',
}
5. 项目组织与扩展
5.1 推荐的项目扩展结构
随着项目规模扩大,建议采用以下结构:
code复制news_crawler/
├── extensions/ # 自定义扩展
│ ├── __init__.py
│ ├── stats.py # 统计扩展
│ └── notifications.py # 通知扩展
├── utils/ # 工具函数
│ ├── __init__.py
│ ├── proxy.py # 代理工具
│ └── text.py # 文本处理
├── spiders/ # 爬虫分类
│ ├── news/ # 新闻类爬虫
│ │ ├── __init__.py
│ │ ├── bbc.py
│ │ └── cnn.py
│ └── blogs/ # 博客类爬虫
│ ├── __init__.py
│ └── tech.py
└── tests/ # 测试
├── __init__.py
├── test_pipelines.py
└── test_spiders.py
5.2 自定义扩展开发
Scrapy的扩展系统非常强大。以下是一个简单的统计扩展示例:
python复制from scrapy import signals
class StatsExtension:
"""自定义统计扩展"""
def __init__(self, stats):
self.stats = stats
@classmethod
def from_crawler(cls, crawler):
ext = cls(crawler.stats)
crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
crawler.signals.connect(ext.item_scraped, signal=signals.item_scraped)
return ext
def spider_opened(self, spider):
self.stats.set_value('start_time', datetime.utcnow())
self.stats.set_value('items_count', 0)
def spider_closed(self, spider, reason):
self.stats.set_value('end_time', datetime.utcnow())
duration = self.stats.get_value('end_time') - self.stats.get_value('start_time')
self.stats.set_value('duration_seconds', duration.total_seconds())
def item_scraped(self, item, spider):
self.stats.inc_value('items_count')
6. 常见问题与解决方案
6.1 性能优化
问题:爬虫速度太慢
解决方案:
- 调整并发设置:
python复制custom_settings = {
'CONCURRENT_REQUESTS': 100, # 增加并发请求数
'REACTOR_THREADPOOL_MAXSIZE': 20, # 增加线程池大小
'DOWNLOAD_TIMEOUT': 30, # 增加超时时间
}
- 使用缓存:
python复制HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 86400 # 缓存24小时
- 启用自动限速:
python复制AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5.0
AUTOTHROTTLE_MAX_DELAY = 60.0
6.2 反爬应对
问题:网站封禁爬虫
解决方案:
- 使用随机User-Agent和代理
- 设置合理的下载延迟
- 实现请求头随机化:
python复制class RandomHeadersMiddleware:
def process_request(self, request, spider):
request.headers['Accept'] = random.choice([
'text/html',
'text/html,application/xhtml+xml',
'*/*'
])
request.headers['Accept-Language'] = random.choice([
'en-US,en;q=0.9',
'zh-CN,zh;q=0.8',
'ja-JP,ja;q=0.7'
])
6.3 数据存储优化
问题:数据库写入成为瓶颈
解决方案:
- 使用批量写入:
python复制class MongoDBPipeline:
def __init__(self):
self.buffer = []
self.batch_size = 100
def process_item(self, item, spider):
self.buffer.append(dict(item))
if len(self.buffer) >= self.batch_size:
self._flush_buffer()
return item
def close_spider(self, spider):
if self.buffer:
self._flush_buffer()
def _flush_buffer(self):
self.db[collection_name].insert_many(self.buffer)
self.buffer = []
- 考虑使用消息队列(如RabbitMQ/Kafka)解耦爬取和存储
7. 项目部署与监控
7.1 使用Scrapyd部署
- 安装Scrapyd:
bash复制pip install scrapyd
- 配置
scrapyd.conf:
ini复制[scrapyd]
eggs_dir = eggs
logs_dir = logs
items_dir = items
jobs_to_keep = 5
dbs_dir = dbs
max_proc = 0
max_proc_per_cpu = 4
finished_to_keep = 100
poll_interval = 5.0
bind_address = 0.0.0.0
http_port = 6800
- 部署项目:
bash复制scrapyd-deploy target_name -p project_name
7.2 监控方案
- 使用
scrapy-statscollector收集统计信息 - 集成Prometheus监控:
python复制from prometheus_client import start_http_server, Counter
class PrometheusStatsCollector:
def __init__(self):
self.items_scraped = Counter(
'scrapy_items_scraped_total',
'Total items scraped'
)
start_http_server(8000)
def item_scraped(self, item, spider):
self.items_scraped.inc()
- 设置异常通知(邮件/Slack):
python复制class NotificationExtension:
def spider_error(self, failure, response, spider):
message = f"Spider {spider.name} error: {failure.value}"
send_slack_notification(message)