| name | scrapy |
| description | [Applies to: **/*.py] This guide provides opinionated, actionable best practices for building robust, high-performance, and maintainable Scrapy web crawlers, emphasizing modern Python standards and ethical scraping. |
| source | cursor_mdc |
Scrapy Best Practices
Scrapy is the definitive framework for high-performance web scraping in Python. This guide outlines the essential practices for building resilient, scalable, and ethical crawlers. Adhere to these guidelines to ensure your Scrapy projects are maintainable, efficient, and robust against the dynamic web landscape of 2025.
1. Code Organization and Structure
Maintain a clean, logical project structure. This enhances readability, testability, and scalability.
1.1. Standard Project Layout
Always use scrapy startproject to initialize your project. This sets up the recommended directory structure.
❌ BAD: Manually creating files and directories
# Don't do this
mkdir my_scraper
cd my_scraper
touch scrapy.cfg items.py spiders/__init__.py ...
✅ GOOD: Use the Scrapy CLI
scrapy startproject my_project_name
cd my_project_name
1.2. Item Definitions (items.py)
Define your data models clearly using scrapy.Item subclasses. Always include type hints for better IDE support and code clarity.
❌ BAD: Generic dictionaries or untyped Item fields
import scrapy
class ProductItem(scrapy.Item):
title = scrapy.Field()
price = scrapy.Field()
✅ GOOD: scrapy.Item with Field and typing hints
import scrapy
from scrapy.item import Field
from typing import Optional, List
class ProductItem(scrapy.Item):
url: str = Field()
title: Optional[str] = Field()
price: Optional[float] = Field()
description: Optional[str] = Field()
image_urls: List[str] = Field()
category: Optional[str] = Field()
1.3. Spiders
Keep spiders focused on crawling logic and initial data extraction. They should yield Request objects and Item objects.
❌ BAD: Complex data processing or storage logic in spiders
import scrapy
from my_project_name.items import ProductItem
class BadSpider(scrapy.Spider):
name = "bad_spider"
start_urls = ["http://example.com"]
def parse(self, response):
item = ProductItem()
item['title'] = response.css('h1::text').get()
yield item
✅ GOOD: Spiders for crawling and raw extraction, pipelines for processing
import scrapy
from my_project_name.items import ProductItem
class GoodSpider(scrapy.Spider):
name = "good_spider"
allowed_domains = ["example.com"]
start_urls = ["http://example.com/products"]
def parse(self, response: scrapy.http.Response):
for product_link in response.css('a.product-link::attr(href)').getall():
yield response.follow(product_link, callback=self.parse_product)
next_page = response.css('a.next-page::attr(href)').get()
if next_page:
yield response.follow(next_page, callback=self.parse)
def parse_product(self, response: scrapy.http.Response):
item = ProductItem(
url=response.url,
title=response.css('h1.product-title::text').get(),
price=float(response.css('.product-price::text').get().replace('$', '')) if response.css('.product-price::text').get() else None,
description=response.css('.product-description::text').get(),
image_urls=response.css('.product-image::attr(src)').getall(),
category=response.css('.product-category::text').get(),
)
item
1.4. Item Pipelines (pipelines.py)
Use pipelines for all data cleaning, validation, deduplication, and storage. This isolates business logic and makes testing straightforward.
❌ BAD: No pipelines, processing in spiders or external scripts
✅ GOOD: Dedicated pipelines for each processing step
from itemadapter import ItemAdapter
from my_project_name.items import ProductItem
from scrapy.exceptions import DropItem
from typing import Any
class PriceConverterPipeline:
def process_item(self, item: ProductItem, spider: Any) -> ProductItem:
adapter = ItemAdapter(item)
if adapter.get('price'):
try:
adapter['price'] = float(adapter['price'])
except (ValueError, TypeError):
raise DropItem(f"Invalid price in {item}")
return item
class DuplicatesPipeline:
def __init__(self):
self.urls_seen = set()
def process_item(self, item: ProductItem, spider: Any) -> ProductItem:
adapter = ItemAdapter(item)
if adapter['url'] in self.urls_seen:
raise DropItem(f"Duplicate item found: {item}")
else:
.urls_seen.add(adapter[])
item
1.5. Middlewares (middlewares.py)
Implement custom downloader and spider middlewares for request/response manipulation, proxy rotation, user-agent management, and advanced error handling.
❌ BAD: Hardcoding user-agents or proxies in spiders
def start_requests(self):
yield scrapy.Request(
url="http://example.com",
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},
meta={"proxy": "http://my.proxy.com:8080"}
)
✅ GOOD: Centralized middleware for robust request handling
import random
from scrapy import signals
from scrapy.http import Request, Response
from scrapy.exceptions import IgnoreRequest
from typing import Any
class RotateUserAgentMiddleware:
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
]
def process_request(self, request: Request, spider: Any) -> None:
request.headers['User-Agent'] = random.choice(self.USER_AGENTS)
class ProxyMiddleware:
PROXIES = [
"http://user:pass@proxy1.com:8080",
"http://user:pass@proxy2.com:8080",
]
def process_request(self, request: Request, spider: Any) -> None:
if not request.meta.get('proxy'):
request.meta['proxy'] = random.choice(self.PROXIES)
2. Common Patterns and Anti-patterns
2.1. Running Spiders from a Script
Always use scrapy.crawler.CrawlerProcess for launching spiders from a Python script. It handles the Twisted reactor and project settings correctly.
❌ BAD: Manually managing the Twisted reactor or CrawlerRunner for simple cases
from scrapy.crawler import CrawlerRunner
from twisted.internet import reactor
runner = CrawlerRunner()
d = runner.crawl(MySpider)
d.addBoth(lambda _: reactor.stop())
reactor.run()
✅ GOOD: scrapy.crawler.CrawlerProcess
import scrapy
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings
from typing import Any
class MySpider(scrapy.Spider):
name = "my_spider"
start_urls = ["http://quotes.toscrape.com/"]
def parse(self, response: scrapy.http.Response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
}
if __name__ == '__main__':
settings = get_project_settings()
process = CrawlerProcess(settings)
process.crawl(MySpider)
process.start()
2.2. Robust Request Handling
Enable AUTOTHROTTLE and set DOWNLOAD_DELAY. Implement exponential backoff for 403/429 responses via a custom middleware or by configuring RETRY_HTTP_CODES.
❌ BAD: Ignoring rate limits, getting banned
✅ GOOD: Respectful crawling with AUTOTHROTTLE and extended RETRY_HTTP_CODES
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1.0
AUTOTHROTTLE_MAX_DELAY = 60.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = False
DOWNLOAD_DELAY = 0.5
RETRY_ENABLED = True
RETRY_TIMES = 5
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 403, 429]
3. Performance Considerations
3.1. Concurrency
Tune CONCURRENT_REQUESTS and CONCURRENT_REQUESTS_PER_DOMAIN carefully. Start low and increase gradually while monitoring server load and your IP's ban rate.
❌ BAD: Default high concurrency or excessively low concurrency
✅ GOOD: Balanced concurrency with AUTOTHROTTLE
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
CONCURRENT_REQUESTS_PER_IP = 0
3.2. Distributed Crawling
For large-scale projects, leverage Scrapy Cluster or Scrapyd. Store crawl state (e.g., visited URLs, pending requests) in a persistent backend like Redis to enable resume after failures and distributed processing.
✅ GOOD: Use scrapy-redis or similar for distributed state management
4. Common Pitfalls and Gotchas
4.1. Ignoring robots.txt
Always check and respect robots.txt. It's an ethical and practical necessity to avoid legal issues and IP bans.
❌ BAD: Disabling ROBOTSTXT_OBEY without justification
ROBOTSTXT_OBEY = False
✅ GOOD: Always obey robots.txt by default
ROBOTSTXT_OBEY = True
4.2. Memory Leaks
Avoid storing large amounts of data in spider attributes. Yield items and requests promptly.
❌ BAD: Appending all items to a list in the spider
class BadSpider(scrapy.Spider):
all_items = []
def parse(self, response):
self.all_items.append(item)
✅ GOOD: Yield items and requests immediately
class GoodSpider(scrapy.Spider):
def parse(self, response):
yield item
yield scrapy.Request(url="next_page", callback=self.parse)
5. Type Hints
Embrace type hints (typing module) throughout your Scrapy project. This improves code quality, enables static analysis, and makes your code easier to understand and maintain.
❌ BAD: Untyped functions and variables
class MyPipeline:
def process_item(self, item, spider):
return item
✅ GOOD: Fully typed functions and variables
from itemadapter import ItemAdapter
from my_project_name.items import ProductItem
from typing import Any
class MyPipeline:
def process_item(self, item: ProductItem, spider: Any) -> ProductItem:
adapter = ItemAdapter(item)
return item
6. Virtual Environments
Always use a virtual environment (venv or conda) for Scrapy projects. This isolates dependencies and prevents conflicts.
❌ BAD: Installing packages globally
pip install scrapy
✅ GOOD: Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate
pip install scrapy
7. Packaging
For larger projects, package your Scrapy application using setuptools and pyproject.toml. This makes deployment and dependency management robust.
✅ GOOD: Use pyproject.toml for project metadata and dependencies
[project]
name = "my-scrapy-project"
version = "0.1.0"
description = "A Scrapy project for scraping X"
dependencies = [
"scrapy>=2.13.0",
"itemadapter>=0.8.0",
]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
8. Testing Approaches
Implement a comprehensive testing strategy to ensure your crawlers are reliable and resilient to website changes.
8.1. Unit Testing Spiders
Write pytest suites that mock scrapy.http.Response objects. Test your parsing logic in isolation.
❌ BAD: No tests, relying on manual verification
✅ GOOD: pytest with mocked responses
import pytest
from scrapy.http import Response, Request
from scrapy.item import Item
from my_project_name.spiders.good_spider import GoodSpider
@pytest.fixture
def mock_response_product_list():
html = """
<html><body>
<a class="product-link" href="/product/1">Product 1</a>
<a class="product-link" href="/product/2">Product 2</a>
<a class="next-page" href="/products?page=2">Next</a>
</body></html>
"""
return Response(url="http://example.com/products", body=html.encode('utf-8'))
@pytest.fixture
def mock_response_product_detail():
html = """
<html><body>
<h1 class="product-title">Awesome Widget</h1>
<span class="product-price">$99.99</span>
<p class="product-description">A great widget.</p>
<img class="product-image" src="/img/widget.jpg">
<span class="product-category">Electronics</span>
</body></html>
"""
return Response(url="http://example.com/product/1", body=html.encode('utf-8'))
def test_parse_product_list(mock_response_product_list):
spider = GoodSpider()
results = list(spider.parse(mock_response_product_list))
assert any(isinstance(r, Request) and r.url == "http://example.com/product/1" for r in results)
((r, Request) r.url == r results)
((r, Request) r.url == r results)
():
spider = GoodSpider()
results = (spider.parse_product(mock_response_product_detail))
(results) ==
item = results[]
(item, Item)
item[] ==
item[] ==
item[] ==
8.2. Linting and Type Checking
Integrate flake8, black, and mypy into your CI/CD pipeline. Enforce code style and type correctness.
✅ GOOD: Automated code quality checks
black . --check
flake8 .
mypy .
9. Ethical Scraping
Always prioritize ethical scraping practices. This protects your project from legal issues and IP bans.
✅ GOOD: Ethical scraping checklist
- Obey
robots.txt: Always.
- Clear User-Agent: Identify your crawler with contact info.
- Rate Limiting: Use
AUTOTHROTTLE and DOWNLOAD_DELAY.
- Handle Errors Gracefully: Implement retries and backoff.
- Distributed Crawling: Use proxies and rotate IPs.
- Avoid Overloading Servers: Monitor your crawl speed and resource usage.