Сообщество - Программирование на python

Программирование на python

1 041 пост • 11 973 подписчика

Популярные теги в сообществе:

Весь вечер в коде: пилю свой YouTube Downloader⁠⁠

Весь вечер в коде: пилю свой YouTube Downloader

Гугл снова поломал классическую скачку,пришлось все менять.

Привет, народ! Весь вечер плотно сижу в IDE и кодинге разрабатываю собственную утилиту для скачивания видео с YouTube.

Почему именно сейчас?

Многие заметили, что за этот октябрь алгоритмы YouTube снова жестко закрутили гайки. Платформа постоянно меняет логику отдачи потоков, шифрование и внутренние скрипты защиты, из-за чего большинство привычных публичных скачивателей и расширений просто отвалились. Чтобы не зависеть от чужих капризов и багов, я решил собрать свой надежный инструмент.

Что под капотом и на экране?

На скриншоте рабочая область PyCharm и запущенный GUI моей софтины.

Программа написана на Python, и вот её главные фишки и технические нюансы, над которыми я сейчас сижу:

Мощный бэкенд на yt-dlp: За парсинг и сборку медиа-потоков отвечает проверенная библиотека yt-dlp. Это сейчас практически единственный живой и оперативно обновляемый движок, который умеет переваривать новые алгоритмы Ютуба.

Параллельные потоки (Multithreading): Чтобы интерфейс программы не намертво зависал во время скачивания (особенно на тяжелых 4K видео), вся сетевая активность, вытягивание инфы и сам процесс загрузки вынесены в отдельные асинхронные потоки. Для этого я использую метод self.after(), который безопасно обновляет статус и GUI из фоновых задач без крашей.

Обход ограничений через Cookies: Ютуб стал активно банить анонимные запросы от скриптов. Решаю это динамической подгрузкой файлов куков (ydl_opts['cookiefile'] = self.cookies_path).

Если софтина ловит ошибку доступа, предусмотрена умная проверка: скрипт логирует проблему и подсказывает, что нужно обновить сессию.

Разделение контента: Скрипт на лету проверяет метаданные (info.get('title')) и автоматически определяет тип ссылки, отделяя стандартные ролики от формата Shorts.

Удобный интерфейс: Никакой сложной консоли

вставил ссылку, нажал одну большую зеленую кнопку СКАЧАТЬ ВИДЕО, а многопоточный бэкенд сделает всё остальное.

Работа идет полным ходом, код пишется, баги отлавливаются. Как всё дотестирую на стабильность обязательно поделюсь результатами! 😉

Показать полностью 1
4

Добавление номеров страниц в нижний колонтитул PDF с помощью Python⁠⁠

При пакетной обработке PDF-документов часто требуется добавить единую нумерацию страниц в уже существующие файлы. С помощью Spire.PDF for Python можно выводить динамические номера страниц непосредственно на холст существующего PDF, без создания отдельного PDF-файла и последующего объединения.

В этой статье показано, как разместить номера страниц по центру нижнего колонтитула. Основное внимание уделяется настройке формата нумерации — например, Страница X или X / Y — и использованию координат для размещения номеров в любой области страницы: слева, по центру или справа в нижнем колонтитуле либо в любом месте верхнего колонтитула.

Единицы измерения: координаты PDF задаются в пунктах, где 1 пункт = 1/72 дюйма. Начало системы координат находится в левом верхнем углу страницы: X увеличивается вправо, а Y — вниз.

Подготовка окружения

Установите необходимую библиотеку:

pip install spire.pdf

Знакомство с двумя динамическими полями

Для нумерации страниц используются два динамических поля:

  • PdfPageNumberField : представляет номер текущей страницы и при отрисовке заменяется фактическим номером.

  • PdfPageCountField : представляет общее количество страниц и при отрисовке заменяется соответствующим значением.

Это динамические поля, а не строки с заранее заданными значениями. Благодаря этому при отрисовке нумерация учитывает количество страниц документа.

Настройка формата с помощью PdfCompositeField

PdfCompositeField объединяет статический текстовый шаблон и динамические поля в одну строку. Формат номера страницы определяется строкой шаблона , поэтому для изменения формата достаточно изменить эту строку.

Базовый синтаксис:

compositeField = PdfCompositeField(font, brush, "Текст шаблона", [field1, field2, ...])

Здесь {0} соответствует первому полю в списке, {1} — второму и так далее.

Полный код: нумерация по центру нижнего колонтитула в формате «Страница X из Y»

В следующем примере используется формат Страница {0} из {1}. Номера страниц размещаются по центру нижнего колонтитула, над ними добавляется разделительная линия:

from spire.pdf.common import *
from spire.pdf import *
# Создать объект PdfDocument
doc = PdfDocument()
# Загрузить локальный PDF-файл
doc.LoadFromFile("input.pdf")
# Задать шрифт, кисть и перо
font = PdfTrueTypeFont("Arial", 14.0, PdfFontStyle.Regular, True)
brush = PdfBrushes.get_Black()
pen = PdfPen(brush, 1.0)
# Задать отступы
leftMargin = 54.0
rightMargin = 54.0
bottomMargin = 72.0
# Создать поля для номера текущей страницы и общего количества страниц
pageNumberField = PdfPageNumberField()
pageCountField = PdfPageCountField()
# Объединить поля с помощью русскоязычного шаблона нумерации
compositeField = PdfCompositeField(
font, brush, "Страница {0} из {1}", [pageNumberField, pageCountField]
)
# Перебрать все страницы
for i in range(doc.Pages.Count):
page = doc.Pages.get_Item(i)
pageSize = page.Size
# Нарисовать разделительную линию в нижнем колонтитуле
page.Canvas.DrawLine(
pen,
leftMargin,
pageSize.Height - bottomMargin + 15.0,
pageSize.Width - rightMargin,
pageSize.Height - bottomMargin + 15.0
)
# Измерить текст номера страницы для расчёта положения по центру
text = "Страница {} из {}".format(i + 1, doc.Pages.Count)
pageNumberSize = font.MeasureString(text)
# Задать положение номера страницы: по центру по горизонтали
compositeField.Location = PointF(
(pageSize.Width - pageNumberSize.Width) / 2,
pageSize.Height - bottomMargin + 18.0
)
# Вывести номер страницы
compositeField.Draw(page.Canvas, 0.0, 0.0)
# Сохранить новый файл
doc.SaveToFile("Output/CenterAlignedPageNumbers.pdf")
# Освободить ресурсы
doc.Dispose()

Добавление номеров страниц в нижний колонтитул PDF с помощью Python

Настройка формата нумерации

Формат нумерации зависит от строки шаблона и переданных динамических полей. Ниже приведены несколько распространённых вариантов. Замените третий аргумент PdfCompositeField и при необходимости измените список полей.

Формат 1: только текущая страница — Страница X

Для этого формата требуется только поле номера страницы:

compositeField = PdfCompositeField(
font, brush, "Страница {0}", [pageNumberField])

Формат 2: компактная числовая запись — X / Y

compositeField = PdfCompositeField(
font, brush, "{0} / {1}", [pageNumberField, pageCountField])

Формат 3: текущая страница и общее количество страниц — Страница X / Всего страниц: Y

compositeField = PdfCompositeField(
font, brush, "Всего страниц: {1}", [pageNumberField, pageCountField])

Формат 4: только общее количество страниц — Всего страниц: Y

compositeField = PdfCompositeField(
font, brush, "Всего страниц: {1}", [pageNumberField, pageCountField])

Примечание: поле может присутствовать в списке, но не использоваться в шаблоне — в таком случае оно просто не отображается. Заполнители полей можно комбинировать в любом порядке, а окружающий текст и разделители, например «Страница», «из», «Всего страниц» или «/», — изменять в соответствии с требованиями к оформлению.

Настройка положения с помощью координат

Положение номера страницы задаётся через compositeField.Location = PointF(x, y):

  • По центру: x = (ширина страницы - ширина текста) / 2

  • Слева: x = leftMargin

  • Справа: x = pageSize.Width - pageNumberSize.Width - rightMargin

  • По вертикали: изменяйте Y, чтобы перемещать текст вверх или вниз. Чем меньше Y, тем ближе текст к верхнему краю страницы, что позволяет разместить его в верхнем колонтитуле.

Следующий пример сочетает пользовательский формат и положение: нумерация X / Y с выравниванием по правому краю нижнего колонтитула.

compositeField = PdfCompositeField(
font, brush, "{0} / {1}", [pageNumberField, pageCountField]
)
for i in range(doc.Pages.Count):
page = doc.Pages.get_Item(i)
pageSize = page.Size
text = "{} / {}".format(i + 1, doc.Pages.Count)
pageNumberSize = font.MeasureString(text)
# Выравнивание справа: X = ширина страницы - ширина текста - правый отступ
compositeField.Location = PointF(
pageSize.Width - pageNumberSize.Width - rightMargin,
pageSize.Height - bottomMargin + 18.0
)
compositeField.Draw(page.Canvas, 0.0, 0.0)

На что обратить внимание

  • В примере используется Arial. Если этот шрифт недоступен в вашем окружении, замените его другим установленным шрифтом. Для русского и другого многоязычного текста выбирайте шрифт, поддерживающий необходимые символы.

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

Заключение

Этот подход позволяет независимо настраивать формат номера страницы и его положение. Изменяйте строку шаблона, чтобы переключаться между вариантами Страница X, X / Y и Страница X из Y, а затем корректируйте координаты Location, чтобы разместить текст в нужном месте нижнего или верхнего колонтитула. Такая гибкость удобна для применения единого формата нумерации при пакетной обработке PDF-документов.

Показать полностью 1
5

Глава 2. Кризис среднего возраста⁠⁠

Серия Из 1С в Python: мой путь

Привет, Пикабушники!

Когда-то писал здесь, как после девяти лет работы с 1С решил попробовать себя в Python. Тогда получал от 200 тысяч в регионе, руководил IT-отделом во франче и по вечерам делал телеграм-ботов. Первым был бот для скачивания видео, которого я сюда и пришел показать.

Подумал, что пора написать продолжение. Тем более рассказать уже есть про что (прошло 3 года с прошлого поста, интересно, какая статья будет следующая через еще 3 года).

Из 1С я в итоге так и не ушел. Зарплата сейчас уже от 450 тысяч, всё еще живу в регионе, в этом плане не жалуюсь. Готовлюсь к экзамену на «1С:Эксперт по технологическим вопросам». В общем, переквалификация пошла по своему сценарию))

За это время в работе появился ИИ. Использую Codex, Claude Code, Cursor, MCP и навыки для агентов. В 1С это тоже вполне применимо, код руками уже практически не пишу)))

Но временами от 1С всё равно устаю. Даже когда нормально платят и в целом всё устраивает, иногда хочется чего-то нового.

Жена играет в «Клуб Романтики». Посмотрел, что это вообще такое, узнал, что у жанра большая аудитория. Давно было желание сделать похожую игру, а тут появились все возможности. Решил попробовать.

Получилась романтическая визуальная новелла. Приложение называется «Секретные вечера», а первая история внутри «Письма без подписи».

Игра в первую очередь для девушек. Некоторые сюжетные повороты мужчинам могут показаться аморальными)))))

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

В «Письмах без подписи» героиня начинает получать анонимные письма от человека, который знает о ней слишком много. Дальше отношения, подозрения и решения, с последствиями которых придется разбираться.

Первый сезон готов целиком: 12 глав, есть разные финалы.

Теперь немного про техническую часть.

Основу сделал на Ren’Py движок для визуальных новелл, в котором используется Python. Он занимается диалогами, картинками, переходами между сценами и сохранениями. Поверх этого уже сделаны отношения с персонажами, сюжетные условия и выборы.

Игру собрал для браузера и встроил в Telegram Mini App. Открываешь бота, запускаешь игру и читаешь прямо в телеграме. Отдельно регистрироваться не надо, есть апк для андройда и можно собрать для  IOS, но пока не хочется заморачиваться, тем более с гражданством рф с этим делом есть нюансы.

Серверная часть тоже на Python: FastAPI, база SQLite. Там хранятся данные игрока, игровой баланс, открытые сцены и облачные сохранения. Аккаунт привязан к Telegram, поэтому можно продолжать с другого устройства. Всё это крутится на VPS через Docker Compose, игру и запросы к API обслуживает Caddy.

По ходу добавил статистику прохождения и обратную связь, пока мониторю) Интересно, где люди бросают читать, какие варианты выбирают и доходят ли вообще до финала.

Кроме облачных ИИ-инструментов использовал свой домашний комп (не основной, работаю с ноута, эта рабочая станция на убунту):

  • Ryzen 9 9900X, материнка MSI MAG X870E TOMAHAWK WIFI.

  • 64 ГБ оперативки Kingston FURY Beast.

  • Две GeForce RTX 3090 GamingPro 24 ГБ.

  • NVMe Kingston KC3000 на 2 ТБ.

  • Блок питания Chieftec Polaris Pro на 1300 Вт.

  • Водянка MSI MAG CORELIQUID I360 и корпус MONTECH KING 95 PRO.

На нем локально запускал ComfyUI. Для картинок использовал Qwen-Image, для редактирования Qwen-Image-Edit-2509, с ускоряющими Lightning LoRA на 8 шагов. Для удаления фона у персонажей BiRefNet. Музыку генерировал через ACE-Step 1.5 turbo.

Правда, по ходу разработки часть арта стал делать через генерацию изображений в Codex: результат мне понравился больше (так что мощный комп нынче для таких дел почти не нужен, только для эмбендингов локальных).

ИИ в разработке использовал много, да что тут греха таить, практически всё сделал за меня))))

На мне: утвердить сюжет, проверять результат, искать ошибки, заставлять перерисовывать картинки.

Есть несколько агентов, арт директор, виртуальные читательницы, которые проверяют сюжет и дают обратную связь, пишут когда догадались про развязки сюжета и почему.

Для меня это хобби и возможность переключиться после работы. В 1С платят хорошо, уходить прямо не собираюсь, но делать что-то свое тоже хочется, тем более в индустрии в связи с развитием технологий будут изменения и хочется найти свою тихую гавань.

Игра бесплатная, без рекламы. Сейчас больше всего интересно понять, захочет ли в неё играть кто-то кроме меня. Если жены или девушки любят такие истории, можете им дать на тест)))

Монетизировать пока смысла нет, попробуем доработать и найти свою аудиторию и не бросить на половине пути, как это обычно бывает.

Показать полностью
8

Навайбкодил LightLayers опенсорс аналог Фотошопа⁠⁠

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

Сейчас уже умеет в принципе все то, что мне нужно. Из фишек - поддерживает скрипты, то есть можно попросить нейронку что-то нарисовать или обработать и запустить в виде python скрипта или работать по API.

Делал полностью с нуля, просто описывая нейросети функционал того или иного инструмента.

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

Выглядит вот так(картинку сгенерировала нейросеть скриптом):

Навайбкодил LightLayers опенсорс аналог Фотошопа

Чтобы работало, нужно сначала запустить install.bat, который установит питон и зависимости в локальную папку, затем запуск через LightLayers.bat. Удалить - просто удалить папку вместе со всеми зависимостями.

Надеюсь я никаким образом не нарушил правила Пикабу.

Скачать тут:

https://disk.yandex.ru/d/yztiD0wHaXsMOw

Показать полностью 1

Дашборд по заказам с маркетплейсов "своими руками"⁠⁠

У нас несколько юридических лиц, магазины на Ozon, Wildberries и Яндекс Маркете, и возникла потребность в аналитике по динамике продаж по разным артикулам. Долго и упорно пытался собрать дашборд в Яндекс даталенс, но переодически, что то глючило, а с последними событиями количество глюков увеличилось. Был небольшой опыт кодинга ручками, потом пару раз делал бота для телеги с помощью Дипсика, с переменным успехом. Решил попробовать собрать дашборд на питоне, с помощью дипсик.

Ниже кратко процесс, потратил 2 дня между делом дополняя код и исправляя дипсиком, можно было конечно клодом воспользоваться, но такого опыта нет и я ж не тру программист, задача была быстро собрать, работающий дашборд

Запрос в дипсик, самый простой и банальный своими словами ))

Запрос в дипсик, самый простой и банальный своими словами ))

Сам код и структура файлов

Сам код и структура файлов

Сам дашборд

Сам дашборд

На выходе получился аналитический дашборд на Streamlit, сейчас айтишники место выделят, залью на сервак, и отдам менеджерам по маркет плейсам, чтобы динамику смотрели.

Функционал дашборда:

  • Сбор данных: По API забирает заказы с Ozon, WB и Яндекс Маркета, сохраняя их в Google Sheets.

  • Фильтрация: В панели настроек можно выбрать маркетплейс, конкретное юрлицо или магазин (например, ПНИО_ОЗОН, РАА_ОЗОН).

  • Поиск: Поиск по названию артикула (например, «neptun») с возможностью выбора нужных позиций галочками.

  • Визуализация: График «Динамика продаж» отображает количество проданных единиц по датам.

  • Сводка: Таблица с общими показателями по маркетплейсам (количество штук и сумма в рублях).

  • Детализация: Полная таблица заказов с указанием маркетплейса, магазина, даты, номера заказа, статуса, артикула, названия товара, количества и суммы.

ПС: ни одна строчка кода не написана руками человека ))

жду полную панамку от тру программеров) обещаю воспринять критику и услышать, в следующий раз буду юзать клод и sql и много других страшных слов )

Показать полностью 3
1

Пока не сделанный переводчик⁠⁠

таймлайн распознанных текстов

таймлайн распознанных текстов

Мне пришлось учился пользоваться своим же переводчиком

Короче из ключевых проблем что скорее всего не решиться, whisper large-3 бедолага, не смотря на всё качество определение реплики, разметить их по нему крайне непростая задача.
Я добавил таймлайн, но редактор для него полноценный ещё не сделал. На сколько это будет надо, покажет желание пользователей уже. Я пока думаю что это не самое приоритетное направление.

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

По этой же причине пришлось добавлять озвучку без генерации субтитров. Чисто взял готовые и бросил озвучивать.

Окно озвучки

Окно озвучки

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

Окно сборки

Окно сборки

Хорошо отработал алгоритм глушения реплик, так называемый "дакинг".

Короче для озвучки кино по готовым субтитрам вообще 0 проблем. А короткие ролики вполне себе можно и с авторазметкой даже делать.

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

Оценить озвучку можете вот тут

Сразу отвечу на вопрос зачем я это делал. Мне надо было быстро перевести видео локально, без претензии на озвучку по ролям, все что я нашел это pyvideotrans. И он мне совершенно не понравился. Вот и всё. Релиз на вот вотах, даже если не получится улучшить перевод, БЕСПЛАТНОВО, не претендую на золотые горы в этом проекте. Надо было мне, но вдруг кому ещё подойдет, у кого непритязательные взгляды в отношении озвучки :D

Показать полностью 3
1

Пакетное форматирование документов Word с помощью python-docx: четыре подводных камня и более безопасный рабочий процесс⁠⁠

Форматирование целой папки документов Word кажется простой задачей: изменить несколько стилей, сохранить каждый файл — и готово. На практике документы разных авторов часто сочетают стили абзацев, вручную заданное форматирование и отдельные определения нумерации списков. Изменения одного лишь стиля может оказаться недостаточно: часть текста сохранит прежний вид.

В этом руководстве показано, как с помощью python-docx задать единые шрифт, размер и цвет для выбранных стилей абзацев в нескольких файлах .docx. Сначала мы рассмотрим четыре подводных камня, влияющих на результат, а затем объединим решения в одном скрипте пакетной обработки.

В примере используются следующие правила:

Пакетное форматирование документов Word с помощью python-docx: четыре подводных камня и более безопасный рабочий процесс

Контрастные цвета позволяют легко заметить изменения. Перед использованием скрипта в рабочем процессе замените их цветами, принятыми в ваших документах.

1. Подводный камень: изменение стиля не всегда меняет существующий текст

Естественный первый шаг — изменить стиль абзаца:

h2 = doc.styles['Heading 2']
h2.font.name = 'Arial'
h2.font.size = Pt(16)
h2.font.color.rgb = RGBColor.from_string('1E90FF')

Этот код изменяет определение стиля. Текст, наследующий эти свойства от данного стиля, сможет использовать новые настройки. Однако фрагмент текста (run), для которого явно задан шрифт или размер, может сохранить собственные настройки.

Например, абзац может иметь стиль Heading 2, а его текст — вручную заданный шрифт Times New Roman размером 11 пт. Изменение Heading 2 не удаляет автоматически такие переопределения на уровне фрагментов текста. На внешний вид также могут влиять символьные стили и другие наследуемые свойства.

Если после выделения текста в Word и нажатия на его стиль абзаца появляется новое форматирование, это полезный признак того, что существующее форматирование стоит изучить внимательнее. Однако это не доказывает, что документу требуется отдельное «обновление стиля» или что повторное назначение того же стиля в Python воспроизведёт поведение Word.

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

Вспомогательная функция настройки шрифта также задаёт XML-атрибуты шрифтов для ASCII, других символов, охватываемых hAnsi, восточноазиатского текста и сложных систем письма, а также удаляет соответствующие ссылки на шрифты темы на том же уровне. Так нужная гарнитура задаётся явно; это не означает, что стили заголовков в принципе игнорируют font.name.

Указание имени шрифта не устанавливает его и не гарантирует наличия всех необходимых символов. При необходимости Word по-прежнему может подставить другой шрифт.

2. Подводный камень: очистка форматирования может изменить больше, чем нужно

Распространённый обходной путь — сбросить свойства шрифта у всех фрагментов текста в документе. Это может удалить значимые выделения и изменить текст за пределами нужной области обработки.

Вместо этого скрипт обрабатывает только абзацы, которым действительно назначен стиль Heading 2, Normal или List Paragraph. Он задаёт свойства, предусмотренные стандартом оформления, и не затрагивает остальные.

Для Heading 2 полужирное начертание явно включается, а курсив отключается. Для Normal и List Paragraph сохраняются существующие настройки полужирного начертания и курсива. Скрипт также сохраняет текст, гиперссылки, интервалы абзацев и другие свойства, которые он явно не изменяет. Текст гиперссылки внутри подходящего абзаца получает заданные шрифт, размер и цвет, при этом сама ссылка сохраняется.

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

В python-docx к встроенным стилям следует обращаться по английским именам, даже если интерфейс Word отображает их локализованные названия. Для пользовательских стилей используются их фактические имена, сохранённые в документе. Если в ваших документах применяются пользовательские стили основного текста или заголовков, добавьте их в правила, не предполагая, что они соответствуют встроенным стилям.

3. Подводный камень: абзацы основного текста не включают текст таблиц

Перебор doc.paragraphs не охватывает абзацы внутри ячеек таблиц. Поэтому скрипт может изменить основной текст документа, оставив текст таблиц с исходным форматированием на уровне фрагментов.

Решение — обходить и абзацы, и таблицы. Для каждой таблицы нужно посетить её ячейки и рекурсивно обработать вложенные таблицы. Скрипт отслеживает уже посещённые элементы ячеек, поскольку объединённые ячейки могут встречаться несколько раз при обращении к таблице через строки и ячейки.

Каждый подходящий абзац внутри ячейки получает такое же форматирование фрагментов текста, как и подходящий абзац в основном тексте. Например, абзац Normal с явно заданным размером шрифта 8 пт будет изменён на 12 пт.

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

4. Подводный камень: у номеров списка есть собственные свойства шрифта

Нумерованный список содержит два отдельных объекта форматирования: текст абзаца и автоматически создаваемый номер, например 1., 2. или 3..

Изменение List Paragraph или его фрагментов текста не обязательно меняет номер. Определения нумерации могут задавать собственные свойства шрифта для каждого уровня списка.

Скрипт определяет идентификатор нумерации абзаца (numId) и его уровень (ilvl), включая значения, унаследованные по цепочке стилей абзаца. Затем он изменяет свойства фрагмента текста уровня нумерации через переопределение уровня конкретного списка в numbering.xml.

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

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

Следует учитывать два ограничения:

  • Один уровень конкретной нумерации используется всеми абзацами с одинаковыми numId и ilvl. Поэтому изменение его форматирования может повлиять на другие абзацы, использующие этот уровень, в том числе на абзацы со стилями, не выбранными для обработки.

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

Эти ограничения позволяют сохранить пример достаточно простым. Документам с более сложной структурой нумерации потребуется дополнительная обработка.

Полный скрипт пакетного форматирования

Установите библиотеку:

pip install "python-docx>=1.2.0"

Сохраните следующий код в файле word_batch_formatting.py. Скрипт читает документы из E:\Documents\input и сохраняет их в E:\Documents\output, создавая папку вывода при необходимости. Каждый выходной файл сохраняет исходное имя. Словарь RULES содержит шрифт, размер и цвет для каждого целевого стиля.

from pathlib import Path
from copy import deepcopy
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Pt, RGBColor
from docx.text.run import Run
INPUT_FOLDER = Path(r'E:\Documents\input')
OUTPUT_FOLDER = Path(r'E:\Documents\output')
RULES = {
'Heading 2': ('Arial', 16, '1E90FF'),
'Normal': ('Calibri', 12, 'FF8C00'),
'List Paragraph': ('Calibri', 12, 'FF8C00'),
}
def set_font(obj, rule, heading=False, bullet=False):
name, size, color = rule
obj.font.size = Pt(size)
obj.font.color.rgb = RGBColor.from_string(color)
if heading:
obj.font.bold, obj.font.italic = True, False
if not bullet:
obj.font.name = name
fonts = obj._element.get_or_add_rPr().get_or_add_rFonts()
for slot in ('ascii', 'hAnsi', 'eastAsia', 'cs'):
fonts.set(qn('w:' + slot), name)
fonts.attrib.pop(qn('w:' + slot + 'Theme'), None)
clr = obj._element.get_or_add_rPr().find(qn('w:color'))
for key in ('themeColor', 'themeTint', 'themeShade'):
clr.attrib.pop(qn('w:' + key), None)
def paragraphs(container, seen=None):
seen = set() if seen is None else seen
for p in container.paragraphs:
yield p
for table in container.tables:
for row in table.rows:
for cell in row.cells:
if cell._tc not in seen:
seen.add(cell._tc)
yield from paragraphs(cell, seen)
def numbering(p):
num_id = level = None
sources, style, seen = [p._p.pPr], p.style, set()
while style is not None and style.style_id not in seen:
seen.add(style.style_id)
sources.append(style.element.pPr)
style = style.base_style
for prop in sources:
if prop is None or prop.numPr is None:
continue
np = prop.numPr
if num_id is None and np.numId is not None:
num_id = np.numId.val
if level is None and np.ilvl is not None:
level = np.ilvl.val
return num_id, level, seen
def format_number(root, p, rule, changed):
num_id, level, styles = numbering(p)
if num_id in (None, 0):
return
num = root.find(f"w:num[@w:numId='{num_id}']", root.nsmap)
abstract_id = num.find(qn('w:abstractNumId')).get(qn('w:val'))
abstract = root.find(f"w:abstractNum[@w:abstractNumId='{abstract_id}']", root.nsmap)
if abstract.find(qn('w:numStyleLink')) is not None:
raise ValueError('Косвенные стили нумерации не поддерживаются')
if level is None:
matches = [x for x in abstract.findall(qn('w:lvl'))
if x.find(qn('w:pStyle')) is not None
and x.find(qn('w:pStyle')).get(qn('w:val')) in styles]
level = int(matches[0].get(qn('w:ilvl'))) if matches else 0
key = (num_id, level)
if key in changed:
if changed[key] != rule:
raise ValueError('Для одного уровня нумерации заданы конфликтующие правила форматирования')
return
override = num.find(f"w:lvlOverride[@w:ilvl='{level}']", root.nsmap)
if override is None:
override = OxmlElement('w:lvlOverride')
override.set(qn('w:ilvl'), str(level))
num.append(override)
lvl = override.find(qn('w:lvl'))
if lvl is None:
lvl = deepcopy(abstract.find(f"w:lvl[@w:ilvl='{level}']", root.nsmap))
override.append(lvl)
fmt = lvl.find(qn('w:numFmt')).get(qn('w:val'))
if lvl.find(qn('w:lvlPicBulletId')) is not None:
raise ValueError('Маркеры в виде изображений не поддерживаются')
old = lvl.find(qn('w:rPr'))
run = Run(OxmlElement('w:r'), None)
if old is not None:
run._r.append(deepcopy(old))
set_font(run, rule, p.style.name == 'Heading 2', bullet=fmt == 'bullet')
if old is not None:
lvl.remove(old)
lvl.append(run._r.rPr)
changed[key] = rule
def format_document(src, dest):
doc = Document(src)
for name, rule in RULES.items():
if name in doc.styles:
set_font(doc.styles[name], rule, name == 'Heading 2')
changed = {}
for p in paragraphs(doc):
if p.style.name not in RULES:
continue
rule = RULES[p.style.name]
for item in p.iter_inner_content():
for run in ([item] if isinstance(item, Run) else item.runs):
set_font(run, rule, p.style.name == 'Heading 2')
format_number(doc.part.numbering_part.element, p, rule, changed)
doc.save(dest)
if __name__ == '__main__':
if not INPUT_FOLDER.is_dir():
raise SystemExit(f'Папка входных файлов не существует: {INPUT_FOLDER}')
OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)
for src in sorted(INPUT_FOLDER.glob('*.docx')):
if src.name.startswith('~$'):
continue
dest = OUTPUT_FOLDER / src.name
if dest.exists():
print(f'Пропущено: {dest.name}')
continue
try:
format_document(src, dest)
print(f'Сохранено: {dest.name}')
except Exception as error:
print(f'Ошибка: {src.name}: {error}')

Запустите скрипт командой:

python word_batch_formatting.py

Например, из E:\Documents\input\report.docx будет создан E:\Documents\output\report.docx. Исходный файл останется доступным для сравнения. Поскольку скрипт просматривает только папку входных файлов, созданные документы не обрабатываются повторно. Пути к этим двум папкам должны различаться.

Временные файлы блокировки Word и уже существующие выходные файлы пропускаются. Чтобы повторно создать документ после изменения правил, удалите его предыдущую копию из папки вывода. Ошибки сообщаются отдельно для каждого файла, после чего скрипт продолжает обработку остальных файлов.

Если папка входных файлов отсутствует, скрипт сообщает об этом до начала обработки. Если в ней нет подходящих файлов, выходные файлы не создаются. Целевые стили, отсутствующие в документе, также пропускаются, а не создаются заново. Перед запуском пакетной обработки проверьте папку и имена стилей.

Результат форматирования:

На сравнении ниже показан документ до и после форматирования. Текст Heading 2 использует Arial размером 16 пт и синий цвет, а основной текст, содержимое таблиц и текст нумерованных списков — Calibri размером 12 пт и оранжевый цвет. Номера списка оформлены так же, как его текст.

Пакетное форматирование документов Word с помощью python-docx: четыре подводных камня и более безопасный рабочий процесс

Проверьте результаты перед обработкой всей папки

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

Ожидаемый результат — единые шрифт, размер и цвет для трёх целевых стилей и их текста; Heading 2 при этом становится полужирным, без курсива. Фрагменты текста в абзацах других стилей напрямую не изменяются, хотя стили, основанные на изменённом целевом стиле, могут унаследовать его изменения. Числовые маркеры списков используют заданный шрифт, а символьные маркеры сохраняют исходную гарнитуру.

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

Скрипт использует низкоуровневый доступ к XML для работы со шрифтами и нумерацией, поэтому сохраняйте исходные файлы и проверяйте результат после обновления библиотеки. Он предлагает практический рабочий процесс для поддерживаемых структур документов, а не полноценный механизм форматирования для всех возможностей Word.

Источники

Показать полностью 2
5

Нужен реальный совет по реализации решения⁠⁠

Привет, товарищи программисты.

Сразу скажу: я не программист. Учусь вайбкодить, так что сильно не пинайте.

Есть сайт, где часто нужно по работе искать документы по реквизитам и скачивать. Хочу сделать пару ИИ-агентов, чтобы они сами там искали нужное по промпту и скачивали на комп.

Проблема: файлы скачиваются только после входа по ЭЦП через NCALayer. Это программка на Java, которая висит в трее и общается с браузером. Если что-то называю неправильно, не бейте.

Gemini и Claude сказали, что это сложно, а может, и невозможно. Решил спросить у живых людей.

Можно ли как-то автоматизировать вход, или это невозможно?

Если у кого-то есть реальное работающее решение, готов обсудить стоимость услуг разработки.

Отличная работа, все прочитано!

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества