Длина строки в байтах в 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 байтов).

Lovasoa
источник
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.

Fuweichin
источник
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. 
chrislau
источник
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.
Рик