Longitud de cadena en bytes en JavaScript

104

En mi código JavaScript, necesito redactar un mensaje al servidor en este formato:

<size in bytes>CRLF
<data>CRLF

Ejemplo:

3
foo

Los datos pueden contener caracteres Unicode. Necesito enviarlos como UTF-8.

Estoy buscando la forma más cruzada de navegador para calcular la longitud de la cadena en bytes en JavaScript.

Intenté esto para componer mi carga útil:

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

Pero no me da resultados precisos para los navegadores más antiguos (o, ¿tal vez las cadenas de esos navegadores en UTF-16?).

¿Alguna pista?

Actualizar:

Ejemplo: la longitud en bytes de la cadena ЭЭХ! Naïve?en UTF-8 es de 15 bytes, pero algunos navegadores reportan 23 bytes en su lugar.

Alejandro Gladysh
fuente
1
¿Posible duplicado? stackoverflow.com/questions/2219526/…
Eli
@Eli: ninguna de las respuestas de la pregunta que ha vinculado funciona para mí.
Alexander Gladysh
Cuando hablas de "ЭЭХ! ¿Ingenuo?" lo ha puesto en una forma normal particular? unicode.org/reports/tr15
Mike Samuel
@ Mike: Lo escribí en el editor de texto aleatorio (en modo UTF-8) y lo guardé. Como haría cualquier usuario de mi biblioteca. Sin embargo, parece que descubrí qué estaba mal, mira mi respuesta.
Alexander Gladysh

Respuestas:

89

No hay forma de hacerlo en JavaScript de forma nativa. (Vea la respuesta de Riccardo Galli para un enfoque moderno).


Para referencia histórica o donde las API de TextEncoder aún no están disponibles .

Si conoce la codificación de caracteres, puede calcularla usted mismo.

encodeURIComponent asume UTF-8 como la codificación de caracteres, por lo que si necesita esa codificación, puede hacerlo,

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

Esto debería funcionar debido a la forma en que UTF-8 codifica secuencias de varios bytes. El primer byte codificado siempre comienza con un bit alto de cero para una secuencia de un solo byte, o un byte cuyo primer dígito hexadecimal es C, D, E o F. El segundo y subsiguientes bytes son aquellos cuyos primeros dos bits son 10 Esos son los bytes adicionales que desea contar en UTF-8.

La tabla en wikipedia lo aclara

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

Si, en cambio, necesita comprender la codificación de la página, puede usar este truco:

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);
}
Mike Samuel
fuente
Bueno, ¿cómo puedo saber la codificación de caracteres de los datos? Necesito codificar cualquier cadena de usuario (programador) suministrada a mi biblioteca JS.
Alexander Gladysh
@Alexander, cuando envía el mensaje al servidor, ¿está especificando la codificación del contenido del cuerpo del mensaje a través de un encabezado HTTP?
Mike Samuel
1
@Alexander, genial. Si está estableciendo un protocolo, exigir UTF-8 es una gran idea para el intercambio de texto. Una variable menos que puede resultar en un desajuste. UTF-8 debe ser el orden de bytes de red de las codificaciones de caracteres.
Mike Samuel
4
@MikeSamuel: La lengthInUtf8Bytesfunción devuelve 5 para caracteres que no son BMP como str.lengthpara estos devuelve 2. Escribiré una versión modificada de esta función en la sección de respuestas.
Lauri Oherd
1
Esta solución es genial, pero no se considera utf8mb4. Por ejemplo, encodeURIComponent('🍀')es '%F0%9F%8D%80'.
Albert
117

Pasaron los años y hoy en día puedes hacerlo de forma nativa

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

Tenga en cuenta que aún no es compatible con IE (o Edge) (puede usar un polyfill para eso).

Documentación MDN

Especificaciones estándar

Riccardo Galli
fuente
4
Qué enfoque tan fantástico y moderno. ¡Gracias!
Con Antonakos
Tenga en cuenta que, según la documentación de MDN , Safari (WebKit) aún no admite TextEncoder.
Maor
TextEncodesolo admite utf-8 desde Chrome 53.
Jehong Ahn
1
Si solo necesita la longitud, podría ser excesivo asignar una nueva cadena, hacer la conversión real, tomar la longitud y luego descartar la cadena. Vea mi respuesta anterior para una función que simplemente calcula la longitud de manera eficiente.
lovasoa
66

Aquí hay una versión mucho más rápida, que no usa expresiones regulares, ni 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;
}

Aquí hay una comparación de rendimiento .

Simplemente calcula la longitud en UTF8 de cada punto de código Unicode devuelto por charCodeAt () (según las descripciones de wikipedia de caracteres sustitutos UTF8 y UTF16).

Sigue a RFC3629 (donde los caracteres UTF-8 tienen una longitud máxima de 4 bytes).

lovasoa
fuente
46

Para la codificación UTF-8 simple, con una compatibilidad ligeramente mejor que TextEncoder, Blob hace el truco. Sin embargo, no funcionará en navegadores muy antiguos.

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

Esta función devolverá el tamaño en bytes de cualquier cadena UTF-8 que le pase.

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

Fuente

Lauri Oherd
fuente
no funciona con la cadena 'ユ ー ザ ー コ ー ド', se espera una longitud de 14 pero 21
May Weather VN
1
@MayWeatherVN, la ユーザーコードlongitud incorrecta en bytes es siempre 21, lo probé en diferentes herramientas; sea ​​más amable con sus comentarios;)
Capitex
Esta cadena que recuerdo haber probado en php es 14
May Weather VN
24

Otro enfoque muy simple usando Buffer(solo para NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length
Iván Pérez
fuente
1
Puede omitir la creación de un búfer con Buffer.byteLength(string, 'utf8').
Joe
1
@Joe Gracias por la sugerencia, acabo de hacer una edición para incluirla.
Iván Pérez
5

Me tomó un tiempo encontrar una solución para React Native, así que la pondré aquí:

Primero instale el bufferpaquete:

npm install --save buffer

Luego use el método de nodo:

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

De hecho, descubrí lo que estaba mal. Para que el código funcione, la página <head>debe tener esta etiqueta:

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

O, como se sugiere en los comentarios, si el servidor envía un Content-Encodingencabezado HTTP , también debería funcionar.

Entonces los resultados de diferentes navegadores son consistentes.

Aquí hay un ejemplo:

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

Nota: sospecho que especificar cualquier codificación (precisa) solucionaría el problema de codificación. Es solo una coincidencia que necesite UTF-8.

Alejandro Gladysh
fuente
2
La unescapefunción de JavaScript no debe usarse para decodificar identificadores uniformes de recursos (URI).
Lauri Oherd
1
@LauriOherd de unescapehecho nunca debería usarse para decodificar URI. Sin embargo, para convertir texto a UTF-8 funciona bien
TS
unescape(encodeURIComponent(...)).lengthsiempre calcula la longitud correcta con o sin meta http-equiv ... utf8. Sin una especificación de codificación, algunos navegadores podrían simplemente tener un texto diferente (después de codificar los bytes del documento en texto html real) cuya longitud calcularon. Se podría probar esto fácilmente, imprimiendo no solo la longitud, sino también el texto en sí.
TS
3

Aquí hay un método independiente y eficiente para contar bytes UTF-8 de una cadena.

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

Tenga en cuenta que el método puede arrojar un error si una cadena de entrada tiene un formato UCS-2 incorrecto

fuweichin
fuente
3

En NodeJS, Buffer.byteLengthes un método específicamente para este propósito:

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

Tenga en cuenta que, de forma predeterminada, el método asume que la cadena está en codificación UTF-8. Si se requiere una codificación diferente, pásala como segundo argumento.

Booz
fuente
¿Es posible calcular strLengthInBytessimplemente conociendo el 'recuento' de caracteres dentro de la cadena? es decir var text = "Hello World!; var text_length = text.length; // pass text_length as argument to some method?. Y, apenas para la referencia, re Buffer- Acabo de encontrar esta respuesta la que se explica new Blob(['test string']).sizey, en nodo, Buffer.from('test string').length. ¿Quizás estos también ayuden a algunas personas?
user1063287
1
@ user1063287 El problema es que el número de caracteres no siempre es equivalente al número de bytes. Por ejemplo, la codificación UTF-8 común es una codificación de ancho variable, en la que un solo carácter puede tener un tamaño de 1 byte a 4 bytes. Es por eso que se necesita un método especial además de la codificación utilizada.
Boaz
Por ejemplo, una cadena UTF-8 con 4 caracteres, puede tener al menos 4 bytes de "longitud", si cada carácter tiene solo 1 byte; y como máximo 16 bytes de "longitud" si cada carácter tiene 4 bytes. Tenga en cuenta que, en cualquier caso, el recuento de caracteres sigue siendo 4 y, por lo tanto, no es una medida confiable para la longitud de bytes .
Boaz
1

Esto funcionaría para caracteres BMP y 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
fuente
0

Puedes probar esto:

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

Esto funciona para mi.

anh tran
fuente
devuelve 1 para "â" en cromo
Rick
el primer problema podría solucionarse cambiando \ xff a \ x7f, pero eso no soluciona el hecho de que se informará que los puntos de código entre 0x800-0xFFFF ocupan 2 bytes, cuando toman 3.
Rick