Двоичные данные: байты, картинки, чтение заголовка файла

Вспомните: в прошлой теме вы читали таблицу расходов из CSV и сохраняли настройки в JSON. Это текст: его можно открыть Блокнотом и прочитать глазами. Фото из «Загрузок», PDF со сканом договора, архив — не текст. Если открыть такой файл в Блокноте, вы увидите кашу из символов. Внутри там байты, и сегодня мы научимся с ними работать.

🎯 После урока сможете: открыть любой файл в двоичном режиме, перевести строку в байты и обратно, посчитать, сколько места строка займёт в UTF-8, узнать тип файла по первым байтам (даже если расширение потеряно или врёт) и скопировать большой файл кусками.

Байт и тип bytes

Байт — это число от 0 до 255. Любой файл на диске — просто длинная цепочка таких чисел. Что они значат (буквы, пиксели, звук), решает программа, которая файл открывает.

В Python для цепочки байтов есть тип bytes. Записывается как строка с буквой b впереди. Байты, которые не похожи на буквы латиницы, Python показывает в виде \x и двух шестнадцатеричных цифр.

data = b"PNG"
print(data)        # b'PNG'
print(data[0])     # 80 — код буквы P
print(len(data))   # 3

raw = bytes([72, 105, 255])
print(raw)         # b'Hi\xff'

Обратите внимание: элемент bytes по индексу — это число, а не буква. Срез data[1:3] вернёт снова bytes: b'NG'. Изменить байт в bytes нельзя, как и символ в строке. Если нужен изменяемый вариант, есть bytearray, но в этой теме он не понадобится.

Откуда там буквы? Обычные числа записывают десятью цифрами 0–9. В шестнадцатеричной записи (hex) цифр шестнадцать: после 9 идут буквы a=10, b=11, c=12, d=13, e=14, f=15. Разряды считаются так же, как в десятичной, только «десяток» здесь равен 16: 0a = 10, 0f = 15, 10 = 16, 50 = 5·16 = 80, ff = 15·16 + 15 = 255. Поэтому любой байт (0–255) — ровно две hex-цифры, от 00 до ff. В коде такое число пишут с приставкой 0x: 0x50 — это 80.

Переводить руками не обязательно, Python сделает сам. ord даёт код символа — номер буквы в таблице ASCII (латиница, цифры, знаки), hex и int(..., 16) переводят туда и обратно:

print(ord("P"))       # 80 — код буквы P
print(hex(80))         # 0x50
print(int("50", 16))   # 80
print(0xff)            # 255

Метод .hex(" ") печатает байты сразу в hex, по две цифры через пробел:

head = b"\x89PNG\r\n\x1a\n"
print(head.hex(" "))   # 89 50 4e 47 0d 0a 1a 0a
Первые 8 байт файла cat.png в виде таблицы. Позиции от 0 до 7. Байты в hex: 89 50 4e 47 0d 0a 1a 0a, первые четыре выделены. Те же байты числами: 137, 80, 78, 71, 13, 10, 26, 10. Символы: ?, P, N, G, \r, \n, ?, \n. Ниже: data[0] даёт 137, data[1:4] даёт b'PNG'. Подпись: эти 8 байт одинаковы у любого PNG.

Проверьте себя

Контрольный вопрос. Что выведет код? Введите число.

Исходный код для этого задания:

data = b"PNG"
print(data[0])

Подсказка: Элемент bytes по индексу — число: код символа по таблице ASCII.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет код? Выберите вариант.

Исходный код для этого задания:

head = b"\x89PNG"
print(head[1:4])
Аb’PNG’
Бb’\x89PN’
ВPNG
Г[80, 78, 71]

Подсказка: Срез bytes возвращает bytes, а не строку и не список. Позиции считаются с нуля.

Онлайн-проверка ответа появится позже

Режимы 'rb' и 'wb'

Чтобы получить байты файла, откройте его с буквой b в режиме: 'rb' — читать, 'wb' — записать (старое содержимое стирается). Кодировку encoding в двоичном режиме не указывают: Python ничего не переводит в текст, отдаёт байты как есть.

from pathlib import Path

photo = Path.home() / "Downloads" / "cat.png"

if photo.exists():
    with open(photo, "rb") as f:
        data = f.read()
    print(type(data))   # <class 'bytes'>
    print(len(data))    # размер файла в байтах

f.read() без числа читает весь файл. f.read(8) — только 8 байт от текущей позиции. Для чтения заголовка этого хватает, весь файл грузить в память не нужно.

В двоичный файл записывают только bytes. Строку туда передать нельзя — Python выдаст ошибку TypeError. Строку сначала переводят в байты.

with open("hello.bin", "wb") as f:
    f.write(b"Hi")          # можно
    # f.write("Привет")    # TypeError: нужен bytes, а не str

Проверьте себя

Контрольный вопрос. Вы хотите прочитать байты фотографии cat.jpg. Какая строка открывает файл правильно?

Аopen(«cat.jpg», «r», encoding=»utf-8″)
Бopen(«cat.jpg», «rb»)
Вopen(«cat.jpg», «wb»)
Гopen(«cat.jpg», «r»)

Подсказка: Для двоичного файла в режиме нужна буква b, а кодировку не указывают.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Программа падает с TypeError. Какой метод нужно вызвать у переменной text, чтобы запись сработала? Введите только имя метода, без точки и скобок.

Исходный код для этого задания:

text = "Привет"
with open("hi.bin", "wb") as f:
    f.write(text)

Подсказка: В файл, открытый в ‘wb’, пишут только bytes. Какой метод превращает строку в байты?

Онлайн-проверка ответа появится позже

Строка в байты и обратно: encode и decode

str.encode() превращает текст в байты, bytes.decode() — байты в текст. По умолчанию используется UTF-8, но лучше писать кодировку явно, как вы делали с open(..., encoding="utf-8").

text = "Я"
b = text.encode("utf-8")
print(b)              # b'\xd0\xaf'
print(list(b))        # [208, 175]
print(b.decode("utf-8"))   # Я

В UTF-8 разные символы занимают разное число байт. Латиница и цифры — 1 байт, кириллица — 2, знак евро — 3, эмодзи — 4. Поэтому len(строки) и размер той же строки в файле часто не совпадают.

Сколько байт занимает символ в UTF-8. Буква 'A' — один байт 41 (латиница и цифры). Буква 'Я' — два байта d0 af (кириллица). Знак евро — три байта e2 82 ac. Эмодзи-смайлик — четыре байта f0 9f 98 80. Внизу: len("Привет") равно 6 символам, а len("Привет".encode("utf-8")) равно 12 байтам.
for s in ["cat", "кот", "Hi😀"]:
    print(s, len(s), len(s.encode("utf-8")))

# cat 3 3
# кот 3 6
# Hi😀 3 6

Если декодировать байты не той кодировкой, получите «кракозябры» или ошибку UnicodeDecodeError. Так бывает с CSV, сохранённым из старого Excel в cp1251. Байты при этом целы — поменяйте кодировку в decode или в open.

Проверьте себя

Контрольный вопрос. Что выведет код? Введите результат ровно как на экране.

Исходный код для этого задания:

print(bytes([72, 105]).decode("utf-8"))

Подсказка: decode переводит коды обратно в символы. Код символа можно узнать у себя через ord(), а символ по коду — через chr().

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет код? Введите число.

Исходный код для этого задания:

print(len("cat1".encode()))

Подсказка: Сколько байт в UTF-8 занимает латинская буква и цифра? Посчитайте символы строки.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Сколько байт займёт строка в UTF-8? Введите число.

Исходный код для этого задания:

print(len("кот".encode("utf-8")))

Подсказка: Сколько байт в UTF-8 занимает одна русская буква? Посмотрите картинку «Сколько байт» в уроке и умножьте на число букв.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Сколько байт займёт знак евро в UTF-8? Введите число.

Исходный код для этого задания:

print(len("€".encode("utf-8")))

Подсказка: Сверьтесь с картинкой «Сколько байт в UTF-8» в уроке или проверьте у себя через len(…encode()).

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет код? Введите число.

Исходный код для этого задания:

print(len("Ок😀".encode("utf-8")))

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

Онлайн-проверка ответа появится позже

Задание. Названия трёх папок: «Ёж», «tea», «чай». Программа сохраняет их в файл через encode(«utf-8») подряд, без разделителей. Сколько байт будет в файле? Введите число.

Исходный код для этого задания:

words = ["Ёж", "tea", "чай"]
with open("names.bin", "wb") as f:
    for w in words:
        f.write(w.encode("utf-8"))

Подсказка: Посчитайте байты каждого слова: кириллица по 2, латиница по 1. Буква Ё — тоже кириллица.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

Онлайн-проверка ответа появится позже

Тип файла по первым байтам

Расширение — просто часть имени, его легко потерять или поменять. А вот начало файла у многих форматов фиксированное. Это называют сигнатурой (или «магическими байтами»). Прочитайте первые байты и сравните методом startswith.

  • PNG: 89 50 4e 47 — то есть b"\x89PNG"
  • JPEG: ff d8 ff — b"\xff\xd8\xff"
  • PDF: 25 50 44 46 — b"%PDF"
SIGNATURES = {
    b"\x89PNG": "png",
    b"\xff\xd8\xff": "jpg",
    b"%PDF": "pdf",
}


def file_kind(path):
    """Вернуть тип файла по первым байтам."""
    with open(path, "rb") as f:
        head = f.read(8)
    for sign, kind in SIGNATURES.items():
        if head.startswith(sign):
            return kind
    return "unknown"


print(file_kind("cat.png"))   # png

Заголовок картинки хранит не только сигнатуру. У PNG сразу за ней лежат размеры: байты 16–19 — ширина, 20–23 — высота. Каждое число записано четырьмя байтами, старший первым. Собрать из них обычное число помогает int.from_bytes(..., "big") ("big" — старший байт идёт первым):

from pathlib import Path

photo = Path.home() / "Downloads" / "cat.png"
if photo.exists():
    with open(photo, "rb") as f:
        head = f.read(24)
    if head.startswith(b"\x89PNG"):
        width = int.from_bytes(head[16:20], "big")
        height = int.from_bytes(head[20:24], "big")
        print(width, "x", height)   # например: 1920 x 1080
Первые 8 байт трёх файлов в hex. cat.png: 89 50 4e 47 0d 0a 1a 0a, выделены первые четыре байта, итог png. IMG_07: ff d8 ff e1 00 18 45 78, выделены первые три, итог jpg. dogovor: 25 50 44 46 2d 31 2e 37 (это текст %PDF-1.7), выделены первые четыре, итог pdf. Выделенные байты — сигнатура, функция смотрит только на них. Внизу словарь: b"\x89PNG" — png, b"\xff\xd8\xff" — jpg, b"%PDF" — pdf.
Окно терминала с Python. Введён цикл по трём файлам cat.png, IMG_07 и dogovor: каждый открывается в режиме rb, читаются первые 8 байт, печатаются имя, байты в hex и результат file_kind. Вывод: cat.png 89 50 4e 47 0d 0a 1a 0a -> png; IMG_07 ff d8 ff e1 00 18 45 78 -> jpg; dogovor 25 50 44 46 2d 31 2e 37 -> pdf.

Проверьте себя

Контрольный вопрос. Первые байты файла без расширения: ff d8 ff e0. Что это за файл?

АPNG-картинка
БJPEG-картинка
ВPDF-документ
ГТекстовый файл

Подсказка: Сравните с сигнатурами из урока: PNG — 89 50 4e 47, JPEG — ff d8 ff, PDF — 25 50 44 46.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что подставить вместо ___, чтобы функция узнавала PDF? Выберите вариант.

Исходный код для этого задания:

def is_pdf(path):
    with open(path, "rb") as f:
        head = f.read(4)
    return head == ___
Аb»%PDF»
Б«%PDF»
Вb»PDF»
Г%PDF

Подсказка: f.read в режиме ‘rb’ возвращает bytes. С чем bytes может оказаться равным? Сверьте сигнатуру PDF со списком в уроке.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет код? Введите результат ровно как на экране.

Исходный код для этого задания:

data = b"PK\x03\x04\x14\x00"
print(data[:3].hex(" "))

Подсказка: Срез [:3] — первые три байта, .hex(» «) печатает их двумя цифрами через пробел.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Программа прочитала первые 24 байта PNG-картинки. Байты с 16-го по 23-й в hex: 00 00 03 20 00 00 02 58. Какая высота картинки в пикселях? Введите число.

Подсказка: Высота — вторые четыре байта (20–23). Соберите из них число методом int.from_bytes со старшим байтом первым, как в примере урока.

Онлайн-проверка ответа появится позже

Задание. Проектная миссия «Разбор Загрузок». Напишите функцию file_kind(head), которая по первым байтам возвращает «png», «jpg», «pdf» или «unknown», и программу, которая раскладывает файлы по папкам «Картинки» (png и jpg), «Документы» (pdf) и «Прочее». Для проверки ниже даны первые 8 байт 42 файлов в hex: превратите строку в bytes через bytes.fromhex(…) и передайте в file_kind. В поле ответа — сколько файлов попадёт в «Картинки». Приложите файл программы .py; в поле ответа — число.
Строки, которые начинаются с #, — пояснения к данным: в файлы их не копируйте.

Принято, если:
1. введённое число совпадает с эталоном;
2. приложен файл .py, который это число выдаёт на данных из условия;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. тип определяется по байтам, а не по расширению;
4. похожие, но неверные заголовки (одна цифра не та) дают «unknown».

# имя — первые байты файла в hex

file_01 — 25 50 44 46 2d 31 2e 37

file_02.png — 49 44 33 04 00 00 00 00

file_03.dat — ff d8 ff e1 00 18 45 78

file_04.png — 50 44 46 2d 31 2e 37 0a

file_05 — 68 65 6c 6c 6f 20 77 6f

file_06 — ff d8 ff e0 00 10 4a 46

file_07.png — 50 44 46 2d 31 2e 37 0a

file_08.dat — ff d8 ff e0 00 10 4a 46

file_09.png — 49 44 33 04 00 00 00 00

file_10.png — 68 65 6c 6c 6f 20 77 6f

file_11.bin — 89 50 4e 58 0d 0a 1a 0a

file_12.dat — 25 50 44 46 2d 31 2e 37

file_13.pdf — ff d8 00 e0 00 10 4a 46

file_14.pdf — 25 50 44 46 2d 31 2e 37

file_15.jpg — 68 65 6c 6c 6f 20 77 6f

file_16.tmp — 49 44 33 04 00 00 00 00

file_17.txt — 25 50 44 46 2d 31 2e 37

file_18.pdf — 47 49 46 38 39 61 01 00

file_19.jpg — ff d8 ff e0 00 10 4a 46

file_20.bin — ff d8 00 e0 00 10 4a 46

file_21.bin — 49 44 33 04 00 00 00 00

file_22.jpg — ff d8 ff e0 00 10 4a 46

file_23.png — 25 50 44 46 2d 31 2e 34

file_24.pdf — ff d8 ff e1 00 18 45 78

file_25.tmp — 89 50 4e 58 0d 0a 1a 0a

file_26.txt — ff d8 ff e1 00 18 45 78

file_27.bin — 25 50 44 46 2d 31 2e 37

file_28.png — 50 44 46 2d 31 2e 37 0a

file_29.dat — 47 49 46 38 39 61 01 00

file_30.jpg — 47 49 46 38 39 61 01 00

file_31.dat — ff d8 00 e0 00 10 4a 46

file_32.txt — 50 4b 03 04 14 00 00 00

file_33.tmp — 50 4b 03 04 14 00 00 00

file_34 — 25 50 44 46 2d 31 2e 37

file_35.txt — 49 44 33 04 00 00 00 00

file_36 — 25 50 44 46 2d 31 2e 37

file_37.bin — ff d8 ff e0 00 10 4a 46

file_38.txt — 89 50 4e 47 0d 0a 1a 0a

file_39.png — ff d8 00 e0 00 10 4a 46

file_40.dat — 49 44 33 04 00 00 00 00

file_41.bin — 68 65 6c 6c 6f 20 77 6f

file_42.dat — 68 65 6c 6c 6f 20 77 6f

# На своих файлах: head = open(path, "rb").read(8)

Подсказка: Разберите каждую строку: имя и hex разделены « — ». head = bytes.fromhex(hex_часть), затем file_kind(head) и счётчик для png и jpg. Имя файла не смотрите.

✅ Готово, если: ответ раскрывает то, что просит критерий приёмки в задании выше.

Онлайн-проверка ответа появится позже

Копирование файла кусками

Видео на 4 ГБ целиком в память читать не стоит. Читайте кусками фиксированного размера и сразу пишите в новый файл. Когда файл кончится, read вернёт пустые байты b"" — это сигнал выйти из цикла.

CHUNK = 64 * 1024   # 64 КБ


def copy_file(src, dst):
    """Скопировать файл кусками по CHUNK байт."""
    with open(src, "rb") as fin, open(dst, "wb") as fout:
        while True:
            piece = fin.read(CHUNK)
            if not piece:
                break
            fout.write(piece)


copy_file("video.mp4", "video_copy.mp4")

Если файл 10 000 байт, а кусок 4096, цикл запишет три куска: 4096, 4096 и 1808 байт. Четвёртый read вернёт b"". Копия совпадёт с оригиналом байт в байт — проверить можно, сравнив Path(src).stat().st_size и размер копии.

Попробуйте у себя: возьмите любую картинку из «Загрузок», переименуйте копию в test.dat и прогоните через file_kind. Функция всё равно скажет, что это картинка.

Проверьте себя

Задание. Файл занимает 20000 байт. Его копируют функцией ниже с CHUNK = 4096. Сколько раз выполнится fout.write? Введите число.

Исходный код для этого задания:

CHUNK = 4096
with open("a.bin", "rb") as fin, open("b.bin", "wb") as fout:
    while True:
        piece = fin.read(CHUNK)
        if not piece:
            break
        fout.write(piece)

Подсказка: Сколько полных кусков по 4096 помещается в 20000 и остаётся ли хвост? Пустой кусок не пишется.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

Онлайн-проверка ответа появится позже

Задание. Проектная миссия «Надёжная копия». Напишите функцию copy_file(src, dst, chunk=65536), которая копирует файл кусками, считает, сколько кусков записано, и возвращает это число; после копирования сравните размеры src и dst через Path.stat().st_size. Для проверки создайте 40 тестовых файлов с размерами из списка ниже (файл нужного размера: f.write(b»\0″ * size) в режиме «wb»), скопируйте каждый и сложите числа кусков. В поле ответа — общее число записанных кусков. Приложите файл программы .py; в поле ответа — число.
Строки, которые начинаются с #, — пояснения к данным: в файлы их не копируйте.

Принято, если:
1. введённое число совпадает с эталоном;
2. приложен файл .py, который это число выдаёт на данных из условия;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. файл читается в режиме ‘rb’ кусками, а не целиком;
4. цикл останавливается на пустом куске, пустой файл даёт 0 кусков;
5. размеры src и dst совпадают.

# chunk = 65536 байт (64 КБ); размеры файлов в байтах

клип_01.mp4 — 1475350

клип_02.mp4 — 939472

клип_03.mp4 — 1688415

клип_04.mp4 — 327680

клип_05.mp4 — 125258

клип_06.mp4 — 90493

клип_07.mp4 — 144216

клип_08.mp4 — 175946

клип_09.mp4 — 756384

клип_10.mp4 — 2386430

клип_11.mp4 — 144588

клип_12.mp4 — 22629

клип_13.mp4 — 96785

клип_14.mp4 — 132379

клип_15.mp4 — 51647

клип_16.mp4 — 350878

клип_17.mp4 — 816549

клип_18.mp4 — 0

клип_19.mp4 — 127924

клип_20.mp4 — 1717367

клип_21.mp4 — 48330

клип_22.mp4 — 2886100

клип_23.mp4 — 414583

клип_24.mp4 — 126366

клип_25.mp4 — 91406

клип_26.mp4 — 972664

клип_27.mp4 — 141446

клип_28.mp4 — 25268

клип_29.mp4 — 107015

клип_30.mp4 — 810911

клип_31.mp4 — 1757193

клип_32.mp4 — 177851

клип_33.mp4 — 426985

клип_34.mp4 — 812611

клип_35.mp4 — 14899

клип_36.mp4 — 71490

клип_37.mp4 — 2017353

клип_38.mp4 — 334459

клип_39.mp4 — 187271

клип_40.mp4 — 100430

def copy_file(src, dst, chunk=65536):

count = 0

# ваш код: open(…, "rb"), open(…, "wb"), цикл read/write

return count

Подсказка: Счётчик увеличивайте только после fout.write(piece), то есть на непустом куске. Итог — сумма возвращённых значений по всем 40 файлам.

✅ Готово, если: ответ раскрывает то, что просит критерий приёмки в задании выше.

Онлайн-проверка ответа появится позже

Что дальше

Теперь вы умеете открывать файлы в режимах 'rb' и 'wb', переводить текст в байты и обратно, считать размер строки в UTF-8, узнавать PNG, JPEG и PDF по сигнатуре и копировать большие файлы кусками. Глава «Файлы и данные» пройдена. Дальше — как сделать программы надёжными: что делать, когда файла нет или данные испорчены.

← Назад · ↑ В начало урока · ⌂ К навигатору курса · Вперёд →

Школа Виктора Комлева