Массовая типографика контента в Django: пользовательские команды управления (Custom Management Commands)
Если вы работали с Django, вы наверняка использовали команду manage.py сотни раз: python manage.py runserver, migrate, createsuperuser.
Но знаете ли вы, что можете легко создавать свои собственные команды? Это один из самых мощных и недооцененных инструментов Django для автоматизации рутины. В этом посте я расскажу, что такое Management Command, зачем она нужна и как её использовать на примере реальной задачи: массовой типографики контента в базе данных.
Что это такое?
Management Command — это обычный Python-скрипт, который «встраивается» в экосистему Django. Он имеет доступ к моделям, настройкам (settings.py) и базе данных, но запускается из консоли через python manage.py ....
Зачем это нужно?
Представьте ситуации:
- Нужно пересчитать рейтинг для десятки тысяч товаров на сайте или сделать типографику всех публикаций после подключения etpgrf-типографа (или любого другого).
- Необходимо обновить цены для всех товарных карточек в соответствии с таблицей excel или по API (или спарсить данные с внешнего сайта).
- Нужно отправить e-mail рассылку пользователям.
- Нужно пометить комментарии или публикации как архивные в соответствии с правилом или почистить базу от старых логов.
Делать это через views.py (views) — плохая идея (страница может отвалиться по тайм-ауту). Писать отделный скрипт рядом manage.py — неудобно (нужно вручную настраивать `DJANGO_SETTINGS_MODULE`). Встроенные команды решают эти проблемы элегантно.
Как это работает?
Django использует систему «автообнаружения» (auto-discovery). Чтобы ваша команда появилась в списке `manage.py`, нужно соблюсти строгую иерархию папок внутри вашего приложения (например, web):
web/
├── __init__.py
├── models.py
└── management/ <-- 1. Создаем папку management
├── __init__.py
└── nginx/
├── commands/ <-- 2. Внутри неё папку commands
├── __init__.py
└── my_cool_script.py <-- 3. Наш файл с командой
Файл my_cool_script.py автоматически превратится в команду: python manage.py my_cool_script. Django будет искать его в папках management/commands/ внутри каждого установленного приложения. Это позволяет легко организовать код и держать все «команды обслуживания» в одном месте.
Для справки: Механизм Custom Management Commands появился еще в Django 0.96 (в глубокой древности) и с тех пор является стандартом де-факто для написания скриптов обслуживания.
Анатомия команды
Вот пример простейшей команды. Мы наследуемся от класса BaseCommand и переопределяем метод handle:
# web/management/commands/hello.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Выводит приветствие' # Описание для --help
def add_arguments(self, parser):
# Можно добавлять аргументы, как в argparse
parser.add_argument('name', type=str, help='Имя пользователя')
def handle(self, *args, **options):
# Главная логика
name = options['name']
self.stdout.write(f"Привет, {name}!")
Запуск:
python manage.py hello Иван
Увидим в консоли:
Привет, Иван!
Реальный пример: Массовая типографика через etpgrf
В моём проекте, DQ – коллекция цитат: место для вдумчивого чтения, контент был частично обработан с помощью Типографа Муравьёа, частично вручную. И вот я установил и настроил etpgrf (тем более что Типограф Муравьёва «почил в бозе», да и до того не обновлялся с 2018 года). Все новые цитаты типографируются через etpgrf, но что делать со старыми? Их сотни, и открывать и «пересохранаять» каждую вручную — это адский труд. Вот тут на помощь и приходит Custom Management Command.
Вот как я это реализовал. В web/management/commands/reprocess_typography.py:
from django.core.management.base import BaseCommand
from web.models import TbDictumAndQuotes
# Импорты библиотеки etpgrf
from etpgrf.typograph import Typographer
from etpgrf.sanitizer import SanitizerProcessor
# ... другие процессоры, если нужно ...
class Command(BaseCommand):
help = 'Переобрабатывает все цитаты'
def add_arguments(self, parser):
# Добавляем флаг "сухой прогон"
parser.add_argument(
'--dry-run',
action='store_true',
help='Запустить без сохранения изменений',
)
# Ограничение количества
parser.add_argument('--limit', type=int, help='Сколько записей обработать')
# Смещение (для пагинации)
parser.add_argument('--offset', type=int, default=0, help='Сколько пропустить')
def handle(self, *args, **options):
# 1. Настраиваем типограф
# ... инициализация процессоров ...
settings = {
# ...
'sanitizer': SanitizerProcessor(mode="html"), # Режим HTML-очистки
'hanging_punctuation': 'left', # Режим висячей пунктуации
# ...
}
typographer = Typographer(**settings)
# 2. Получаем записи из базы с учетом limit/offset
qs = TbDictumAndQuotes.objects.all().order_by('id')
start = options['offset']
if options['limit']:
qs = qs[start : start + options['limit']]
else:
qs = qs[start:]
self.stdout.write(f"Найдено {qs.count()} цитат...")
# 3. Бежим по циклу с прогресс-баром (tqdm если есть)
try:
from tqdm import tqdm
iterator = tqdm(qs)
except ImportError:
iterator = qs
for dq in iterator:
try:
# Обрабатываем текст
new_html = typographer.process(dq.szContent)
if options['dry_run']:
self.stdout.write(f"[{dq.id}] Предпросмотр: {new_html[:50]}...")
else:
dq.szContentHTML = new_html
dq.save(update_fields=['szContentHTML'])
except Exception as e:
self.stdout.write(self.style.ERROR(f"Ошибка id={dq.id}: {e}"))
self.stdout.write(self.style.SUCCESS(f"\nГотово!"))
Возможно вы заметили, что в моем проекте для контента есть два поля: szContent и szContentHTML. В первом хранится «сырой» текст, во втором — результат типографирования. В вашем проекте, скорее всего, только одно поле для контента, но по сути это ничего не меняет.
Еще интересные фишки, которые использовались при типографировании:
self.stdout.writeвместоprint– позволяет Django перехватывать вывод (например, для тестов) и корректно работать с кодировками.self.style.SUCCESSиself.style.ERROR– раскрашивает текст в консоли (зеленый/красный) и это очень удобно для визуального восприятия логов.- Аргумент --dry-run — позволяют безопасно тестировать скрипт на продакшене перед тем, как реально менять данные.
- Аргументы --limit и --offset — позволяют обрабатывать базу «порциями», что полезно для больших объемов данных (можно запустить несколько процессов параллельно с разными offset).
- Используется
update_fields— позволяет перезаписывать не всю модель целиком (что могло бы затереть изменения, сделанные кем-то другим в ту же секунду), а обновляем только конкретные поля.
Теперь, чтобы типографировать весь контент, нам достаточно одной строку в терминале, и Django сделает всю грязную работу за нас (не забудьте инициировать виртуальное окружение вашего проекта).
Тестовый прогон (безопасно, ничего не сохраняет):
python manage.py reprocess_typography --dry-run
Боевой запуск, с изменениями данные в базе:
python manage.py reprocess_typography
Если у вас очень много записей, можно запускать по частям:
python manage.py reprocess_typography --limit 1000 --offset 0 python manage.py reprocess_typography --limit 1000 --offset 1000 python manage.py reprocess_typography --limit 1000 --offset 2000