Как устроены QR коды

data-science
visualization
tutorial
В этом посте я расскажу как работают QR коды, как они кодируют информацию, как их можно визуализировать с помощью библиотеки qr-verbose.
Автор

Николай Мальковский

Дата публикации

1 февраля 2026 г.

Изначально я хотел написать статью про коды Рида-Соломона с иллюстрацией на примере работы QR-кодов, в процессе подготовки сделал инструмент для упрощения визуализации и обнаружил, что у меня уже есть достаточно отсутствующего на Хабре материала, в связи с чем поменял план. Кому не интересно читать, QR с КДПВ сгенерированы с помощью qr-verbose.

pip install qr-verbose

Документация (англ.)

Про QR-коды на Хабре писали много, например одна из самых популярных статей за всё время — “Читаем QR код” и куча переводных статей. К сожалению, у этих статей есть кое-что общее: если вы попытаетесь прочитать код версии 4 и больше, а также некоторые коды версии 3, следуя описанию из статей, то потерпите фиаско. Не верите? Попробуйте прочитать нижний код из КДПВ.

В статье напомню, как устроен QR-код и какие проблемы начинаются с версии 3, а также надеюсь, что qr-verbose поможет разобраться тем, кто ещё не разобрался.

Как читать QR код, опять

Итак, берём QR-код.

qr-verbose -m 0 -v 3 \
    -t regular \
    -e M \
    -o a_zachem_regular.png \
    t.me/a_zachem_eto_nuzhno

Сначала определяем метаданные

qr-verbose -m 0 -v 3 \
    -t color \
    -e M \
    -o a_zachem_colors.png \
    t.me/a_zachem_eto_nuzhno

Итак, на что здесь стоит смотреть:

  • 3 больших красных квадрата - шаблон поиска, эти квадраты всегда обрамлены белой границей

  • Черно-белые полоски, обрамленные фиолетовой границей, — информация об уровне коррекции ошибок и маске данных. Уровней коррекции ошибок всегда 4: L (~7%), M (~15%), Q (~25%), H (~30%), в скобках указана доля ошибок, которую можно восстановить. Доля символов коррекции в два раза больше указанной. Маска — один из 8 шаблонов флипа данных, маска предназначена для исключения из итогового QR-кода шаблонов, запутывающих сканеры. Первые 2 бита отвечают за уровень коррекции 01-L, 00-M, 11-Q, 10-H. В нашем случае это M. Биты 2–4 задают маску, оставшиеся 10 бит — биты коррекции систематического кода BCH(15, 5); наконец, нужно эти 15 бит поксорить с определённой в стандарте константой 10101 00000 10010, и получившиеся биты записать. Все эти 15 бит дублируются, номера отмечают индекс бита.

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

  • Зелёный квадрат — шаблон выравнивания; также служит для помощи при сканировании.

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

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

  • Оставшиеся участки QR-кода разбиты на блоки по 8 бит (за исключением одного блока из 4 бит), из которых образуются байты. Синие байты — исходные данные, жёлтые — байты коррекции ошибок. Блоки данных с красной границей — реальные исходные данные, с зелёной — паддинг.

Внутри каждого байта подпись в формате <индекс_байта>:<индекс_бита>. Байты нумеруются в порядке змейки (правый рисунок), биты — в порядке движения, сначала обрабатывая правый бит (рисунок слева).

Едем дальше, давайте уберём маску

qr-verbose -m -1 -v 3 \
    -t color \
    -e M \
    -o a_zachem_colors.png \
    t.me/a_zachem_eto_nuzhno

Обратите внимание, что теперь оставшиеся 7 бит слева над нижним красным квадратом стали нулевыми, режим кодирования 0100 соответствует байтовому кодированию, в длине записано число 24 (отмечу, что 0 соответствует старшему биту, но конкретно для числа 24 это не имеет значения), а 24-й блок содержит 4 нулевых бита — это стандартный нулевой терминатор; после него идут паддинговые байты одного из двух значений: 17, 236. А вот и более наглядно с переводом в ASCII: попутно добавляю лого, чтобы было видно, какие байты оно закрывает (да, лого именно закрывает байты; QR-код всё ещё можно просканировать за счёт кодов дополнительных байтов избыточности кода Рида-Соломона, хотя предназначались они не для этого).

Что происходит начиная с версии 3

Давайте посмотрим на код из КДПВ и проделаем для него всё то же самое, что делали выше.

qr-verbose -m -1 -v 3 \
    -t ascii \
    -e Q \
    -o misterious_ascii.png \
    "Happy new year, Habr!"

Как же так? Раньше там была вполне содержательная информация: жалкая попытка незаметно впихнуть рекламу личного блога, а теперь просто какая-то билиберда. Проблема возникает на этапе построения кодов Рида-Соломона, если быть точнее, то на этапе разбиения и укладки данных. Дело в том, что, как и с BCH(15, 5) кодом, так и с кодом Рида-Соломона в QR используется систематическое кодирование — это когда исходное сообщение является частью закодированного сообщения, что и позволяет прочитать сообщение в предыдущем случае, просто игнорируя то, что записано в дополнительных байтах. У кодов Рида-Соломона есть ограничение на размер блока данных: если код построен над полем \(\mathbb{F}_q\), то блок не может быть размером больше \(q\). В QR-кодах Рид-Соломон работает над полем \(\mathbb{F}_{256}\) — самым удобным с практической точки зрения; это накладывает ограничение размера блока в 256 элементов. Решение этой проблемы оказалось простым: данные образуют не один блок коррекции, а несколько, и как раз впервые два блока появляются в QR-коде версии 3 с уровнем кодирования Q. На этом этапе уже важно понимать, как именно происходит кодирование, по крайней мере часть про разбиение данных.

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

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

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

На этом всё, спасибо за внимание и с Новым годом!