Scrapy – веб-скрейпинг

онлайн тренажер по питону
Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое Scrapy и зачем он нужен

Scrapy представляет собой мощный асинхронный фреймворк для веб-скрейпинга на языке Python, который обеспечивает высокопроизводительный и масштабируемый сбор данных с веб-сайтов. Этот инструмент позволяет автоматизировать процесс извлечения структурированной информации из веб-страниц, что делает его незаменимым для анализа данных, мониторинга цен, парсинга новостей и многих других задач.

Ключевые особенности и преимущества Scrapy

Асинхронная архитектура

Scrapy построен на основе библиотеки Twisted, что обеспечивает асинхронную обработку запросов и высокую производительность при работе с большим количеством веб-страниц. Это позволяет одновременно обрабатывать множество запросов без блокировки выполнения программы.

Гибкость извлечения данных

Фреймворк поддерживает различные способы извлечения данных:

  • CSS селекторы для простого выбора элементов
  • XPath селекторы для более сложных запросов
  • Регулярные выражения для точного извлечения данных
  • Поддержка JSON и XML форматов

Масштабируемость и производительность

Scrapy спроектирован для работы с проектами любого размера - от небольших скриптов до крупных промышленных решений. Встроенные механизмы кеширования, параллельной обработки и управления памятью обеспечивают стабильную работу даже при обработке миллионов страниц.

Расширяемость через middleware

Система middleware позволяет легко расширять функциональность фреймворка, добавляя собственную логику обработки запросов и ответов, управления заголовками, работы с прокси-серверами и многое другое.

Установка и настройка Scrapy

Установка фреймворка

pip install scrapy

Для работы с дополнительными форматами данных рекомендуется установить дополнительные пакеты:

pip install scrapy[all]

Создание нового проекта

scrapy startproject myproject
cd myproject

Создание паука

scrapy genspider example example.com

Структура проекта Scrapy

После создания проекта формируется следующая структура каталогов:

myproject/
├── myproject/
│   ├── __init__.py
│   ├── items.py          # Определение структуры данных
│   ├── middlewares.py    # Middleware для обработки запросов/ответов
│   ├── pipelines.py      # Обработка и сохранение данных
│   ├── settings.py       # Конфигурация проекта
│   └── spiders/          # Директория с пауками
│       ├── __init__.py
│       └── example.py
└── scrapy.cfg           # Конфигурационный файл проекта

Назначение основных файлов

  • spiders/ - содержит логику парсинга и извлечения данных
  • items.py - определяет структуру извлекаемых данных
  • pipelines.py - обрабатывает и сохраняет извлеченные данные
  • middlewares.py - настраивает обработку запросов и ответов
  • settings.py - содержит все настройки проекта

Создание и настройка паука

Базовый паук

import scrapy

class ExampleSpider(scrapy.Spider):
    name = "example"
    allowed_domains = ["example.com"]
    start_urls = ["https://example.com"]

    def parse(self, response):
        # Извлечение данных
        title = response.css('title::text').get()
        
        # Возврат данных
        yield {
            'title': title,
            'url': response.url,
            'status': response.status
        }

Расширенный паук с навигацией

import scrapy

class AdvancedSpider(scrapy.Spider):
    name = "advanced"
    start_urls = ["https://example.com/catalog"]

    def parse(self, response):
        # Извлечение ссылок на товары
        product_links = response.css('a.product-link::attr(href)').getall()
        
        for link in product_links:
            yield response.follow(link, callback=self.parse_product)
        
        # Переход на следующую страницу
        next_page = response.css('a.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::text').get(),
            'price': response.css('.price::text').get(),
            'description': response.css('.description::text').get(),
            'url': response.url
        }

Методы извлечения данных

CSS селекторы

# Получение первого элемента
title = response.css('h1::text').get()

# Получение всех элементов
links = response.css('a::attr(href)').getall()

# Получение текста с очисткой
clean_text = response.css('p::text').get().strip()

XPath селекторы

# Поиск по атрибутам
price = response.xpath('//span[@class="price"]/text()').get()

# Сложные запросы
items = response.xpath('//div[contains(@class, "item")]//h2/text()').getall()

# Условные выражения
discount = response.xpath('//span[contains(text(), "Скидка")]/following-sibling::span/text()').get()

Комбинирование методов

def parse_product(self, response):
    # Использование CSS для основных элементов
    name = response.css('h1::text').get()
    
    # Использование XPath для сложных запросов
    features = response.xpath('//div[@class="features"]//li/text()').getall()
    
    # Обработка данных
    yield {
        'name': name.strip() if name else None,
        'features': [f.strip() for f in features if f.strip()],
        'url': response.url
    }

Работа с Items и ItemLoader

Определение структуры данных

# items.py
import scrapy

class ProductItem(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    description = scrapy.Field()
    images = scrapy.Field()
    availability = scrapy.Field()
    category = scrapy.Field()

Использование ItemLoader

from scrapy.loader import ItemLoader
from itemloaders.processors import TakeFirst, MapCompose
from myproject.items import ProductItem

class ProductLoader(ItemLoader):
    default_item_class = ProductItem
    default_output_processor = TakeFirst()
    
    price_in = MapCompose(lambda x: x.replace('$', ''), float)
    description_in = MapCompose(str.strip)

# Использование в пауке
def parse_product(self, response):
    loader = ProductLoader(selector=response)
    loader.add_css('name', 'h1::text')
    loader.add_css('price', '.price::text')
    loader.add_css('description', '.description::text')
    
    return loader.load_item()

Система Pipeline для обработки данных

Базовый pipeline

# pipelines.py
import json

class JsonPipeline:
    def open_spider(self, spider):
        self.file = open('items.json', 'w', encoding='utf-8')
        self.file.write('[\n')
        self.first_item = True

    def close_spider(self, spider):
        self.file.write('\n]')
        self.file.close()

    def process_item(self, item, spider):
        if not self.first_item:
            self.file.write(',\n')
        else:
            self.first_item = False
        
        line = json.dumps(dict(item), ensure_ascii=False, indent=2)
        self.file.write(line)
        return item

Pipeline для очистки данных

class CleanDataPipeline:
    def process_item(self, item, spider):
        # Очистка цены
        if item.get('price'):
            item['price'] = item['price'].replace('$', '').replace(',', '')
            try:
                item['price'] = float(item['price'])
            except ValueError:
                item['price'] = None
        
        # Очистка описания
        if item.get('description'):
            item['description'] = item['description'].strip()
        
        return item

Pipeline для сохранения в базу данных

import sqlite3

class DatabasePipeline:
    def open_spider(self, spider):
        self.connection = sqlite3.connect('items.db')
        self.cursor = self.connection.cursor()
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS products (
                id INTEGER PRIMARY KEY,
                name TEXT,
                price REAL,
                description TEXT,
                url TEXT
            )
        ''')
        self.connection.commit()

    def close_spider(self, spider):
        self.connection.close()

    def process_item(self, item, spider):
        self.cursor.execute('''
            INSERT INTO products (name, price, description, url)
            VALUES (?, ?, ?, ?)
        ''', (
            item.get('name'),
            item.get('price'),
            item.get('description'),
            item.get('url')
        ))
        self.connection.commit()
        return item

Настройка и конфигурация проекта

Основные настройки в settings.py

# Базовые настройки
BOT_NAME = 'myproject'
SPIDER_MODULES = ['myproject.spiders']
NEWSPIDER_MODULE = 'myproject.spiders'

# Соблюдение robots.txt
ROBOTSTXT_OBEY = False

# User-Agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'

# Заголовки по умолчанию
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
    'Accept-Encoding': 'gzip, deflate',
}

# Настройки производительности
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_DELAY = 1

# AutoThrottle
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 10
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0

# Pipelines
ITEM_PIPELINES = {
    'myproject.pipelines.CleanDataPipeline': 300,
    'myproject.pipelines.DatabasePipeline': 400,
}

# Middleware
DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    'myproject.middlewares.CustomUserAgentMiddleware': 400,
}

Работа с формами и аутентификацией

Заполнение форм

def parse_login(self, response):
    return scrapy.FormRequest.from_response(
        response,
        formdata={
            'username': 'myuser',
            'password': 'mypassword'
        },
        callback=self.after_login
    )

def after_login(self, response):
    # Проверка успешной авторизации
    if "Welcome" in response.text:
        # Переход к защищенным страницам
        yield response.follow('/protected-page', callback=self.parse_protected)

Работа с CSRF токенами

def parse_form(self, response):
    csrf_token = response.css('input[name="csrf_token"]::attr(value)').get()
    
    return scrapy.FormRequest(
        url='https://example.com/submit',
        formdata={
            'csrf_token': csrf_token,
            'data': 'value'
        },
        callback=self.handle_response
    )

Обработка JavaScript и динамического контента

Интеграция с Splash

# Установка scrapy-splash
# pip install scrapy-splash

# settings.py
SPLASH_URL = 'http://localhost:8050'
DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}

# Использование в пауке
def start_requests(self):
    for url in self.start_urls:
        yield scrapy.Request(url, self.parse, meta={
            'splash': {
                'args': {'wait': 0.5, 'png': 1, 'width': 1024, 'height': 768}
            }
        })

Интеграция с Selenium

from scrapy import signals
from selenium import webdriver
from scrapy.http import HtmlResponse

class SeleniumMiddleware:
    def __init__(self):
        self.driver = webdriver.Chrome()

    @classmethod
    def from_crawler(cls, crawler):
        middleware = cls()
        crawler.signals.connect(middleware.spider_closed, signals.spider_closed)
        return middleware

    def process_request(self, request, spider):
        if 'selenium' in request.meta:
            self.driver.get(request.url)
            body = self.driver.page_source
            return HtmlResponse(request.url, body=body, encoding='utf-8', request=request)

    def spider_closed(self):
        self.driver.quit()

Управление сессиями и cookies

Работа с cookies

def parse(self, response):
    # Получение cookies
    cookies = response.headers.getlist('Set-Cookie')
    
    # Отправка запроса с cookies
    yield scrapy.Request(
        url='https://example.com/page',
        cookies={'session_id': 'abc123'},
        callback=self.parse_with_session
    )

Сохранение сессии

class SessionSpider(scrapy.Spider):
    name = 'session'
    
    def start_requests(self):
        # Первый запрос для получения сессии
        yield scrapy.Request(
            url='https://example.com/login',
            callback=self.parse_login,
            meta={'cookiejar': 1}
        )
    
    def parse_login(self, response):
        # Авторизация с сохранением сессии
        return scrapy.FormRequest.from_response(
            response,
            formdata={'username': 'user', 'password': 'pass'},
            callback=self.parse_protected,
            meta={'cookiejar': response.meta['cookiejar']}
        )
    
    def parse_protected(self, response):
        # Использование сохраненной сессии
        yield scrapy.Request(
            url='https://example.com/data',
            callback=self.parse_data,
            meta={'cookiejar': response.meta['cookiejar']}
        )

Работа с API и JSON данными

Парсинг JSON API

import json

class ApiSpider(scrapy.Spider):
    name = 'api'
    
    def start_requests(self):
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer token123'
        }
        yield scrapy.Request(
            url='https://api.example.com/data',
            headers=headers,
            callback=self.parse_json
        )
    
    def parse_json(self, response):
        data = json.loads(response.text)
        
        for item in data.get('items', []):
            yield {
                'id': item.get('id'),
                'name': item.get('name'),
                'value': item.get('value')
            }
        
        # Пагинация
        next_page = data.get('next_page')
        if next_page:
            yield response.follow(next_page, callback=self.parse_json)

POST запросы к API

def make_api_request(self, response):
    payload = {
        'query': 'search term',
        'page': 1,
        'limit': 50
    }
    
    yield scrapy.Request(
        url='https://api.example.com/search',
        method='POST',
        body=json.dumps(payload),
        headers={'Content-Type': 'application/json'},
        callback=self.parse_api_response
    )

Обработка ошибок и логирование

Настройка логирования

# settings.py
LOG_LEVEL = 'INFO'
LOG_FILE = 'scrapy.log'

# Кастомные настройки логирования
import logging

logging.getLogger('scrapy').setLevel(logging.WARNING)

Обработка ошибок в пауке

class ErrorHandlingSpider(scrapy.Spider):
    name = 'error_handling'
    
    def parse(self, response):
        try:
            title = response.css('title::text').get()
            if not title:
                self.logger.warning(f"No title found for {response.url}")
                return
            
            yield {'title': title, 'url': response.url}
            
        except Exception as e:
            self.logger.error(f"Error parsing {response.url}: {str(e)}")
    
    def errback(self, failure):
        self.logger.error(f"Request failed: {failure.request.url}")
        if failure.check(HttpError):
            response = failure.value.response
            self.logger.error(f"HTTP error {response.status}: {response.url}")

Таблица основных методов и функций Scrapy

Метод/Функция Описание Пример использования
scrapy.Spider Базовый класс для создания пауков class MySpider(scrapy.Spider)
start_requests() Генерирует начальные запросы yield scrapy.Request(url, callback=self.parse)
parse() Основной метод обработки ответов def parse(self, response)
response.css() Извлечение данных через CSS селекторы response.css('h1::text').get()
response.xpath() Извлечение данных через XPath response.xpath('//h1/text()').get()
response.follow() Переход по ссылкам yield response.follow(link, callback=self.parse)
scrapy.Request() Создание HTTP запроса scrapy.Request(url, callback=self.parse)
scrapy.FormRequest() Отправка форм scrapy.FormRequest(url, formdata={})
scrapy.FormRequest.from_response() Заполнение формы из ответа scrapy.FormRequest.from_response(response, formdata={})
ItemLoader Загрузчик для обработки данных loader = ItemLoader(item=MyItem())
scrapy.Field() Определение поля в Item name = scrapy.Field()
yield Возврат данных или запросов yield {'title': title}
response.meta Передача данных между запросами response.meta['custom_data']
response.headers Доступ к заголовкам ответа response.headers.get('Content-Type')
response.status HTTP статус ответа if response.status == 200:
response.url URL текущего ответа item['url'] = response.url
response.text Текстовое содержимое ответа data = json.loads(response.text)
response.body Байтовое содержимое ответа response.body
self.logger Логгер паука self.logger.info('Message')
scrapy.signals Система сигналов crawler.signals.connect()
process_item() Обработка элементов в pipeline def process_item(self, item, spider)
open_spider() Инициализация при запуске паука def open_spider(self, spider)
close_spider() Очистка при завершении паука def close_spider(self, spider)
process_request() Обработка запроса в middleware def process_request(self, request, spider)
process_response() Обработка ответа в middleware def process_response(self, request, response, spider)

Производительность и оптимизация

Настройка производительности

# settings.py
# Количество одновременных запросов
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 16

# Задержка между запросами
DOWNLOAD_DELAY = 0.5
RANDOMIZE_DOWNLOAD_DELAY = 0.5

# Таймауты
DOWNLOAD_TIMEOUT = 180
RETRY_TIMES = 3

# Кеширование
HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 3600
HTTPCACHE_DIR = 'httpcache'

# Сжатие
COMPRESSION_ENABLED = True

Мониторинг производительности

# Включение статистики
STATS_CLASS = 'scrapy.statscollectors.MemoryStatsCollector'

# Кастомные метрики
class StatsSpider(scrapy.Spider):
    name = 'stats'
    
    def parse(self, response):
        # Увеличение счетчика
        self.crawler.stats.inc_value('pages_scraped')
        
        # Установка значения
        self.crawler.stats.set_value('last_page_url', response.url)
        
        yield {'url': response.url}

Развертывание и масштабирование

Dockerization

# Dockerfile
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

CMD ["scrapy", "crawl", "myspider"]

Использование с планировщиком задач

# schedule_spider.py
import schedule
import time
import subprocess

def run_spider():
    subprocess.run(['scrapy', 'crawl', 'myspider'])

schedule.every().day.at("08:00").do(run_spider)
schedule.every().hour.do(run_spider)

while True:
    schedule.run_pending()
    time.sleep(1)

Масштабирование с помощью Scrapyd

# Установка Scrapyd
pip install scrapyd

# Запуск сервера
scrapyd

# Развертывание проекта
scrapyd-deploy

# Запуск паука через API
curl http://localhost:6800/schedule.json -d project=myproject -d spider=myspider

Лучшие практики использования Scrapy

Структура кода

  • Разделяйте логику парсинга на отдельные методы
  • Используйте Items для структурирования данных
  • Применяйте Pipeline для постобработки
  • Настраивайте middleware для общих задач

Обработка данных

  • Всегда проверяйте наличие данных перед обработкой
  • Используйте try/except для обработки ошибок
  • Нормализуйте данные в Pipeline
  • Ведите логи для отладки

Этичность и соблюдение правил

  • Соблюдайте robots.txt файлы сайтов
  • Устанавливайте разумные задержки между запросами
  • Не перегружайте серверы большим количеством запросов
  • Используйте корректные User-Agent заголовки

Безопасность

  • Не храните пароли в коде
  • Используйте переменные окружения для конфиденциальных данных
  • Применяйте прокси для анонимности
  • Регулярно обновляйте зависимости

Частые проблемы и их решения

Блокировка по IP

# Использование прокси
DOWNLOADER_MIDDLEWARES = {
    'scrapy_proxy_middleware.middlewares.ProxyMiddleware': 110,
}

PROXY_LIST = [
    'http://proxy1:8000',
    'http://proxy2:8000',
    'http://proxy3:8000',
]

Обход капчи

class CaptchaMiddleware:
    def process_response(self, request, response, spider):
        if 'captcha' in response.text.lower():
            # Отправка на решение капчи
            return self.solve_captcha(request, response, spider)
        return response

Работа с большими объемами данных

# Использование генераторов
def parse_large_data(self, response):
    for item in self.extract_items(response):
        yield item
        
    # Освобождение памяти
    del response

Заключение

Scrapy представляет собой мощный и гибкий инструмент для веб-скрейпинга, который подходит как для простых задач извлечения данных, так и для сложных промышленных проектов. Благодаря своей архитектуре, богатому функционалу и активному сообществу, Scrapy остается одним из лучших выборов для разработчиков Python, работающих с веб-данными.

Освоение Scrapy открывает широкие возможности для автоматизации сбора данных, анализа рынка, мониторинга конкурентов и многих других задач. Правильное использование всех возможностей фреймворка позволяет создавать эффективные и надежные решения для работы с веб-данными любой сложности.

Новости