Довжина рядка в байтах у JavaScript


104

У своєму коді JavaScript мені потрібно скласти повідомлення серверу в такому форматі:

<size in bytes>CRLF
<data>CRLF

Приклад:

3
foo

Дані можуть містити символи unicode. Мені потрібно надіслати їх як 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 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) (ви можете використовувати для цього polyfill ).

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

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


4
Який фантастичний, сучасний підхід. Дякую!
Con Antonakos

Зверніть увагу, що згідно з документацією MDN , Safari (WebKit) TextEncoder ще не підтримується.
Маор

TextEncodeпідтримує лише utf-8 з Chrome 53.
Jehong Ahn

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 () (на основі описів у wikipedia 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
погода

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

Цей рядок я пам’ятаю тестування на php - 14
погода травня VN

23

Ще один дуже простий підхід із використанням Buffer(лише для NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length

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

1
@Joe Дякую за пропозицію, я щойно змінив її, щоб включити її.
Іван Перес

5

Зайняв у мене час, щоб знайти рішення для 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
Функцію unescapeJavaScript не слід використовувати для декодування Уніфікованих ідентифікаторів ресурсів (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. Може, вони теж допоможуть деяким людям?
користувач1063287

1
@ user1063287 Проблема в тому, що кількість символів не завжди еквівалентна кількості байтів. Наприклад, загальне кодування UTF-8 - це кодування змінної ширини, в якому один символ може мати розмір від 1 байта до 4 байт. Ось чому потрібен спеціальний метод, а також використовуване кодування.
Боаз

Наприклад, рядок 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 будуть повідомлятися як два байти, коли вони займуть 3.
Рік
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.