Panjang string dalam byte di JavaScript

104

Dalam kode JavaScript saya, saya perlu menulis pesan ke server dalam format ini:

<size in bytes>CRLF
<data>CRLF

Contoh:

3
foo

Data mungkin berisi karakter unicode. Saya perlu mengirimnya sebagai UTF-8.

Saya mencari cara paling lintas browser untuk menghitung panjang string dalam byte di JavaScript.

Saya sudah mencoba ini untuk menyusun muatan saya:

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

Tapi itu tidak memberi saya hasil yang akurat untuk browser lama (atau, mungkin string di browser tersebut di UTF-16?).

Ada petunjuk?

Memperbarui:

Contoh: panjang dalam byte dari string ЭЭХ! Naïve?di UTF-8 adalah 15 byte, tetapi sebagai gantinya beberapa browser melaporkan 23 byte.

Alexander Gladysh
sumber
1
Kemungkinan duplikat? stackoverflow.com/questions/2219526/…
Eli
@Eli: tidak ada jawaban dalam pertanyaan yang Anda tautkan yang cocok untuk saya.
Alexander Gladysh
Ketika Anda berbicara tentang "ЭЭХ! Naïve?" sudahkah Anda memasukkannya ke dalam bentuk normal tertentu? unicode.org/reports/tr15
Mike Samuel
@ Mike: Saya mengetiknya di editor teks acak (dalam mode UTF-8) dan menyimpannya. Seperti yang dilakukan oleh semua pengguna perpustakaan saya. Namun, sepertinya saya menemukan apa yang salah - lihat jawaban saya.
Alexander Gladysh

Jawaban:

89

Tidak ada cara untuk melakukannya di JavaScript secara native. (Lihat jawaban Riccardo Galli untuk pendekatan modern.)


Untuk referensi historis atau di mana API TextEncoder masih tidak tersedia .

Jika Anda mengetahui pengkodean karakter, Anda dapat menghitungnya sendiri.

encodeURIComponent mengasumsikan UTF-8 sebagai pengkodean karakter, jadi jika Anda membutuhkan pengkodean itu, Anda dapat melakukannya,

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

Ini seharusnya berfungsi karena cara UTF-8 mengkodekan urutan multi-byte. Byte pertama yang dikodekan selalu dimulai dengan bit nol tinggi untuk urutan byte tunggal, atau byte yang digit hex pertamanya adalah C, D, E, atau F. Byte kedua dan selanjutnya adalah byte yang dua bit pertamanya adalah 10 Itu adalah byte ekstra yang ingin Anda hitung dalam UTF-8.

Tabel di wikipedia membuatnya lebih jelas

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

Jika Anda perlu memahami pengkodean halaman, Anda dapat menggunakan trik ini:

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
sumber
Nah, bagaimana saya tahu pengkodean karakter dari data? Saya perlu menyandikan pengguna string apa pun (pemrogram) yang diberikan ke perpustakaan JS saya.
Alexander Gladysh
@Alexander, saat Anda mengirim pesan ke server, apakah Anda menentukan pengkodean konten badan pesan melalui header HTTP?
Mike Samuel
1
@Alexander, keren. Jika Anda membuat protokol, mengamanatkan UTF-8 adalah ide bagus untuk pertukaran teks. Lebih sedikit variabel yang dapat menyebabkan ketidakcocokan. UTF-8 harus merupakan urutan byte jaringan dari pengkodean karakter.
Mike Samuel
4
@ MikeSamuel: lengthInUtf8BytesFungsi mengembalikan 5 untuk karakter non-BMP seperti str.lengthuntuk pengembalian ini 2. Saya akan menulis versi modifikasi dari fungsi ini ke bagian jawaban.
Lauri Oherd
1
Solusi ini keren tetapi utf8mb4 tidak dipertimbangkan. Misalnya, encodeURIComponent('🍀')adalah '%F0%9F%8D%80'.
albert
117

Tahun-tahun berlalu dan saat ini Anda dapat melakukannya secara native

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

Perhatikan bahwa itu belum didukung oleh IE (atau Edge) (Anda dapat menggunakan polyfill untuk itu).

Dokumentasi MDN

Spesifikasi standar

Riccardo Galli
sumber
4
Sungguh pendekatan yang fantastis dan modern. Terima kasih!
Con Antonakos
Perhatikan bahwa menurut dokumentasi MDN , TextEncoder belum didukung oleh Safari (WebKit).
Maor
TextEncodehanya mendukung utf-8 sejak Chrome 53.
Jehong Ahn
1
Jika Anda hanya membutuhkan panjang, mungkin berlebihan untuk mengalokasikan string baru, melakukan konversi yang sebenarnya, mengambil panjangnya, dan kemudian membuang string tersebut. Lihat jawaban saya di atas untuk fungsi yang hanya menghitung panjang dengan cara yang efisien.
lovasoa
66

Berikut adalah versi yang jauh lebih cepat, yang tidak menggunakan ekspresi reguler, atau 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;
}

Berikut perbandingan performanya .

Ini hanya menghitung panjang dalam UTF8 dari setiap titik kode unicode yang dikembalikan oleh charCodeAt () (berdasarkan deskripsi wikipedia tentang UTF8 , dan karakter pengganti UTF16).

Ini mengikuti RFC3629 (di mana karakter UTF-8 paling panjang 4-byte).

lovasoa
sumber
46

Untuk pengkodean UTF-8 sederhana, dengan kompatibilitas yang sedikit lebih baik daripada TextEncoder, Blob melakukan triknya. Tidak akan berfungsi di browser yang sangat lama.

new Blob(["😀"]).size; // -> 4  
simap
sumber
29

Fungsi ini akan mengembalikan ukuran byte dari setiap string UTF-8 yang Anda berikan padanya.

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

Sumber

Lauri Oherd
sumber
itu tidak bekerja dengan string 'ユ ー ザ ー コ ー ド', diharapkan 14 panjang tetapi 21
Mei Cuaca VN
1
@MayWeatherVN Anda salah ユーザーコードpanjang dalam byte selalu 21, saya mengujinya pada alat differents; lebih ramah dengan komentar Anda;)
Capitex
String ini saya ingat pengujian pada php 14
Mei
23

Pendekatan lain yang sangat sederhana menggunakan Buffer(hanya untuk NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length
Iván Pérez
sumber
1
Anda dapat melewati pembuatan buffer dengan Buffer.byteLength(string, 'utf8').
Joe
1
@Joe Terima kasih atas sarannya, saya baru saja mengedit untuk memasukkannya.
Iván Pérez
5

Butuh beberapa saat bagi saya untuk menemukan solusi untuk React Native jadi saya akan taruh di sini:

Pertama instal bufferpaketnya:

npm install --save buffer

Kemudian gunakan metode node:

const { Buffer } = require('buffer');
const length = Buffer.byteLength(string, 'utf-8');
laurent
sumber
4

Sebenarnya, saya menemukan apa yang salah. Agar kode berfungsi, halaman <head>harus memiliki tag ini:

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

Atau, seperti yang disarankan dalam komentar, jika server mengirimkan Content-Encodingheader HTTP , seharusnya juga berfungsi.

Kemudian hasil dari browser yang berbeda menjadi konsisten.

Berikut ini contohnya:

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

Catatan: Saya menduga bahwa menentukan pengkodean (akurat) apa pun akan memperbaiki masalah pengkodean. Saya hanya kebetulan membutuhkan UTF-8.

Alexander Gladysh
sumber
2
Fungsi unescapeJavaScript tidak boleh digunakan untuk mendekode Uniform Resource Identifiers (URI).
Lauri Oherd
1
@LauriOherd unescapememang tidak boleh digunakan untuk mendekode URI. Namun, untuk mengonversi teks ke UTF-8, ini berfungsi dengan baik
TS
unescape(encodeURIComponent(...)).lengthselalu menghitung panjang yang benar dengan atau tanpa meta http-equiv ... utf8. Tanpa spesifikasi pengkodean beberapa browser mungkin hanya memiliki teks yang berbeda (setelah mengkodekan byte dokumen menjadi teks html sebenarnya) yang panjangnya mereka hitung. Seseorang dapat menguji ini dengan mudah, dengan mencetak tidak hanya panjangnya, tetapi juga teksnya sendiri.
TS
3

Berikut adalah metode independen dan efisien untuk menghitung byte UTF-8 dari sebuah string.

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

Perhatikan bahwa metode ini dapat memunculkan kesalahan jika string input berformat UCS-2 salah

fuweichin
sumber
3

Di NodeJS, Buffer.byteLengthadalah metode yang khusus untuk tujuan ini:

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

Perhatikan bahwa secara default metode ini mengasumsikan string dalam encoding UTF-8. Jika encoding berbeda diperlukan, teruskan sebagai argumen kedua.

Boaz
sumber
Apakah mungkin untuk menghitung strLengthInByteshanya dengan mengetahui 'jumlah' karakter dalam string? mis var text = "Hello World!; var text_length = text.length; // pass text_length as argument to some method?. Dan, hanya untuk referensi, Buffersaya baru saja menemukan jawaban ini yang membahas new Blob(['test string']).sizedan, dalam simpul Buffer.from('test string').length,. Mungkin ini akan membantu beberapa orang juga?
pengguna1063287
1
@ user1063287 Masalahnya adalah jumlah karakter tidak selalu sama dengan jumlah byte. Misalnya, pengkodean UTF-8 yang umum adalah pengkodean lebar variabel, di mana satu karakter dapat berukuran 1 byte hingga 4 byte. Itulah mengapa dibutuhkan metode khusus serta pengkodean yang digunakan.
Boaz
Misalnya, string UTF-8 dengan 4 karakter, setidaknya dapat berukuran 4 byte "panjang", jika setiap karakter hanya 1 byte; dan paling banyak 16 byte "panjang" jika setiap karakter adalah 4 byte. Perhatikan dalam kedua kasus jumlah karakter masih 4 dan karena itu merupakan ukuran yang tidak dapat diandalkan untuk panjang byte .
Boaz
1

Ini akan bekerja untuk karakter BMP dan 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
sumber
0

Anda bisa mencoba ini:

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

Ini bekerja untuk saya.

anh tran
sumber
mengembalikan 1 untuk "â" di chrome
Rick
masalah pertama dapat diperbaiki dengan mengubah \ xff menjadi \ x7f, tetapi itu tidak memperbaiki fakta bahwa titik kode antara 0x800-0xFFFF akan dilaporkan mengambil 2 byte, ketika mereka mengambil 3.
Rick