Разбираю, как разработчик автоматизирует таблицы: какие библиотеки брать, как читать и оформлять файлы, чистить выгрузки и собирать отчёты с диаграммами. Весь код запущен и проверен. Есть замеры скорости на 100 000 строк и калькулятор окупаемости.
Привет, я Дени, разработчик Hated.Ru. Пишу бэкенд на Python, работаю с PostgreSQL, Docker и Linux-серверами. Excel встречается в любом проекте: выгрузки заказов, отчёты по рекламе, прайсы, каталоги товаров. Файл приходит, его надо почистить, свести с другим, оформить и отправить дальше. Когда это повторяется каждую неделю, пора отдать работу скрипту.
Я изучил курс по автоматизации Excel на Python, затем собрал примеры заново и прогнал их на свежих версиях библиотек. Ниже только то, что сработало, и ошибки, на которые я наткнулся по пути. К концу статьи у вас будет набор рабочих скриптов, понимание, чем openpyxl отличается от pandas и XlsxWriter, и расчёт, окупится ли автоматизация вашей задачи.
- Python читает и пишет Excel-файлы без запуска самого Excel. Хватает трёх библиотек: openpyxl правит и оформляет файлы, pandas чистит и считает, XlsxWriter создаёт отчёты с диаграммами.
- Автоматизация окупается, когда одна и та же операция повторяется каждую неделю или строк слишком много для ручной проверки. Срок окупаемости покажет калькулятор в разделе 15.
- Файл, который сохранил openpyxl, хранит формулы без результатов, пока его не откроют в Excel. Это самая частая причина пустых ячеек при чтении.
- На 100 000 строк быстрее всего читает pandas с движком calamine: около 1,4 секунды против 6,5 секунды у стандартного пути.
- Если в файле персональные данные клиентов, сначала сверьтесь с требованиями 152-ФЗ и только потом отправляйте файл в сторонние сервисы, включая нейросети.
- Когда Excel пора отдать Python
- Какая библиотека для чего
- Подготовка: окружение за десять минут
- Читаем файл: листы, ячейки, формулы
- Пишем, переименовываем, сохраняем
- Оформление: шрифты, границы, форматы
- pandas: чистим выгрузку
- Отчёт с диаграммами в XlsxWriter
- Условное форматирование: план и факт
- Свод из папки с выгрузками
- Скорость и память: замеры
- Десять частых ошибок
- Нейросеть как помощник и три промпта
- Безопасность и порядок
- Окупится ли автоматизация и ТЗ на скрипт
- Чек-лист перед запуском
- Что мы делаем в Hated.Ru
Когда Excel пора отдать Python
Автоматизация нужна далеко не везде. Для правки сорока строк быстрее формула и пять минут ручной работы. Python включается там, где задача повторяется, данные приходят снаружи или строк столько, что глазами не проверить.
Мой порядок такой. Сначала смотрю, что даёт сам Excel: формулы, сводные таблицы, Power Query. Если их хватает, скрипт не нужен. Если файл надо забрать из почты или API, обработать по правилам и положить результат туда, где его увидит команда, подключаю Python.
| Ситуация | Что взять | Комментарий |
|---|---|---|
| Разовая правка до сотни строк | Формулы и руки | Скрипт писать и проверять дольше, чем править таблицу. |
| Свод файлов одного формата раз в неделю | Power Query или Python | Python удобнее, когда после свода нужны очистка, диаграммы и отправка. |
| Кнопка внутри Excel для сотрудников | VBA-макрос | Макрос живёт в книге и запускается кнопкой, Python ставить не надо. |
| Данные из API, CRM или почты, результат в файл | Python | Авторизация, постраничная загрузка и обработка ошибок удобнее в скрипте. |
| Подсветка отклонений в готовой таблице | Условное форматирование Excel | Python нужен, если таблицу создаёт скрипт. |
| Больше 1 048 576 строк | База данных и pandas | Столько строк на одном листе Excel не помещается. |
Выберите свою задачу ниже, и я подскажу, что бы взял на вашем месте.
Какая библиотека для чего
Новичку помогает простое правило. pandas берёт таблицу как данные: очистка, группировки, свод. openpyxl правит и оформляет готовые файлы. XlsxWriter создаёт новые отчёты с диаграммами. Остальное нужно реже.
| Библиотека | Читает | Пишет | Меняет готовый файл | Для чего берут |
|---|---|---|---|---|
| openpyxl | xlsx, xlsm | Да | Да, с оговорками | Правка, оформление, условное форматирование, формулы |
| pandas | Через движки | Да | Пишет новый файл | Очистка, группировки, сводные таблицы, объединение файлов |
| XlsxWriter | Нет | Да | Нет | Новые отчёты с диаграммами и аккуратным оформлением, быстрая запись |
| python-calamine | xlsx, xls, xlsb, ods | Нет | Нет | Быстрое чтение больших файлов, подключается к pandas как движок |
| pywin32 | Через настоящий Excel | Да | Да | Управление запущенным Excel через COM: макросы, сводные, экспорт в PDF |
XlsxWriter умеет только создавать новые файлы. Если нужно дописать лист в чужую книгу, берите openpyxl.
pywin32 работает только на Windows и только при установленном Excel, потому что управляет им через COM. Мои примеры запускались на стенде с Linux, где нет ни Windows, ни Excel, поэтому про pywin32 здесь обзор без кода.
Отдельно стоит функция Microsoft 365 «Python в Excel». Код в ней выполняется в облаке Microsoft внутри таблицы и не имеет доступа к вашему компьютеру и сети. Для скриптов, которые собирают файлы из папок и отправляют отчёты, она не предназначена.
Подготовка: окружение за десять минут
Нужен Python 3.11 или новее, так как pandas 3 требует именно такую версию. Для каждого проекта я завожу отдельное виртуальное окружение. Версии библиотек остаются в нём, и обновление pandas в одном проекте не ломает скрипты в другом.
Папка проекта и окружение
Создайте папку, например excel-auto, откройте в ней терминал и создайте окружение.
python -m venv .venv .venv\Scripts\activate # Windows source .venv/bin/activate # macOS и Linux
Библиотеки
Четыре пакета: три основные и calamine для быстрого чтения. Версии из моего прогона зафиксированы в файле requirements.
pip install openpyxl==3.1.5 pandas==3.0.2 xlsxwriter==3.2.9 python-calamine==0.8.2 pip freeze > requirements.txt
Тестовые данные
Скрипт ниже создаёт файл orders.xlsx с типичным беспорядком: пропуски, дубли, лишние пробелы, даты текстом. Ещё он кладёт в папку exports три файла выгрузок для раздела 10. На этих данных построены все следующие примеры.
Показать скрипт make_demo.py
В итоге в папке проекта лежат файлы с понятными именами: orders.xlsx на входе, скрипты s01_read.py, s02_write.py и так далее, папка reports для готовых отчётов. Исходные файлы я держу отдельно от результатов, чтобы случайно не перезаписать данные.
Читаем файл: листы, ячейки, формулы
Начинаем с чтения через openpyxl. Откроем книгу, найдём лист, возьмём одну ячейку, пройдём по строкам и соберём столбец целиком.
from openpyxl import load_workbook wb = load_workbook("orders.xlsx") # открываем книгу print(wb.sheetnames) # ['Заказы'] ws = wb["Заказы"] # нужный лист по имени print(ws.max_row, ws.max_column) # 124 10 print(ws["C2"].value) # одна ячейка: менеджер в строке 2 # все строки, начиная со второй, сразу значениями total = 0 for date, order, manager, city, good, qty, price, amount, *_ in ws.iter_rows(min_row=2, values_only=True): total += amount or 0 print("Сумма всех заказов:", total) # один столбец целиком managers = [c.value for c in ws["C"][1:]] # без заголовка print(len(managers), "значений в столбце «Менеджер»")
['Заказы'] 124 10 Иванова Сумма всех заказов: 3395370 123 значений в столбце «Менеджер»
- ws["C2"].value возвращает содержимое одной ячейки. Адрес пишется так же, как в Excel.
- iter_rows(min_row=2, values_only=True) отдаёт готовые значения без объектов ячеек. Цикл работает быстрее, а код короче.
- ws["C"] возвращает все ячейки столбца C. Срез [1:] убирает заголовок.
- Пустая ячейка приходит как None. Поэтому в сумме стоит amount or 0.
По умолчанию openpyxl возвращает формулу как текст. Параметр data_only=True просит вернуть значение, которое сохранил Excel при последнем открытии файла. Если файл создала сама библиотека, значений в нём нет, и вместо числа придёт None. Проверим на минимальном примере.
from openpyxl import Workbook, load_workbook wb = Workbook() ws = wb.active ws["A1"], ws["A2"], ws["A3"] = 100, 250, "=SUM(A1:A2)" wb.save("formula_test.xlsx") print(load_workbook("formula_test.xlsx")["Sheet"]["A3"].value) # =SUM(A1:A2) print(load_workbook("formula_test.xlsx", data_only=True)["Sheet"]["A3"].value) # None
=SUM(A1:A2) None
Если вам нужны итоги формул, откройте файл в Excel и сохраните его, либо считайте итоги в Python и записывайте числа.
Пишем, переименовываем, сохраняем
Задача: взять из выгрузки только оплаченные заказы, положить их на новый лист, добавить итог формулой и сохранить отчёт под именем с датой.
from datetime import date from pathlib import Path from openpyxl import Workbook, load_workbook src = load_workbook("orders.xlsx")["Заказы"] wb = Workbook() ws = wb.active ws.title = "Оплаченные" # переименовали стандартный лист header = [c.value for c in src[1]] ws.append(header) for row in src.iter_rows(min_row=2, values_only=True): if row[8] == "Оплачен": # столбец «Статус» ws.append(row) ws["L1"] = "Всего оплачено" # запись в конкретную ячейку ws["L2"] = f"=SUM(H2:H{ws.max_row})" # формулу пишем строкой wb.create_sheet("Черновик") # лишний лист создали... del wb["Черновик"] # ...и удалили out = Path("reports") out.mkdir(exist_ok=True) path = out / f"orders_paid_{date.today():%Y-%m-%d}.xlsx" # исходник не перезаписываем wb.save(path) print("Сохранено:", path, "| листы:", wb.sheetnames, "| строк:", ws.max_row)
Сохранено: reports/orders_paid_2026-10-07.xlsx | листы: ['Оплаченные'] | строк: 63
- ws.title переименовывает лист. Стандартный лист новой книги называется Sheet, и оставлять его так в отчёте нехорошо.
- ws.append(строка) дописывает строку в конец листа. Это самый короткий способ записи.
- create_sheet и del wb["Черновик"] создают и удаляют листы.
- Формулу записываем обычной строкой, которая начинается со знака равенства. Excel пересчитает её при открытии.
Метод save перезаписывает существующий файл без предупреждения. Поэтому исходник остаётся нетронутым, а отчёт сохраняется под новым именем с датой.
Ещё одна оговорка: openpyxl читает не все элементы файла, и фигуры из старой книги пропадут, если открыть её и сохранить под тем же именем. Для книги с макросами в формате xlsm нужен параметр keep_vba=True. Макросы тогда сохранятся, но редактировать их из Python нельзя.
Оформление: шрифты, границы, форматы
Сырая выгрузка в таком виде руководителю не уйдёт. Добавим заголовок отчёта с объединёнными ячейками, цветную шапку, границы, форматы денег и дат, ширину столбцов, закрепление шапки и фильтр.
from openpyxl import load_workbook from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter wb = load_workbook("orders.xlsx") ws = wb["Заказы"] ws.insert_rows(1) # строка под заголовок отчёта ws["A1"] = "Заказы за сентябрь 2026" ws.merge_cells("A1:J1") # объединили ячейки ws["A1"].font = Font(name="Calibri", size=16, bold=True) ws["A1"].alignment = Alignment(horizontal="center", vertical="center") ws.row_dimensions[1].height = 30 # высота строки thin = Side(style="thin", color="BBBBBB") border = Border(left=thin, right=thin, top=thin, bottom=thin) head_fill = PatternFill("solid", fgColor="1F3864") for cell in ws[2]: # шапка таблицы cell.font = Font(bold=True, color="FFFFFF") cell.fill = head_fill cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) cell.border = border for row in ws.iter_rows(min_row=3): for cell in row: cell.border = border row[0].number_format = "DD.MM.YYYY" # дата row[6].number_format = '#,##0 "₽"' # цена row[7].number_format = '#,##0 "₽"' # сумма row[5].alignment = Alignment(horizontal="center") widths = {"A": 12, "B": 9, "C": 14, "D": 12, "E": 16, "F": 8, "G": 12, "H": 14, "I": 11, "J": 15} for col, w in widths.items(): ws.column_dimensions[col].width = w # ширина столбцов ws.freeze_panes = "A3" # шапка остаётся на экране ws.auto_filter.ref = f"A2:{get_column_letter(ws.max_column)}{ws.max_row}" wb.save("orders_formatted.xlsx") print("Готово: orders_formatted.xlsx")
- merge_cells("A1:J1") объединяет ячейки. Значение и оформление берутся из верхней левой.
- Font, PatternFill, Alignment и Border задают шрифт, заливку, выравнивание и рамку. Цвета пишутся шестнадцатеричным кодом без решётки.
- number_format управляет тем, как число выглядит в ячейке, при этом значение остаётся числом. Формат '#,##0 "₽"' показывает 31900 как «31 900 ₽», разделитель разрядов зависит от региональных настроек.
- Автоподбора ширины в openpyxl нет. Ширину столбцов задаёте числом в column_dimensions.
- freeze_panes = "A3" закрепляет две верхние строки. Всё выше указанной ячейки остаётся на экране при прокрутке.
- auto_filter.ref включает фильтр на заданном диапазоне.
На картинке видны и проблемы самих данных: дата текстом у заказа 8017, лишний пробел перед названием товара, пустые менеджеры и города. Оформление их не лечит. Чистить данные будем в следующем разделе.
pandas: чистим выгрузку
В нашей выгрузке четыре типичные проблемы: пропуски в менеджерах и городах, дубли заказов, лишние пробелы в названиях и даты, часть которых записана текстом. pandas закрывает их несколькими строками. Читаем файл, чистим, считаем итоги и пишем результат сразу на три листа.
import pandas as pd # телефон читаем строкой, иначе потеряются ведущие цифры у длинных номеров df = pd.read_excel("orders.xlsx", sheet_name="Заказы", dtype={"Телефон": str}) print(df.shape) # (123, 10) print(df.isna().sum()[["Менеджер", "Город"]]) df["Товар"] = df["Товар"].str.strip() # убрали пробелы df["Дата"] = pd.to_datetime(df["Дата"], format="mixed", dayfirst=True) # даты текстом тоже df = df.drop_duplicates() # дубли строк целиком df["Менеджер"] = df["Менеджер"].fillna("Не назначен") # пропуски заполняем df = df.dropna(subset=["Город"]) # без города заказ не нужен paid = df[df["Статус"] == "Оплачен"] by_manager = (paid.groupby("Менеджер", as_index=False) .agg(Заказов=("Заказ", "count"), Сумма=("Сумма", "sum")) .sort_values("Сумма", ascending=False)) by_city = paid.pivot_table(index="Город", columns="Товар", values="Сумма", aggfunc="sum", fill_value=0) with pd.ExcelWriter("orders_clean.xlsx") as writer: # несколько листов за раз df.to_excel(writer, sheet_name="Чистые данные", index=False) by_manager.to_excel(writer, sheet_name="По менеджерам", index=False) by_city.to_excel(writer, sheet_name="Город и товар") print(by_manager) print("Строк после очистки:", len(df))
(123, 10)
Менеджер 25
Город 25
dtype: int64
Менеджер Заказов Сумма
0 Иванова 10 452570
1 Кузнецов 12 248270
3 Петров 8 229150
2 Не назначен 10 204740
4 Смирнова 7 151460
Строк после очистки: 97- dtype={"Телефон": str} читает столбец строкой, чтобы номера не превратились в числа.
- str.strip() убирает пробелы по краям, и « Пылесос » становится «Пылесос».
- to_datetime(..., format="mixed", dayfirst=True) приводит к одному типу и настоящие даты, и текст вида 03.09.2026.
- drop_duplicates() убирает строки, полностью повторяющие другие. Здесь уходят три дубля.
- fillna заполняет пропуски значением, а dropna(subset=...) выбрасывает строки, где пусто в нужном столбце.
- groupby с именованными агрегатами считает заказы и сумму по менеджерам, pivot_table строит сводную по городам и товарам.
- ExcelWriter с несколькими вызовами to_excel складывает всё в одну книгу на разные листы.
Правило для пропусков выбираете вы. Код его только исполняет. В учебном примере я выбросил заказы без города. В реальной работе такие строки лучше сохранить на отдельный лист и показать тому, кто отвечает за данные.
Номера с хвостом .0
Если в столбце с телефонами или ИНН есть пустые ячейки, pandas читает его как дробные числа. В моём тесте 79157135241 превратилось в 79157135241.0.
Читайте такие столбцы строкой: dtype=str.
inplace на столбце молчит
В pandas 3 запись df["a"].fillna(0, inplace=True) ничего не меняет и выдаёт предупреждение. Пропуски остаются на месте.
Пишите df["a"] = df["a"].fillna(0).
Даты вперемешку
В тестовой выгрузке 116 дат хранятся как даты и 7 как текст. Без приведения к одному типу сортировка и группировка по дате дадут странный результат.
Решает to_datetime с форматом mixed.
Отчёт с диаграммами в XlsxWriter
Для отчёта с нуля я беру XlsxWriter. Он быстрый, умеет основные типы диаграмм Excel и оформляет файл прямо при создании. Построим таблицу по рекламным кампаниям, комбинированную диаграмму «расход и заявки» и круговую с долями расхода. Числа в таблице учебные.
import xlsxwriter rows = [ ("Тула, поиск", 18400, 920, 36800, 31), ("Рязань, поиск", 12100, 610, 22400, 17), ("Москва, поиск", 30500, 1180, 71200, 44), ("РСЯ, ремаркетинг", 54000, 760, 15900, 12), ("Карты", 9800, 330, 11300, 14), ] wb = xlsxwriter.Workbook("ads_report.xlsx") ws = wb.add_worksheet("Реклама") head = wb.add_format({"bold": True, "bg_color": "#1F3864", "font_color": "white", "align": "center", "valign": "vcenter", "border": 1}) money = wb.add_format({"num_format": '#,##0 "₽"', "border": 1}) num = wb.add_format({"num_format": "#,##0", "border": 1}) pct = wb.add_format({"num_format": "0.0%", "border": 1}) txt = wb.add_format({"border": 1}) ws.write_row(0, 0, ["Кампания", "Показы", "Клики", "Расход", "Заявки", "CTR", "Цена заявки"], head) for r, (name, shows, clicks, cost, leads) in enumerate(rows, start=1): ws.write(r, 0, name, txt) ws.write(r, 1, shows, num) ws.write(r, 2, clicks, num) ws.write(r, 3, cost, money) ws.write(r, 4, leads, num) # формулы пишем вместе с результатом: просмотрщики без пересчёта покажут число вместо нуля ws.write_formula(r, 5, f"=C{r+1}/B{r+1}", pct, clicks / shows) ws.write_formula(r, 6, f"=D{r+1}/E{r+1}", money, cost / leads) ws.set_column("A:A", 20) ws.set_column("B:G", 13) ws.freeze_panes(1, 0) ws.set_landscape() # печать на одну страницу ws.fit_to_pages(1, 1) last = len(rows) col = wb.add_chart({"type": "column"}) # столбцы: расход col.add_series({"name": "Расход, ₽", "categories": ["Реклама", 1, 0, last, 0], "values": ["Реклама", 1, 3, last, 3], "fill": {"color": "#F98318"}}) line = wb.add_chart({"type": "line"}) # линия: заявки line.add_series({"name": "Заявки", "categories": ["Реклама", 1, 0, last, 0], "values": ["Реклама", 1, 4, last, 4], "y2_axis": True, "marker": {"type": "circle", "size": 7}, "line": {"color": "#1F3864", "width": 2.5}}) col.combine(line) col.set_title({"name": "Расход и заявки по кампаниям"}) col.set_y_axis({"name": "Расход, ₽"}) line.set_y2_axis({"name": "Заявки"}) col.set_size({"width": 620, "height": 320}) ws.insert_chart("A9", col) pie = wb.add_chart({"type": "pie"}) # круг: доли расхода pie.add_series({"categories": ["Реклама", 1, 0, last, 0], "values": ["Реклама", 1, 3, last, 3], "data_labels": {"percentage": True}}) pie.set_title({"name": "Доли расхода"}) pie.set_size({"width": 420, "height": 320}) ws.insert_chart("H9", pie) wb.close() print("Готово: ads_report.xlsx")
- Данные диаграммы задаются адресом [лист, строка_начала, столбец_начала, строка_конца, столбец_конца]. Нумерация идёт с нуля.
- combine() накладывает линию на столбцы, а y2_axis выводит вторую шкалу для заявок.
- set_landscape() и fit_to_pages(1, 1) настраивают печать: отчёт ложится на одну страницу.
- На учебных числах сразу видно разницу: CTR у ремаркетинга в РСЯ 1,4% против 5,0% у поиска по Туле и Рязани.
XlsxWriter сам формулы не считает. Он записывает в файл ноль и просит Excel пересчитать всё при открытии. Excel так и делает. Но PDF-конвертеры, часть мобильных приложений и программы просмотра без пересчёта покажут нули. Я столкнулся с этим при подготовке картинки для статьи: без четвёртого параметра write_formula столбцы CTR и цены заявки выглядели как 0.
Если отчёт будут смотреть не только в Excel, передавайте результат заранее, как в коде выше.
Для очень больших файлов у XlsxWriter есть режим constant_memory. Он записывает данные построчно и держит в памяти одну строку, но тогда строки нужно писать строго по порядку. Сколько это даёт на практике, смотрите в замерах в разделе 11.
Условное форматирование: план и факт
Условное форматирование красит ячейки по правилам, и правила остаются в файле. Поменяете число в Excel, цвет пересчитается сам. Возьмём лист «По менеджерам» из раздела 7, добавим план и выполнение и раскрасим таблицу по уровню выполнения плана.
from openpyxl import load_workbook from openpyxl.formatting.rule import CellIsRule, DataBarRule, FormulaRule from openpyxl.styles import Font, PatternFill wb = load_workbook("orders_clean.xlsx") ws = wb["По менеджерам"] # лист из прошлого шага plans = {"Иванова": 400000, "Кузнецов": 300000, "Петров": 250000, "Смирнова": 180000, "Не назначен": 100000} ws["D1"], ws["E1"] = "План", "Выполнение" for r in range(2, ws.max_row + 1): ws[f"D{r}"] = plans[ws[f"A{r}"].value] ws[f"E{r}"] = f"=C{r}/D{r}" ws[f"E{r}"].number_format = "0%" ws[f"C{r}"].number_format = ws[f"D{r}"].number_format = '#,##0 "₽"' for c in ws[1]: c.font = Font(bold=True) rng = f"E2:E{ws.max_row}" green = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid") yellow = PatternFill(start_color="FFEB9C", end_color="FFEB9C", fill_type="solid") red = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") ws.conditional_formatting.add(rng, CellIsRule(operator="greaterThanOrEqual", formula=["1"], fill=green)) ws.conditional_formatting.add(rng, CellIsRule(operator="between", formula=["0.8", "0.9999"], fill=yellow)) ws.conditional_formatting.add(rng, CellIsRule(operator="lessThan", formula=["0.8"], fill=red)) ws.conditional_formatting.add(f"C2:C{ws.max_row}", DataBarRule(start_type="num", start_value=0, end_type="max", color="F98318")) ws.conditional_formatting.add(f"A2:A{ws.max_row}", FormulaRule(formula=['$A2="Не назначен"'], font=Font(italic=True, color="9C0006"))) for col, w in {"A": 16, "B": 10, "C": 16, "D": 14, "E": 13}.items(): ws.column_dimensions[col].width = w wb.save("plan_fact.xlsx") print("Готово: plan_fact.xlsx")
- CellIsRule сравнивает значение ячейки с порогом. Для жёлтой зоны я взял between с границами 0,8 и 0,9999, чтобы ровно 100% попало в зелёное правило.
- DataBarRule рисует полосу внутри ячейки пропорционально сумме.
- FormulaRule с формулой $A2="Не назначен" красит по условию на другую ячейку. Знак доллара фиксирует столбец, а номер строки сдвигается для каждой строки диапазона.
- Заливка задаётся парой start_color и end_color. Для сплошного цвета они одинаковые.
Пороги в примере такие: от 100% зелёный, от 80 до 99% жёлтый, ниже 80% красный. Свои числа поставьте исходя из плана вашего отдела. Подробный список правил есть в документации openpyxl, ссылка в источниках.
Свод из папки с выгрузками
Типичная задача: три филиала присылают выгрузки отдельными файлами, и раз в неделю их надо свести в один. Складываем файлы в папку exports. Скрипт читает все xlsx, добавляет столбец с именем файла, убирает повторы заказов и пишет свод.
from pathlib import Path import pandas as pd folder = Path("exports") # сюда складывают выгрузки frames = [] for path in sorted(folder.glob("*.xlsx")): part = pd.read_excel(path, dtype={"Телефон": str}) part["Источник"] = path.stem # из какого файла строка frames.append(part) if not frames: raise SystemExit("В папке exports нет файлов .xlsx") svod = pd.concat(frames, ignore_index=True) svod = svod.drop_duplicates(subset=["Заказ"]) # заказ не должен считаться дважды with pd.ExcelWriter("svod.xlsx", engine="xlsxwriter") as writer: # движок указываем явно svod.to_excel(writer, sheet_name="Свод", index=False) ws = writer.sheets["Свод"] ws.freeze_panes(1, 0) # это уже API XlsxWriter ws.autofilter(0, 0, len(svod), len(svod.columns) - 1) ws.set_column("A:J", 14) print(f"Файлов: {len(frames)}, строк в своде: {len(svod)}")
Файлов: 3, строк в своде: 120
- Path.glob("*.xlsx") находит все файлы нужного типа, а sorted делает порядок стабильным.
- Столбец «Источник» нужен, чтобы при ошибке в своде найти файл, из которого пришла строка.
- drop_duplicates(subset=["Заказ"]) убирает повторы по ключу. В моих трёх тестовых файлах заказы пересекаются: из 133 прочитанных строк в своде остаётся 120.
- Движок записи указан явно. По документации pandas для xlsx по умолчанию использует XlsxWriter и переключается на openpyxl, если XlsxWriter не установлен. Явный выбор избавляет от сюрпризов на чужом компьютере.
- Раз движок XlsxWriter, закрепление и фильтр делаются его методами freeze_panes и autofilter, а ширина столбцов задаётся через set_column.
Печатайте в консоль, сколько строк прочитано и сколько записано. Если разница не совпадает с числом дублей, которое вы ожидаете, смотрите столбец «Источник» и ищите файл с лишним форматом.
Скорость и память: замеры на 100 000 строк
На маленьких файлах разницы между библиотеками не заметно. На десятках тысяч строк она превращается в минуты. Я измерил запись и чтение файла в 100 000 строк и 8 столбцов: даты, числа, текст. Каждый вариант запускался в отдельном процессе пять раз, в таблице медиана.
Стенд: 2 ядра Intel Xeon 2,1 ГГц, 8 ГБ памяти, Python 3.11, openpyxl 3.1.5, pandas 3.0.2, XlsxWriter 3.2.9, python-calamine 0.8.2. Память указана как пиковый размер процесса вместе с исходными данными в списке, а это около 42 МБ. Читали файл, который записал XlsxWriter. На вашем компьютере секунды будут другими, сравнивайте соотношения.
Показать скрипт замеров bench.py
import random import resource import sys import time from datetime import date, datetime, time as dtime, timedelta N = 100_000 variant = sys.argv[1] random.seed(1) base = date(2026, 1, 1) rows = [(base + timedelta(days=i % 300), 1000 + i, f"Клиент {i % 500}", random.choice(["Тула", "Рязань", "Москва"]), random.randint(1, 9), round(random.random() * 1000, 2), random.randint(100, 99999), "Оплачен") for i in range(N)] head = ["Дата", "Заказ", "Клиент", "Город", "Кол-во", "Скидка", "Сумма", "Статус"] def write_xlsxwriter(path, **opts): import xlsxwriter wb = xlsxwriter.Workbook(path, opts) ws = wb.add_worksheet() date_fmt = wb.add_format({"num_format": "dd.mm.yyyy"}) ws.write_row(0, 0, head) for i, r in enumerate(rows, 1): ws.write_datetime(i, 0, datetime.combine(r[0], dtime()), date_fmt) ws.write_row(i, 1, r[1:]) wb.close() t0 = time.perf_counter() if variant == "baseline": pass elif variant == "w_openpyxl": from openpyxl import Workbook wb = Workbook(); ws = wb.active; ws.append(head) for r in rows: ws.append(r) wb.save("bench/o1.xlsx") elif variant == "w_openpyxl_wo": from openpyxl import Workbook wb = Workbook(write_only=True); ws = wb.create_sheet(); ws.append(head) for r in rows: ws.append(r) wb.save("bench/o2.xlsx") elif variant == "w_xlsxwriter": write_xlsxwriter("bench/o3.xlsx") elif variant == "w_xlsxwriter_cm": write_xlsxwriter("bench/o4.xlsx", constant_memory=True) elif variant in ("w_pandas_openpyxl", "w_pandas_xlsxwriter"): import pandas as pd df = pd.DataFrame(rows, columns=head) df["Дата"] = pd.to_datetime(df["Дата"]) engine = variant.split("_")[-1] df.to_excel(f"bench/o5_{engine}.xlsx", index=False, engine=engine) elif variant == "r_openpyxl": from openpyxl import load_workbook ws = load_workbook("bench/o3.xlsx").active total = sum(r[6] for r in ws.iter_rows(min_row=2, values_only=True)) elif variant == "r_openpyxl_ro": from openpyxl import load_workbook wb = load_workbook("bench/o3.xlsx", read_only=True) total = sum(r[6] for r in wb.active.iter_rows(min_row=2, values_only=True)) wb.close() elif variant == "r_pandas": import pandas as pd total = pd.read_excel("bench/o3.xlsx")["Сумма"].sum() elif variant == "r_pandas_calamine": import pandas as pd total = pd.read_excel("bench/o3.xlsx", engine="calamine")["Сумма"].sum() elapsed = time.perf_counter() - t0 peak_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 print(f"{variant};{elapsed:.2f};{peak_mb:.0f}")
Что из замеров следует
- Запись нового отчёта: XlsxWriter быстрее всех. Через pandas с движком openpyxl тот же файл пишется почти втрое дольше.
- Память при записи: режимы write_only у openpyxl и constant_memory у XlsxWriter держат пик в разы ниже обычного. Платите за это ограничениями: строки только по порядку, правки ячеек задним числом невозможны.
- Чтение: режим read_only у openpyxl почти вдвое быстрее обычного и держит память в разы ниже. Взамен недоступны диаграммы, картинки и часть других возможностей, это прямо сказано в документации.
- Самое быстрое чтение: pandas с движком calamine. Он подключается одним словом engine="calamine" после установки пакета python-calamine. Память при этом выше, чем у read_only.
- Лимит самого Excel: 1 048 576 строк и 16 384 столбца на листе, до 32 767 символов в ячейке. Если данных больше, храните их в базе, а в Excel отдавайте итоговую сводку.
Десять частых ошибок
Большинство этих ошибок я воспроизвёл на тестовых файлах, остальные описаны в документации библиотек. В последнем столбце указан раздел, где ошибка разобрана подробнее.
| № | Ошибка | Как проявляется | Что делать | Где |
|---|---|---|---|---|
| 1 | Лист не найден | В openpyxl приходит KeyError, в pandas ValueError. Частая причина: лишний пробел в названии листа | Вывести wb.sheetnames и взять имя оттуда | 4 |
| 2 | Сохранили поверх исходника | Исходные данные потеряны, предупреждения нет | Исходники в отдельной папке, отчёт с датой в имени | 5 |
| 3 | Формулы без значений | В openpyxl с data_only=True приходит None, в pandas приходит NaN | Открыть и сохранить файл в Excel или считать итоги в Python | 4 |
| 4 | Нули вместо результатов | Просмотрщик показывает 0 на месте формул из XlsxWriter | Передавать результат формулы параметром value | 8 |
| 5 | Номера с хвостом .0 | Телефон превращается в 79157135241.0, если в столбце есть пустые ячейки | Читать столбец строкой: dtype=str | 7 |
| 6 | Даты вперемешку | Часть строк не сортируется и не группируется по дате | to_datetime с format="mixed" и dayfirst=True | 7 |
| 7 | inplace на столбце | В pandas 3 пропуски остаются на месте, в консоли предупреждение | Присваивание df["a"] = df["a"].fillna(0) | 7 |
| 8 | Старый формат xls | openpyxl выдаёт InvalidFileException с просьбой взять xlrd | Пересохранить в xlsx или читать через pandas с xlrd либо calamine | 2 |
| 9 | Пропали фигуры после сохранения | openpyxl читает не все элементы файла, фигуры в старой книге теряются | Править копию, не сохранять поверх, проверять результат глазами | 5 |
| 10 | Объединённые ячейки читаются пустыми | Значение лежит только в верхней левой ячейке, остальные дают None | Читать верхнюю левую или снять объединение до обработки | 6 |
Нейросеть как помощник и три промпта
Нейросеть быстро пишет скрипты для Excel. Ошибается она с тем же спокойствием. Поэтому её код я считаю черновиком: проверяю на копии, сверяю итоги и только потом запускаю на настоящих данных.
Обезличьте пример
Возьмите пять строк из файла без имён, телефонов и закупочных цен. Структура столбцов и типы данных сохраняются, модели этого достаточно.
Опишите вход, выход и правила
Какие столбцы на входе, какой файл нужен на выходе, что делать с пропусками и дублями.
Назовите версии
Python, pandas, openpyxl. Модель охотно пишет под старые версии, и такой код падает на свежих.
Просите код порциями
Сорок строк с комментариями читать проще, чем двести без них.
Проверьте на копии
Запустите на десяти строках и сверьте итог с ручным расчётом.
Запускайте на полном файле
Только после сверки, и желательно с записью в журнал, сколько строк прочитано и сколько записано.
Файлы с персональными данными клиентов, договоры, закупочные цены и пароли. Для запроса хватает пяти обезличенных строк. Если задача шире и вам нужен помощник, который работает с вашими файлами в защищённом контуре, это отдельный проект. Общую картину по ИИ-помощникам я разбирал в статье про ИИ-агентов для работы.
Безопасность и порядок
Скрипт, который меняет файлы, должен быть аккуратнее человека. Человек заметит странный результат, скрипт отправит его дальше без раздумий. Шесть правил, которых я придерживаюсь.
Копии и версии
Исходники лежат в отдельной папке, результат сохраняется под новым именем. Сами скрипты живут в Git, и любое изменение можно откатить.
Чужие файлы
По документации openpyxl по умолчанию не защищает от атак через специально собранный XML. Для файлов от незнакомых отправителей установите пакет defusedxml.
Макросы
Книги xlsm открывайте с keep_vba=True, если макросы нужно сохранить. Макросы из чужих файлов не запускайте.
Персональные данные
Статья 18 закона 152-ФЗ требует хранить персональные данные граждан России в базах на территории России. Если скрипт складывает такие данные на сервер, берите российское облако. Детали для вашего случая уточните у юриста.
Секреты
Пароли и ключи доступа к API не пишите в код. Берите их из переменных окружения и не коммитьте в Git.
Журнал
Скрипт пишет, сколько строк прочитал, сколько записал и какие строки пропустил. По журналу видно, когда и почему отчёт разошёлся с ручным счётом.
Окупится ли автоматизация и ТЗ на скрипт
Скрипт стоит денег: часы разработки и немного поддержки. Экономит он часы сотрудников. Посчитайте оба числа на своих данных. В калькуляторе стоят учебные значения: ежедневный отчёт на полчаса, восемь часов на разработку и ставка разработчика 3 500 ₽, как на нашей странице про backend-разработку. Подставьте свои цифры.
Формула простая. Экономия в месяц равна числу часов ручной работы, умноженному на стоимость часа сотрудника, минус стоимость поддержки. Окупаемость равна стоимости разработки, делённой на экономию в месяц. Ошибки ручной работы в расчёт не входят, хотя тоже стоят денег.
Шаблон ТЗ на скрипт
Разработчику для старта нужны десять ответов. Нейросети они нужны не меньше. Скопируйте шаблон и заполните.
Чек-лист перед запуском
Отметьте то, что у вас уже сделано. Внизу появится вердикт.
Отмечено: 0 из 12
- Исходные файлы лежат в отдельной папке, скрипт их не перезаписывает
- Есть резервная копия или Git
- Скрипт запускался на копии из 10 или 20 строк
- Итоги сверены с ручным расчётом
- Телефоны, ИНН и артикулы читаются строкой
- Даты приведены к одному формату
- Дубли удаляются по ключу, например по номеру заказа
- Правило для пустых значений записано в коде
- Версии библиотек записаны в requirements.txt
- Скрипт пишет в журнал, сколько строк прочитано и записано
- Готовый файл открыт в Excel и просмотрен глазами
- Понятно, кто запускает скрипт и что делать при ошибке
Что мы делаем в Hated.Ru
Когда задача вырастает из одного скрипта в систему, подключаемся мы. Ниже то, что связано с темой статьи. Цены стартовые, со страниц услуг на 7 октября 2026 года. Точную смету считаем после разбора задачи.
| Задача | Что делаем | Цена |
|---|---|---|
| Автоматизация и интеграции | Серверная часть на Python: обработка файлов, связь с 1С, CRM и другими сервисами | от 60 000 ₽, срок от 2 недель |
| Небольшие задачи по часам | Доработка вашего скрипта, разовая обработка файлов. Оценку объёма даём письменно до начала | от 3 500 ₽/час |
| Telegram-бот | Бот, который принимает файл или присылает отчёт в чат, интеграция с CRM | от 30 000 ₽ |
| Сервер в российском облаке | Виртуальная машина в Yandex Cloud или VK Cloud, где скрипт запускается по расписанию | базовая настройка от 30 000 ₽ |
| Безопасность и 152-ФЗ | Права доступа, шифрование дисков и бэкапов, размещение данных внутри страны | от 40 000 ₽ |
Про меня и мой стек есть отдельная страница. Если нужен совет до начала проекта, напишите на почту, ответим по существу.
Вопросы и ответы про автоматизацию Excel на Python
Можно. openpyxl, XlsxWriter и pandas читают и пишут файлы формата xlsx напрямую, Excel для этого не нужен. Он понадобится только для pywin32, которая управляет запущенным Excel через COM на Windows.
VBA живёт внутри книги и запускается кнопкой, поэтому удобен сотрудникам, которым нужна готовая кнопка. Python работает снаружи: ходит за данными в API, обрабатывает папки с файлами, отправляет результат и запускается по расписанию. Часто используют оба инструмента.
pandas подходит для очистки и расчётов, openpyxl для правки и оформления готовых файлов, XlsxWriter для новых отчётов с диаграммами. В одном скрипте их спокойно сочетают: чистят в pandas, пишут через XlsxWriter, дорабатывают в openpyxl.
Файл, который создала или сохранила библиотека, хранит формулы без результатов, пока его не пересчитает Excel. Параметр data_only=True возвращает значение, которое Excel сохранил при последнем открытии. Если его нет, придёт None. Откройте файл в Excel и сохраните, либо считайте итоги в Python.
Самый быстрый вариант в моих замерах: pandas с движком calamine. На 100 000 строк он справился примерно за 1,4 секунды против 6,5 у обычного read_excel. Если нужен openpyxl, включите режим read_only, он быстрее и расходует в разы меньше памяти, но диаграммы и картинки в нём недоступны.
Согласно документации Microsoft, 1 048 576 строк и 16 384 столбца на листе, в одной ячейке до 32 767 символов. Для больших массивов используйте базу данных, а в Excel выгружайте итоговую сводку.
Нет. По документации XlsxWriter только создаёт новые файлы. Для правки готовой книги берите openpyxl.
Открывайте книгу с параметром keep_vba=True и сохраняйте с расширением xlsm. Макросы сохранятся, но редактировать их из Python нельзя.
Файлы с персональными данными клиентов, договорами и закупочными ценами отправлять нельзя. Для запроса хватает пяти обезличенных строк. Требования 152-ФЗ к хранению персональных данных граждан России в базах на территории России уточняйте у юриста.
В Hated.Ru автоматизация и интеграции на Python стоят от 60 000 ₽, срок от двух недель. Небольшие задачи берём по часам от 3 500 ₽ в час, оценку объёма даём письменно до начала. Точную стоимость называем после разбора файла и правил обработки.
Нужны основы Python: переменные, циклы, функции. Базовые примеры из статьи читаются без опыта, а остальное приходит с практикой. Если времени на это нет, задачу можно поручить разработчику и приложить ТЗ по шаблону из раздела 15.
Где проверить цифры и правила
Данные проверены 7 октября 2026 года. Версии библиотек и правила меняются, перед внедрением сверяйте их с первоисточником.
- Документация openpyxl 3.1: руководство, сохранение, data_only, keep_vba
- Документация openpyxl: режимы read_only и write_only
- Документация openpyxl: условное форматирование
- Документация openpyxl: введение и раздел о безопасности
- Документация XlsxWriter: введение и ограничения
- Документация XlsxWriter: формулы и их результаты
- Документация XlsxWriter: память и режим constant_memory
- Документация pandas: read_excel и доступные движки
- Документация pandas: выбор движка записи Excel
- Документация pandas: Copy-on-Write и цепочечное присваивание
- PyPI: python-calamine
- PyPI: defusedxml
- PyPI: pywin32
- Microsoft: спецификации и лимиты листов и книг Excel
- Microsoft: безопасность данных и Python в Excel
- Гарант: локализация персональных данных, статья 18 закона 152-ФЗ
- Hated.Ru: страница Дени Булатова
Пришлите пример файла без персональных данных и опишите, что на входе и что нужно получить на выходе. Скажем, стоит ли автоматизировать задачу, назовём срок и стоимость до начала работ. Работаем по всей России, офисы в Москве, Туле и Рязани.
