Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 6 из 20 Средний 40 мин 130 XP

Выравнивание и объединение ячеек в openpyxl: merge_cells

Заголовок на всю ширину таблицы по центру: merge_cells объединяет ячейки, Alignment выравнивает и переносит текст — и не наступаем на MergedCell.

Редакция Питоники

Шапка из пятого урока уже синяя и жирная, но название «Прайс-лист» всё ещё сидит в одной ячейке A1, а длинные описания товаров вылезают за границы столбца. В Excel эта проблема решается двумя кнопками: «Объединить и поместить в центре» и «Переносить текст». У openpyxl те же возможности — стиль Alignment и метод merge_cells. Обе работать умеют, но у объединения есть характер: три ловушки, на которых спотыкаются даже опытные.

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

Выравнивание: Alignment

Выравнивание — такой же стиль, как Font из прошлого урока: объект Alignment присваивается атрибуту alignment. Два главных параметра: horizontal — по горизонтали со значениями left, center, right и vertical — по вертикали: top, center, bottom.

по центру ячейки
from openpyxl import Workbook
from openpyxl.styles import Alignment

wb = Workbook()
ws = wb.active

ws["A1"] = "Прайс-лист"
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")

print("По горизонтали:", ws["A1"].alignment.horizontal)
print("Вертикально:", ws["A1"].alignment.vertical)
Вывод
По горизонтали: center
Вертикально: center

Выравнивание никак не влияет на значение — только на отрисовку. vertical="center" особенно заметен на высоких строках: без него текст прилипает к нижнему краю, с ним — сидит по середине. Третий рабочий параметр — wrap_text, он заслуживает отдельного примера.

ПараметрЗначенияЧто делает
horizontalleft / center / rightвыравнивание по горизонтали
verticaltop / center / bottomвыравнивание по вертикали
wrap_textTrue / Falseперенос текста по словам

Перенос текста: wrap_text

Длинный текст в узком столбце без переноса либо обрезается, либо наезжает на соседние ячейки — смотря что правее. wrap_text=True включает перенос по словам: текст остаётся в своей ячейке, а строка визуально растёт в высоту.

Пара слов о том, где перенос нужен, а где вредит. Колонки с описаниями, комментариями и адресами просят wrap_text как милости: без него таблица разваливается. Числам и датам перенос не нужен никогда — они короткие, а выровненный вправо денежный столбец с переносом выглядит как ошибка вёрстки. Правило одного взгляда: текстовые колонки длиннее двадцати символов — кандидаты на перенос, остальным достаточно ширины, о которой поговорим в седьмом уроке.

Тонкость, которую замечают не сразу: перенос происходит на границе слов, и Excel подбирает высоту строки сам — если ты не задал её руками. Вредный совет наоборот: задать высоту вручную «с запасом» и забыть про неё при изменении текста. Если высотой не управляешь сознательно, оставляй её на откуп Excel: wrap_text работает и без настройки высоты.

перенос по словам
from openpyxl import Workbook
from openpyxl.styles import Alignment

wb = Workbook()
ws = wb.active

ws["A2"] = "Длинное описание товара, которое не влезает в узкий столбец"
ws["A2"].alignment = Alignment(wrap_text=True, vertical="top")

print("Перенос включён:", ws["A2"].alignment.wrap_text)
Вывод
Перенос включён: True

Перенос и выравнивание часто идут парой: описания товаров — wrap_text=True с прижимом к верху vertical="top", шапки и итоги — по центру. Параметры не исключают, а дополняют друг друга — их можно задать все разом в одном объекте Alignment. Вот самая ходовая комбинация — «по центру и с переносом».

центр плюс перенос
from openpyxl import Workbook
from openpyxl.styles import Alignment

wb = Workbook()
ws = wb.active

ws["B2"] = "Комментарий клиента"
ws["B2"].alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)

print(ws["B2"].alignment.horizontal, ws["B2"].alignment.wrap_text)
Вывод
center True

Осталось научиться растягивать заголовок на всю таблицу.

Объединение ячеек: merge_cells

Метод ws.merge_cells("A1:D1") склеивает диапазон в одно поле: четыре ячейки первой строки от A до D становятся единым заголовком. Значение после объединения живёт только в верхней левой ячейке — в A1.

заголовок на всю ширину
from openpyxl import Workbook
from openpyxl.styles import Alignment

wb = Workbook()
ws = wb.active

ws.merge_cells("A1:D1")            # объединили четыре ячейки
ws["A1"] = "Отчёт за март"         # значение - только в верхней левой
ws["A1"].alignment = Alignment(horizontal="center")

print("A1:", ws["A1"].value)
print("C1:", ws["C1"].value)       # "скрытая" ячейка - None
Вывод
A1: Отчёт за март
C1: None

Кроме строки-диапазона merge_cells понимает и числа: ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=4) — то же объединение A1:D1, но с координатами, которые удобно считать в цикле. Числовая форма незаменима, когда размер заголовка зависит от данных: три колонки сегодня, семь завтра. Список всех объединений листа живёт в ws.merged_cells.ranges — это множество, и по его длине удобно проверять, сколько полей уже склеено.

объединение по координатам
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws.merge_cells(start_row=1, start_column=1, end_row=2, end_column=3)
ws["A1"] = "Шапка на два ряда"

print(ws["A1"].value)
print(len(ws.merged_cells.ranges))
Вывод
Шапка на два ряда
1

Получился прямоугольник два на три: A1:C2, шесть ячеек в одном поле. Многорядные объединения — это подзаголовки над парами колонок, общие названия для групп столбцов «Цены» и «Остатки». Правило живой ячейки прежнее: значение — в верхней левой, здесь тоже в A1.

сколько объединений на листе
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws.merge_cells("A1:D1")
ws.merge_cells("A3:B3")

print(len(ws.merged_cells.ranges))   # два объединения на листе
Вывод
2
MergedCell не принимает значения
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws.merge_cells("A1:B1")

try:
    ws["B1"] = "сюда нельзя"       # B1 - MergedCell
    print("записалось")
except AttributeError:
    print("AttributeError: ячейка B1 внутри объединения только для чтения")
Вывод
AttributeError: ячейка B1 внутри объединения только для чтения
данные под объединением пропали
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws["A1"] = "Заголовок"
ws["B1"] = "мусор"
ws.merge_cells("A1:B1")            # openpyxl чистит скрытые ячейки

print("B1 после объединения:", ws["B1"].value)
Вывод
B1 после объединения: None

Заголовок на всю ширину — лишь половина пользы объединения. Вторая половина — подзаголовки над группами колонок: над «Цена» и «Скидка» клеится общий «Деньги», над «Остаток» и «Резерв» — «Склад». Такая двухэтажная шапка читается замечательно, но именно она чаще всего ломает автоматическую обработку, поэтому держи её подальше от строк данных — а лучше дублируй смысл плоскими именами колонок вроде «Цена, руб.».

Разъединение: unmerge_cells

Обратная операция — ws.unmerge_cells с тем же диапазоном. Ячейки становятся обычными, значение остаётся в верхней левой, а все прочие снова принимают значения.

вернули как было
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws.merge_cells("A1:D1")
ws["A1"] = "Шапка"
ws.unmerge_cells("A1:D1")          # вернули как было

ws["C1"] = "теперь можно писать"   # ячейки снова отдельные
print("C1:", ws["C1"].value)
print("A1:", ws["A1"].value)
Вывод
C1: теперь можно писать
A1: Шапка

unmerge тоже требует точный диапазон: разъединять нужно ровно то, что объединяли — «A1:D1», а не «A1:C1». Скриптам, которые правят чужие файлы, диапазон удобно взять из того же ws.merged_cells.ranges, а не угадывать.

  • объединить можно только смежный прямоугольник: два куска через дырку — нельзя, Excel это тоже не умеет;
  • сортировка и фильтры по листу с объединениями в теле данных ломаются — держи склейки в шапке;
  • wrap_text внутри объединённой ячейки работает, но высоту строки Excel автоматически не подбирает — задавай height сам;
  • копирование объединённой ячейки переносит и объединение — иногда против твоего желания.

Последний пункт списка — частый источник сюрпризов в скриптах, которые «дочищают» чужие файлы: скопировал строку вместе с объединённой шапкой — получил на листе ещё одно объединение, о котором не просил. Перед массовыми правками листа полезно печатать ws.merged_cells.ranges и смотреть, с чем имеешь дело: неожиданное множество объединений объясняет странности лучше долгой отладки.

Практическое правило про меру: объединения — для шапок и подзаголовков, а не для данных. Таблица, в которой склеены ячейки в теле, становится непригодной для сортировки, фильтров и чтения pandas: объединённая область ломает прямоугольную структуру, на которой держится всё остальное. Одна склейка на всю ширину сверху, максимум пара подзаголовков — дальше оставь ячейки прямыми.

И ещё одна причина не склеивать данные: файлы живут дольше своих генераторов. Через полгода коллега захочет отсортировать твой прайс или прогнать его через pandas — и объединённые ячейки в теле превратятся в его проблему. Шапка склеена и красива, данные плоские и машинно-читаемые: так файл работает и для человека, и для кода.

Первая программа: шапка прайса с заголовком

Теперь финальная картинка: заголовок на всю ширину таблицы по центру, под ним — шапка и данные. Всё вместе: merge, Alignment, стили из прошлого урока.

Обрати внимание на порядок операций в этой программе: сначала объединение, потом оформление, потом данные. Так безопаснее всего: живая ячейка A1 существует с самого начала, и ни один append не рискует наступить на объединённый диапазон. Приучи себя читать подобные скрипты сверху вниз — порядок строк здесь и есть порядок работы.

прайс с большим заголовком
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
import os

wb = Workbook()
ws = wb.active

ws.merge_cells("A1:D1")
top = ws["A1"]                     # у объединения - одна живая ячейка
top.value = "Прайс-лист: компьютерный отдел"
top.font = Font(bold=True, size=14, color="FFFFFF")
top.fill = PatternFill(start_color="1F4E79", end_color="1F4E79", fill_type="solid")
top.alignment = Alignment(horizontal="center", vertical="center")

ws.append(["Товар", "Цена", "Штук"])
ws.append(["Монитор", 9500, 7])

wb.save("price-l6.xlsx")

print("A1:", ws["A1"].value)
print("Строка данных:", [c.value for c in ws[2]])

os.remove("price-l6.xlsx")
Вывод
A1: Прайс-лист: компьютерный отдел
Строка данных: ['Товар', 'Цена', 'Штук', None]

Заголовок растянулся на четыре колонки и центрируется по ним — ровно то, что в Excel зовётся «Объединить и поместить в центре». В списке данных четвёртым элементом пришёл None: строка с товарами лежит под объединённой ячейкой, и её четвёртый столбец пуст. Стили применились к top — верхней левой ячейке, этого достаточно: заливка и шрифт объединённого поля берутся из неё.

Наиболее частый вопрос после этого примера: почему данные записались во вторую строку, а не в первую? Ответ: метод append пишет в очередную свободную строку, а объединённый диапазон увеличил счётчик занятых строк до единицы. Сначала объединение — потом append — данные лягут со второй строки. Сначала append — потом объединение первой строки — append затрёт шапку. Порядок операций в openpyxl виден только в коде, и он же — документация твоего замысла.

Что дальше

Заголовок по центру, перенос длинных описаний, знание трёх ловушек MergedCell — таблица выглядит по-взрослому. Осталась последняя деталь внешнего вида: длинные названия по-прежнему обрезаются узкими столбцами. Урок 7 настраивает ширины столбцов и высоты строк — включая автоматический подбор по самой длинной строке, и внешний вид прайса закрыт полностью.

Объединённый диапазон — одно поле с одной живой ячейкой: значение в верхней левой, всё остальное — MergedCell без значений.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws.merge_cells("A1:C1")
ws["A1"] = "Отчёт"

print(ws["A1"].value, "|", ws["B1"].value)
from openpyxl import Workbook

wb = Workbook()
ws = wb.active
ws.merge_cells("A1:B1")

try:
    ws["B1"] = "тест"
    print("записалось")
except AttributeError:
    print("AttributeError")
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws["A1"] = "Шапка"
ws.merge_cells("A1:B1")
ws.unmerge_cells("A1:B1")

print(ws["A1"].value, ws["B1"].value)
Проверь себя
0 / 5

1. Где хранится значение объединённого диапазона A1:D1?

2. Что произойдёт при ws["C1"] = "текст", если A1:C1 объединены?

3. Что делает merge_cells с уже существующими значениями в скрытых ячейках?

4. Как включить перенос длинного текста в ячейке?

5. Как разъединить диапазон A1:D1?

Карточки терминов
Запомнено: 0 / 6
Практика

Собери шапку отчёта: объедини A1:C2 (две строки на три столбца), запиши в верхнюю левую ячейку текст «Продажи за квартал», выровняй по центру и по вертикали, включи жирный шрифт. Проверь: выведи значение A1 и длину списка объединений ws.merged_cells.ranges, затем разъедини диапазон и запиши в C1 слово «Готово». Сохрани и удали файл head-ex-l6.xlsx.

practice.py
Вопросы и ответы по уроку

Как объединить ячейки в Excel через openpyxl?

Методом листа: ws.merge_cells("A1:D1") — строкой-диапазоном или ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=4) — числами. Значение объединённого поля хранится в верхней левой ячейке, поэтому писать нужно в неё. Разъединение — ws.unmerge_cells с тем же диапазоном.

Почему openpyxl не даёт записать в ячейку объединённого диапазона?

Все ячейки диапазона, кроме верхней левой, становятся объектами MergedCell: значения у них нет, оно только для чтения. Присваивание ws["B1"] = ... вызывает AttributeError. Решение: писать в верхнюю левую ячейку, предварительно разъединять диапазон или проверять по ws.merged_cells.ranges, что ячейка не внутри объединения.

Как выровнять текст по центру ячейки в openpyxl?

Стилем Alignment: cell.alignment = Alignment(horizontal="center", vertical="center"). Горизонталь принимает left, center и right, вертикаль — top, center и bottom. Для длинного текста добавь wrap_text=True — включится перенос по словам.

Пропадают ли данные при объединении ячеек в openpyxl?

Да: merge_cells удаляет значения всех скрытых ячеек диапазона без предупреждения — остаётся только значение верхней левой. Перед объединением проверь, что под диапазоном нет данных, либо вынеси их заранее. Обратное разъединение unmerge_cells вернёт ячейкам возможность хранить значения, но не вернёт стёртые.

Понравился урок? Сошлитесь на него

«Значение объединённой ячейки живёт только в верхней левой: остальные четыре — это MergedCell без собственных значений, писать в них нельзя.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.