Uint8Array para string em Javascript

122

Tenho alguns dados codificados em UTF-8 em uma variedade de elementos Uint8Array em Javascript. Existe uma maneira eficiente de decodificá-los em uma string javascript regular (acredito que o Javascript usa Unicode de 16 bits)? Não quero adicionar um caractere de cada vez, pois a concatenação de strings se tornaria muito intensa na CPU.

Jack Wester
fonte
Não tenho certeza se vai funcionar, mas eu uso u8array.toString()ao ler arquivos do BrowserFS que expõem o objeto Uint8Array quando você chama fs.readFile.
jcubic
1
@jcubic para mim, toStringem Uint8Arrayvolta números separados por vírgulas, tais como "91,50,48,49,57,45"(79 Chrome)
Kolen

Respostas:

171

TextEncodere TextDecoderdo padrão de codificação , que é polyfilled pela biblioteca stringencoding , converte entre strings e ArrayBuffers:

var uint8array = new TextEncoder("utf-8").encode("¢");
var string = new TextDecoder("utf-8").decode(uint8array);
Vincent Scheib
fonte
39
Para qualquer um preguiçoso como eu, npm install text-encoding, var textEncoding = require('text-encoding'); var TextDecoder = textEncoding.TextDecoder;. Não, obrigado.
Evan Hu
16
cuidado com a biblioteca de codificação de texto NPM, Webpack feixe analisador mostra a biblioteca é enorme
wayofthefuture
3
Os navegadores @VincentScheib removeram o suporte para qualquer outro formato, exceto utf-8. Portanto, o TextEncoderargumento é desnecessário!
tripulse
1
nodejs.org/api/string_decoder.html do exemplo: const {StringDecoder} = require ('string_decoder'); decodificador const = new StringDecoder ('utf8'); cent constante = Buffer.from ([0xC2, 0xA2]); console.log (decoder.write (cent));
curista de
4
Observe que o Node.js adicionou as TextEncoder/ TextDecoderAPIs na v11, portanto, não há necessidade de instalar nenhum pacote extra se você direcionar apenas as versões atuais do Node.
Loilo
42

Isso deve funcionar:

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

/* utf.js - UTF-8 <=> UTF-16 convertion
 *
 * Copyright (C) 1999 Masanao Izumo <[email protected]>
 * 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;
}

É um pouco mais limpo como as outras soluções porque não usa nenhum hacks nem depende das funções do navegador JS, por exemplo, funciona também em outros ambientes JS.

Confira a demonstração JSFiddle .

Veja também as questões relacionadas: aqui e aqui

Albert
fonte
6
Isso parece meio lento. Mas o único fragmento no universo que encontrei que funciona. Boa descoberta + adoção!
Redsandro
6
Eu não entendo por que isso não tem mais votos positivos. Parece eminentemente sensato utilizar a convenção UTF-8 para pequenos trechos. Async Blob + Filereader funciona muito bem para textos grandes, como outros indicaram.
DanHorner
2
A questão era como fazer isso sem concatenação de strings
Jack Wester
5
Funciona muito bem, exceto que não lida com sequências de 4 ou mais bytes, por exemplo, fromUTF8Array([240,159,154,133])fica vazio (enquanto fromUTF8Array([226,152,131])→"☃")
descompactar
1
Por que os casos 8, 9, 10 e 11 são excluídos? Algum motivo particular? E o caso 15 também é possível, certo? 15 (1111) denotará que 4 bytes são usados, não é?
RaR de
31

Aqui está o que eu uso:

var str = String.fromCharCode.apply(null, uint8Arr);
dlchambers
fonte
7
Do doc , isso não parece decodificar UTF8.
Albert
29
Isso gerará RangeErrortextos maiores. "Tamanho máximo da pilha de chamadas excedido"
Redsandro
1
Se você estiver convertendo Uint8Arrays grandes em strings binárias e estiver obtendo RangeError, consulte a função Uint8ToString em stackoverflow.com/a/12713326/471341 .
yonran,
O IE 11 lança SCRIPT28: Out of stack spacequando eu o alimentar com 300 + k caracteres, ou RangeErrorpara o Chrome 39. Firefox 33 está ok. 100 + k funciona bem com todos os três.
Sheepy
Isso não produz o resultado correto dos caracteres Unicode de exemplo em en.wikipedia.org/wiki/UTF-8 . por exemplo, String.fromCharCode.apply (null, new Uint8Array ([0xc2, 0xa2])) não produz ¢.
Vincent Scheib
16

Encontrado em um dos aplicativos de amostra do Chrome, embora seja destinado a grandes blocos de dados em que você está bem com uma conversão assíncrona.

/**
 * 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);
}
Will Scott
fonte
2
Como você disse, isso teria um desempenho terrível, a menos que o buffer a ser convertido fosse realmente muito grande. A conversão síncrona de UTF-8 em wchar de uma string simples (digamos 10-40 bytes) implementada em, digamos, V8 deve ser muito menos do que um microssegundo, enquanto eu suporia que seu código exigiria centenas de vezes isso. Agradece a todos a mesma.
Jack Wester
15

No Node " Bufferinstâncias também são Uint8Arrayinstâncias ", então buf.toString()funciona neste caso.

kpowz
fonte
Funciona muito bem para mim. E tão simples! Mas, na verdade, Uint8Array tem o método toString ().
condenação
Simples e elegante, não sabia Buffertambém é Uint8Array. Obrigado!
LeOn - Han Li
1
@doom No lado do navegador, Uint8Array.toString () não irá compilar uma string utf-8, ele irá listar os valores numéricos no array. Então, se o que você tem é um Uint8Array de outra fonte que não é também um Buffer, você precisará criar um para fazer a mágica:Buffer.from(uint8array).toString('utf-8')
Joachim Lous
12

A solução fornecida por Albert funciona bem, desde que a função fornecida seja invocada com pouca frequência e seja usada apenas para arrays de tamanho modesto, caso contrário, é notoriamente ineficiente. Aqui está uma solução JavaScript vanilla aprimorada que funciona para Node e navegadores e tem as seguintes vantagens:

• Funciona de forma eficiente para todos os tamanhos de matriz de octeto

• Não gera strings descartáveis ​​intermediárias

• Suporta caracteres de 4 bytes em motores JS modernos (caso contrário, "?" É substituído)

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('');
    };
})();
Bob Arlof
fonte
2
A melhor solução aqui, já que também lida com caracteres de 4 bytes (por exemplo, emojis). Obrigado!
5 de
1
e qual é o inverso disso?
simbo1905
6

Faça o que @Sudhir disse e, para obter uma string da lista de números separados por vírgulas, use:

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

Isso lhe dará a string que você deseja, se ainda for relevante

shuki
fonte
Desculpe, não notei a última sentença em que você disse que não deseja adicionar um caractere de cada vez. Espero que isso ajude outras pessoas que não têm problemas com o uso da CPU.
shuki,
14
Isso não faz decodificação UTF8.
Albert
Ainda mais curto: String.fromCharCode.apply(null, unitArr);. Conforme mencionado, ele não lida com a codificação UTF8, mas às vezes isso é simples o suficiente se você precisar apenas de suporte ASCII, mas não tiver acesso a TextEncoder / TextDecoder.
Ravenstine,
A resposta menciona um @Sudhir, mas eu procurei na página e encontrei agora essa resposta. Portanto, seria melhor inserir o que ele disse
Joakim
Isso terá um desempenho terrível em cordas mais longas. Não use o operador + em strings.
Máx.
3

Se você não puder usar a API TextDecoder porque ela não é compatível com o IE :

  1. Você pode usar o polyfill FastestSmallestTextEncoderDecoder recomendado pelo site da Mozilla Developer Network ;
  2. Você pode usar esta função também fornecida no site do 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);

Rosberg Linhares
fonte
2

Experimente essas funções,

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)
}

fonte: https://gist.github.com/tomfa/706d10fed78c497731ac , parabéns para Tomfa

Serdarsenay
fonte
2

Fiquei frustrado ao ver que as pessoas não estavam mostrando como fazer as duas coisas ou mostrando que as coisas funcionam em strings UTF8 não triviais. Eu encontrei uma postagem no codereview.stackexchange.com que contém um código que funciona bem. Usei-o para transformar runas antigas em bytes, para testar alguns crypo nos bytes e, em seguida, converter as coisas de volta em uma string. O código de trabalho está no github aqui . Renomeei os métodos para maior clareza:

// 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;
}

O teste de unidade usa esta string UTF-8:

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

Observe que o comprimento da string é de apenas 117 caracteres, mas o comprimento do byte, quando codificado, é 234.

Se eu descomentar as linhas console.log, posso ver que a string decodificada é a mesma string que foi codificada (com os bytes passados ​​pelo algoritmo de compartilhamento secreto de Shamir!):

teste de unidade que demonstra codificação e decodificação

simbo1905
fonte
String.fromCharCode.apply(null, chars)irá errar se charsfor muito grande.
Marc J. Schmidt
1

No NodeJS, temos Buffers disponíveis, e a conversão de strings com eles é realmente fácil. Melhor, é fácil converter um Uint8Array em Buffer. Tente este código, ele funcionou para mim no Node para basicamente qualquer conversão envolvendo Uint8Arrays:

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

Estamos apenas extraindo o ArrayBuffer do Uint8Array e, em seguida, convertendo-o em um Buffer NodeJS adequado. Em seguida, convertemos o Buffer em uma string (você pode adicionar uma codificação hex ou base64, se desejar).

Se quisermos converter de volta para um Uint8Array a partir de uma string, faremos o seguinte:

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

Esteja ciente de que se você declarou uma codificação como base64 ao converter para uma string, então você teria que usar Buffer.from(str, "base64") se usou base64, ou qualquer outra codificação que você usou.

Isso não funcionará no navegador sem um módulo! Buffers NodeJS simplesmente não existem no navegador, portanto, este método não funcionará a menos que você adicione a funcionalidade Buffer ao navegador. Na verdade, isso é muito fácil de fazer, basta usar um módulo como este , que é pequeno e rápido!

Arctic_Hen7
fonte
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;
}

`

terrano
fonte
Adicione alguma descrição para responder. @terran
Rohit Poudel
-3

Estou usando este snippet de 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;
}

Remova as anotações de tipo se precisar da versão JavaScript. Espero que isto ajude!

Bernd Paradies
fonte
3
O OP pediu para não adicionar um caractere de cada vez. Além disso, ele não deseja exibi-lo como uma representação de string de lista, mas apenas como uma string. Além disso, isso não converte os caracteres em string, mas exibe seu número.
Albert