IT-инфраструктура • инструкции • практикаПоиск по сайту

Серверы и системное администрирование

Найти материал →

Ошибки автоматизации публикаций: кодировка stdout, закрытый поток и капча

Три поломки одного вечера: как рубль в заголовке, stdout и капча сорвали публикацию

Вечером 2 сентября плановый пост не появился в TenChat. Планировщик Windows отработал задачу, но в журнале остались лишь ненулевой код возврата и тревожное сообщение "слот не закрыт". Сначала казалось, что проблема связана с самой публикацией. Однако подробный разбор показал: последовательно проявились сразу три независимые неисправности.

Наш контур автоматизации публикации постов в соцсетях состоит из нескольких Python-скриптов. Они отправляют материалы в TenChat и Дзен, обновляются из Git-репозитория и запускаются на Windows-сервере через `.cmd`-обёртки и `schtasks`. Интерактивной консоли у таких процессов нет, поэтому любая ошибка в стандартном выводе способна выглядеть как сбой основной операции.

Первая проблема обнаружилась в проверке дублей. Заголовок с символом рубля ₽ не проходил через `dup_check.py`, хотя сама логика сравнения, построенная на `difflib`, была исправна. Скрипт завершался ещё до выполнения бизнес-логики - на отладочном `print`, выводившем разобранный заголовок.

Причина заключалась в кодировке консоли. Планировщик запускал Python с кодовой страницей `cp1251`, в которой нет символа ₽ с кодовой позицией U+20BD. Попытка вывести строку приводила к исключению, процесс завершался с ненулевым кодом, а вызывающий публикатор воспринимал любой такой результат как запрет на публикацию. В итоге гейт формально действовал корректно, но сигнализировал не о дубликате, а о проблеме вывода.

Исправление оказалось простым: вместо создания новой обёртки для `sys.stdout` применили перенастройку существующего потока:

```python
import sys

sys.stdout.reconfigure(encoding="utf-8", errors="replace")
```

Параметр `errors="replace"` важен именно для диагностического вывода. Если в консоли не поддерживается отдельный символ, он будет заменён, но отладочное сообщение не сможет сорвать публикацию. При этом сам заголовок, который попадает в тело поста, не изменяется.

Подробнее о причинах подобных сбоев и практическом подходе к исправлению можно прочитать в материале о кодировках stdout в Python и надёжной автоматизации публикаций.

Вторая неисправность возникла как раз из-за прежнего способа исправления кодировки. В пяти файлах уже использовалась конструкция:

```python
sys.stdout = io.TextIOWrapper(
sys.stdout.buffer,
encoding="utf-8",
errors="replace"
)
```

Она работает, если выполняется один раз. Но один из публикаторов импортировал модуль, который повторно переопределял `sys.stdout`. Так появлялись две `TextIOWrapper`, наложенные друг на друга.

В CPython при исчезновении последней ссылки первая обёртка уничтожается немедленно. Её деструктор закрывает не только сам объект, но и нижележащий буфер `sys.stdout.buffer`. Вторая обёртка продолжает ссылаться на тот же буфер, который уже закрыт. Поэтому следующий `print` завершается ошибкой `ValueError`.

Публикация при этом успевала пройти. Сбой происходил позже, когда скрипт печатал финальный статус и переносил файл из `outbox/` в `outbox/published/`. Материал оставался в исходной папке, а при следующем запуске мог отправиться повторно - уже как настоящий дубль.

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

Во всех модулях заменили ручное создание `TextIOWrapper` на `sys.stdout.reconfigure()`. Метод изменяет настройки существующего объекта, не создаёт новую оболочку и не закрывает общий буфер. Поэтому повторный вызов из разных импортируемых модулей безопасен.

Одновременно исправили порядок операций. Перенос файла в `published/` выполняется сразу после регистрации успешной публикации в базе:

```python
publib.register(post_id, url)
shutil.move(path, published_path)

Только после этого - закрытие вкладки и прочая очистка

```

Раньше перемещение стояло после закрытия браузерной вкладки. Если закрытие завершалось исключением, файл не помечался как опубликованный, хотя публикация уже состоялась. Надёжное правило здесь простое: между фактом публикации и фиксацией этого факта не должно быть необязательных операций, способных завершиться ошибкой.

Третья проблема относилась к публикации в Дзен. Там изменилась капча: виджет стал занимать гораздо большую область экрана и перестал совпадать с прежними координатами и визуальными шаблонами. Автоматическое решение капчи, рассчитанное на старый размер элемента, больше не находило нужную область либо нажимало мимо неё.

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

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

После исправлений сценарий стал разделён на независимые этапы: проверка материала, публикация, регистрация результата, перенос файла и очистка браузера. Для каждого этапа записываются статус, длительность и текст исключения. Благодаря этому ошибки публикации постов в TenChat больше не маскируются под абстрактный ненулевой код возврата, а повторная отправка возможна только после явной проверки состояния.

Главный итог вечера оказался полезнее одного исправленного бага. Автоматизация должна быть устойчивой не только к ошибкам внешних площадок, но и к собственным обёрткам, кодировкам и порядку операций. Символ ₽ в заголовке, повторная настройка stdout и изменившаяся капча по отдельности выглядели мелочами. Вместе они показали, насколько важно разделять этапы, подробно логировать результат и не считать завершение процесса доказательством успешной публикации.

Прокрутить вверх