Когда парсинг нужен не как разовый скрипт, а как часть полноценного веб-сервиса — с интерфейсом, расписанием, хранением в БД и фоновой обработкой — на первый план выходит Django. Фреймворк даёт ORM для хранения, management-команды для запуска, а в связке с Celery — фоновые и периодические задачи.
Эта статья — практическое продолжение обзорного материала «Парсинг на Python». Базовые приёмы (requests, BeautifulSoup, кодировки) разобраны там, здесь акцент на интеграции в Django.
Оглавление
- Зачем парсить в Django
- Как забираем страницу
- Библиотеки для парсинга содержимого
- Хранение результата в моделях
- Запуск парсинга: management-команды и Celery
- Кириллица в Django
- Многопоточность и фоновые задачи
- Прокси
- Парсинг через TOR
- HTTPS/SSL
- Работа с cookie
- Статус ответа и заголовки
- Хранение URL и очередей через ORM
- Плюсы и минусы
1. Зачем парсить в Django
Django оправдан, когда парсинг — лишь часть продукта:
- агрегатор товаров/вакансий/новостей с веб-витриной;
- мониторинг цен с историей в БД и дашбордом;
- регулярное обновление каталога по расписанию;
- админ-панель для управления источниками и просмотра результатов.
Django даёт «из коробки»: ORM (хранение и дедупликация), админку (управление источниками), систему миграций, а с Celery — фоновое и периодическое выполнение, чтобы тяжёлый парсинг не блокировал веб-запросы.
2. Как забираем страницу
HTTP-клиент в Django ничем не отличается от обычного Python — берём requests или httpx. Логику парсинга держим вне views: views должны быть быстрыми, а сетевые запросы выносим в сервисный слой или Celery-задачи.
# 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 для скорости:
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. Описываем модель, и дедупликация, фильтрация, история обновлений становятся тривиальными.
# 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:
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-запуска
# 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 — для фоновых и периодических задач
# 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 с пулом воркеров. Запустив несколько воркеров, вы получаете параллельную обработку очереди без ручного управления потоками:
celery -A myproject worker --concurrency=8 --loglevel=info
Если же нужно быстро обойти список URL внутри одной задачи, подойдёт ThreadPoolExecutor (см. раздел о многопоточности в хабе). Для очень высокой параллельности внутри задачи можно применить async — но в Django это требует аккуратности с ORM (sync_to_async). Тема разобрана в «Асинхронный парсинг на Python».
8. Прокси
Прокси подключаются на уровне сервисного слоя — Django тут ни при чём. Список прокси удобно хранить в модели и помечать «здоровые»:
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 в виртуальном окружении проекта. Подробности — в хабе.
11. Работа с cookie
Для парсинга с авторизацией используйте requests.Session внутри задачи. Если нужно переиспользовать сессию между задачами Celery, сохраните cookie-jar (например, сериализованным в Redis или в модели) и восстанавливайте перед запросом. Базовые приёмы — в хабе, раздел «Cookie».
12. Статус ответа и заголовки
Логируйте код ответа и решайте, повторять ли задачу. В Celery это элегантно ложится на механизм retry:
@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 удобно реализовать таблицей со статусами — это переживает перезапуск и видно в админке.
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 эффективнее.