Długość ciągu w bajtach w JavaScript

104

W moim kodzie JavaScript muszę utworzyć wiadomość do serwera w następującym formacie:

<size in bytes>CRLF
<data>CRLF

Przykład:

3
foo

Dane mogą zawierać znaki Unicode. Muszę je wysłać jako UTF-8.

Szukam najbardziej używanego w różnych przeglądarkach sposobu obliczenia długości ciągu w bajtach w JavaScript.

Próbowałem tak skomponować mój ładunek:

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

Ale nie daje mi dokładnych wyników dla starszych przeglądarek (a może ciągów znaków w tych przeglądarkach w UTF-16?).

Jakieś wskazówki?

Aktualizacja:

Przykład: długość w bajtach łańcucha ЭЭХ! Naïve?w UTF-8 wynosi 15 bajtów, ale niektóre przeglądarki zamiast tego zgłaszają 23 bajty.

Alexander Gladysh
źródło
1
Możliwy duplikat? stackoverflow.com/questions/2219526/…
Eli
@Eli: żadna z odpowiedzi w pytaniu, które podałeś, nie działa dla mnie.
Alexander Gladysh
Kiedy mówisz o „ЭЭХ! Naiwny?” czy nadałeś mu jakąś szczególną normalną formę? unicode.org/reports/tr15
Mike Samuel
@Mike: Wpisałem go w edytorze losowego tekstu (w trybie UTF-8) i zapisałem. Tak, jak zrobiłby to każdy użytkownik mojej biblioteki. Wygląda jednak na to, że zorientowałem się, co jest nie tak - zobacz moją odpowiedź.
Alexander Gladysh

Odpowiedzi:

89

Nie ma możliwości zrobienia tego natywnie w JavaScript. (Zobacz odpowiedź Riccardo Galli na nowoczesne podejście.)


W celach informacyjnych lub w przypadku, gdy interfejsy API TextEncoder są nadal niedostępne .

Jeśli znasz kodowanie znaków, możesz to obliczyć samodzielnie.

encodeURIComponent przyjmuje UTF-8 jako kodowanie znaków, więc jeśli potrzebujesz tego kodowania, możesz to zrobić,

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

Powinno to działać ze względu na sposób, w jaki UTF-8 koduje sekwencje wielobajtowe. Pierwszy zakodowany bajt zawsze zaczyna się od wysokiego bitu zerowego dla sekwencji jednobajtowej lub bajtu, którego pierwsza cyfra szesnastkowa to C, D, E lub F.Drugi i kolejne bajty to te, których pierwsze dwa bity to 10 To są dodatkowe bajty, które chcesz policzyć w UTF-8.

Stół w Wikipedii czyni to bardziej przejrzystym

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
...

Jeśli zamiast tego chcesz zrozumieć kodowanie strony, możesz użyć tej sztuczki:

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);
}
Mike Samuel
źródło
Skąd mam wiedzieć, jakie jest kodowanie znaków w danych? Muszę zakodować dowolny ciąg użytkownika (programista) dostarczony do mojej biblioteki JS.
Alexander Gladysh
@Alexander, kiedy wysyłasz wiadomość do serwera, czy określasz kodowanie treści wiadomości za pomocą nagłówka HTTP?
Mike Samuel
1
@Alexander, spoko. Jeśli tworzysz protokół, wymaganie UTF-8 to świetny pomysł na wymianę tekstu. Jedna zmienna mniej, która może spowodować niedopasowanie. UTF-8 powinien być sieciowym porządkiem bajtów w kodowaniu znaków.
Mike Samuel
4
@MikeSamuel: lengthInUtf8BytesFunkcja zwraca 5 dla znaków innych niż BMP, jak str.lengthdla tych zwraca 2. Napiszę zmodyfikowaną wersję tej funkcji do sekcji odpowiedzi.
Lauri Oherd
1
To rozwiązanie jest fajne, ale utf8mb4 nie jest brane pod uwagę. Na przykład encodeURIComponent('🍀')jest '%F0%9F%8D%80'.
Albert
117

Minęły lata i teraz możesz to zrobić natywnie

(new TextEncoder().encode('foo')).length

Zauważ, że nie jest jeszcze obsługiwany przez IE (lub Edge) (możesz do tego użyć polyfill ).

Dokumentacja MDN

Standardowe specyfikacje

Riccardo Galli
źródło
4
Cóż za fantastyczne, nowoczesne podejście. Dzięki!
Con Antonakos
Zauważ, że zgodnie z dokumentacją MDN TextEncoder nie jest jeszcze obsługiwany przez Safari (WebKit).
Maor
TextEncodeobsługuje tylko utf-8 od Chrome 53.
Jehong Ahn,
1
Jeśli potrzebujesz tylko długości, przydzielenie nowego ciągu, dokonanie właściwej konwersji, pobranie długości, a następnie odrzucenie ciągu może być przesadą. Zobacz moją odpowiedź powyżej dla funkcji, która po prostu oblicza długość w efektywny sposób.
lovasoa
66

Oto znacznie szybsza wersja, która nie używa wyrażeń regularnych ani 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;
}

Oto porównanie wydajności .

Po prostu oblicza długość w UTF8 każdego punktu kodowego Unicode zwróconego przez charCodeAt () (na podstawie opisów UTF8 w Wikipedii i znaków zastępczych UTF16).

Jest zgodny z RFC3629 (gdzie znaki UTF-8 mają co najwyżej 4 bajty).

lovasoa
źródło
46

Dla prostego kodowania UTF-8, z nieco lepszą kompatybilnością niż TextEncoderBlob załatwia sprawę. Nie będzie jednak działać w bardzo starych przeglądarkach.

new Blob(["😀"]).size; // -> 4  
simap
źródło
29

Ta funkcja zwróci rozmiar bajtu dowolnego przekazanego do niej ciągu znaków UTF-8.

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

Źródło

Lauri Oherd
źródło
nie działa ze stringiem 'ユ ー ザ ー コ ー ド', oczekiwana długość to 14, ale 21
maj Pogoda VN
1
@MayWeatherVN źle ユーザーコードdługość w bajtach wynosi zawsze 21, przetestowałem to na różnych narzędziach; bądźcie bardziej uprzejmi z komentarzami;)
Capitex
Ten ciąg, który pamiętam, testowałem na php, to 14
May Weather VN
23

Kolejne bardzo proste podejście wykorzystujące Buffer(tylko dla NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length
Iván Pérez
źródło
1
Możesz pominąć tworzenie bufora za pomocą Buffer.byteLength(string, 'utf8').
Joe
1
@Joe Dzięki za sugestię, właśnie dokonałem edycji, aby ją uwzględnić.
Iván Pérez
5

Trochę mi zajęło znalezienie rozwiązania dla React Native, więc umieszczę je tutaj:

Najpierw zainstaluj bufferpakiet:

npm install --save buffer

Następnie użyj metody węzła:

const { Buffer } = require('buffer');
const length = Buffer.byteLength(string, 'utf-8');
laurent
źródło
4

Właściwie to odkryłem, co jest nie tak. Aby kod działał, strona <head>powinna mieć następujący tag:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

Lub, jak sugerowano w komentarzach, jeśli serwer wysyła Content-Encodingnagłówek HTTP , to również powinno działać.

Wtedy wyniki z różnych przeglądarek są spójne.

Oto przykład:

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
  <title>mini string length test</title>
</head>
<body>

<script type="text/javascript">
document.write('<div style="font-size:100px">' 
    + (unescape(encodeURIComponent("ЭЭХ! Naïve?")).length) + '</div>'
  );
</script>
</body>
</html>

Uwaga: Podejrzewam, że określenie dowolnego (dokładnego) kodowania rozwiązałoby problem z kodowaniem. To tylko zbieg okoliczności, że potrzebuję UTF-8.

Alexander Gladysh
źródło
2
unescapeFunkcja JavaScript nie powinien być używany do dekodowania Uniform Resource Identifier (URI).
Lauri Oherd
1
@LauriOherd unescapenigdy nie powinno być używane do dekodowania identyfikatorów URI. Jednak konwersja tekstu do UTF-8 działa dobrze
TS
unescape(encodeURIComponent(...)).lengthzawsze oblicza prawidłową długość z lub bez meta http-equiv ... utf8. Bez specyfikacji kodowania niektóre przeglądarki mogą po prostu mieć inny tekst (po zakodowaniu bajtów dokumentu do rzeczywistego tekstu HTML), którego długość obliczają. Można to łatwo sprawdzić, drukując nie tylko długość, ale także sam tekst.
TS
3

Oto niezależna i wydajna metoda zliczania bajtów łańcucha w formacie UTF-8.

//count UTF-8 bytes of a string
function byteLengthOf(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;
}

var s="\u0000\u007F\u07FF\uD7FF\uDBFF\uDFFF\uFFFF";
console.log("expect byteLengthOf(s) to be 14, actually it is %s.",byteLengthOf(s));

Należy zauważyć, że metoda może zgłosić błąd, jeśli ciąg wejściowy ma nieprawidłowy format UCS-2

fuweichin
źródło
3

W NodeJS Buffer.byteLengthjest to metoda specjalnie do tego celu:

let strLengthInBytes = Buffer.byteLength(str); // str is UTF-8

Zwróć uwagę, że domyślnie metoda zakłada, że ​​ciąg jest w kodowaniu UTF-8. Jeśli wymagane jest inne kodowanie, przekaż je jako drugi argument.

Boaz
źródło
Czy można obliczyć strLengthInBytestylko znając „liczbę” znaków w ciągu? tj var text = "Hello World!; var text_length = text.length; // pass text_length as argument to some method?. I tylko w celach informacyjnych, re Buffer- Właśnie natknąłem tej odpowiedzi , że omówione new Blob(['test string']).size, oraz w węźle Buffer.from('test string').length. Może te też pomogą niektórym ludziom?
user1063287
1
@ user1063287 Problem polega na tym, że liczba znaków nie zawsze jest równa liczbie bajtów. Na przykład powszechne kodowanie UTF-8 to kodowanie o zmiennej szerokości, w którym pojedynczy znak może mieć rozmiar od 1 do 4 bajtów. Dlatego potrzebna jest specjalna metoda, a także zastosowane kodowanie.
Boaz
Na przykład łańcuch UTF-8 z 4 znakami może mieć co najmniej 4 bajty „długości”, jeśli każdy znak ma tylko 1 bajt; i maksymalnie 16 bajtów „długości”, jeśli każdy znak ma 4 bajty. Zauważ, że w każdym przypadku liczba znaków nadal wynosi 4 i dlatego jest niewiarygodną miarą długości bajtów .
Boaz
1

To zadziała dla znaków BMP i SIP / SMP.

    String.prototype.lengthInUtf8 = function() {
        var asciiLength = this.match(/[\u0000-\u007f]/g) ? this.match(/[\u0000-\u007f]/g).length : 0;
        var multiByteLength = encodeURI(this.replace(/[\u0000-\u007f]/g)).match(/%/g) ? encodeURI(this.replace(/[\u0000-\u007f]/g, '')).match(/%/g).length : 0;
        return asciiLength + multiByteLength;
    }

    'test'.lengthInUtf8();
    // returns 4
    '\u{2f894}'.lengthInUtf8();
    // returns 4
    'سلام علیکم'.lengthInUtf8();
    // returns 19, each Arabic/Persian alphabet character takes 2 bytes. 
    '你好,JavaScript 世界'.lengthInUtf8();
    // returns 26, each Chinese character/punctuation takes 3 bytes. 
chrislau
źródło
0

Możesz spróbować tego:

function getLengthInBytes(str) {
  var b = str.match(/[^\x00-\xff]/g);
  return (str.length + (!b ? 0: b.length)); 
}

Mi to pasuje.

anh tran
źródło
zwraca 1 dla „â” w chrome
Rick
pierwszy problem można rozwiązać, zmieniając \ xff na \ x7f, ale to nie naprawia faktu, że punkty kodu między 0x800-0xFFFF będą zgłaszane jako zajmujące 2 bajty, gdy zajmują 3 bajty.
Rick