unicode - Stränglängd i byte i JavaScript


I min JavaScript-kod behöver jag komponera ett meddelande till servern i detta format:

<size in bytes>CRLF
<data>CRLF

Exempel :

3
foo

Datan kan innehålla Unicode-tecken. Jag behöver skicka dem som UTF-8.

Jag letar efter det mest webbläsaroberoende sättet att beräkna längden på strängen i byte i JavaScript.

Jag har försökt detta för att komponera min nyttolast:

return unescape(encodeURIComponent(str)).length + "\n" + str + "\n"

Men det ger mig inte exakta resultat för de äldre webbläsarna (eller kanske är strängarna i de webbläsarna i UTF-16?).

Några ledtrådar?

Uppdatering:

Exempel: längden i byte av strängen ЭЭХ! Naiv? i UTF-8 är 15 byte, men vissa webbläsare rapporterar 23 byte istället.

Svar #1

Åren gick och numera kan du göra det på ett naturligt sätt

const textEncoder = new TextEncoder();
console.log(textEncoder.encode('foo').length);

Observera att det inte stöds av IE (du kan använda en polyfill för det).

MDN-dokumentation

Standard specifikationer

Svar #2

Det finns inget sätt att göra det i JavaScript nativt. (Se Riccardo Gallis svar för en modern metod.)

För historisk referens eller där TextEncoder API:er fortfarande inte är tillgängliga.

Om du känner till teckenkodningen kan du dock räkna ut det själv.

encodeURIComponent antar UTF-8 som teckenkodning, så om du behöver den kodningen kan du göra,

function lengthInUtf8Bytes(str) {
  // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence.
  var m = encodeURIComponent(str).match(/%[89ABab]/g);
  return str.length + (m ? m.length : 0);
}

Detta borde fungera på grund av hur UTF-8 kodar flermängdiga byte-sekvenser. Den första kodade byten börjar alltid antingen med en hög bit av noll för en enkel byte-sekvens, eller en byte vars första hexadecimala siffra är C, D, E eller F. De andra och efterföljande byten är de vars första två bitar är 10. Det är dessa extra byten du vill räkna i UTF-8.

Tabellen på Wikipedia gör det tydligare

Bits        Last code point Byte 1          Byte 2          Byte 3
  7         U+007F          0xxxxxxx
 11         U+07FF          110xxxxx        10xxxxxx
 16         U+FFFF          1110xxxx        10xxxxxx        10xxxxxx
...

Om du istället behöver förstå sidans kodning kan du använda detta trick:

function lengthInPageEncoding(s) {
  var a = document.createElement('A');
  a.href = '#' + s;
  var sEncoded = a.href;
  sEncoded = sEncoded.substring(sEncoded.indexOf('#') + 1);
  var m = sEncoded.match(/%[0-9a-f]{2}/g);
  return sEncoded.length - (m ? m.length * 2 : 0);
}

Svar #3

För enkel UTF-8-kodning, med något bättre kompatibilitet än TextEncoder, fungerar Blob. Fungerar dock inte i mycket gamla webbläsare.

const blob = new Blob(["😀"]);
console.log(blob.size);

Svar #4

Här är en mycket snabbare version, som varken använder reguljära uttryck eller encodeURIComponent():

function byteLength(str) {
  // returns the byte length of an utf8 string
  var s = str.length;
  for (var i=str.length-1; i>=0; i--) {
    var code = str.charCodeAt(i);
    if (code > 0x7f && code <= 0x7ff) s++;
    else if (code > 0x7ff && code <= 0xffff) s+=2;
    if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
  }
  return s;
}

Här är en prestanda-jämförelse.

Det beräknar bara längden i UTF8 för varje Unicode-kodpunkt som returneras av charCodeAt() (baserat på Wikipedias beskrivningar av UTF8 och UTF16-surrogattecken).

Den följer RFC3629 (där UTF-8-tecken är högst 4 byte långa).

Svar #5

Ett annat mycket enkelt tillvägagångssätt med Buffer (endast för NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length

Svar #6

Denna funktion kommer att returnera storleken i byte för vilken UTF-8-sträng som helst du skickar till den.

function byteCount(s) {
    return encodeURI(s).split(/%..|./).length - 1;
}

Källa

Svar #7

Jag jämförde några av de metoder som föreslås här i Firefox för hastighet.

Strängen jag använde innehöll följande tecken: 😀œ´®†¥¨ˆøπ¬˚∆˙©ƒ∂ßåΩ≈ç√∫˜µ≤

Alla resultat är genomsnitt av 3 körningar vardera. Tiderna är i millisekunder. Observera att alla URIEncoding-metoder uppträdde på liknande sätt och hade extrema resultat, så jag inkluderade bara en.

Även om det finns vissa variationer beroende på strängens storlek, presterar charCode-metoderna (lovasoa och fuweichin) på samma sätt och är snabbast totalt sett, med fuweichins charCode-metod som den snabbaste. Blob- och TextEncoder-metoderna presterade på liknande sätt som varandra. Generellt var charCode-metoderna ungefär 75% snabbare än Blob- och TextEncoder-metoderna. URIEncoding-metoden var i princip oacceptabel.

Här är resultaten jag fick:

Storlek 6,4 * 10^6 byte:

Lauri Oherd - URIEncoding:     6400000    et: 796
lovasoa - charCode:            6400000    et: 15
fuweichin - charCode2:         6400000    et: 16
simap - Blob:                  6400000    et: 26
Riccardo Galli - TextEncoder:  6400000    et: 23

Storlek 19,2 * 10^6 byte: Blob gör på något sätt en konstig sak här.

Lauri Oherd - URIEncoding:     19200000    et: 2322
lovasoa - charCode:            19200000    et: 42
fuweichin - charCode2:         19200000    et: 45
simap - Blob:                  19200000    et: 169
Riccardo Galli - TextEncoder:  19200000    et: 70

Storlek 64 * 10^6 byte:

Lauri Oherd - URIEncoding:     64000000    et: 12565
lovasoa - charCode:            64000000    et: 138
fuweichin - charCode2:         64000000    et: 133
simap - Blob:                  64000000    et: 231
Riccardo Galli - TextEncoder:  64000000    et: 211

Storlek 192 * 10^6 byte: URIEncoding-metoder fryser webbläsaren vid denna punkt.

lovasoa - charCode:            192000000    et: 754
fuweichin - charCode2:         192000000    et: 480
simap - Blob:                  192000000    et: 701
Riccardo Galli - TextEncoder:  192000000    et: 654

Storlek 640 * 10^6 byte:

lovasoa - charCode:            640000000    et: 2417
fuweichin - charCode2:         640000000    et: 1602
simap - Blob:                  640000000    et: 2492
Riccardo Galli - TextEncoder:  640000000    et: 2338

Storlek 1280 * 10^6 byte: Blob- och TextEncoder-metoder börjar stöta på begränsningar här.

lovasoa - charCode:            1280000000    et: 4780
fuweichin - charCode2:         1280000000    et: 3177
simap - Blob:                  1280000000    et: 6588
Riccardo Galli - TextEncoder:  1280000000    et: 5074

Storlek 1920 * 10^6 byte:

lovasoa - charCode:            1920000000    et: 7465
fuweichin - charCode2:         1920000000    et: 4968
JavaScript error: file:///Users/xxx/Desktop/test.html, line 74: NS_ERROR_OUT_OF_MEMORY:

Här är koden:

function byteLengthURIEncoding(str) {
  return encodeURI(str).split(/%..|./).length - 1;
}

function byteLengthCharCode(str) {
  // returns the byte length of an utf8 string
  var s = str.length;
  for (var i=str.length-1; i>=0; i--) {
    var code = str.charCodeAt(i);
    if (code > 0x7f && code <= 0x7ff) s++;
    else if (code > 0x7ff && code <= 0xffff) s+=2;
    if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
  }
  return s;
}

function byteLengthCharCode2(s){
  //assuming the String is UCS-2(aka UTF-16) encoded
  var n=0;
  for(var i=0,l=s.length; i<l; i++){
    var hi=s.charCodeAt(i);
    if(hi<0x0080){ //[0x0000, 0x007F]
      n+=1;
    }else if(hi<0x0800){ //[0x0080, 0x07FF]
      n+=2;
    }else if(hi<0xD800){ //[0x0800, 0xD7FF]
      n+=3;
    }else if(hi<0xDC00){ //[0xD800, 0xDBFF]
      var lo=s.charCodeAt(++i);
      if(i<l&&lo>=0xDC00&&lo<=0xDFFF){ //followed by [0xDC00, 0xDFFF]
        n+=4;
      }else{
        throw new Error("UCS-2 String malformed");
      }
    }else if(hi<0xE000){ //[0xDC00, 0xDFFF]
      throw new Error("UCS-2 String malformed");
    }else{ //[0xE000, 0xFFFF]
      n+=3;
    }
  }
  return n;
}

function byteLengthBlob(str) {
  return new Blob([str]).size;
}

function byteLengthTE(str) {
  return (new TextEncoder().encode(str)).length;
}

var sample = "😀œ´®†¥¨ˆøπ¬˚∆˙©ƒ∂ßåΩ≈ç√∫˜µ≤i";
var string = "";

// Adjust multiplier to change length of string.
let mult = 1000000;

for (var i = 0; i < mult; i++) {
  string += sample;
}

let t0;

try {
  t0 = Date.now();
  console.log("Lauri Oherd - URIEncoding:   " + byteLengthURIEncoding(string) + "    et: " + (Date.now() - t0));
} catch(e) {}

t0 = Date.now();
console.log("lovasoa - charCode:            " + byteLengthCharCode(string) + "    et: " + (Date.now() - t0));

t0 = Date.now();
console.log("fuweichin - charCode2:         " + byteLengthCharCode2(string) + "    et: " + (Date.now() - t0));

t0 = Date.now();
console.log("simap - Blob:                  " + byteLengthBlob(string) + "    et: " + (Date.now() - t0));

t0 = Date.now();
console.log("Riccardo Galli - TextEncoder:  " + byteLengthTE(string) + "    et: " + (Date.now() - t0));

Relaterade artiklar