Парсинг на Django: сбор данных внутри веб-приложения

Как встроить сбор данных прямо в Django-приложение: management-команды, Celery-задачи, хранение результатов в моделях и админке.

КP
Команда Parsing.agency
Сбор данных под задачи бизнеса
Опубликовано: 7 января 2025

Когда парсинг нужен не как разовый скрипт, а как часть полноценного веб-сервиса — с интерфейсом, расписанием, хранением в БД и фоновой обработкой — на первый план выходит Django. Фреймворк даёт ORM для хранения, management-команды для запуска, а в связке с Celery — фоновые и периодические задачи.

Эта статья — практическое продолжение обзорного материала «Парсинг на Python». Базовые приёмы (requests, BeautifulSoup, кодировки) разобраны там, здесь акцент на интеграции в Django.

Оглавление

  1. Зачем парсить в Django
  2. Как забираем страницу
  3. Библиотеки для парсинга содержимого
  4. Хранение результата в моделях
  5. Запуск парсинга: management-команды и Celery
  6. Кириллица в Django
  7. Многопоточность и фоновые задачи
  8. Прокси
  9. Парсинг через TOR
  10. HTTPS/SSL
  11. Работа с cookie
  12. Статус ответа и заголовки
  13. Хранение URL и очередей через ORM
  14. Плюсы и минусы

1. Зачем парсить в Django

Django оправдан, когда парсинг — лишь часть продукта:

  • агрегатор товаров/вакансий/новостей с веб-витриной;
  • мониторинг цен с историей в БД и дашбордом;
  • регулярное обновление каталога по расписанию;
  • админ-панель для управления источниками и просмотра результатов.

Django даёт «из коробки»: ORM (хранение и дедупликация), админку (управление источниками), систему миграций, а с Celery — фоновое и периодическое выполнение, чтобы тяжёлый парсинг не блокировал веб-запросы.


2. Как забираем страницу

HTTP-клиент в Django ничем не отличается от обычного Python — берём requests или httpx. Логику парсинга держим вне views: views должны быть быстрыми, а сетевые запросы выносим в сервисный слой или Celery-задачи.

python
# parser/services.py
import requests

HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; MyParserBot/1.0)",
    "Accept-Language": "ru-RU,ru;q=0.9",
}

def fetch_page(url: str) -> str | None:
    try:
        resp = requests.get(url, headers=HEADERS, timeout=15)
        resp.raise_for_status()
        resp.encoding = resp.apparent_encoding
        return resp.text
    except requests.RequestException as exc:
        # логируем и не роняем приложение
        import logging
        logging.getLogger("parser").warning("fetch failed %s: %s", url, exc)
        return None

Главное правило: никогда не запускайте долгий парсинг прямо во view — пользователь будет ждать, воркер веб-сервера заблокируется. Запрос ставится в очередь, а ответ возвращается сразу.


3. Библиотеки для парсинга содержимого

Внутри сервиса используются те же инструменты, что и везде — BeautifulSoup для удобства, lxml для скорости:

python
from bs4 import BeautifulSoup

def parse_products(html: str) -> list[dict]:
    soup = BeautifulSoup(html, "lxml")
    items = []
    for card in soup.select(".product-card"):
        items.append({
            "title": card.select_one(".title").get_text(strip=True),
            "price": card.select_one(".price").get_text(strip=True),
            "url": card.select_one("a")["href"],
        })
    return items

Подробный разбор парсеров — в «Парсинг на Python» и «lxml». Если источник — таблицы, см. «Парсинг таблиц на BeautifulSoup». Если API отдаёт JSON — «Парсинг JSON».


4. Хранение результата в моделях

Сила Django — ORM. Описываем модель, и дедупликация, фильтрация, история обновлений становятся тривиальными.

python
# parser/models.py
from django.db import models

class Product(models.Model):
    source_url = models.URLField(unique=True)   # уникальность = защита от дублей
    title = models.CharField(max_length=500)
    price = models.DecimalField(max_digits=10, decimal_places=2, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

Сохранение с защитой от дублей через update_or_create:

python
from .models import Product

def save_products(items: list[dict]):
    for item in items:
        Product.objects.update_or_create(
            source_url=item["url"],
            defaults={"title": item["title"], "price": item["price"]},
        )

update_or_create обновит существующую запись или создаст новую — идеально для повторного парсинга.


5. Запуск парсинга: management-команды и Celery

Management-команда — для ручного и cron-запуска

python
# parser/management/commands/run_parser.py
from django.core.management.base import BaseCommand
from parser.services import fetch_page, parse_products, save_products

class Command(BaseCommand):
    help = "Запускает парсинг каталога"

    def add_arguments(self, parser):
        parser.add_argument("--url", required=True)

    def handle(self, *args, **options):
        html = fetch_page(options["url"])
        if html:
            items = parse_products(html)
            save_products(items)
            self.stdout.write(self.style.SUCCESS(f"Сохранено {len(items)} товаров"))

Запуск: python manage.py run_parser --url https://example.com/catalog. Такую команду удобно вешать на системный cron.

Celery — для фоновых и периодических задач

python
# parser/tasks.py
from celery import shared_task
from .services import fetch_page, parse_products, save_products

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def parse_url_task(self, url):
    html = fetch_page(url)
    if html is None:
        raise self.retry()       # повтор при сбое
    items = parse_products(html)
    save_products(items)
    return len(items)

Периодический запуск настраивается через django-celery-beat прямо в админке — например, обновлять каталог каждые 6 часов. Celery retry даёт устойчивость к временным сбоям сети без ручной обработки.


6. Кириллица в Django

Проблема та же, что в любом Python-парсере: неверная кодировка ответа. Лечится так же — resp.apparent_encoding или работа с resp.content (см. подробно «Парсинг на Python», раздел о кириллице).

Django-специфика: убедитесь, что БД использует UTF-8 (для PostgreSQL — кодировка UTF8, для MySQL — utf8mb4), иначе кириллица сломается уже на стадии сохранения, а не парсинга. В настройках проекта это значение по умолчанию, но при работе со старой БД стоит проверить.


7. Многопоточность и фоновые задачи

В Django редко используют «голые» потоки — для параллелизма есть Celery с пулом воркеров. Запустив несколько воркеров, вы получаете параллельную обработку очереди без ручного управления потоками:

bash
celery -A myproject worker --concurrency=8 --loglevel=info

Если же нужно быстро обойти список URL внутри одной задачи, подойдёт ThreadPoolExecutor (см. раздел о многопоточности в хабе). Для очень высокой параллельности внутри задачи можно применить async — но в Django это требует аккуратности с ORM (sync_to_async). Тема разобрана в «Асинхронный парсинг на Python».


8. Прокси

Прокси подключаются на уровне сервисного слоя — Django тут ни при чём. Список прокси удобно хранить в модели и помечать «здоровые»:

python
class Proxy(models.Model):
    address = models.CharField(max_length=200)   # http://user:pass@ip:port
    is_active = models.BooleanField(default=True)
    fail_count = models.IntegerField(default=0)

Тогда сервис берёт случайный активный прокси, а при ошибке увеличивает fail_count и при превышении порога деактивирует. Общие приёмы ротации — в хабе, раздел «Прокси».


9. Парсинг через TOR

TOR подключается как SOCKS5-прокси (socks5h://127.0.0.1:9050) в том же fetch_page. Никакой специфики у Django нет — смотрите раздел про TOR в хабе. На сервере с Django демон TOR запускается отдельным процессом (systemd-сервис).


10. HTTPS/SSL

Проверка сертификатов работает на уровне requests. В проде оставляйте verify=True. Если на сервере устаревшие корневые сертификаты, обновите certifi в виртуальном окружении проекта. Подробности — в хабе.


Для парсинга с авторизацией используйте requests.Session внутри задачи. Если нужно переиспользовать сессию между задачами Celery, сохраните cookie-jar (например, сериализованным в Redis или в модели) и восстанавливайте перед запросом. Базовые приёмы — в хабе, раздел «Cookie».


12. Статус ответа и заголовки

Логируйте код ответа и решайте, повторять ли задачу. В Celery это элегантно ложится на механизм retry:

python
@shared_task(bind=True, max_retries=5)
def fetch_task(self, url):
    resp = requests.get(url, timeout=15)
    if resp.status_code == 429:
        delay = int(resp.headers.get("Retry-After", 60))
        raise self.retry(countdown=delay)
    if resp.status_code in (403, 503):
        raise self.retry(countdown=120)   # вероятно бан, ждём дольше
    resp.raise_for_status()
    return resp.text

Так анти-бот-блокировки (429/403/503) превращаются в управляемые повторы с задержкой, а не в потерю данных.


13. Хранение URL и очередей через ORM

В Django frontier удобно реализовать таблицей со статусами — это переживает перезапуск и видно в админке.

python
class CrawlURL(models.Model):
    STATUS = [("new", "new"), ("in_progress", "in_progress"),
              ("done", "done"), ("failed", "failed")]
    url = models.URLField(unique=True)     # unique = автодедупликация
    status = models.CharField(max_length=20, choices=STATUS, default="new")
    attempts = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

Воркер забирает «новые» URL, помечает in_progress, обрабатывает и переводит в done/failed. Уникальный индекс по url исключает дубли. Для высоконагруженных проектов реальную очередь лучше держать в Redis/брокере Celery, а в БД хранить только результат и историю. Общий обзор подходов к очередям — в хабе, раздел «Очереди».


14. Плюсы и минусы парсинга на Django

Плюсы:

  • ORM снимает заботу о хранении, дедупликации и миграциях.
  • Админка из коробки — управление источниками и просмотр данных.
  • Celery + beat — надёжные фоновые и периодические задачи с retry.
  • Единая кодовая база: парсер и веб-витрина в одном проекте.

Минусы:

  • Избыточен для разовых скриптов — проще обычный .py.
  • Требует инфраструктуры: брокер (Redis/RabbitMQ), воркеры Celery.
  • Async в Django всё ещё с оговорками (ORM преимущественно синхронный).
  • Для обхода миллионов страниц специализированный Scrapy эффективнее.