179 lines
14 KiB
HTML
179 lines
14 KiB
HTML
{% extends 'typograph/base.html' %}
|
||
{% load static %}
|
||
|
||
{% block title %}Песочница верстки — ETPGRF{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row">
|
||
|
||
{# Левая колонка: Дата и Картинка #}
|
||
<div class="col-lg-2 align-self-start text-end mb-4">
|
||
<p class="small align-self-end">
|
||
<small class="bg-secondary bg-opacity-10 p-2 text-nowrap">
|
||
12.фев.2026
|
||
</small>
|
||
</p>
|
||
<p>
|
||
<img src="{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}" class="w-100" alt="Django Admin" />
|
||
</p>
|
||
</div>
|
||
|
||
{# Правая колонка: Контент #}
|
||
<div class="col-lg-10 border-start ps-lg-4 post-page-content">
|
||
|
||
<h1>Массовая типографика контента в Django: пользова­тельские команды управления (Custom Management Commands)</h1>
|
||
|
||
<div class="lead bg-secondary bg-opacity-10 p-3 rounded">
|
||
|
||
<p>Если вы работали с Django, вы наверняка использовали команду <code>manage.py</code> сотни раз: <code>python manage.py runserver</code>, <code>migrate</code>, <code>createsuperuser</code>.</p>
|
||
<p>Но знаете ли вы, что можете легко создавать <strong>свои собственные</strong> команды? Это один из самых мощных и недоо­цененных инструментов Django для автома­тизации рутины. В этом посте я расскажу, что такое <strong>Management Command</strong>, зачем она нужна и как её использовать на примере реальной задачи: массовой типографики контента в базе данных.</p>
|
||
|
||
</div>
|
||
<hr>
|
||
<div class="post-content mt-4">
|
||
|
||
<h2>Что это такое?</h2>
|
||
<p><strong>Management Command</strong> — это обычный Python-скрипт, который «встраивается» в экосистему Django. Он имеет доступ к моделям, настройкам (settings.py) и базе данных, но запускается из консоли через <code>python manage.py ...</code>.</p>
|
||
<h2>Зачем это нужно?</h2>
|
||
<p>Представьте ситуации:</p>
|
||
<ul>
|
||
<li>Нужно пересчитать рейтинг для десятки тысяч товаров на сайте или сделать типографику всех публикаций после подключения etpgrf-типографа (или любого другого).</li>
|
||
<li>Необходимо обновить цены для всех товарных карточек в соответствии с таблицей excel или по API (или спарсить данные с внешнего сайта).</li>
|
||
<li>Нужно отправить e-mail рассылку пользо­вателям.</li>
|
||
<li>Нужно пометить комментарии или публикации как архивные в соответствии с правилом или почистить базу от старых логов.</li>
|
||
</ul>
|
||
<p>Делать это через <tt>views.py</tt> (views) — плохая идея (страница может отвалиться по тайм-ауту). Писать отделный скрипт рядом <tt>manage.py</tt> — неудобно (нужно вручную настраивать `DJANGO_SETTINGS_MODULE`). Встроенные команды решают эти проблемы элегантно.</p>
|
||
<h2>Как это работает?</h2>
|
||
<p>Django использует систему «автообна­ружения» (auto-discovery). Чтобы ваша команда появилась в списке `manage.py`, нужно соблюсти строгую иерархию папок внутри вашего приложения (например, <tt>web</tt>):</p>
|
||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">web/
|
||
├── __init__.py
|
||
├── models.py
|
||
└── management/ <-- 1. Создаем папку management
|
||
├── __init__.py
|
||
└── nginx/
|
||
├── commands/ <-- 2. Внутри неё папку commands
|
||
├── __init__.py
|
||
└── my_cool_script.py <-- 3. Наш файл с командой</pre>
|
||
<p>Файл <tt>my_cool_script.py</tt> автома­тически превратится в команду: <code>python manage.py my_cool_script</code>. Django будет искать его в папках <tt>management/commands/</tt> внутри каждого устано­вленного приложения. Это позволяет легко организовать код и держать все «команды обслуживания» в одном месте.</p>
|
||
<p><strong>Для справки:</strong> Механизм Custom Management Commands появился еще в Django 0.96 (в глубокой древности) и с тех пор является стандартом де-факто для написания скриптов обслуживания.</p>
|
||
<h2>Анатомия команды</h2>
|
||
<p>Вот пример простейшей команды. Мы наследуемся от класса <code>BaseCommand</code> и переопре­деляем метод <code>handle</code>:</p>
|
||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||
# 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}!")</pre>
|
||
<p>Запуск:</p>
|
||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||
python manage.py hello Иван</pre>
|
||
<p>Увидим в консоли:</p>
|
||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||
Привет, Иван!</pre>
|
||
<h2>Реальный пример: Массовая типографика через etpgrf</h2>
|
||
<p>В моём проекте, <a href="https://dq.cube2.ru/" target="_blank">DQ – коллекция цитат: место для вдумчивого чтения</a>, контент был частично обработан с помощью Типографа Муравьёа, частично вручную. И вот я установил и настроил etpgrf (тем более что Типограф Муравьёва «почил в бозе», да и до того не обновлялся с 2018 года). Все новые цитаты типогра­фируются через etpgrf, но что делать со старыми? Их сотни, и открывать и «пересо­хранаять» каждую вручную — это адский труд. Вот тут на помощь и приходит Custom Management Command.</p>
|
||
<p>Вот как я это реализовал. В <tt>web/management/commands/reprocess_typography.py</tt>:</p>
|
||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||
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Готово!"))</pre>
|
||
<p>Возможно вы заметили, что в моем проекте для контента есть два поля: <code>szContent</code> и <code>szContentHTML</code>. В первом хранится «сырой» текст, во втором — результат типогра­фирования. В вашем проекте, скорее всего, только одно поле для контента, но по сути это ничего не меняет.</p>
|
||
<p>Еще интересные фишки, которые исполь­зовались при типогра­фировании:</p>
|
||
<ol>
|
||
<li><code>self.stdout.write</code> вместо <code>print</code> – позволяет Django перехва­тывать вывод (например, для тестов) и корректно работать с кодировками.</li>
|
||
<li><code>self.style.SUCCESS</code> и <code>self.style.ERROR</code> – раскрашивает текст в консоли (зеленый/красный) и это очень удобно для визуального восприятия логов.</li>
|
||
<li>Аргумент <tt>--dry-run</tt> — позволяют безопасно тестировать скрипт на продакшене перед тем, как реально менять данные.</li>
|
||
<li>Аргументы <tt>--limit</tt> и <tt>--offset</tt> — позволяют обрабатывать базу «порциями», что полезно для больших объемов данных (можно запустить несколько процессов параллельно с разными <tt>offset</tt>).</li>
|
||
<li>Используется <code>update_fields</code> — позволяет переза­писывать не всю модель целиком (что могло бы затереть изменения, сделанные кем-то другим в ту же секунду), а обновляем только конкретные поля.</li>
|
||
</ol>
|
||
<p>Теперь, чтобы типогра­фировать весь контент, нам достаточно одной строку в терминале, и Django сделает всю грязную работу за нас (не забудьте инициировать виртуальное окружение вашего проекта).</p>
|
||
<p>Тестовый прогон (безопасно, ничего не сохраняет):</p>
|
||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||
python manage.py reprocess_typography --dry-run</pre>
|
||
<p>Боевой запуск, с изменениями данные в базе:</p>
|
||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||
python manage.py reprocess_typography</pre>
|
||
<p>Если у вас очень много записей, можно запускать по частям:</p>
|
||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
||
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</pre>
|
||
|
||
|
||
|
||
</div>
|
||
{% endblock %}
|