Когда нужно обойти не одну страницу, а весь сайт — тысячи и миллионы URL — самописный скрипт быстро превращается в кашу из очередей, повторных попыток и ротации прокси. Scrapy решает это за вас: это полноценный асинхронный фреймворк для краулинга с встроенными очередями, дедупликацией, пайплайнами обработки и системой middleware.
Эта статья продолжает обзорный материал «Парсинг на Python». Если вам нужен разовый сбор с пары страниц — хватит requests + BeautifulSoup; Scrapy раскрывается на масштабе.
Оглавление
- Архитектура Scrapy
- Как забираем страницу: Spider и Request
- Парсинг содержимого: селекторы
- Кириллица в Scrapy
- Параллельность и скорость
- Прокси
- Парсинг через TOR
- HTTPS/SSL
- Работа с cookie
- Статус ответа и заголовки
- Очереди и дедупликация URL
- Item Pipeline: хранение данных
- Плюсы и минусы
1. Архитектура Scrapy
Scrapy построен на асинхронном движке (Twisted) и состоит из связанных компонентов:
- Spider — ваш класс: с каких URL стартовать и как разбирать ответы.
- Scheduler — очередь запросов с дедупликацией и приоритетами.
- Downloader — асинхронно качает страницы.
- Middlewares — перехватчики запросов/ответов (прокси, заголовки, повторы).
- Item Pipeline — обработка и сохранение извлечённых данных.
Вы пишете только Spider и Pipeline — всё остальное фреймворк берёт на себя.
Создание проекта
pip install scrapy
scrapy startproject myparser
cd myparser
scrapy genspider example example.com
2. Как забираем страницу: Spider и Request
В Scrapy вы не пишете цикл запросов вручную — вы возвращаете (yield) объекты Request, а движок сам их выполняет асинхронно.
import scrapy
class CatalogSpider(scrapy.Spider):
name = "catalog"
start_urls = ["https://example.com/catalog"]
custom_settings = {
"USER_AGENT": "Mozilla/5.0 (compatible; MyBot/1.0)",
"DOWNLOAD_DELAY": 1.0, # пауза между запросами
"ROBOTSTXT_OBEY": True, # уважаем robots.txt
}
def parse(self, response):
# извлекаем карточки товаров
for card in response.css(".product-card"):
yield {
"title": card.css(".title::text").get(),
"price": card.css(".price::text").get(),
"url": response.urljoin(card.css("a::attr(href)").get()),
}
# переходим на следующую страницу
next_page = response.css("a.next::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
response.follow автоматически достроит относительный URL и поставит запрос в очередь. Так пагинация описывается в пару строк. Запуск:
scrapy crawl catalog -o products.json
3. Парсинг содержимого: селекторы
Под капотом Scrapy использует библиотеку parsel (на базе lxml), поддерживающую и CSS, и XPath:
# CSS
response.css("h1::text").get()
response.css(".price::text").getall()
response.css("a::attr(href)").getall()
# XPath
response.xpath("//h1/text()").get()
response.xpath('//div[@class="price"]/text()').get()
# регулярки прямо в селекторе
response.css(".price::text").re_first(r"\d+")
get() возвращает первый результат (или None), getall() — список. Это удобнее «голого» lxml за счёт безопасной обработки отсутствующих элементов. Подробнее о XPath — в «Парсинг на Python с lxml».
Если же страница на деле обращается к API и возвращает не HTML, а JSON, в Scrapy его разбирают напрямую через response.json() — без всяких селекторов. Как чистить и раскладывать такие ответы, подробно показано в «Парсинг JSON на Python».
Items и ItemLoader
Для структурированных проектов данные удобно описывать как Item и наполнять через ItemLoader с процессорами очистки (обрезка пробелов, приведение типов). Для простых пауков достаточно обычных словарей, как выше.
4. Кириллица в Scrapy
Чаще всего Scrapy сам корректно определяет кодировку по заголовкам и <meta charset> — кириллица «просто работает». Если попались кракозябры, можно явно указать кодировку при создании ответа или декодировать тело вручную:
def parse(self, response):
# принудительно перечитать тело в нужной кодировке
text = response.body.decode("windows-1251", errors="replace")
sel = scrapy.Selector(text=text)
Общая теория проблемы с кодировками — в хабе, раздел «Кириллица». Также проверьте FEED_EXPORT_ENCODING = "utf-8" в настройках, чтобы кириллица не превратилась в \uXXXX в итоговом JSON.
5. Параллельность и скорость
Главное преимущество Scrapy — асинхронность из коробки. Десятки запросов выполняются одновременно без потоков. Регулируется настройками:
# settings.py
CONCURRENT_REQUESTS = 16 # всего одновременных запросов
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # на один домен
DOWNLOAD_DELAY = 0.5 # базовая пауза
AUTOTHROTTLE_ENABLED = True # авто-подстройка скорости
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0
AutoThrottle — умная фишка: Scrapy сам снижает скорость, если сервер начинает отвечать медленнее, балансируя между скоростью и вежливостью. Это избавляет от ручного подбора задержек. Поскольку всё асинхронно, многопоточность вам не нужна — сравните с подходом в «Асинхронный парсинг на Python», который Scrapy реализует «под капотом».
6. Прокси
Простейший способ — задать прокси в meta запроса:
yield scrapy.Request(url, meta={"proxy": "http://user:pass@ip:port"})
Для ротации пула удобнее готовый пакет:
pip install scrapy-rotating-proxies
# settings.py
ROTATING_PROXY_LIST = [
"ip1:port",
"ip2:port",
"ip3:port",
]
DOWNLOADER_MIDDLEWARES = {
"rotating_proxies.middlewares.RotatingProxyMiddleware": 610,
"rotating_proxies.middlewares.BanDetectionMiddleware": 620,
}
Middleware сам ротирует прокси, отслеживает «забаненные» и исключает мёртвые. Общая теория — в хабе, раздел «Прокси».
7. Парсинг через TOR
TOR подключается как SOCKS5-прокси через meta:
yield scrapy.Request(url, meta={"proxy": "socks5h://127.0.0.1:9050"})
Смена выходного узла через stem (сигнал NEWNYM) описана в хабе, раздел «TOR». На практике для Scrapy чаще берут платные ротируемые прокси — они быстрее и реже банятся, чем выходные узлы TOR.
8. HTTPS/SSL
По умолчанию Scrapy проверяет сертификаты. Если нужно ослабить проверку для проблемного сайта (только осознанно):
# settings.py
DOWNLOADER_CLIENT_TLS_METHOD = "TLS"
# для самоподписанных сертификатов можно настроить контекст-фабрику
В большинстве случаев SSL «просто работает». Общие принципы безопасности соединения — в хабе, раздел «HTTPS/SSL».
9. Работа с cookie
Cookie в Scrapy включены по умолчанию (COOKIES_ENABLED = True) — движок хранит сессию между запросами автоматически. Передать cookie вручную:
yield scrapy.Request(url, cookies={"sessionid": "abc123"})
Для авторизации удобен FormRequest:
def parse(self, response):
return scrapy.FormRequest.from_response(
response,
formdata={"username": "user", "password": "pass"},
callback=self.after_login,
)
from_response сам подхватит скрытые поля формы (включая CSRF-токен) — это снимает типичную головную боль авторизации.
10. Статус ответа и заголовки
Доступ к статусу и заголовкам — через объект response:
def parse(self, response):
print(response.status) # 200, 404 ...
print(response.headers.get("Content-Type"))
По умолчанию Scrapy обрабатывает только 2xx, а 4xx/5xx пропускает. Управление повторами — встроенный RetryMiddleware:
# settings.py
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 403]
Статус 429 (Too Many Requests) Scrapy умеет уважать вместе с заголовком Retry-After. Логика статусов в целом — в хабе, раздел «Статус и заголовки».
11. Очереди и дедупликация URL
Здесь Scrapy особенно силён — то, что в самописном парсере приходится строить руками (см. хаб, раздел «Очереди»), здесь встроено:
- Scheduler держит очередь запросов с приоритетами.
- Dupefilter автоматически отбрасывает повторно встреченные URL (по отпечатку запроса).
- Очередь можно вынести на диск (
JOBDIR), чтобы возобновить прерванный обход:
scrapy crawl catalog -s JOBDIR=crawls/catalog-1
Для распределённого краулинга на нескольких машинах есть scrapy-redis — общая очередь и dupefilter в Redis, что позволяет нескольким воркерам обходить один сайт совместно.
12. Item Pipeline: хранение данных
Извлечённые элементы проходят через пайплайн — там их валидируют, чистят и сохраняют:
# pipelines.py
import pymongo
class MongoPipeline:
def open_spider(self, spider):
self.client = pymongo.MongoClient("mongodb://localhost:27017")
self.db = self.client["scraping"]
def process_item(self, item, spider):
self.db["products"].update_one(
{"url": item["url"]}, {"$set": dict(item)}, upsert=True
)
return item
def close_spider(self, spider):
self.client.close()
# settings.py
ITEM_PIPELINES = {"myparser.pipelines.MongoPipeline": 300}
Для простого экспорта пайплайн не нужен — флаг -o products.csv (или .json, .jsonl) сохранит результат сразу.
13. Плюсы и минусы Scrapy
Плюсы:
- Асинхронность, очереди, дедупликация и повторы — из коробки.
- Высокая производительность на большом числе страниц.
- Чистая архитектура: Spider, Middleware, Pipeline разделены.
- Готовые расширения: ротация прокси, scrapy-redis, авто-троттлинг.
- Возобновляемые задания и удобный экспорт в любой формат.
Минусы:
- Высокий порог входа — нужно понять архитектуру и Twisted.
- Избыточен для пары страниц (там проще requests + BeautifulSoup).
- JavaScript-сайты требуют интеграции (scrapy-playwright или Splash).
- Асинхронная модель Twisted непривычна на фоне современного asyncio.
Для динамических сайтов подключают scrapy-playwright, который рендерит страницы реальным браузером. Если же контент простой и статический, а Django-инфраструктура уже есть, иногда проще парсить в Django.