TextDecoder: как читать UTF-8, если символ разрезан между чанками

TextDecoder: как читать UTF-8, если символ разрезан между чанками
Содержание статьи

Граница порции байтов не обязана совпадать с границей символа UTF-8. Для последовательных чанков используйте один TextDecoder и decode(chunk, { stream: true }), а после последней порции вызовите decode(). На Mentoring.digital разберём это на символе евро, разделённом между двумя массивами.

Почему отдельное декодирование портит текст

Знак евро кодируется тремя байтами: E2 82 AC. Первый учебный чанк содержит только два из них. Если декодировать его как законченный текст, декодер увидит неполную последовательность. Создание нового декодера для каждой порции лишает его состояния предыдущего чтения.

const chunks = [
  new Uint8Array([0xE2, 0x82]),
  new Uint8Array([0xAC, 0x21]),
];

const decoder = new TextDecoder("utf-8", { fatal: true });
let text = "";

for (const chunk of chunks) {
  text += decoder.decode(chunk, { stream: true });
}
text += decoder.decode();
console.log(text); // €!

При первом вызове декодер сохраняет незавершённый символ, при втором получает недостающий байт. Параметр stream сообщает, что продолжение ещё ожидается. Он не запускает чтение сети и сам не создаёт поток.

Зачем нужен финальный вызов decode

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

const decoder = new TextDecoder("utf-8", { fatal: true });
decoder.decode(new Uint8Array([0xE2]), { stream: true });

try {
  decoder.decode();
} catch (error) {
  console.log(error instanceof TypeError); // true
}

С fatal:true некорректная последовательность приводит к TypeError. В обычном режиме декодер использует символ замены. Выбор режима зависит от задачи: для строгого импорта повреждение данных может требовать отказа, а при отображении сообщения приложение может выбрать восстановление.

Не путайте декодирование и разбор сообщений

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

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

Как проверить обработчик

Закодируйте короткий русский текст через TextEncoder и разделите массив в каждой возможной позиции. Для каждого разделения результат должен совпасть с исходной строкой. Отдельно уберите последний байт многобайтового символа и проверьте реакцию выбранного режима.

Подобные упражнения полезны при изучении JavaScript. Разбор потоков и границ сообщений можно обсудить с ментором по JavaScript.

Похожие статьи по данной теме