Độ dài chuỗi tính bằng byte trong JavaScript


104

Trong mã JavaScript của mình, tôi cần soạn tin nhắn tới máy chủ ở định dạng sau:

<size in bytes>CRLF
<data>CRLF

Thí dụ:

3
foo

Dữ liệu có thể chứa các ký tự unicode. Tôi cần gửi chúng dưới dạng UTF-8.

Tôi đang tìm cách nhiều trình duyệt nhất để tính độ dài của chuỗi theo byte trong JavaScript.

Tôi đã thử điều này để soạn tải trọng của mình:

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

Nhưng nó không cung cấp cho tôi kết quả chính xác cho các trình duyệt cũ hơn (hoặc, có thể các chuỗi trong các trình duyệt đó trong UTF-16?).

Bất kì manh mối nào?

Cập nhật:

Ví dụ: độ dài tính bằng byte của chuỗi ЭЭХ! Naïve?trong UTF-8 là 15 byte, nhưng một số trình duyệt lại báo cáo là 23 byte.


1
Có thể trùng lặp? stackoverflow.com/questions/2219526/…
Eli

@Eli: không có câu trả lời nào trong câu hỏi bạn đã liên kết để làm việc cho tôi.
Alexander Gladysh

Khi bạn nói về "ЭЭХ! Ngây thơ?" bạn đã đặt nó vào một hình thức bình thường cụ thể chưa? unicode.org/reports/tr15
Mike Samuel

@Mike: Tôi đã nhập nó vào trình soạn thảo văn bản ngẫu nhiên (ở chế độ UTF-8) và lưu nó. Cũng giống như bất kỳ người dùng thư viện của tôi sẽ làm. Tuy nhiên, có vẻ như tôi đã tìm ra điều gì sai - hãy xem câu trả lời của tôi.
Alexander Gladysh

Câu trả lời:


89

Không có cách nào để làm điều đó trong JavaScript nguyên bản. (Xem câu trả lời của Riccardo Galli để biết cách tiếp cận hiện đại.)


Để tham khảo lịch sử hoặc nơi API TextEncoder vẫn không khả dụng .

Nếu bạn biết mã hóa ký tự, bạn có thể tự tính toán nó.

encodeURIComponent giả sử UTF-8 là mã hóa ký tự, vì vậy nếu bạn cần mã hóa đó, bạn có thể thực hiện,

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

Điều này sẽ hoạt động vì cách UTF-8 mã hóa chuỗi nhiều byte. Byte được mã hóa đầu tiên luôn bắt đầu bằng bit cao bằng 0 đối với chuỗi byte đơn hoặc byte có chữ số hex đầu tiên là C, D, E hoặc F. Các byte thứ hai và các byte tiếp theo là byte có hai bit đầu tiên là 10 . Đó là những byte phụ mà bạn muốn đếm trong UTF-8.

Bảng trong wikipedia làm cho nó rõ ràng hơn

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

Thay vào đó, nếu bạn cần hiểu mã hóa trang, bạn có thể sử dụng thủ thuật này:

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

Chà, làm sao tôi biết được mã hóa ký tự của dữ liệu? Tôi cần mã hóa bất kỳ chuỗi nào mà người dùng (lập trình viên) cung cấp cho thư viện JS của tôi.
Alexander Gladysh

@Alexander, khi bạn đang gửi thư đến máy chủ, bạn có đang chỉ định mã hóa nội dung của nội dung thư thông qua tiêu đề HTTP không?
Mike Samuel

1
@Alexander, tuyệt. Nếu bạn đang thiết lập một giao thức, ủy thác UTF-8 là một ý tưởng tuyệt vời để trao đổi văn bản. Một ít biến hơn có thể dẫn đến không khớp. UTF-8 phải là thứ tự byte mạng của các mã ký tự.
Mike Samuel

4
@MikeSamuel: lengthInUtf8BytesHàm trả về 5 đối với các ký tự không phải BMP như str.lengthđối với các ký tự này là 2. Tôi sẽ viết phiên bản sửa đổi của hàm này cho phần câu trả lời.
Lauri Oherd

1
Giải pháp này rất tuyệt nhưng utf8mb4 không được xem xét. Ví dụ, encodeURIComponent('🍀')'%F0%9F%8D%80'.
albert

117

Nhiều năm trôi qua và ngày nay bạn có thể làm điều đó một cách tự nhiên

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

Lưu ý rằng nó chưa được hỗ trợ bởi IE (hoặc Edge) (bạn có thể sử dụng polyfill cho điều đó).

Tài liệu MDN

Chi tiết kỹ thuật tiêu chuẩn


4
Thật là một cách tiếp cận hiện đại, tuyệt vời. Cảm ơn!
Con Antonakos,

Lưu ý rằng theo tài liệu MDN , TextEncoder chưa được Safari (WebKit) hỗ trợ.
Maor

TextEncodechỉ hỗ trợ utf-8 kể từ Chrome 53.
Jehong Ahn

1
Nếu bạn chỉ cần độ dài, có thể là quá mức cần thiết để phân bổ một chuỗi mới, thực hiện chuyển đổi thực tế, lấy độ dài và sau đó loại bỏ chuỗi. Xem câu trả lời của tôi ở trên cho một hàm chỉ tính độ dài một cách hiệu quả.
lovasoa

66

Đây là một phiên bản nhanh hơn nhiều, không sử dụng biểu thức chính quy, cũng không 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;
}

Đây là một so sánh hiệu suất .

Nó chỉ tính độ dài theo UTF8 của mỗi điểm mã unicode được trả về bởi charCodeAt () (dựa trên mô tả của wikipedia về UTF8 và các ký tự thay thế UTF16).

Nó tuân theo RFC3629 (trong đó các ký tự UTF-8 có độ dài tối đa là 4 byte).


46

Đối với mã hóa UTF-8 đơn giản, với khả năng tương thích tốt hơn một chút TextEncoder, Blob thực hiện thủ thuật. Tuy nhiên, sẽ không hoạt động trong các trình duyệt rất cũ.

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

29

Hàm này sẽ trả về kích thước byte của bất kỳ chuỗi UTF-8 nào mà bạn chuyển cho nó.

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

Nguồn


nó không hoạt động với chuỗi 'ユ ー ザ ー コ ー ド', độ dài dự kiến ​​là 14 nhưng 21
Tháng 5 Thời tiết VN

1
@MayWeatherVN bạn sai ユーザーコードđộ dài theo byte luôn là 21, tôi đã kiểm tra nó trên các công cụ khác nhau; tử tế hơn với nhận xét của bạn;)
Capitex

Chuỗi này mình nhớ test trên php là 14
May Thời tiết VN

24

Một cách tiếp cận rất đơn giản khác bằng cách sử dụng Buffer(chỉ dành cho NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length

1
Bạn có thể bỏ qua việc tạo bộ đệm với Buffer.byteLength(string, 'utf8').
Joe

1
@Joe Cảm ơn đề xuất, tôi vừa thực hiện một chỉnh sửa để đưa vào.
Iván Pérez

5

Tôi đã mất một lúc để tìm giải pháp cho React Native nên tôi sẽ đặt nó ở đây:

Đầu tiên cài đặt buffergói:

npm install --save buffer

Sau đó, sử dụng phương thức nút:

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

4

Trên thực tế, tôi đã tìm ra điều gì sai. Để mã hoạt động, trang <head>phải có thẻ này:

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

Hoặc, như được đề xuất trong nhận xét, nếu máy chủ gửi Content-Encodingtiêu đề HTTP , nó cũng sẽ hoạt động.

Sau đó, kết quả từ các trình duyệt khác nhau là nhất quán.

Đây là một ví dụ:

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

Lưu ý: Tôi nghi ngờ rằng việc chỉ định bất kỳ mã hóa (chính xác) nào sẽ khắc phục được sự cố mã hóa. Việc tôi cần UTF-8 chỉ là ngẫu nhiên.


2
Không nên sử dụng unescapehàm JavaScript để giải mã Số phân định tài nguyên đồng nhất (URI).
Lauri Oherd,

1
@LauriOherd unescapethực sự không bao giờ được sử dụng để giải mã URI. Tuy nhiên, để chuyển đổi văn bản thành UTF-8, nó hoạt động tốt
TS

unescape(encodeURIComponent(...)).lengthluôn tính toán độ dài chính xác có hoặc không có meta http-equiv ... utf8. Nếu không có thông số kỹ thuật mã hóa, một số trình duyệt có thể chỉ cần có một văn bản khác (sau khi mã hóa các byte của tài liệu thành văn bản html thực tế) có độ dài mà chúng đã tính toán. Người ta có thể kiểm tra điều này một cách dễ dàng, bằng cách in không chỉ độ dài, mà còn cả bản thân văn bản.
TS

3

Đây là một phương pháp độc lập và hiệu quả để đếm UTF-8 byte của một chuỗi.

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

Lưu ý rằng phương pháp có thể gây ra lỗi nếu chuỗi đầu vào không đúng định dạng UCS-2


3

Trong NodeJS, Buffer.byteLengthlà một phương thức dành riêng cho mục đích này:

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

Lưu ý rằng theo mặc định, phương pháp này giả định chuỗi ở dạng mã hóa UTF-8. Nếu cần mã hóa khác, hãy chuyển nó làm đối số thứ hai.


Có thể tính toán strLengthInByteschỉ bằng cách biết 'số lượng' các ký tự trong chuỗi không? tức là var text = "Hello World!; var text_length = text.length; // pass text_length as argument to some method?. Và, chỉ để tham khảo, Buffertôi vừa xem qua câu trả lời này thảo luận new Blob(['test string']).sizevà, trong nút Buffer.from('test string').length,. Có thể những điều này cũng sẽ giúp một số người?
user1063287,

1
@ user1063287 Vấn đề là số ký tự không phải lúc nào cũng tương đương với số byte. Ví dụ: mã hóa UTF-8 phổ biến là mã hóa độ rộng thay đổi, trong đó một ký tự đơn lẻ có thể có kích thước từ 1 byte đến 4 byte. Đó là lý do tại sao cần có một phương pháp đặc biệt cũng như bảng mã được sử dụng.
Boaz

Ví dụ, một chuỗi UTF-8 có 4 ký tự, ít nhất có thể dài 4 byte, nếu mỗi ký tự chỉ là 1 byte; và nhiều nhất là 16 byte "dài" nếu mỗi ký tự là 4 byte. Lưu ý trong cả hai trường hợp, số ký tự vẫn là 4 và do đó, là một thước đo không đáng tin cậy cho độ dài byte .
Boaz

1

Điều này sẽ hoạt động cho các ký tự BMP và 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. 

0

Bạn có thể thử điều này:

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

Nó làm việc cho tôi.


trả về 1 cho "â" trong chrome
Rick

vấn đề đầu tiên có thể được khắc phục bằng cách thay đổi \ xff thành \ x7f, nhưng điều đó không khắc phục được thực tế là các điểm mã giữa 0x800-0xFFFF sẽ được báo cáo là chiếm 2 byte, khi chúng chiếm 3
Rick
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.