Нельзя использовать считанное значение QR-кода как есть ── прохождение коррекции ошибок не гарантирует правильность значения

· Обновлено: · · QR-код, Штрихкод, Коррекция ошибок, Проверка входных данных, Качество данных, Бизнес-системы, C#, Проектирование, Эксплуатация

Терминал проверки на складе издаёт короткий «пик». Система учёта принимает его за сигнал о том, что QR-код считан, подбирает накладную и подтверждает отгрузку. Разграничить здесь нужно «декодер вернул значение» и «с этим значением можно продолжать работу».

В QR-коде есть коррекция ошибок, которая восстанавливает данные из загрязнения и повреждений. От уровня L до H она позволяет восстановить примерно от 7% до 30% кодовых слов.1 Однако из этого не следует, что «вернувшееся значение обязательно правильно».

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

Мы также подготовили инструмент сравнения чтения QR-кодов, который переключает jsQR и OpenCV.js прямо в браузере. Он показывает результаты чтения образцов и различия, которые вносит кодировка символов. Но различия между сборкой OpenCV для Python и браузерной сборкой, о которых речь ниже, нужно рассматривать отдельно.

1. Сначала вывод

Считанное значение принимайте как «непроверенные данные извне» — точно так же, как ввод с клавиатуры. Базовый порядок — три этапа: формальный контроль, затем контрольная цифра, затем прикладная проверка. Коррекция ошибок эту проверку не заменяет.

Что проверяем Чего не говорит одно лишь «прошло» Что делает принимающая сторона
Удалось ли декодировать QR-код Совпадает ли значение с исходным. Не фрагмент ли это составного QR и не кракозябры ли это Проверять формат — длину, классы символов, префикс — полным совпадением
Согласовано ли значение как кодовая схема Не изменилось ли несколько символов и не другой ли это существующий номер После контрольной цифры сверять с мастер-данными и бизнес-контекстом
Существует ли пригодная накладная Та ли это накладная для коробки, рейса или работы, которую нужно обрабатывать сейчас Сопоставлять с целью работы, определённой другим способом
Ждёт ли она отгрузки прямо сейчас Не выполняет ли другой терминал ту же операцию одновременно Не допускать двойной обработки атомарным переходом состояния при выполнении или ключом идемпотентности

Как читать измерения, тоже нужно разграничить заранее. При случайных переворотах модулей и случайном повреждении кодовых слов во всех 9 700 попытках неверных значений не было ни одного. А при намеренно сконструированном повреждении достаточно изменить 7 из 26 кодовых слов, чтобы 004873 прочиталось как 104873, причём оба декодера вернули одну и ту же ошибку. «Шаблон, который читается неверно, существует» и «как часто это случается на практике» — два разных вопроса.

Составной QR, кодировка символов и захват не того QR-кода в кадре происходят вне коррекции ошибок. Чем это обернётся — предупреждением, исключением или пустой строкой — тоже зависит от реализации, поэтому нельзя считать отсутствие ошибки самостоятельным признаком успеха.

С чего начать в зависимости от того, что нужно узнать

Что нужно узнать Где читать
Действительно ли возвращается другое значение Реальные образцы в главе 2
Почему коррекция ошибок этого не предотвращает Механизм в главе 3 и измерения с ограничениями в главе 4
Во что это превращается как сбой в работе Четыре шаблона отказа в главе 5
Как исправить приложение Проектирование проверок, пример на C# и оставшиеся меры в главе 6
Как решать вопросы дизайна этикетки и процедур на местах Эксплуатация в главе 7 и шаги для воспроизведения в главе 8

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 16, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Образцы ── два QR-кода, которые выглядят почти одинаково

2.1. Похоже на успех, но номер накладной другой

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

Сверху неповреждённый QR-код. Снизу QR-код с одной вертикальной полосой повреждения. Оба выглядят почти одинаково

Одно замечание перед тем, как пробовать: стандартная камера iOS может не отреагировать. Если она не читает, попробуйте приложение для чтения QR-кодов (причина — в примечании ниже).

  • A (сверху)NO:20260725-004873
  • B (снизу)NO:20260725-104873

Это настоящие коды, поэтому их можно сразу попробовать подручным сканером. Номер накладной построен по схеме «8 цифр даты заказа + 5 цифр порядкового номера + 1 контрольная цифра», так что изменилась именно порядковая часть — 00487 стало 10487, что указывает на другую накладную, отстоящую на 10 000 записей.

Стандартная камера iOS может не отреагировать. Стандартная камера устроена так, что отдаёт приоритет содержимому, которое можно «открыть», например URL, поэтому QR-код с одной лишь строкой, как в этой статье, может не дать вообще ничего. Приложение для чтения QR-кодов его прочитает. Изображение одно и то же, а поведение меняется в зависимости от реализации на стороне чтения — тема этой статьи обнаруживается уже на первом образце.

2.2. Изменились всего 31 модуль — и всё равно никакого уведомления

B отличается от A только 31 модулем, и все они лежат в одной вертикальной полосе в столбцах с 10-го по 14-й слева. Это 15% от 208 модулей области кодовых слов.

Схема, на которой красным обведены 31 модуль B, изменившиеся относительно A. Они распределены длинной узкой вертикальной полосой

И самое главное — декодер не возвращает ошибку ни для одного из них. Нет даже уведомления «коррекция применена». С точки зрения приложения это два одинаково успешных чтения.

Чтобы понять этот результат, нужно смотреть не только на площадь повреждения, но и на то, к какому шаблону, как к кодовому слову, он приблизился. Механизм разобран в главе 3, способ построения образца — в разделе 4.2, а насколько это далеко от реального загрязнения — в разделе 4.3.

3. Почему это происходит ── что внутри коррекции ошибок

3.1. Единица коррекции и запас на защиту от ошибочного декодирования, оставленный стандартом

Коррекция ошибок в QR-коде — это код Рида–Соломона, и работает она по кодовым словам, единицами по 8 бит. Вот расклад для использованного здесь символа версии 1 с уровнем коррекции M (21×21 модулей).2

Всего кодовых слов Кодовых слов данных Кодовых слов коррекции ошибок Исправляющая способность по стандарту
26 16 10 4 кодовых слова

Внимание привлекает последний столбец. При 10 кодовых словах коррекции код Рида–Соломона способен исправить до 5. Но исправляющая способность, которую задаёт стандарт, — 4. Одно кодовое слово разницы намеренно отведено как кодовые слова защиты от ошибочного декодирования p (для версии 1-M p = 2). В сноске к таблице 13 стандарта также прямо сказано, что исправляющая способность установлена менее половины числа кодовых слов коррекции ошибок, чтобы снизить вероятность ошибочного декодирования.2

Иными словами, сам стандарт спроектирован исходя из того, что коррекция ошибок может выдать неверное значение. В том же разделе сказано и следующее.

Поскольку QR-код является матричной символикой, дефект, меняющий модуль с тёмного на светлый (или наоборот), приводит к тому, что соответствующий знак символа ошибочно декодируется как внешне допустимое, но другое кодовое слово.2

3.2. Он не знает «исходного значения» — он ищет исправляемый кандидат

Причина — в самом принципе коррекции. Декодирование Рида–Соломона ищет кодовое слово внутри фиксированного расстояния (исправляющей способности) от принятого шаблона. Если оно найдено, оно и возвращается как ответ; если нет — всё заканчивается на «не читается». Это не поиск ближайшего кодового слова независимо от того, насколько сильно повреждён символ.

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

4. Где заканчивается коррекция и откуда начинается опасность

Здесь мы измеряем отдельно случайное повреждение и повреждение, сконструированное так, чтобы увести значение к другому. По объёму повреждения нельзя судить о правильности возвращаемого значения. При одном и том же механизме коррекции результат разошёлся на «не читается» и «возвращается другое значение» в зависимости от того, куда пришлось повреждение.

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

Пункт Содержание
Целевой символ Версия 1-M / 21×21 (26 кодовых слов = 16 данных + 10 коррекции)
Генерация segno 1.6.6 / Python 3.11
Декодер 1 OpenCV 5.0.0 cv2.QRCodeDetector (Python 3.11)
Декодер 2 jsQR 1.4.0 (Node.js 22)
Как вносилось повреждение (раздел 4.1) Переворот случайно выбранных модулей из 208 в области кодовых слов / случайное повреждение целых кодовых слов / равномерное размытие, шум и снижение контраста по всему символу
Как вносилось повреждение (раздел 4.2) Полный перебор всех сочетаний кодовых слов, сдвигаемых к целевому значению B
Число попыток 3 900 переворотов модулей (по 300 на уровень), 1 800 повреждений кодовых слов (по 200 на уровень) плюс 4 000 дополнительных попыток за пределами исправляющей способности, 1 000 ухудшений качества изображения (по 200 на условие). В разделе 4.2 — полный перебор 792 и 495 сочетаний
Классификация Возвращаемое декодером значение считается «прочитано верно», если оно совпадает с ожидаемым, «не читается», если значение не получено, и «неверное значение», если возвращена непустая строка, отличная от ожидаемой

Окружение для всей статьи, включая образцы из главы 5, собрано в разделе «Среда проверки» в конце статьи.

4.1. Случайное загрязнение попадает на сторону «не читается»

Случайный переворот модулей

Для символа версии 1-M несколько из 208 модулей области кодовых слов переворачивались случайным образом, а результаты классифицировались (по 300 попыток на уровень, всего 3 900).

Сравнение QR-кода с 6 перевёрнутыми модулями и QR-кода с 9 перевёрнутыми. Верхний читается, нижний — нет

Сверху перевёрнуто 6, снизу — 9. Верхний читается верно, нижний не читается вовсе. Разница в 3 модуля почти незаметна человеческому глазу. Граница лежит там, где внешний вид ничего не показывает.

Перевёрнуто модулей Прочитано верно Не читается Неверное значение
0–5 1 799 1 0
6 131 169 0
7 32 268 0
8 11 289 0
9–12 0 1 200 0

Читаемость обрывается между 5 и 6 модулями, а начиная с 9 исчезает полностью. И не появилось ни одного неверного значения. Повреждение, которое не удаётся исправить полностью, попадает на сторону «не читается» — это прямолинейная хорошая новость. У jsQR числа были почти такими же (все 1 800 попыток при 0–5 верны, а начиная с 6 — в пределах одной попытки от OpenCV).

Повреждение целых кодовых слов и сравнение способности по стандарту с поведением реализаций

Если посмотреть на то же самое по кодовым словам, граница становится резче (по 200 попыток на уровень, всего 1 800).

Повреждено кодовых слов Прочитано верно Не читается Неверное значение
0–5 1 194 6 0
6–8 0 600 0

Коррекция работает вплоть до 5 кодовых слов. Как показано в предыдущем разделе, исправляющая способность по стандарту равна 4, а остальное было запасом на обнаружение без исправления. jsQR прочитал верно все 1 200 попыток при 0–5, то есть обе реализации расходуют этот запас целиком на исправление. На запас, который стандарт отвёл на защиту от ошибочного декодирования, на уровне реализации рассчитывать нельзя.

Повреждение, явно выходящее за исправляющую способность (6 и 8 кодовых слов), тоже было проверено по 2 000 раз, и снова неверных значений не было ни одного: все попытки закончились на «не читается».

Равномерное ухудшение качества изображения по всему символу

Ухудшение, источник которого — камера, показывает ту же тенденцию. Ниже результаты равномерного размытия, шума и снижения контраста по всему символу (по 200 попыток на условие, всего 1 000).

Размытие σ Шум σ Контраст Прочитано верно Не читается Неверное значение
0 0 1.00 200 0 0
1.5 10 0.90 183 17 0
3.0 20 0.70 1 199 0
4.5 30 0.50 0 200 0
6.0 40 0.35 0 200 0

Результат — либо «читается верно», либо «не читается», промежуточного нет. По мере роста деградации доля успешных чтений падает, но всё потерянное превращается в неудачное чтение.

Однако это верно для равномерной деградации. У реального дрожания камеры есть направление, а съёмка под углом или неравномерное освещение разрушают лишь часть изображения. Как показывает следующий раздел, опасность в том, что повреждение сосредоточено, поэтому не обобщайте это до «проблемы с качеством изображения не приводят к ошибочному чтению».

4.2. Когда повреждение ложится неудачно, другое значение получается гарантированно

Образец B из главы 2 был построен намеренно, чтобы дать именно такое сосредоточенное повреждение.

Если выписать 26 кодовых слов для NO:20260725-004873 (A) и NO:20260725-104873 (B), различий окажется 12: 2 кодовых слова данных плюс 10 кодовых слов коррекции ошибок, которые за ними потянулись.

Если сдвинуть 7 из этих 12 к значениям B, получившийся шаблон окажется на расстоянии 7 кодовых слов от A и на 5 кодовых слов от B. При исправляющей способности 5 декодер воспримет это как «B с 5 испорченными кодовыми словами» и исправит в B.

Условие Испробовано сочетаний Прочитано как B
7 кодовых слов сдвинуто к B (расстояние до B равно 5) 792 792 (100%)
8 кодовых слов сдвинуто к B (расстояние до B равно 4) 495 495 (100%)

Все испробованные здесь сочетания были прочитаны как B, без исключений. Нижняя строка — расстояние 4 до B, то есть, если смотреть со стороны B, внутри исправляющей способности, заданной стандартом. Даже реализация, которая уважает кодовые слова защиты от ошибочного декодирования p, даёт тот же результат, как только повреждение вырастает ещё на одно кодовое слово. p лишь снижает вероятность, но не предотвращает это.

B, показанный в главе 2, — это то из сочетаний, в котором повреждение собирается в одну вертикальную полосу (31 модуль). При минимизации его удалось уменьшить до 23 модулей. Две не связанные между собой реализации, OpenCV 5.0.0 и jsQR 1.4.0, обе возвращают NO:20260725-104873.

4.3. Как воспринимать этот разрыв

Если честно, такое повреждение вряд ли возникает случайно. При беспорядочном загрязнении 9 700 попыток дали ноль ошибочных чтений. Это не история про «завтра же случится».

И всё же есть три причины, по которым это нельзя игнорировать.

  • Реальные повреждения не случайны. Складка идёт по прямой, истирание при транспортировке сосредоточено на одной кромке, а забитая печатающая головка даёт вертикальную полосу. Использованное здесь полосовое повреждение — один из примеров такого повреждения, смещённого по положению. При этом B содержит изменения в обе стороны: 17 модулей из белого в чёрный и 14 из чёрного в белый, поэтому его нельзя воспроизвести дефектом, который только убирает краску. Оба направления встречаются вместе в случаях, когда тень от складки смещает порог бинаризации, загрязнение и выцветшая печать накладываются друг на друга или сверху частично наклеена другая наклейка.
  • Число сканирований на порядки больше. Вероятность, которой можно пренебречь при одном сканировании, выглядит иначе на площадке, где за день читают десятки тысяч кодов. К тому же ошибочное чтение не выдаёт ошибку, поэтому не оставляет записи и в итоге обрабатывается как расхождение при инвентаризации без объяснимой причины.
  • Если это можно сконструировать намеренно, то и другие смогут. Здешний шаблон был построен механически после выбора целевого значения. Для QR-кодов, где есть мотив переписать значение, — ценников и купонов — это становится приёмом атаки.

5. «Прочиталось, но неверно» — случаи, не связанные с коррекцией ошибок

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

5.1. Чтение только первого символа из набора Structured Append

В QR-кодах есть механизм (Structured Append) для разбиения длинных данных на несколько символов и их объединения на стороне чтения. Ниже — первый из трёх символов, на которые были разбиты данные NO:20260725-004873/LOT:AB-77/QTY:120/EXP:20270131.

Первый из трёх символов Structured Append. При чтении отдельно возвращается номер накладной с отсутствующим концом

Выглядит как обычный QR-код, без единой подсказки, что это один из трёх. При чтении отдельно OpenCV возвращает вот это.

NO:20260725-00487

Ни ошибки, ни предупреждения. Вполне правдоподобный номер накладной, у которого отсутствует только замыкающая контрольная цифра 3. Второй и третий символы дают 3/LOT:AB-77/QTY: и 120/EXP:20270131. Та же картинка, переданная jsQR, вернула пустую строку. Что произойдёт, если приложение, не рассчитанное на Structured Append, случайно считает первый символ, зависит от декодера.

Вот как это проявляется в бизнес-системе. На экране, который ищет номера накладных по совпадению начала строки или через LIKE, NO:20260725-00487 с потерянной последней цифрой всё равно попадёт в исходную накладную, и, поскольку «прочиталось и накладная нашлась», никто ничего не заметит. Если длину не проверять как фиксированное значение, этот фрагмент пройдёт до конца как обычное чтение.

5.2. Кодировка символов и ECI

Дальше — QR-код, в который строка 部品番号 東-004873 записана в Shift_JIS без указания ECI.

QR-код, сгенерированный в Shift_JIS без указания ECI. При чтении получаются кракозябры

Это тоже настоящий код. Прочитайте его подручным сканером — и получите 部品番号 東-004873, строку из кракозябр или вообще ничего: это и покажет, какую интерпретацию использует ваш сканер.

Если подать это изображение в инструмент сравнения чтения QR-кодов, рядом с результатами повторной интерпретации как UTF-8, Shift_JIS, EUC-JP и так далее будет показана сырая последовательность байтов, которую извлёк jsQR. Можно сразу увидеть, как одна и та же последовательность байтов превращается в нечто совсем другое в зависимости от кодировки.

Изменение условий генерации и сравнение результатов декодеров

Ниже — результаты генерации одного и того же содержимого с разными кодировками и указаниями ECI и чтения их двумя декодерами. Все четыре таких символа включены в образцы инструмента. Но значения в таблице измерены на cv2 для Python, и только третья строка отличается от браузерной сборки (подробно об этом сразу после таблицы). Инструмент позволяет убедиться в поведении браузерной сборки, поэтому для воспроизведения третьей строки «падает с исключением» нужен cv2 для Python.

Условия генерации OpenCV 5.0.0 jsQR 1.4.0
Shift_JIS / без ECI Возвращает строку из кракозябр как успех Пустая строка
UTF-8 / без ECI 部品番号 東-004873 部品番号 東-004873
Shift_JIS / с ECI Выдаёт предупреждение и не может декодировать Пустая строка
UTF-8 / с ECI 部品番号 東-004873 部品番号 東-004873

Первая строка — худшая. OpenCV интерпретировал последовательность байтов как Latin-1 и вернул испорченную строку \x95\x94\x95i... вообще без ошибки. С точки зрения приложения это нормальное чтение, и если она сразу попадёт в базу данных, появится одна запись с кракозябрами.

Вот как это проявляется в бизнес-системе. В поле наименования записи о приёмке регистрируется испорченная строка, а на следующий день это превращается в обращение: «поиск по этому номеру детали не находит записей». Повторное чтение той же этикетки кладёт то же значение, поэтому на площадке сообщают, что «сломался поиск в системе», и переписка продолжается, пока кто-нибудь не поймёт, что причина — кодировка на стороне чтения.

Когда ECI не указан и когда указан, но реализация не умеет с ним работать

Это не ошибка OpenCV, а поведение точно в соответствии с интерпретацией по умолчанию (ISO/IEC 8859-1), заданной действующим стандартом.3 От стандарта отходит как раз тот, кто записал Shift_JIS без ECI.

Третью строку тоже нельзя упускать. Хотя ECI — механизм явного объявления кодировки — был указан правильно, OpenCV выдал предупреждение QR: ECI is not supported properly и затем упал с исключением, не сумев интерпретировать возвращаемое значение как UTF-8. Инверсия, при которой QR-код, собранный строго по стандарту, как раз и не читается, действительно случается.

Не считайте сборку для Python и браузерную сборку одним и тем же результатом

Более того, эта третья строка даёт разные результаты даже в рамках одного OpenCV — в зависимости от языковой привязки. Таблица выше — результат на cv2 для Python, но если передать то же изображение браузерной сборке (opencv.js 5.0.0), исключение не возникает: она возвращает как успех строку ���i��� ��-004873, полную заменяющих символов. Причина в том, что Emscripten, преобразуя std::string как UTF-8, отбрасывает недопустимые байты в U+FFFD, а не бросает исключение. Версия и изображение те же, изменился только язык вызывающей стороны. Python, где исключение хотя бы позволяет заметить проблему, из двух лучше; браузерная сборка говорит «прочиталось», возвращая при этом испорченное значение. Это можно проверить на образцах инструмента.

5.3. Несколько QR-кодов в кадре

Несколько QR-кодов, напечатанных на одной накладной, или этикетка соседней коробки, попавшая в поле зрения, — обычные ситуации. Мы попробовали три QR-кода, выстроенных рядом.

Изображение с тремя QR-кодами рядом. Слева направо: NO:, ITEM:, LOT:

API одиночного чтения не гарантирует отказа от нескольких символов

Во-первых, на этом изображении API одиночного чтения OpenCV не вернул вообще ничего. Однако это не гарантия, что «при нескольких кодах вход отвергается». API одиночного чтения документирован лишь как обнаруживающий и декодирующий один QR-код; нигде не сказано, что он отвергает несколько символов, и в зависимости от расположения он вполне может вернуть один из них. Не подменяйте обнаружение нескольких кодов наличием или отсутствием возвращаемого значения у API одиночного чтения.

Даже с API множественного чтения порядок результатов нельзя использовать для идентификации

Использование API множественного чтения тоже не означает, что можно полагаться на порядок возврата.

Ниже — результаты чтения приведённого выше изображения (слева NO: / ITEM: / LOT:) как есть, с изменением только размеров в пикселях. Это соответствует реальному сканеру, у которого меняется расстояние до объекта или разрешение камеры.

Ширина изображения Порядок возврата
1 001 px NO: / LOT: / ITEM:
1 502 px Ничего не возвращается
2 002 px NO: / LOT: / ITEM:
3 003 px NO: / ITEM: / LOT:
4 004 px LOT: / NO: / ITEM:

Изображение одно и то же, а порядок меняется только из-за изменения разрешения. Он не слева направо и не от большего к меньшему. Есть даже разрешение, при котором ничего не читается. Порядок определяется внутренним устройством алгоритма обнаружения, а поскольку он не нормирован, получается вот это.

А значит, код, который берёт индекс 0 исходя из предположения «первым должен быть номер накладной», может сегодня случайно работать, а завтра схватить другой код просто потому, что камеру придвинули ближе. Это тот вид дефекта, который трудно воспроизвести и трудно отследить.

Вот как это проявляется в бизнес-системе. На столе проверки нужная накладная и этикетка соседней коробки одновременно попадают в поле зрения. Приложение, берущее индекс 0, подбирает соседнюю накладную, и задание на отгрузку подтверждается по ней. Оператор уверен, что навёл камеру на правильную этикетку, поэтому никто ничего не замечает, пока после отгрузки не прозвучит «пришёл не тот товар».

Мера: выбирать по содержимому и убеждаться, что кандидат ровно один

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

5.4. Содержимое вообще не обязательно верно

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

QR-код говорит только о том, что там написано. Правильно ли это и выпустила ли это ваша компания — может проверить только принимающая сторона.

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

6. Что делать с полученным значением

Мера сводится к одному: надстроить собственную проверку поверх коррекции ошибок.

Этап Что проверяется Что он ловит
1. Формальный контроль Полное совпадение длины, классов символов, разделителей и префикса Чтение другого кода, фрагмент составного QR, кракозябры
2. Самопроверка Контрольная цифра Надёжно обнаруживает изменение одного символа. Пропускает часть изменений нескольких символов
3. Прикладная проверка Сверка с мастер-данными и совпадение с целью, которую нужно обрабатывать прямо сейчас Старая этикетка, этикетка другой компании, путаница с другой накладной

Номер накладной в образце — это NO: плюс 8 цифр даты заказа, 5 цифр порядкового номера и 1 контрольная цифра, причём последняя использует ту же схему «модуль 10, вес 3», что и GS1. Ошибочно прочитанное NO:20260725-104873 из главы 2 останавливается именно здесь: правильная контрольная цифра для 20260725104870, и она не совпадает с 3 на этикетке.

Что эти три этапа защитить не могут

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

Изменение нескольких символов одной контрольной цифрой не остановить

Контрольная цифра пропускает изменения нескольких символов. То, что «модуль 10, вес 3» ловит надёжно, — это ошибка в одном символе. В самом деле, 2026072500487 и 2026072517487 оба дают контрольную цифру 3, поэтому NO:20260725-174873 проходит второй этап насквозь. Ошибочная коррекция не обязательно меняет только один символ, поэтому третий этап пропустить нельзя.

Верное существование и статус ещё не делают объект текущей целью работы

Не позволяйте сверке с мастер-данными закончиться подтверждением существования. Вызов repo.Find() и проверка статуса в примере на C# ниже подтверждают лишь то, что пригодная накладная где-то существует. Если оператор считает этикетку соседней коробки, та этикетка тоже удовлетворяет и формату, и контрольной цифре, и статусу «ждёт отгрузки», поэтому проходит насквозь. Считанное значение нужно сопоставлять с целью, которую вы должны обрабатывать прямо сейчас: совпадает ли она со следующей позицией листа подбора, привязана ли к уже отсканированному идентификатору контейнера, тот ли пункт назначения, что у текущего рейса. С чем именно сопоставлять, зависит от бизнес-процесса, поэтому только это место не сводится к универсальному коду.

Двойную обработку предотвращают на стороне выполнения, после проверки

Проверка не может предотвратить двойную обработку. Если два терминала читают одну этикетку почти одновременно, оба подтверждают статус «ждёт отгрузки» и затем его обновляют, поэтому проходят оба. Предотвратить это — задача стороны выполнения: сделать условный переход состояния вида UPDATE ... WHERE status = 'WaitingForShipment' одной атомарной операцией или погасить повторное выполнение ключом идемпотентности. Проверка — это решение на входе, и она не заменяет управление конкурентным доступом.

6.1. Сначала определите кодировку и формат

Определите предпосылку о кодировке до того, как писать код. Приведённая ниже реализация исходит из того, что номер накладной состоит только из ASCII-цифр, и вычисляет контрольную цифру как «символ минус '0'». Если до этого вычисления дойдут полноширинные цифры или испорченная последовательность байтов из раздела 5.2, результат не будет иметь смысла. Поэтому регулярное выражение формального контроля написано с [0-9], а не с \d, в таком порядке, чтобы ни одна не-ASCII цифра не дошла до вычисления контрольной цифры. Сначала определите предпосылку, затем обеспечьте её формальным контролем, и только после этого вычисляйте. Как только этот порядок нарушается, все последующие проверки крутятся вхолостую.

6.2. Реализация проверки на входе на C#

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

using System.Linq;
using System.Text.RegularExpressions;

public sealed record ScanOutcome(bool Accepted, string? SlipNo, string Reason);

public static class SlipScanValidator
{
    // NO: + 8 цифр даты заказа + '-' + 5 цифр порядкового номера + 1 контрольная цифра
    // Терминатор — \z, а не $. В .NET $ совпадает и непосредственно перед
    // завершающим переводом строки, поэтому пропустил бы "NO:20260725-004873\n".
    // Цифры — [0-9], а не \d. В .NET \d совпадает с цифрами Unicode вообще,
    // включая полноширинные, а вычисление контрольной цифры ниже рассчитано на ASCII
    private static readonly Regex Format =
        new(@"\ANO:(?<date>[0-9]{8})-(?<seq>[0-9]{5})(?<cd>[0-9])\z", RegexOptions.Compiled);

    public static ScanOutcome Validate(string? raw, ISlipRepository repo)
    {
        // 1. Не превращайте «не удалось прочитать» в «пустой успех».
        //    Декодеры по-разному сообщают о неудаче: null, пустая строка или исключение
        if (string.IsNullOrEmpty(raw))
            return new(false, null, "Не удалось прочитать код. Отсканируйте ещё раз");

        // 2. Формальный контроль. Не совпадение начала, а полное совпадение с длиной.
        //    Фрагмент составного QR "NO:20260725-00487" отсеивается здесь
        var m = Format.Match(raw);
        if (!m.Success)
            return new(false, null, $"Не формат QR-кода накладной ({Describe(raw)})");

        // 3. Самопроверка. Если ошибочная коррекция испортила цифру, это ловится здесь
        var body = m.Groups["date"].Value + m.Groups["seq"].Value;
        if (Modulus10Weight3(body) != m.Groups["cd"].Value[0] - '0')
            return new(false, null, "Контрольная цифра не совпадает. Проверьте этикетку на загрязнение");

        // 4. Прикладная проверка. Существует ли и в состоянии ли, в котором можно обрабатывать
        var slip = repo.Find(raw);
        if (slip is null)
            return new(false, null, "Подходящей накладной не существует");
        if (slip.Status != SlipStatus.WaitingForShipment)
            return new(false, null, $"Эта накладная имеет статус '{slip.Status}'. Она не является объектом отгрузки");

        return new(true, raw, "OK");
    }

    // Тот же модуль 10 с весом 3, что и в GS1. Веса 3,1,3,1... применяются с самой правой цифры
    private static int Modulus10Weight3(string body)
    {
        var sum = 0;
        for (var i = 0; i < body.Length; i++)
        {
            var weight = (body.Length - i) % 2 == 1 ? 3 : 1;
            sum += (body[i] - '0') * weight;
        }
        return (10 - sum % 10) % 10;
    }

    // Считанное значение — внешние данные. Оставляйте только печатаемый ASCII,
    // прежде чем показывать его на экране или писать в журнал.
    // char.IsControl отбрасывает только Unicode Control (Cc) и пропускает символы
    // Format (Cf), такие как U+202E (переопределение направления справа налево).
    // Пишите список разрешённых, а не список запрещённых
    private static string Describe(string raw)
    {
        var kept = raw.Where(c => c >= ' ' && c <= '~').Take(40).ToArray();
        if (kept.Length == 0) return "(строку невозможно отобразить)";
        var safe = new string(kept);
        return kept.Length < raw.Length ? safe + "... (часть символов удалена)" : safe;
    }
}

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

«Авария» и «атака» — разные задачи

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

Для применений, где есть мотив переписать значение — ценники, купоны, входные билеты, платежи, — ничто из этого не защита. Атакующий может удовлетворить формату, пересчитать контрольную цифру и свободно изготовить QR-код, указывающий на другой существующий номер. Сверка с мастер-данными проверяет только существование, поэтому он проходит насквозь.

Защита от подделки требует способа убедиться в эмитенте

Если обладание QR-кодом означает ценность или полномочие, само значение должно нести подлинность. Либо сделайте его не угадываемым токеном, который выдаёт сервер — случайным числом достаточной длины, — чтобы по одному номеру нельзя было угадать другой, либо добавьте к полезной нагрузке MAC с ключом или цифровую подпись и пусть принимающая сторона проверяет её ключом. В обоих случаях управляйте состоянием «использовано» на сервере, чтобы дубликат нельзя было применить дважды.

Подмену подлинного QR-кода предотвращает привязка к объекту

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

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

7. Что решить на стороне эксплуатации

Эксплуатационные решения проще упорядочить, если разделить их на дизайн этикетки, действия при неудачном чтении и подтверждение перед фиксацией.

7.1. Дизайном этикетки разложить причины нечитаемости и средства проверки

  • Всегда печатайте под QR-кодом строку, читаемую человеком. Это та же идея, что и Human Readable Interpretation (HRI), определённая GS1.4 Когда ошибочное чтение вызывает сомнения, остаётся способ для человека сверить значения. В реальной эксплуатации нередко только это и позволяет обнаружить ошибочное чтение. Практика эксплуатации штрихкодов в целом разобрана в статье Стандарты штрихкодов GS1: основы и типичные ошибки на практике.
  • Не используйте Structured Append. Если данные не помещаются, поднимите версию или положите в QR-код только идентификатор, а остальное подтягивайте из мастер-данных. Второй вариант позволяет держать этикетку маленькой и исправлять содержимое без перевыпуска этикетки.
  • Кодировку решайте тем, что ничего не кладёте. Держите бизнес-QR-коды в пределах ASCII. Байтовый режим не объявляет кодировку, если не указан ECI, а интерпретация по умолчанию менялась между редакциями стандарта.3 Простая запись в UTF-8 совместимости не даёт, и сканер с другой интерпретацией выдаст кракозябры. Если не-ASCII всё же нужно вложить, правильный по стандарту ответ — объявить UTF-8 с указанием ECI, но, как показывает раздел 5.2, реализации с сомнительной обработкой ECI реально существуют, поэтому проверки на реальной машине ожидаемых моделей не избежать.

7.2. Определить процедуру при неудачном чтении и хранить записи, пригодные для разбора

  • Определите процедуру на случай, когда код не читается. Предельное число повторных сканирований, откат к ручному вводу и того, кто это утверждает. Если здесь неопределённость, площадка движется в сторону «менять угол и пробовать, пока не прочитается», а это повышает вероятность ошибочного чтения.
  • Записывайте в журнал отвергнутые значения. Если они группируются на определённой этикетке или терминале, неисправность принтера или сканера можно найти рано. Только не пишите сырую строку прямо в построчный журнал. Значение с переводами строк или управляющими символами может подделать строки журнала или сломать отображение. Обрезайте и экранируйте оригинал и храните его в поле структурированного журнала или в столбце базы данных, а в строки, которые читает человек, выводите только обезвреженное представление — заложите это разделение с самого начала. По возможности сохраняйте и изображение чтения.

7.3. Чем тяжелее отменить операцию, тем основательнее подтверждение перед фиксацией

  • Вставляйте подтверждение перед необратимой операцией. Для операций, которые тяжело отменить — подтверждение отгрузки, списание запасов, закрытие платежа по дебиторской задолженности, — покажите человеку наименование товара и сумму, полученные из считанного значения. Ошибочно прочитанное значение может выглядеть правдоподобно, но в бизнес-контексте часто выглядит неуместно.

Насколько далеко заходить в проверках, определяется ущербом от ошибки.

Применение Формальный контроль Контрольная цифра Сверка с мастер-данными Подтверждение человеком
Внутренние места и номера стеллажей Обязательно По желанию Рекомендуется Не нужно
Приёмка, отгрузка и инвентаризация Обязательно Рекомендуется Обязательно Не нужно
Подтверждение отгрузки и списание запасов Обязательно Обязательно Обязательно Рекомендуется
Выставление счетов и закрытие платежей Обязательно Обязательно Обязательно Обязательно
Предотвращение путаницы лекарств и опасных материалов Обязательно Обязательно Обязательно Обязательно

8. Итог

Коррекция ошибок в QR-коде — механизм восстановления исходных кодовых слов из напечатанного шаблона. С этой работой она справляется. В измерениях тоже: при случайном загрязнении и равномерной деградации качества изображения она читала верно в пределах исправимого и честно отказывалась от чтения за этими пределами.

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

То, что сам стандарт пишет «ошибочно декодируется как внешне допустимое, но другое кодовое слово» и специально резервирует кодовые слова для защиты от ошибочного декодирования, прямо выражает эту структуру. Даже после всего этого вероятность можно только снизить — это предел, до которого доходит стандарт. Закрыть оставшийся разрыв способно только приложение, получившее значение.

Успешно декодированное значение — это непроверенные данные извне. Обращайтесь с ним точно так же, как со строкой, набранной на клавиатуре, — вот правильный, на мой взгляд, способ работы с QR-кодами.

Три шага, чтобы проверить это на своих QR-кодах

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

  1. Сгенерируйте. Возьмите одно значение в том формате, с которым вы действительно работаете, и превратите его в QR-код подручным генератором (образцы в этой статье сгенерированы segno 1.6.6). Сначала убедитесь, что он читается неповреждённым.
  2. Нанесите повреждение. В графическом редакторе нарисуйте одну вертикальную полосу, изображающую складку или забитую печатающую головку. Важна не величина, а сосредоточенность, поэтому вместо того, чтобы рассыпать слабый шум по всему символу, соберите повреждение в узкой области.
  3. Сравните два декодера. Подайте изображение в инструмент сравнения чтения QR-кодов и сравните результаты jsQR и OpenCV.js. Поведение вроде того, что значение возвращает только один из них или оба возвращают одно и то же значение, отличающееся от исходного, можно проверить сразу.

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


Среда проверки

В разделах 2–4, где рассматривается поведение коррекции ошибок, везде используется версия 1-M (21×21 модулей). Образцы в главе 5 меняют версию в зависимости от объёма данных, поэтому указаны по подразделам.

Пункт Содержание
Декодер 1 OpenCV 5.0.0 cv2.QRCodeDetector
Декодер 2 jsQR 1.4.0 (Node.js 22)
Генерация segno 1.6.6 / Python 3.11
Символы в разделах 2–4 Версия 1-M / 21×21 (26 кодовых слов = 16 данных + 10 коррекции)
Раздел 5.1 (Structured Append) Все три символа версии 1-M / 21×21
Раздел 5.2 (кодировка и ECI) Shift_JIS — версия 2-Q, UTF-8 — версия 2-M (обе 25×25), поскольку 18–23 байта японского текста не помещаются в 16 кодовых слов данных версии 1-M
Раздел 5.3 (несколько кодов) NO: и ITEM: — версия 1-M; LOT:AB-77 — версия 1-H, потому что данных мало и segno поднимает уровень коррекции ошибок (все 21×21)
  1. DENSO WAVE Incorporated, Error correction feature | QRcode.com 

  2. ISO/IEC 18004 Information technology — Automatic identification and data capture techniques — QR code bar code symbology specification. Формула исправляющей способности e + 2t ≦ d - p, значение кодовых слов защиты от ошибочного декодирования p и утверждение об «ошибочном декодировании как внешне допустимом, но другом кодовом слове» находятся в 8.5.1 Error correction capacity; (26,16,4) для версии 1-M и сноска о том, что исправляющая способность установлена менее половины числа кодовых слов коррекции ошибок для снижения вероятности ошибочного декодирования, — в Table 13 (цитаты по редакции 2000 года). Действующая редакция — ISO/IEC 18004:2024 2 3

  3. Интерпретация по умолчанию в байтовом режиме при отсутствии указания ECI менялась между редакциями стандарта. ISO/IEC 18004:2000, пункт 8.3.1, устанавливал: «The default interpretation for QR Code is ECI 000020 representing the JIS8 and Shift JIS character sets», но начиная с редакции 2006 года (QR Code 2005) по умолчанию используется ECI 000003, то есть ISO/IEC 8859-1. Это означает и то, что при опоре на необъявленное значение по умолчанию интерпретация может измениться просто из-за смены редакции стандарта 2

  4. GS1, GS1 General Specifications 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Как создать и эксплуатировать службу Windows — от выбора между Планировщиком заданий и службой до превращения BackgroundService в службу

Стоит ли держать постоянно работающую обработку как службу Windows или хватит Планировщика заданий. Практический разбор: таблица выбора, ...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Если выставить уровень коррекции ошибок H, ошибочные чтения исчезнут?
Нет. Более высокий уровень увеличивает объём загрязнения, которое можно исправить, но само явление — «коррекция срабатывает и даёт другое значение» — не устраняет. Декодирование Рида–Соломона работает так: в принятом шаблоне ищется кодовое слово внутри исправляющей способности, поэтому если повреждение попадает в окрестность другого кодового слова, именно оно и возвращается как ответ (повреждение, попавшее далеко, просто заканчивается «не читается»). ISO/IEC 18004 прямо указывает, что это «ошибочно декодируется как внешне допустимое, но другое кодовое слово», и отдельно резервирует кодовые слова для защиты от ошибочного декодирования, однако и эта мера лишь снижает вероятность, а не гарантирует. Поймать ошибочное чтение может только приложение, получившее значение.
Насколько часто ошибочные чтения QR случаются в реальности?
При случайном загрязнении — практически никогда. В измерениях для этой статьи из 3 900 попыток со случайным переворотом модулей и 5 800 попыток со случайным повреждением кодовых слов не было ни одного случая, когда вернулось неверное значение. Повреждение, выходящее за пределы исправляемого, попадает на сторону «не читается». Однако реальные повреждения не случайны: складки, истирание и забитые печатающие головки имеют смещение по положению. Более того, пропуск символа Structured Append или захват не того кода из нескольких — вообще не вопрос вероятности: при подходящих условиях это происходит каждый раз.
В QR-коде есть коррекция ошибок — значит, контрольная цифра уже не нужна?
Нужна. Они защищают разные слои. Коррекция ошибок отвечает за согласованность внутри символа и гарантирует восстановление только в пределах своей исправляющей способности. За этими пределами повреждение может «восстановиться» в другое допустимое кодовое слово. Контрольная цифра, напротив, проверяет, состоятельна ли полученная приложением строка как кодовая схема. Она надёжно ловит изменение одного символа, но пропускает часть случаев, когда меняется несколько символов. У контрольного значения по модулю 10 всего десять вариантов, и в этой статье приведён пример, где изменение двух цифр всё равно совпадает с контрольной цифрой. Поэтому рассматривайте контрольную цифру как слой, останавливающий большинство ошибочных чтений, а окончательное решение оставляйте сверке с мастер-данными и сопоставлению в бизнес-контексте. Одно из преимуществ в том, что та же проверка защищает и ручной ввод, и перепечатывание, и импорт, приходящий другими путями.
Приложение камеры на телефоне прочитало код — значит, значение верное?
«Прочиталось» означает лишь то, что декодер вернул непустую строку; о правильности содержимого это не говорит ничего. То, как сообщается о неудаче, тоже зависит от реализации: возможны и исключение, и пустая строка, и null, поэтому использовать «исключение не выброшено» как критерий успеха тоже опасно. В измерениях для этой статьи было несколько случаев, когда OpenCV и jsQR возвращали разные результаты для одного и того же изображения. Для QR-кода с японским текстом в Shift_JIS один вернул строку из кракозябр как успех, а другой — пустую строку. Результаты разошлись и на первом символе набора Structured Append. То, прочиталось ли значение, ничего не говорит о его правильности.
Стоит ли избегать Structured Append (составного QR) в бизнесе?
Если нет особой причины, избегать — более безопасный выбор. Structured Append — механизм, в котором данные разбиваются на несколько символов, а считыватель собирает и объединяет их, но поведение декодера, который его не поддерживает и получает только первый символ, зависит от реализации. В измерениях для этой статьи OpenCV вернул усечённый номер накладной без ошибки, а jsQR — пустую строку. Раз существуют реализации, которые пропускают фрагмент как правдоподобное значение, то, если вы не рассчитываете на Structured Append, нужна проверка, отбрасывающая неполные результаты. Если данные не помещаются, безопаснее поднять версию QR-кода или сократить код и опереться на выборку из мастер-данных.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог