Длина строки в байтах в JavaScript


104

В моем коде JavaScript мне нужно составить сообщение на сервер в следующем формате:

<size in bytes>CRLF
<data>CRLF

Пример:

3
foo

Данные могут содержать символы Юникода. Мне нужно отправить их как UTF-8.

Я ищу наиболее кроссбраузерный способ вычисления длины строки в байтах в JavaScript.

Я пробовал это, чтобы составить свою полезную нагрузку:

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

Но он не дает мне точных результатов для старых браузеров (или, может быть, строк в этих браузерах в UTF-16?).

Какие-нибудь подсказки?

Обновить:

Пример: длина строки в байтах ЭЭХ! Naïve?в UTF-8 составляет 15 байтов, но некоторые браузеры вместо этого сообщают 23 байта.


1
Возможный дубликат? stackoverflow.com/questions/2219526/…
Эли

@Eli: ни один из ответов на вопрос, который вы связали, не работает для меня.
Александр Гладыш

Когда вы говорите об "ЭЭХ! Наивно?" вы придали ему определенную нормальную форму? unicode.org/reports/tr15
Майк Сэмюэл

@Mike: Я набрал его в редакторе случайного текста (в режиме UTF-8) и сохранил. Так же, как и любой пользователь моей библиотеки. Однако, похоже, я разобрался, в чем дело - см. Мой ответ.
Александр Гладыш

Ответы:


89

В JavaScript нет возможности сделать это изначально. (См . Ответ Риккардо Галли о современном подходе.)


Для исторической справки или для тех случаев, когда API-интерфейсы TextEncoder все еще недоступны .

Если вы знаете кодировку символов, вы можете рассчитать ее самостоятельно.

encodeURIComponent предполагает UTF-8 в качестве кодировки символов, поэтому, если вам нужна эта кодировка, вы можете сделать,

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

Это должно работать, потому что UTF-8 кодирует многобайтовые последовательности. Первый закодированный байт всегда начинается либо со старшего бита нуля для однобайтовой последовательности, либо с байта, первая шестнадцатеричная цифра которого равна C, D, E или F. Второй и последующие байты - это те, у которых первые два бита равны 10. Это дополнительные байты, которые вы хотите посчитать в UTF-8.

Таблица в википедии делает понятнее

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

Если вместо этого вам нужно понять кодировку страницы, вы можете использовать этот трюк:

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

Ну, а как мне узнать кодировку символов данных? Мне нужно закодировать любую строку, которую пользователь (программист) предоставил моей библиотеке JS.
Александр Гладыш

@Alexander, когда вы отправляете сообщение на сервер, указываете ли вы кодировку содержимого тела сообщения через заголовок HTTP?
Майк Сэмюэл

1
@ Александр, круто. Если вы устанавливаете протокол, указание UTF-8 - отличная идея для обмена текстом. На одну переменную меньше, которая может привести к несоответствию. UTF-8 должен быть сетевым байтовым порядком кодировки символов.
Майк Сэмюэл

4
@MikeSamuel: lengthInUtf8Bytesфункция возвращает 5 для символов, отличных от BMP, как str.lengthдля этих возвратов 2. Я напишу измененную версию этой функции в разделе ответов.
Лаури Охерд

1
Решение крутое, но utf8mb4 не рассматривается. Например, encodeURIComponent('🍀')есть '%F0%9F%8D%80'.
Альберт

117

Прошли годы, и теперь вы можете делать это изначально

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

Обратите внимание, что он еще не поддерживается IE (или Edge) (для этого вы можете использовать полифилл ).

Документация MDN

Стандартные характеристики


4
Какой фантастический, современный подход. Спасибо!
Con Antonakos

Обратите внимание, что в соответствии с документацией MDN TextEncoder еще не поддерживается Safari (WebKit).
Maor

TextEncodeподдерживает только utf-8 с Chrome 53.
Джехонг Ан

1
Если вам нужна только длина, может оказаться излишним выделить новую строку, выполнить фактическое преобразование, взять длину и затем отбросить строку. См. Мой ответ выше для функции, которая просто эффективно вычисляет длину.
lovasoa

66

Вот гораздо более быстрая версия, в которой не используются ни регулярные выражения, ни 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;
}

Вот сравнение производительности .

Он просто вычисляет длину в UTF8 каждой кодовой точки Unicode, возвращаемой charCodeAt () (на основе описаний в Википедии UTF8 и суррогатных символов UTF16).

Он соответствует RFC3629 (где символы UTF-8 имеют длину не более 4 байтов).


46

Для простой кодировки UTF-8 с немного лучшей совместимостью, чем TextEncoderBlob, подойдет. Однако не будет работать в очень старых браузерах.

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

29

Эта функция вернет размер в байтах любой строки UTF-8, которую вы ей передали.

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

Источник


он не работает со строкой 'ユ ー ザ ー コ ー ド', ожидаемой длины 14, но 21
May Weather VN

1
@MayWeatherVN неправильная ユーザーコードдлина в байтах всегда 21, я тестировал это на разных инструментах; будьте любезны с вашими комментариями;)
Capitex

Эта строка, которую я помню, тестировала на php, 14
May Weather VN

23

Еще один очень простой подход с использованием Buffer(только для NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length

1
Вы можете пропустить создание буфера с помощью Buffer.byteLength(string, 'utf8').
Джо

1
@Joe Спасибо за предложение, я только что внес правку, чтобы включить его.
Иван Перес,

6

Мне потребовалось время, чтобы найти решение для React Native, поэтому я помещу его здесь:

Сначала установите bufferпакет:

npm install --save buffer

Затем используйте метод узла:

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

4

Собственно, в чем дело, разобрался. Для работы кода на странице <head>должен быть такой тег:

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

Или, как предлагается в комментариях, если сервер отправляет HTTP- Content-Encodingзаголовок, он также должен работать.

Тогда результаты из разных браузеров будут согласованы.

Вот пример:

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

Примечание: я подозреваю, что указание любой (точной) кодировки решит проблему с кодировкой. Просто совпадение, что мне нужен UTF-8.


2
unescapeФункция JavaScript не должна быть использована для декодирования Uniform Resource идентификаторов (URI).
Лаури Охерд

1
@LauriOherd unescapeдействительно никогда не следует использовать для декодирования URI. Однако для преобразования текста в UTF-8 он отлично
TS

unescape(encodeURIComponent(...)).lengthвсегда рассчитывает правильную длину с учетом или без meta http-equiv ... utf8. Без спецификации кодировки некоторые браузеры могли просто иметь другой текст (после кодирования байтов документа в фактический текст html), длину которого они вычисляли. Это можно легко проверить, напечатав не только длину, но и сам текст.
TS

3

Вот независимый и эффективный метод подсчета байтов 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));

Обратите внимание, что метод может вызвать ошибку, если входная строка имеет неправильный формат UCS-2.


3

В NodeJS Buffer.byteLengthэто метод специально для этой цели:

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

Обратите внимание, что по умолчанию метод предполагает, что строка находится в кодировке UTF-8. Если требуется другая кодировка, передайте ее как второй аргумент.


Можно ли рассчитать, strLengthInBytesпросто зная «количество» символов в строке? то есть var text = "Hello World!; var text_length = text.length; // pass text_length as argument to some method?. И, для справки, повторно Buffer- я только что наткнулся на этот ответ, в котором обсуждается, new Blob(['test string']).sizeи в узле Buffer.from('test string').length. Может быть, это кому-то тоже поможет?
user1063287

1
@ user1063287 Проблема в том, что количество символов не всегда равно количеству байтов. Например, обычная кодировка UTF-8 - это кодировка переменной ширины, в которой один символ может иметь размер от 1 до 4 байтов. Вот почему необходим специальный метод, а также используемая кодировка.
Boaz

Например, строка UTF-8 с 4 символами может иметь длину не менее 4 байтов, если каждый символ составляет всего 1 байт; и не более 16 байтов, если каждый символ составляет 4 байта. Обратите внимание, что в любом случае количество символов по-прежнему равно 4, и, следовательно, это ненадежный показатель длины в байтах .
Вооз,

1

Это будет работать для символов BMP и 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

Вы можете попробовать это:

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

Меня устраивает.


возвращает 1 для "â" в хроме
Рик

первую проблему можно решить, изменив \ xff на \ x7f, но это не устраняет того факта, что кодовые точки между 0x800-0xFFFF будут считаться занимающими 2 байта, когда они занимают 3.
Рик
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.