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

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

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

Числовые форматы в openpyxl: рубли, проценты и разделители

Строка number_format превращает голое 1234.5 в «1 234.50 ₽»: задаём рубли, проценты и разделители тысяч — и разбираемся, почему значение ячейки при этом не меняется.

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

Прайс собран, шапка отформатирована, ширины выставлены — но колонка «Цена» выглядит бедно: голые 1234.5 и 250. Человек же ждёт 1 234.50 ₽. За показ числа отвечает не значение, а числовой формат — маска-строка, которую Excel применяет при отрисовке ячейки. В openpyxl за неё отвечает атрибут number_format, и в этом уроке мы научимся писать такие маски: рубли, проценты, штучные счётчики и разделители тысяч.

Все примеры — живые: openpyxl — чистый Python, и песочница ставит его автоматически при импорте. Жми «Запустить» и смотри на настоящие значения и форматы.

Что такое number_format: маска поверх значения

У каждой ячейки есть значение — то, что лежит в файле, — и формат — то, как это значение показывают. Формат это обычная строка с кодами: #,##0.00 значит «разделяй тысячи пробелами и показывай два знака после точки». По умолчанию у ячейки стоит формат General — «показывай как есть».

первая маска: рубли с копейками
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws["A1"] = "Капучино"
ws["B1"] = 250                        # число хранится как число

print("Значение:", ws["B1"].value)
print("Формат до:", ws["B1"].number_format)      # General

ws["B1"].number_format = "#,##0.00 ₽"            # маска: рубли с копейками

print("Формат после:", ws["B1"].number_format)
print("Значение не изменилось:", ws["B1"].value)
Вывод
Значение: 250
Формат до: General
Формат после: #,##0.00 ₽
Значение не изменилось: 250

Разбираем маску по кусочкам. # — цифра, если она есть; , — разделитель групп разрядов; 0.00 — обязательная цифра и два знака после точки, хоть значение целое; ₽ — литерал, который просто дописывается к числу. Итог для 250: 250,00 ₽. А для 1234.5 маска даст 1 234.50 ₽ — именно то, чего мы хотели от прайса.

Почему нельзя просто записать в ячейку строку "250,00 ₽" и не возиться с форматами? Строка обманет глаз ровно до первого вычисления: SUM по столбцу с текстом посчитает ноль, сортировка выстроит цены как слова, а фильтры предложат список из сотни уникальных строк. Число под маской — это единственный вариант, при котором прайс и красиво выглядит, и остаётся рабочей таблицей.

  • # — необязательная цифра: незначащие позиции в начале не рисуются;
  • 0 — обязательная цифра: недостающие знаки дополняются нулями (0.00 для 5 даст 5.00);
  • , — делит целую часть на группы разрядов;
  • . — отделяет дробную часть; сколько нулей после неё — столько знаков видно;
  • любой другой символ — литерал: ₽, шт, чел; процент % особый — он ещё и умножает показ на 100.
МаскаЧто в ячейкеЧто видит человек в Excel
General1234.51234.5
#,##0.00 ₽1234.51 234.50 ₽
0%0.1515%
0.0%0.156715.7%
#,##012345671 234 567
#,##0 шт120120 шт

Формат ≠ значение: 0.15 выглядит как 15%

Самая коварная маска — процентная. Excel не хранит «пятнадцать процентов» как число 15: он хранит долю от единицы. Хочешь показать 15% — запиши 0.15 и поставь маску 0%. Формат не меняет значение ячейки: в ячейке по-прежнему 0.15, а человек видит 15%.

процент - это доля
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws["A1"] = "Скидка"
ws["B1"] = 0.15                     # доля: пятнадцать сотых
ws["B1"].number_format = "0%"

print("Значение:", ws["B1"].value)
print("Маска:", ws["B1"].number_format)
# в Excel человек увидит: 15%
Вывод
Значение: 0.15
Маска: 0%

Почему это удобно: формулы и сортировки работают с настоящим значением. Скидка 15% от цены считается как цена * 0.15 — и маска 0% тут ни при чём, она только про картинку. Запишешь вместо доли число 15 — получишь показательные 1500%.

маска против round
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws["A1"] = 0.1567
ws["A1"].number_format = "0%"     # в Excel видно 16%

print("Значение под маской:", ws["A1"].value)   # не округлилось!

ws["A2"] = round(0.1567, 2)       # round() меняет само значение
print("После round:", ws["A2"].value)
Вывод
Значение под маской: 0.1567
После round: 0.16

Разделитель тысяч и литералы в маске

На больших числах маска #,##0 окупается мгновенно: 42517 чашек за месяц глазами читаются как 42 517, а не как кашу из цифр. А литерал в конце — любой текст, который надо приставить к числу: ₽, шт, чел.

большие числа прайса
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

ws["A1"] = "Выручка за месяц"
ws["B1"] = 1234567.891
ws["B1"].number_format = "#,##0.00 ₽"

ws["A2"] = "Чашек продано"
ws["B2"] = 42517
ws["B2"].number_format = "#,##0"

print("B1:", ws["B1"].value, "->", ws["B1"].number_format)
print("B2:", ws["B2"].value, "->", ws["B2"].number_format)
# в Excel: 1 234 567.89 ₽ и 42 517
Вывод
B1: 1234567.891 -> #,##0.00 ₽
B2: 42517 -> #,##0

У маски есть и вторая строка — для отрицательных чисел: секции разделяются точкой с запятой, и каждая описывает свой случай. Запись "#,##0.00 ₽;-#,##0.00 ₽" показывает долги и возвраты со знаком минус, а "#,##0.00 ₽;[Red]-#,##0.00 ₽" — ещё и красным цветом. openpyxl не разбирает маску по секциям — она сохраняет строку как есть, и Excel сам применит нужную секцию к каждому значению. Для убытков в отчёте это бесплатная читаемость.

Проценты в прайсе живут в связке с числами: скидка 0.15 лежит в своей ячейке под маской 0%, а цена со скидкой считается формулой =B2*(1-C2) — Excel перемножает настоящие доли, не глядя на то, что человек видит проценты. Ещё одна причина держать в ячейках значения, а не оформленный текст.

Как применить формат к целому столбцу прайса?

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

прайс с тремя форматами разом
from openpyxl import Workbook

wb = Workbook()
ws = wb.active

data = [
    ["Товар", "Цена", "Скидка", "Остаток"],
    ["Капучино", 250, 0.1, 120],
    ["Латте", 280, 0.15, 80],
]
for row in data:
    ws.append(row)

for row in ws.iter_rows(min_row=2):
    row[1].number_format = "#,##0.00 ₽"   # Цена
    row[2].number_format = "0%"           # Скидка
    row[3].number_format = "#,##0 шт"     # Остаток

for row in ws.iter_rows(min_row=2):
    for cell in row[1:]:
        print(cell.coordinate, "|", cell.value, "|", cell.number_format)
Вывод
B2 | 250 | #,##0.00 ₽
C2 | 0.1 | 0%
D2 | 120 | #,##0 шт
B3 | 280 | #,##0.00 ₽
C3 | 0.15 | 0%
D3 | 80 | #,##0 шт

Вот он, живой прогон «формат против значения»: в строке C2 значение 0.1, а увидит человек 10%. Печать показывает сырое содержимое файла — Excel покажет то же самое, но уже наряженное в маски. Обрати внимание на индексацию в row[1], row[2], row[3]: iter_rows отдаёт кортеж ячеек строки, и счёт в нём начинается с нуля, от колонки A. И маски едут в файл вместе с книгой:

формат переживает сохранение
from openpyxl import Workbook, load_workbook
import os

NAME = "price_format.xlsx"

wb = Workbook()
ws = wb.active
ws.append(["Товар", "Цена"])
ws.append(["Эспрессо", 120])
ws["B2"].number_format = "#,##0.00 ₽"

wb.save(NAME)
print("Файл создан:", os.path.exists(NAME))

wb2 = load_workbook(NAME)
print("Формат из файла:", wb2.active["B2"].number_format)
print("Значение из файла:", wb2.active["B2"].value)

os.remove(NAME)
print("Файл удалён:", os.path.exists(NAME))
Вывод
Файл создан: True
Формат из файла: #,##0.00 ₽
Значение из файла: 120
Файл удалён: False

Маска хранится в самом xlsx — это не property Python-объекта, а часть файла. Поэтому файл с форматами можно отправить кому угодно: Excel, Google Sheets и LibreOffice покажут 120,00 ₽ без единой строки кода с их стороны.

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

Собираем прайс с форматами целиком

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

прайс: шапка шрифтом, цены - маской
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font
import os

NAME = "pricelist_fmt.xlsx"

wb = Workbook()
ws = wb.active
ws.append(["Позиция", "Цена, шт"])
ws["A1"].font = Font(bold=True)
ws["B1"].font = Font(bold=True)

price = [("Эспрессо", 120), ("Капучино", 250), ("Латте", 280),
         ("Раф", 320), ("Какао", 260)]
for item, cost in price:
    ws.append([item, cost])

for row in ws.iter_rows(min_row=2, min_col=2, max_col=2):
    for cell in row:
        cell.number_format = "#,##0.00 ₽"

wb.save(NAME)
print("Прайс сохранён:", os.path.exists(NAME))

wb2 = load_workbook(NAME)
print("Формат B6:", wb2.active["B6"].number_format)
print("Жирная шапка:", wb2.active["A1"].font.bold)
os.remove(NAME)
Вывод
Прайс сохранён: True
Формат B6: #,##0.00 ₽
Жирная шапка: True
одна ячейка глазами Python и глазами человека
ws["B2"] = 250
ws["B2"].number_format = "#,##0.00 ₽"

print(ws["B2"].value)
# то же самое в Excel:  250,00 ₽
Блок иллюстрирует главную мысль урока: Python и openpyxl видят сырое значение, человек в Excel — значение через маску. Файл, который ты скачал из песочницы в предыдущих блоках, покажет то же самое.

Что дальше

Ты умеешь наряжать числа: рубли с копейками, проценты из долей, тысячи с пробелами. Следующий логичный гость прайса — даты: «действует с … по …» и сроки годности зёрен. В следующем уроке запишем в ячейку настоящий datetime, поставим маску DD.MM.YYYY и посчитаем, сколько дней между двумя датами. А если захочешь обрабатывать столбцы цен массово — загляни в groupby из Pandas: после Excel его сводные кажутся волшебством.

Числовой формат — это костюм на числе: значение под ним не меняется, меняется то, что видит человек.

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

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

from openpyxl import Workbook

wb = Workbook()
ws = wb.active
ws["A1"] = 0.25
ws["A1"].number_format = "0%"
print(ws["A1"].value)
print(ws["A1"].number_format)
from openpyxl import Workbook

wb = Workbook()
ws = wb.active
ws["A1"] = 1234.5
print(ws["A1"].value)
ws["A1"].number_format = "#,##0.00 ₽"
print(ws["A1"].value)
print(ws["A1"].number_format)
from openpyxl import Workbook

wb = Workbook()
ws = wb.active
ws.append(["Товар", "Цена"])
ws.append(["Чай", 90])
ws["B2"].number_format = "#,##0 шт"
print(ws["A2"].value, ws["B2"].value, ws["B2"].number_format)
Проверь себя
0 / 5

1. В ячейку записали 250 и поставили number_format "#,##0.00 ₽". Что лежит в ячейке?

2. Как записать в ячейку скидку 15%, чтобы Excel показал «15%»?

3. Что означает формат General?

4. Что сделает маска "#,##0" со значением 1234567?

5. Значение ячейки 0.1567, маска "0%" — в Excel видно «16%». Что вернёт cell.value?

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

В редакторе собран прайс из одной строки: Капучино, цена 250, скидка 0.15, остаток 120. Задай ячейкам B2, C2 и D2 числовые форматы: цене — «#,##0.00 ₽», скидке — «0%», остатку — «#,##0 шт». Затем выведи три строки: значение и маску каждой ячейки через разделитель « | ».

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

Как записать формат рублей в openpyxl?

Присвой ячейке числовой формат-строку: cell.number_format = "#,##0.00 ₽" — разделитель групп разрядов, два знака после точки и литерал рубля. В ячейку при этом записывают обычное число: 1234.5. Один и тот же формат подходит и для цен, и для выручек — сумма разрядов маске безразлична.

Почему в ячейке 0.15, а Excel показывает 15%?

Процентный формат Excel интерпретирует значение как долю от единицы: 0.15 — это 15 сотых, то есть 15%. Меняется только отображение: формулы и сортировки работают с 0.15. Записывать «15» не нужно — под маской «0%» оно показалось бы как 1500%.

Чем number_format отличается от round()?

round() меняет само значение ячейки: 0.1567 превращается в 0.16, и все расчёты дальше идут по 0.16. number_format меняет только количество отображаемых знаков: значение остаётся 0.1567, а видно 16%. Для цен и выручек формат обычно безопаснее — он не искажает данные.

Как узнать числовой формат ячейки готового файла?

Открой файл через load_workbook и прочитай cell.number_format — вернётся строка-маска, например «#,##0.00 ₽». В самом Excel тот же код виден через Ctrl+1 → «Все форматы». Удобный приём: подсмотреть формат в файле-эталоне и переиспользовать его в своём скрипте один в один.

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

«Формат не меняет значение ячейки: в ячейке по-прежнему 0.15, а человек видит 15%.»

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

TelegramVK

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

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