Uint8Array в строку в Javascript

122

У меня есть данные в кодировке UTF-8, которые находятся в диапазоне элементов Uint8Array в Javascript. Есть ли эффективный способ декодировать их в обычную строку javascript (я считаю, что Javascript использует 16-битный Unicode)? Я не хочу добавлять по одному символу за раз, так как конкатернация строк будет сильно загружать процессор.

Джек Вестер
источник
Не уверен, что это сработает, но я использую u8array.toString()при чтении файлов из BrowserFS, которые открывают объект Uint8Array при вызове fs.readFile.
jcubic
1
@jcubic для меня, toStringпри Uint8Arrayвозврате чисел, разделенных запятыми, таких как "91,50,48,49,57,45"(Chrome 79)
Колен

Ответы:

172

TextEncoderи TextDecoderиз стандарта Encoding , который полифицируется библиотекой строкового кодирования , преобразует строки в ArrayBuffers:

var uint8array = new TextEncoder("utf-8").encode("¢");
var string = new TextDecoder("utf-8").decode(uint8array);
Винсент Шейб
источник
40
Для тех , кто ленивый , как я, npm install text-encoding, var textEncoding = require('text-encoding'); var TextDecoder = textEncoding.TextDecoder;. Нет, спасибо.
Эван Ху
16
остерегайтесь библиотеки кодирования текста npm, анализатор пакетов веб-пакетов показывает, что библиотека
ОГРОМНА
3
@VincentScheib Браузеры удалили поддержку любых других форматов, кроме utf-8. Итак, TextEncoderаргумент излишний!
tripulse
1
nodejs.org/api/string_decoder.html из примера: const {StringDecoder} = require ('string_decoder'); const decoder = новый StringDecoder ('utf8'); const cent = Buffer.from ([0xC2, 0xA2]); console.log (decoder.write (цент));
curist
4
Обратите внимание, что Node.js добавил TextEncoder/ TextDecoderAPI в v11, поэтому нет необходимости устанавливать какие-либо дополнительные пакеты, если вы нацелены только на текущие версии Node.
Loilo
42

Это должно работать:

// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt

/* utf.js - UTF-8 <=> UTF-16 convertion
 *
 * Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
 * Version: 1.0
 * LastModified: Dec 25 1999
 * This library is free.  You can redistribute it and/or modify it.
 */

function Utf8ArrayToStr(array) {
    var out, i, len, c;
    var char2, char3;

    out = "";
    len = array.length;
    i = 0;
    while(i < len) {
    c = array[i++];
    switch(c >> 4)
    { 
      case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
        // 0xxxxxxx
        out += String.fromCharCode(c);
        break;
      case 12: case 13:
        // 110x xxxx   10xx xxxx
        char2 = array[i++];
        out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
        break;
      case 14:
        // 1110 xxxx  10xx xxxx  10xx xxxx
        char2 = array[i++];
        char3 = array[i++];
        out += String.fromCharCode(((c & 0x0F) << 12) |
                       ((char2 & 0x3F) << 6) |
                       ((char3 & 0x3F) << 0));
        break;
    }
    }

    return out;
}

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

Посмотрите демонстрацию JSFiddle .

Также см. Связанные вопросы: здесь и здесь

Альберт
источник
6
Это кажется немного медленным. Но единственный фрагмент во вселенной, который я нашел, работает. Хорошая находка + усыновление!
Redsandro
6
Я не понимаю, почему за это не набирают больше голосов. Кажется в высшей степени разумным использовать соглашение UTF-8 для небольших фрагментов. Async Blob + Filereader отлично работает для больших текстов, как указывали другие.
DanHorner
2
Вопрос был в том, как это сделать без конкатенации строк
Джек Вестер
5
fromUTF8Array([240,159,154,133])fromUTF8Array([226,152,131])→"☃"
Отлично
1
Почему случаи 8, 9, 10 и 11 исключены? Какая-то конкретная причина? И случай 15 тоже возможен, правда? 15 (1111) будет означать, что используются 4 байта, не так ли?
RaR
31

Вот что я использую:

var str = String.fromCharCode.apply(null, uint8Arr);
dlchambers
источник
7
Из документа это, похоже, не декодирует UTF8.
Альберт
29
Это приведет RangeErrorк увеличению текста. «
Превышен
1
Если вы конвертируете большие массивы Uint8Arrays в двоичные строки и получаете RangeError, см. Функцию Uint8ToString на сайте stackoverflow.com/a/12713326/471341 .
yonran
IE 11 выдает, SCRIPT28: Out of stack spaceкогда я кормлю его 300 + k символов или RangeErrorдля Chrome 39. Firefox 33 в порядке. 100 + k работает нормально со всеми тремя.
Sheepy
Это не дает правильного результата из примеров символов Юникода на en.wikipedia.org/wiki/UTF-8 . например String.fromCharCode.apply (null, new Uint8Array ([0xc2, 0xa2])) не создает ¢.
Винсент Шейб
16

Найдено в одном из примеров приложений Chrome, хотя оно предназначено для больших блоков данных, в которых вас устраивает асинхронное преобразование.

/**
 * Converts an array buffer to a string
 *
 * @private
 * @param {ArrayBuffer} buf The buffer to convert
 * @param {Function} callback The function to call when conversion is complete
 */
function _arrayBufferToString(buf, callback) {
  var bb = new Blob([new Uint8Array(buf)]);
  var f = new FileReader();
  f.onload = function(e) {
    callback(e.target.result);
  };
  f.readAsText(bb);
}
Уилл Скотт
источник
2
Как вы сказали, это будет ужасно работать, если буфер для преобразования действительно не велик. Синхронное преобразование UTF-8 в wchar простой строки (скажем, 10-40 байтов), реализованное, скажем, в V8, должно быть намного меньше микросекунды, тогда как я предполагаю, что ваш код потребует в сотни раз больше. Все равно спасибо.
Джек Вестер
15

В Node « Bufferэкземпляры также являются Uint8Arrayэкземплярами », поэтому buf.toString()в этом случае работает.

kpowz
источник
У меня отлично работает. И так просто! Но на самом деле Uint8Array имеет метод toString ().
doom
Просто и элегантно, не знал Bufferтакже Uint8Array. Спасибо!
LeOn - Хан Ли
1
@doom На стороне браузера Uint8Array.toString () не будет компилировать строку utf-8, она перечислит числовые значения в массиве. Так что, если у вас есть Uint8Array из другого источника, который не является буфером, вам нужно будет создать его, чтобы творить чудеса:Buffer.from(uint8array).toString('utf-8')
Иоахим Лоус,
12

Решение, данное Альбертом, работает хорошо до тех пор, пока предоставленная функция вызывается нечасто и используется только для массивов небольшого размера, в противном случае это совершенно неэффективно. Вот усовершенствованное решение ванильного JavaScript, которое работает как для Node, так и для браузеров и имеет следующие преимущества:

• Эффективно работает для всех размеров массивов октетов.

• Не создает промежуточных отбрасываемых струн

• Поддерживает 4-байтовые символы на современных движках JS (в противном случае заменяется "?")

var utf8ArrayToStr = (function () {
    var charCache = new Array(128);  // Preallocate the cache for the common single byte chars
    var charFromCodePt = String.fromCodePoint || String.fromCharCode;
    var result = [];

    return function (array) {
        var codePt, byte1;
        var buffLen = array.length;

        result.length = 0;

        for (var i = 0; i < buffLen;) {
            byte1 = array[i++];

            if (byte1 <= 0x7F) {
                codePt = byte1;
            } else if (byte1 <= 0xDF) {
                codePt = ((byte1 & 0x1F) << 6) | (array[i++] & 0x3F);
            } else if (byte1 <= 0xEF) {
                codePt = ((byte1 & 0x0F) << 12) | ((array[i++] & 0x3F) << 6) | (array[i++] & 0x3F);
            } else if (String.fromCodePoint) {
                codePt = ((byte1 & 0x07) << 18) | ((array[i++] & 0x3F) << 12) | ((array[i++] & 0x3F) << 6) | (array[i++] & 0x3F);
            } else {
                codePt = 63;    // Cannot convert four byte code points, so use "?" instead
                i += 3;
            }

            result.push(charCache[codePt] || (charCache[codePt] = charFromCodePt(codePt)));
        }

        return result.join('');
    };
})();
Боб Арлоф
источник
2
Лучшее решение здесь, так как он также обрабатывает 4-байтовые символы (например, смайлы). Спасибо!
fiffy 08
1
а что обратное?
simbo1905
6

Сделайте то, что сказал @Sudhir, а затем, чтобы получить строку из списка чисел, разделенных запятыми, используйте:

for (var i=0; i<unitArr.byteLength; i++) {
            myString += String.fromCharCode(unitArr[i])
        }

Это даст вам нужную строку, если она все еще актуальна

Shuki
источник
Извините, я не заметил последнее предложение, в котором вы сказали, что не хотите добавлять по одному символу за раз. Надеюсь, это поможет другим, у кого нет проблем с загрузкой процессора.
shuki 03
14
Это не выполняет декодирование UTF8.
Альберт
Еще короче: String.fromCharCode.apply(null, unitArr);. Как уже упоминалось, он не обрабатывает кодировку UTF8, но иногда это бывает достаточно просто, если вам нужна только поддержка ASCII, но нет доступа к TextEncoder / TextDecoder.
Ravenstine
В ответе упоминается @Sudhir, но я поискал страницу и нашел такой ответ. Так что было бы лучше встроить все, что он сказал
Иоаким
Это будет ужасно работать на более длинных струнах. Не используйте оператор + для строк.
Макс
3

Если вы не можете использовать API TextDecoder, потому что он не поддерживается в IE :

  1. Вы можете использовать полифилл FastestSmallestTextEncoderDecoder, рекомендованный веб-сайтом Mozilla Developer Network ;
  2. Вы можете использовать эту функцию, также предоставленную на сайте MDN :

function utf8ArrayToString(aBytes) {
    var sView = "";
    
    for (var nPart, nLen = aBytes.length, nIdx = 0; nIdx < nLen; nIdx++) {
        nPart = aBytes[nIdx];
        
        sView += String.fromCharCode(
            nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? /* six bytes */
                /* (nPart - 252 << 30) may be not so safe in ECMAScript! So...: */
                (nPart - 252) * 1073741824 + (aBytes[++nIdx] - 128 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
            : nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? /* five bytes */
                (nPart - 248 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
            : nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? /* four bytes */
                (nPart - 240 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
            : nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? /* three bytes */
                (nPart - 224 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
            : nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? /* two bytes */
                (nPart - 192 << 6) + aBytes[++nIdx] - 128
            : /* nPart < 127 ? */ /* one byte */
                nPart
        );
    }
    
    return sView;
}

let str = utf8ArrayToString([50,72,226,130,130,32,43,32,79,226,130,130,32,226,135,140,32,50,72,226,130,130,79]);

// Must show 2H₂ + O₂ ⇌ 2H₂O
console.log(str);

Росберг Линьярес
источник
2

Попробуйте эти функции,

var JsonToArray = function(json)
{
    var str = JSON.stringify(json, null, 0);
    var ret = new Uint8Array(str.length);
    for (var i = 0; i < str.length; i++) {
        ret[i] = str.charCodeAt(i);
    }
    return ret
};

var binArrayToJson = function(binArray)
{
    var str = "";
    for (var i = 0; i < binArray.length; i++) {
        str += String.fromCharCode(parseInt(binArray[i]));
    }
    return JSON.parse(str)
}

источник: https://gist.github.com/tomfa/706d10fed78c497731ac , слава Tomfa

serdarsenay
источник
2

Я был разочарован, увидев, что люди не демонстрируют, как идти обоими путями, и не показывают, что все работает с нетривиальными строками UTF8. Я нашел сообщение на codereview.stackexchange.com, в котором есть код, который хорошо работает. Я использовал его, чтобы превратить древние руны в байты, проверить некоторую криптографию на байтах, а затем преобразовать вещи обратно в строку. Рабочий код находится на github здесь . Для наглядности переименовал методы:

// https://codereview.stackexchange.com/a/3589/75693
function bytesToSring(bytes) {
    var chars = [];
    for(var i = 0, n = bytes.length; i < n;) {
        chars.push(((bytes[i++] & 0xff) << 8) | (bytes[i++] & 0xff));
    }
    return String.fromCharCode.apply(null, chars);
}

// https://codereview.stackexchange.com/a/3589/75693
function stringToBytes(str) {
    var bytes = [];
    for(var i = 0, n = str.length; i < n; i++) {
        var char = str.charCodeAt(i);
        bytes.push(char >>> 8, char & 0xFF);
    }
    return bytes;
}

Модульный тест использует эту строку UTF-8:

    // http://kermitproject.org/utf8.html
    // From the Anglo-Saxon Rune Poem (Rune version) 
    const secretUtf8 = `ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ
ᛋᚳᛖᚪᛚ᛫ᚦᛖᚪᚻ᛫ᛗᚪᚾᚾᚪ᛫ᚷᛖᚻᚹᛦᛚᚳ᛫ᛗᛁᚳᛚᚢᚾ᛫ᚻᛦᛏ᛫ᛞᚫᛚᚪᚾ
ᚷᛁᚠ᛫ᚻᛖ᛫ᚹᛁᛚᛖ᛫ᚠᚩᚱ᛫ᛞᚱᛁᚻᛏᚾᛖ᛫ᛞᚩᛗᛖᛋ᛫ᚻᛚᛇᛏᚪᚾ᛬`;

Обратите внимание, что длина строки составляет всего 117 символов, но длина в байтах при кодировании составляет 234 байта.

Если я раскомментирую строки console.log, я вижу, что декодируемая строка - это та же строка, которая была закодирована (с байтами, переданными через алгоритм совместного использования секрета Шамира!):

модульный тест, демонстрирующий кодирование и декодирование

simbo1905
источник
String.fromCharCode.apply(null, chars)выдаст ошибку, если charsбудет слишком большим.
Марк Дж. Шмидт,
это везде или только в некоторых браузерах, и документировано ли это вообще?
simbo1905,
например, здесь developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… But beware: by using apply this way, you run the risk of exceeding the JavaScript engine's argument length limit. The consequences of applying a function with too many arguments (that is, more than tens of thousands of arguments) varies across engines. (The JavaScriptCore engine has hard-coded argument limit of 65536.
Марк Дж. Шмидт,
Спасибо. В моем случае я делал криптовалюту с небольшими строками, так что это не проблема. у вас есть исправление для длинных струн? :-)
simbo1905
1
Решение состоит в том, чтобы набрать 64 КБ символов.
Марк Дж. Шмидт,
1

В NodeJS есть буферы, и преобразование строк с их помощью очень просто. Более того, Uint8Array легко преобразовать в буфер. Попробуйте этот код, он работал у меня в Node практически для любого преобразования, связанного с Uint8Arrays:

let str = Buffer.from(uint8arr.buffer).toString();

Мы просто извлекаем ArrayBuffer из Uint8Array и затем конвертируем его в правильный буфер NodeJS. Затем мы конвертируем буфер в строку (вы можете использовать шестнадцатеричную кодировку или кодировку base64, если хотите).

Если мы хотим преобразовать обратно в Uint8Array из строки, мы сделаем следующее:

let uint8arr = new Uint8Array(Buffer.from(str));

Имейте в виду, что если вы объявили кодировку, такую ​​как base64, при преобразовании в строку, вам придется использовать, Buffer.from(str, "base64")если вы использовали base64, или любую другую кодировку, которую вы использовали.

Это не будет работать в браузере без модуля! Буферы NodeJS просто не существуют в браузере, поэтому этот метод не будет работать, если вы не добавите в браузер функциональность буфера. Это на самом деле очень легко сделать , хотя, просто использовать модуль как это , который является малым и быстро!

Arctic_Hen7
источник
0
class UTF8{
static encode(str:string){return new UTF8().encode(str)}
static decode(data:Uint8Array){return new UTF8().decode(data)}

private EOF_byte:number = -1;
private EOF_code_point:number = -1;
private encoderError(code_point) {
    console.error("UTF8 encoderError",code_point)
}
private decoderError(fatal, opt_code_point?):number {
    if (fatal) console.error("UTF8 decoderError",opt_code_point)
    return opt_code_point || 0xFFFD;
}
private inRange(a:number, min:number, max:number) {
    return min <= a && a <= max;
}
private div(n:number, d:number) {
    return Math.floor(n / d);
}
private stringToCodePoints(string:string) {
    /** @type {Array.<number>} */
    let cps = [];
    // Based on http://www.w3.org/TR/WebIDL/#idl-DOMString
    let i = 0, n = string.length;
    while (i < string.length) {
        let c = string.charCodeAt(i);
        if (!this.inRange(c, 0xD800, 0xDFFF)) {
            cps.push(c);
        } else if (this.inRange(c, 0xDC00, 0xDFFF)) {
            cps.push(0xFFFD);
        } else { // (inRange(c, 0xD800, 0xDBFF))
            if (i == n - 1) {
                cps.push(0xFFFD);
            } else {
                let d = string.charCodeAt(i + 1);
                if (this.inRange(d, 0xDC00, 0xDFFF)) {
                    let a = c & 0x3FF;
                    let b = d & 0x3FF;
                    i += 1;
                    cps.push(0x10000 + (a << 10) + b);
                } else {
                    cps.push(0xFFFD);
                }
            }
        }
        i += 1;
    }
    return cps;
}

private encode(str:string):Uint8Array {
    let pos:number = 0;
    let codePoints = this.stringToCodePoints(str);
    let outputBytes = [];

    while (codePoints.length > pos) {
        let code_point:number = codePoints[pos++];

        if (this.inRange(code_point, 0xD800, 0xDFFF)) {
            this.encoderError(code_point);
        }
        else if (this.inRange(code_point, 0x0000, 0x007f)) {
            outputBytes.push(code_point);
        } else {
            let count = 0, offset = 0;
            if (this.inRange(code_point, 0x0080, 0x07FF)) {
                count = 1;
                offset = 0xC0;
            } else if (this.inRange(code_point, 0x0800, 0xFFFF)) {
                count = 2;
                offset = 0xE0;
            } else if (this.inRange(code_point, 0x10000, 0x10FFFF)) {
                count = 3;
                offset = 0xF0;
            }

            outputBytes.push(this.div(code_point, Math.pow(64, count)) + offset);

            while (count > 0) {
                let temp = this.div(code_point, Math.pow(64, count - 1));
                outputBytes.push(0x80 + (temp % 64));
                count -= 1;
            }
        }
    }
    return new Uint8Array(outputBytes);
}

private decode(data:Uint8Array):string {
    let fatal:boolean = false;
    let pos:number = 0;
    let result:string = "";
    let code_point:number;
    let utf8_code_point = 0;
    let utf8_bytes_needed = 0;
    let utf8_bytes_seen = 0;
    let utf8_lower_boundary = 0;

    while (data.length > pos) {
        let _byte = data[pos++];

        if (_byte == this.EOF_byte) {
            if (utf8_bytes_needed != 0) {
                code_point = this.decoderError(fatal);
            } else {
                code_point = this.EOF_code_point;
            }
        } else {
            if (utf8_bytes_needed == 0) {
                if (this.inRange(_byte, 0x00, 0x7F)) {
                    code_point = _byte;
                } else {
                    if (this.inRange(_byte, 0xC2, 0xDF)) {
                        utf8_bytes_needed = 1;
                        utf8_lower_boundary = 0x80;
                        utf8_code_point = _byte - 0xC0;
                    } else if (this.inRange(_byte, 0xE0, 0xEF)) {
                        utf8_bytes_needed = 2;
                        utf8_lower_boundary = 0x800;
                        utf8_code_point = _byte - 0xE0;
                    } else if (this.inRange(_byte, 0xF0, 0xF4)) {
                        utf8_bytes_needed = 3;
                        utf8_lower_boundary = 0x10000;
                        utf8_code_point = _byte - 0xF0;
                    } else {
                        this.decoderError(fatal);
                    }
                    utf8_code_point = utf8_code_point * Math.pow(64, utf8_bytes_needed);
                    code_point = null;
                }
            } else if (!this.inRange(_byte, 0x80, 0xBF)) {
                utf8_code_point = 0;
                utf8_bytes_needed = 0;
                utf8_bytes_seen = 0;
                utf8_lower_boundary = 0;
                pos--;
                code_point = this.decoderError(fatal, _byte);
            } else {
                utf8_bytes_seen += 1;
                utf8_code_point = utf8_code_point + (_byte - 0x80) * Math.pow(64, utf8_bytes_needed - utf8_bytes_seen);

                if (utf8_bytes_seen !== utf8_bytes_needed) {
                    code_point = null;
                } else {
                    let cp = utf8_code_point;
                    let lower_boundary = utf8_lower_boundary;
                    utf8_code_point = 0;
                    utf8_bytes_needed = 0;
                    utf8_bytes_seen = 0;
                    utf8_lower_boundary = 0;
                    if (this.inRange(cp, lower_boundary, 0x10FFFF) && !this.inRange(cp, 0xD800, 0xDFFF)) {
                        code_point = cp;
                    } else {
                        code_point = this.decoderError(fatal, _byte);
                    }
                }

            }
        }
        //Decode string
        if (code_point !== null && code_point !== this.EOF_code_point) {
            if (code_point <= 0xFFFF) {
                if (code_point > 0)result += String.fromCharCode(code_point);
            } else {
                code_point -= 0x10000;
                result += String.fromCharCode(0xD800 + ((code_point >> 10) & 0x3ff));
                result += String.fromCharCode(0xDC00 + (code_point & 0x3ff));
            }
        }
    }
    return result;
}

`

терран
источник
Добавьте описание к ответу. @terran
Рохит
-3

Я использую этот фрагмент Typescript:

function UInt8ArrayToString(uInt8Array: Uint8Array): string
{
    var s: string = "[";
    for(var i: number = 0; i < uInt8Array.byteLength; i++)
    {
        if( i > 0 )
            s += ", ";
        s += uInt8Array[i];
    }
    s += "]";
    return s;
}

Удалите аннотации типов, если вам нужна версия JavaScript. Надеюсь это поможет!

Бернд Паради
источник
3
OP попросил не добавлять по одному символу за раз. Кроме того, он не хочет отображать его как строковое представление списка, а скорее как строку. Кроме того, это не преобразует символы в строку, а отображает ее номер.
Альберт