Uint8Array to string en Javascript


122

J'ai des données encodées en UTF-8 vivant dans une gamme d'éléments Uint8Array en Javascript. Existe-t-il un moyen efficace de les décoder en une chaîne javascript régulière (je crois que Javascript utilise Unicode 16 bits)? Je ne veux pas ajouter un caractère à la fois car la concaternation de chaînes deviendrait trop gourmande en CPU.


Je ne sais pas si cela fonctionnera, mais je l'utilise u8array.toString()lors de la lecture de fichiers à partir de BrowserFS qui exposent l'objet Uint8Array lorsque vous appelez fs.readFile.
jcubic

1
@jcubic pour moi, toStringsur Uint8Arrayrenvoie des nombres séparés par des virgules tels que "91,50,48,49,57,45"(Chrome 79)
kolen

Réponses:


172

TextEncoderet TextDecoderdu standard Encoding , qui est polyfilled par la bibliothèque stringencoding , convertit entre les chaînes et les ArrayBuffers:

var uint8array = new TextEncoder("utf-8").encode("¢");
var string = new TextDecoder("utf-8").decode(uint8array);

40
Pour tout le monde paresseux comme moi, npm install text-encoding, var textEncoding = require('text-encoding'); var TextDecoder = textEncoding.TextDecoder;. Non merci.
Evan Hu

16
méfiez-vous de la bibliothèque de codage de texte npm, l'analyseur de bundle webpack montre que la bibliothèque est ÉNORME
wayofthefuture

3
@VincentScheib Browsers a supprimé la prise en charge de tous les autres formats sauf utf-8. Donc, l' TextEncoderargument est inutile!
tripulse

1
nodejs.org/api/string_decoder.html de l'exemple: const {StringDecoder} = require ('string_decoder'); décodeur const = new StringDecoder ('utf8'); const cent = Buffer.from ([0xC2, 0xA2]); console.log (décodeur.écriture (cent));
curiste

4
Notez que Node.js a ajouté les TextEncoder/ TextDecoderAPI dans la v11, donc pas besoin d'installer de packages supplémentaires si vous ciblez uniquement les versions actuelles de Node.
Loilo du

42

Cela devrait fonctionner:

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

C'est un peu plus propre que les autres solutions car il n'utilise pas de hacks et ne dépend pas des fonctions du navigateur JS, par exemple fonctionne également dans d'autres environnements JS.

Découvrez la démo de JSFiddle .

Voir également les questions associées: ici et ici


6
Cela semble un peu lent. Mais le seul extrait de l'univers que j'ai trouvé fonctionne. Bonne trouvaille + adoption!
Redsandro

6
Je ne comprends pas pourquoi il n'y a pas plus de votes positifs. Il semble tout à fait judicieux de passer par la convention UTF-8 pour les petits extraits. Async Blob + Filereader fonctionne très bien pour les gros textes comme d'autres l'ont indiqué.
DanHorner

2
La question était de savoir comment faire cela sans concaténation de chaînes
Jack Wester

5
Fonctionne très bien, sauf qu'il ne gère pas les séquences de 4 octets et plus, par exemple fromUTF8Array([240,159,154,133])s'avère vide (while fromUTF8Array([226,152,131])→"☃")
unhammer

1
Pourquoi les cas 8, 9, 10 et 11 sont-ils exclus? N'importe quelle raison? Et le cas 15 est également possible, non? 15 (1111) signifiera que 4 octets sont utilisés, n'est-ce pas?
RaR

31

Voici ce que j'utilise:

var str = String.fromCharCode.apply(null, uint8Arr);

7
D'après la doc , cela ne semble pas décoder UTF8.
Albert

29
Cela jettera RangeErrorsur des textes plus gros. "
Dépassement de la

1
Si vous convertissez des Uint8Arrays volumineux en chaînes binaires et que vous obtenez RangeError, consultez la fonction Uint8ToString de stackoverflow.com/a/12713326/471341 .
yonran

IE 11 jette SCRIPT28: Out of stack spacequand je lui donne 300 + k caractères, ou RangeErrorpour Chrome 39. Firefox 33 est ok. 100 + k fonctionne bien avec les trois.
Sheepy

Cela ne produit pas le résultat correct à partir des exemples de caractères Unicode sur en.wikipedia.org/wiki/UTF-8 . par exemple String.fromCharCode.apply (null, new Uint8Array ([0xc2, 0xa2])) ne produit pas ¢.
Vincent Scheib

16

Trouvé dans l'un des exemples d'applications Chrome, bien que cela soit destiné à des blocs de données plus volumineux pour lesquels vous êtes d'accord avec une conversion asynchrone.

/**
 * 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
Comme vous l'avez dit, cela fonctionnerait terriblement à moins que le tampon à convertir ne soit vraiment énorme. La conversion UTF-8 synchrone à wchar d'une chaîne simple (disons 10-40 octets) implémentée dans, disons, V8 devrait être beaucoup moins d'une microseconde alors que je suppose que votre code en exigerait des centaines de fois. Merci tout de même.
Jack Wester

15

Dans Node, "les Bufferinstances sont également des Uint8Arrayinstances ", donc cela buf.toString()fonctionne dans ce cas.


Fonctionne très bien pour moi. Et si simple! Mais en fait, Uint8Array a la méthode toString ().
doom

Simple et élégant, n'était pas au courant Bufferest également Uint8Array. Merci!
LeOn - Han Li

1
@doom Du côté du navigateur, Uint8Array.toString () ne compilera pas de chaîne utf-8, il listera les valeurs numériques dans le tableau. Donc, si ce que vous avez est un Uint8Array d'une autre source qui ne se trouve pas être également un Buffer, vous devrez en créer un pour faire la magie:Buffer.from(uint8array).toString('utf-8')
Joachim Lous

12

La solution proposée par Albert fonctionne bien tant que la fonction fournie est rarement invoquée et n'est utilisée que pour des tableaux de taille modeste, sinon elle est extrêmement inefficace. Voici une solution JavaScript vanilla améliorée qui fonctionne à la fois pour Node et les navigateurs et présente les avantages suivants:

• Fonctionne efficacement pour toutes les tailles de tableaux d’octets

• Ne génère aucune chaîne intermédiaire jetable

• Prend en charge les caractères de 4 octets sur les moteurs JS modernes (sinon "?" Est remplacé)

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
La meilleure solution ici, car il gère également les caractères de 4 octets (par exemple les émojis) Merci!
fiffy

1
et quel est l'inverse de cela?
simbo1905

6

Faites ce que @Sudhir a dit, puis pour extraire une chaîne de la liste de nombres séparés par des virgules, utilisez:

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

Cela vous donnera la chaîne que vous voulez, si elle est toujours pertinente


Désolé, je n'ai pas remarqué le dernier sens dans lequel vous avez dit que vous ne voulez pas ajouter un caractère à la fois. J'espère que cela aidera les autres qui n'ont pas de problème avec l'utilisation du processeur.
shuki

14
Cela ne fait pas le décodage UTF8.
Albert

Encore plus court: String.fromCharCode.apply(null, unitArr);. Comme mentionné, il ne gère pas l'encodage UTF8, mais parfois c'est assez simple si vous n'avez besoin que du support ASCII mais n'avez pas accès à TextEncoder / TextDecoder.
Ravenstine

La réponse mentionne un @Sudhir mais j'ai cherché la page et j'ai trouvé maintenant une telle réponse. Il serait donc préférable d'intégrer tout ce qu'il a dit
Joakim

Cela aura des performances terribles sur des cordes plus longues. N'utilisez pas l'opérateur + sur les chaînes.
Max

3

Si vous ne pouvez pas utiliser l' API TextDecoder car elle n'est pas prise en charge sur IE :

  1. Vous pouvez utiliser le polyfill FastestSmallestTextEncoderDecoder recommandé par le site Web du Mozilla Developer Network ;
  2. Vous pouvez utiliser cette fonction également fournie sur le site Web de 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

Essayez ces fonctions,

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

source: https://gist.github.com/tomfa/706d10fed78c497731ac , bravo à Tomfa


2

J'étais frustré de voir que les gens ne montraient pas comment aller dans les deux sens ou ne montraient pas que les choses ne fonctionnent pas sur des chaînes UTF8 triviales. J'ai trouvé un article sur codereview.stackexchange.com qui contient du code qui fonctionne bien. Je l'ai utilisé pour transformer d'anciennes runes en octets, pour tester du crypo sur les octets, puis reconvertir les choses en une chaîne. Le code de travail est sur github ici . J'ai renommé les méthodes pour plus de clarté:

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

Le test unitaire utilise cette chaîne UTF-8:

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

Notez que la longueur de la chaîne n'est que de 117 caractères mais que la longueur d'octet, une fois codée, est de 234.

Si je décommente les lignes console.log, je peux voir que la chaîne qui est décodée est la même chaîne qui a été encodée (avec les octets passés via l'algorithme de partage de secret de Shamir!):

test unitaire qui démontre l'encodage et le décodage


String.fromCharCode.apply(null, chars)sera une erreur si charsest trop grand.
Marc J. Schmidt le

est-ce partout ou juste certains navigateurs et est-ce documenté du tout?
simbo1905

par exemple ici 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.
Marc J. Schmidt

Merci. dans mon cas, je faisais de la cryptographie sur de petites chaînes, donc pas de problème. avez-vous une solution pour les longues chaînes? :-)
simbo1905 il y a

la solution consiste à regrouper 64 000 caractères.
Marc J. Schmidt il y a

1

Dans NodeJS, nous avons des tampons disponibles, et la conversion de chaînes avec eux est vraiment facile. Mieux, il est facile de convertir un Uint8Array en Buffer. Essayez ce code, cela a fonctionné pour moi dans Node pour pratiquement toute conversion impliquant Uint8Arrays:

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

Nous extrayons simplement le ArrayBuffer du Uint8Array, puis le convertissons en un tampon NodeJS approprié. Ensuite, nous convertissons le tampon en une chaîne (vous pouvez ajouter un encodage hexadécimal ou base64 si vous le souhaitez).

Si nous voulons reconvertir en Uint8Array à partir d'une chaîne, nous le ferons:

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

Sachez que si vous avez déclaré un encodage comme base64 lors de la conversion en chaîne, vous devrez l'utiliser Buffer.from(str, "base64")si vous avez utilisé base64, ou tout autre encodage que vous avez utilisé.

Cela ne fonctionnera pas dans le navigateur sans module! Les tampons NodeJS n'existent tout simplement pas dans le navigateur, donc cette méthode ne fonctionnera que si vous ajoutez la fonctionnalité de tampon au navigateur. C'est en fait assez facile à faire, utilisez simplement un module comme celui-ci , à la fois petit et rapide!


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

»


Ajoutez une description pour répondre. @terran
Rohit Poudel

-3

J'utilise cet extrait de code 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;
}

Supprimez les annotations de type si vous avez besoin de la version JavaScript. J'espère que cela t'aides!


3
L'OP a demandé de ne pas ajouter un caractère à la fois. De plus, il ne veut pas l'afficher comme une représentation sous forme de chaîne de liste, mais plutôt simplement comme une chaîne. De plus, cela ne convertit pas les caractères en chaîne mais affiche son numéro.
Albert
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.