Довжина рядка в байтах у 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.