Scrapy: Python Web Scraping Framework
In This Guide
- What Is Scrapy and Why Use It
- Scrapy Architecture and How It Works
- Setting Up Scrapy
- Writing Your First Spider
- Selectors: Extracting Data
- Following Links and Crawling
- Item Pipelines and Data Processing
- Middleware and Request Handling
- Scrapy vs Other Python Tools
- Advanced Scrapy Features
- Best Practices for Production
What Is Scrapy and Why Use It
Scrapy is a Python framework designed specifically for web scraping and web crawling at scale. Originally released in 2008 and maintained by Zyte (formerly Scrapinghub), Scrapy has grown into the most popular Python scraping framework with over 53,000 stars on GitHub. Unlike simple HTTP libraries that only fetch pages, Scrapy provides a complete ecosystem for requesting pages, parsing responses, extracting data, processing items, and storing results.
The framework excels in scenarios where you need to scrape thousands or millions of pages efficiently. Its asynchronous architecture means a single Scrapy process can handle hundreds of concurrent connections without threading complexity. For data engineers, researchers, and developers who need reliable, repeatable data extraction, Scrapy offers a structured approach that scales from small scripts to enterprise crawling systems.
Key reasons developers choose Scrapy over simpler alternatives include built-in request scheduling with configurable concurrency, automatic retry and error handling, a middleware system for proxies and authentication, item pipelines for data validation and storage, and extensive community support with hundreds of third-party extensions. Scrapy also respects robots.txt by default, includes built-in throttling, and provides detailed logging and statistics for monitoring crawl performance.
Scrapy Architecture and How It Works
Scrapy operates on a component-based architecture where each piece handles a specific responsibility. Understanding this architecture is essential for writing efficient spiders and debugging crawl issues. The core components work together in an event-driven loop powered by Twisted, Python's asynchronous networking framework.
The Scrapy Engine orchestrates the entire crawling process. It receives requests from spiders, passes them through downloader middleware, sends them to the downloader, and routes responses back through spider middleware to the appropriate spider callback. The engine also manages the scheduler, which queues and prioritizes pending requests.
The Scheduler receives requests from the engine and stores them in a priority queue. It handles deduplication by default, ensuring the same URL is not fetched twice within a crawl. You can configure the scheduler to use disk-based queues for pause and resume functionality, which is critical for long-running crawls that might be interrupted.
The Downloader fetches web pages and returns HTTP responses. It operates asynchronously, managing connection pools and respecting concurrency limits you define in settings. The downloader handles HTTP features like cookies, redirects, and compression transparently.
Downloader Middleware sits between the engine and the downloader, processing requests on the way out and responses on the way in. This is where proxy rotation, user-agent randomization, retry logic, and cookie management happen. You can stack multiple middleware components in priority order, each handling one concern.
Spiders are the classes you write. They define the starting URLs, parse responses to extract data, and yield new requests to follow links. Each spider is a self-contained crawler for a specific website or type of content.
Spider Middleware processes spider input (responses) and output (items and requests). It can filter, modify, or augment the data flowing between the engine and your spider callbacks.
Item Pipelines process extracted items after your spider yields them. Pipelines run in sequence, with each stage handling one task: validation, cleaning, deduplication, or storage. A single crawl can write to databases, files, and APIs simultaneously through separate pipeline stages.
Setting Up Scrapy
Installing Scrapy requires Python 3.8 or later. The recommended approach uses pip within a virtual environment to isolate dependencies. Scrapy depends on several C libraries (lxml, OpenSSL, Twisted), so system-level packages may be needed on some Linux distributions.
To install Scrapy, create a virtual environment and use pip:
python -m venv scrapy-env
source scrapy-env/bin/activate
pip install scrapy
On Ubuntu or Debian systems, you may need to install development headers first:
sudo apt-get install python3-dev libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev
Once installed, create a new Scrapy project using the command-line tool:
scrapy startproject myproject
cd myproject
This generates a standard project structure with dedicated directories for spiders, items, pipelines, and middleware. The structure looks like this:
myproject/
scrapy.cfg
myproject/
__init__.py
items.py
middlewares.py
pipelines.py
settings.py
spiders/
__init__.py
The scrapy.cfg file configures deployment settings. The settings.py file contains all project configuration including concurrency limits, download delays, enabled pipelines, and middleware. The items.py file defines the data structures your spiders will extract, similar to database models.
Writing Your First Spider
A Scrapy spider is a Python class that inherits from scrapy.Spider and defines how to crawl and parse a website. Every spider needs a unique name, a list of starting URLs, and at least one parse method that processes responses.
Here is a minimal spider that scrapes quotes from a website:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
"tags": quote.css("div.tags a.tag::text").getall(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
Run the spider from the command line:
scrapy crawl quotes -O quotes.json
This spider demonstrates several core Scrapy concepts. The start_urls list defines where crawling begins. The parse method receives each response and uses CSS selectors to extract data. Yielding a dictionary creates an item that flows through the pipeline. Yielding a request (via response.follow) adds a new URL to the crawl queue.
The -O flag overwrites the output file with JSON. Use -o to append instead. Scrapy supports JSON, JSON Lines, CSV, and XML output formats natively. For database storage, you configure item pipelines.
Selectors: Extracting Data with CSS and XPath
Scrapy provides two selector systems for extracting data from HTML: CSS selectors and XPath expressions. Both are built on the lxml library and offer high performance parsing. Most developers use CSS selectors for simple extraction and switch to XPath when they need more complex queries.
CSS selectors in Scrapy use an extended syntax with pseudo-elements for text and attribute extraction:
# Get text content of an element
response.css("h1::text").get()
# Get an attribute value
response.css("a::attr(href)").get()
# Get all matching elements
response.css("ul li::text").getall()
# Nested selection
response.css("div.product").css("span.price::text").get()
XPath provides more power for complex document traversal:
# Select by text content
response.xpath('//a[contains(text(), "Next")]/@href').get()
# Navigate to parent elements
response.xpath('//span[@class="price"]/parent::div')
# Select by position
response.xpath('//table/tr[position() > 1]')
# Combine conditions
response.xpath('//div[@class="item" and @data-available="true"]')
The .get() method returns the first match as a string, or None if nothing matches. The .getall() method returns a list of all matches. You can chain selectors to narrow down within a selection, which is particularly useful when iterating over repeated page elements like product cards or list items.
For debugging selectors interactively, Scrapy provides the shell command:
scrapy shell "https://example.com"
This opens an IPython session with the response pre-loaded, letting you test selectors against live pages before writing them into your spider code.
Following Links and Crawling Multiple Pages
Real scraping projects need to navigate through pagination, category listings, and detail pages. Scrapy handles this through request yielding, where your parse methods generate new requests that the engine schedules for downloading.
The most common patterns for following links include pagination (next page buttons), listing-to-detail (clicking into each item), and recursive crawling (following all links matching a pattern). Here is a spider that handles both listing and detail pages:
class ProductSpider(scrapy.Spider):
name = "products"
start_urls = ["https://shop.example.com/category/electronics"]
def parse(self, response):
# Extract product links from listing page
for link in response.css("a.product-card::attr(href)").getall():
yield response.follow(link, callback=self.parse_product)
# Follow pagination
next_page = response.css("a.pagination-next::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
def parse_product(self, response):
yield {
"name": response.css("h1.product-title::text").get(),
"price": response.css("span.price::text").get(),
"description": response.css("div.description p::text").getall(),
"url": response.url,
}
The response.follow() method handles relative URLs automatically, resolving them against the current page URL. For absolute URLs, you can also use scrapy.Request(url, callback=self.method) directly. Both approaches add the request to the scheduler queue, where deduplication and prioritization happen before the downloader fetches the page.
For broader crawling, Scrapy offers CrawlSpider, a specialized spider class with rule-based link following. You define rules with regular expressions that match URLs, and the spider automatically follows matching links and routes responses to the correct callbacks.
Item Pipelines and Data Processing
Item pipelines process every item your spiders yield. They run in sequence by priority number, with each pipeline class implementing a process_item method. Common pipeline tasks include data cleaning, validation, deduplication, and storage.
A pipeline class has a simple interface:
class CleaningPipeline:
def process_item(self, item, spider):
# Strip whitespace from all string fields
for field, value in item.items():
if isinstance(value, str):
item[field] = value.strip()
return item
class DatabasePipeline:
def open_spider(self, spider):
self.connection = create_db_connection()
def close_spider(self, spider):
self.connection.close()
def process_item(self, item, spider):
self.connection.insert(item)
return item
Enable pipelines in settings.py with priority numbers:
ITEM_PIPELINES = {
"myproject.pipelines.CleaningPipeline": 100,
"myproject.pipelines.DatabasePipeline": 300,
}
Lower numbers run first. This ordering matters because you want validation and cleaning to happen before storage. If a pipeline raises a DropItem exception, the item is discarded and subsequent pipelines never see it, which is the standard approach for deduplication and filtering invalid records.
Pipelines also support open_spider and close_spider hooks for resource management like database connections, file handles, or API clients that need initialization and cleanup.
Middleware and Request Handling
Middleware components intercept requests and responses as they flow through the Scrapy engine. Downloader middleware is the most commonly customized layer, handling concerns like proxy rotation, user-agent randomization, and retry policies.
A typical proxy rotation middleware looks like this:
class ProxyMiddleware:
def __init__(self):
self.proxies = [
"http://proxy1:8080",
"http://proxy2:8080",
"http://proxy3:8080",
]
def process_request(self, request, spider):
request.meta["proxy"] = random.choice(self.proxies)
def process_response(self, request, response, spider):
if response.status in (403, 429):
# Retry with a different proxy
request.meta["proxy"] = random.choice(self.proxies)
return request
return response
Scrapy includes several built-in middleware components that handle common needs: HttpProxyMiddleware for proxy support, UserAgentMiddleware for user-agent headers, RetryMiddleware for automatic retries on failures, CookiesMiddleware for session management, and HttpCompressionMiddleware for gzip decoding.
You configure middleware priority in settings, and Scrapy processes them in order. Request middleware runs from lowest to highest priority going out, while response middleware runs highest to lowest coming back. This design lets you layer concerns cleanly without coupling.
Scrapy vs Other Python Scraping Tools
Python offers several scraping libraries, each suited to different use cases. Understanding where Scrapy fits helps you choose the right tool for your project.
Scrapy vs Requests + BeautifulSoup: The Requests library paired with BeautifulSoup is the simplest approach to web scraping in Python. It works well for scraping a handful of pages synchronously. However, it lacks built-in concurrency, scheduling, retry logic, and pipeline processing. For projects that grow beyond a few dozen pages, you end up reimplementing features that Scrapy provides out of the box. Scrapy is the better choice when you need to crawl many pages, handle failures gracefully, or process data through multiple stages.
Scrapy vs Playwright and Selenium: Browser automation tools like Playwright and Selenium render JavaScript and interact with dynamic pages. Scrapy operates at the HTTP level without a browser, making it faster and more resource-efficient for static content. When you need JavaScript rendering, you can integrate Scrapy with Playwright through the scrapy-playwright plugin rather than replacing Scrapy entirely. This gives you browser rendering where needed while keeping Scrapy's scheduling and pipeline infrastructure.
Scrapy vs Scrapling and newer frameworks: Newer Python scraping libraries like Scrapling focus on adaptive selectors and anti-bot evasion. While these tools offer modern features, they typically lack Scrapy's mature pipeline system, extensive middleware ecosystem, and production-proven reliability. For most projects, Scrapy's extensibility through plugins covers the same ground while providing a more battle-tested foundation.
Advanced Scrapy Features
Beyond basic crawling, Scrapy supports several advanced patterns that production systems rely on.
Signals: Scrapy emits signals at various points during the crawl lifecycle, letting you hook into events like spider_opened, item_scraped, spider_error, and spider_closed. Extensions use signals to implement monitoring, alerting, and custom statistics collection.
Feed exports: Scrapy can write scraped items directly to files, S3 buckets, Google Cloud Storage, or FTP servers. Configure multiple feeds simultaneously to write the same data in different formats to different destinations.
Contracts: Spider contracts are a lightweight testing system built into Scrapy. You write docstring annotations on callback methods that specify expected behavior, and Scrapy validates them during test runs.
AutoThrottle: The AutoThrottle extension dynamically adjusts download delays based on server response times. When a target server slows down, Scrapy backs off automatically. When it responds quickly, Scrapy increases request rate up to your configured limit. This balances speed with politeness without manual tuning.
Job persistence: Scrapy can save crawl state to disk, allowing you to pause and resume long crawls. The scheduler saves pending requests, and the duplicate filter saves seen URLs. This is essential for crawls that take hours or days and might be interrupted by deployments or system maintenance.
Async support: Modern Scrapy versions support async/await syntax in spider callbacks and pipeline methods, enabling cleaner code when working with async databases or APIs during the scrape process.
Best Practices for Production Scraping
Running Scrapy in production requires attention to reliability, performance, and ethical scraping practices.
Respect robots.txt and rate limits. Always enable ROBOTSTXT_OBEY = True in settings. Configure DOWNLOAD_DELAY to at least 1 second between requests to the same domain. Use AutoThrottle to adapt dynamically to server capacity. Aggressive scraping can get your IPs blocked and potentially violate terms of service.
Use items and loaders. Define formal Item classes for your data structures rather than yielding raw dictionaries. Item loaders provide input and output processors that standardize data cleaning, reducing repetitive code in your spider callbacks.
Handle errors gracefully. Configure retry middleware for transient failures. Use errback parameters on requests to handle connection errors and timeouts without crashing the spider. Log failures for later investigation rather than silently dropping pages.
Monitor crawl health. Scrapy provides detailed statistics at the end of each crawl including request counts, response status distribution, item counts, and error rates. For long-running crawls, integrate with monitoring systems through extensions. Watch for increasing error rates that indicate blocking or site structure changes.
Structure projects for maintainability. Keep one spider per file. Use base spider classes for shared logic across multiple spiders targeting similar sites. Store selectors in separate configuration when they change frequently. Version control your spiders and settings alongside the rest of your codebase.
Manage dependencies carefully. Pin your Scrapy version and all middleware dependencies. Test spider behavior after upgrading, as selector libraries and middleware APIs can change between versions. Use virtual environments to isolate project dependencies.