StringDecoder: UTF-8 в стримах
Декодер байтов в строку, который буферизует неполные multibyte UTF-8 символы на границе чанков. Без него ручная склейка
chunk.toString()ломает кириллицу/эмодзи.
Зачем
UTF-8 — variable-width: 1 байт для ASCII, 2 для кириллицы, 3 для большинства не-латиницы, 4 для эмодзи. Когда стрим читает файл буферами по 64 КБ, граница чанка может попасть в середину codepoint:
// Плохо
let text = '';
stream.on('data', (chunk) => {
text += chunk.toString('utf8'); // chunk может закончиться на половине 'ё'
});
// результат: «знак замены» (U+FFFD) на стыке чанков
StringDecoder помнит «хвост» неполного символа и приклеивает к началу следующего чанка.
API
const { StringDecoder } = require('node:string_decoder');
const decoder = new StringDecoder('utf8');
stream.on('data', (chunk) => {
text += decoder.write(chunk); // вернёт только полные codepoint
});
stream.on('end', () => {
text += decoder.end(); // оставшийся хвост (если есть)
});
Использовать готовое: setEncoding
В большинстве случаев StringDecoder не вызывают напрямую — это внутренность стримов. Достаточно сказать стриму кодировку:
const stream = fs.createReadStream('big.txt', { encoding: 'utf8' });
// или
stream.setEncoding('utf8');
stream.on('data', (chunk) => {
// chunk — уже строка, склеена правильно (StringDecoder под капотом)
});
Юникод: codepoint ≠ символ
- JS строки внутри — UTF-16: 4-байтовый codepoint = 2 surrogate (
'😀'.length === 2) .lengthсчитает code units, не codepoints и не графемы- В Unicode вообще нет понятия «символ»: есть code points, графемы, графемные кластеры (например, эмодзи флага = 2 regional indicator + ZWJ)
- Для подсчёта «настоящих символов» —
Intl.Segmenterили[...str].length
Подводные камни
- Ручной
chunk.toString('utf8')без StringDecoder → битые символы на границах - Передача между Worker'ами через
postMessageсо строкой — структурное клонирование сохраняет UTF-16 корректно, проблема только при работе с сырыми байтами setEncoding('utf8')нельзя менять на лету — после первогоreadповедение неопределено
🎓 Источник: Архив 2018 — Часть 17 Потоки (Streams) в Node.js
- 📅 2020-01-15 · YouTube ·
3ZRkNvs_SaE - Тезисы:
- Для строк лучше
setEncoding('utf8')вместоtoStringпосле получения чанка - Под капотом —
StringDecoderиз stdlib: один codepoint UTF-8 занимает 1–4 байта - Ручная склейка байтов рвёт codepoint на границе чанков → невалидная UTF-8 строка
- StringDecoder буферизует неполный символ и склеивает с началом следующего чанка
- JS использует UTF-16: 4-байтовый codepoint представлен двумя surrogate-парами
- В Unicode нет «символа» — есть графемы, графемные кластеры, codepoints;
.lengthсчитает code units
- Для строк лучше
- Цитата:
«Если будете по кусочкам эти потоки байтов склеивать, то, что у вас получится в итоге, может быть невалидной UTF-8 строкой.»