Comprimento da string em bytes em JavaScript

104

No meu código JavaScript, preciso escrever uma mensagem para o servidor neste formato:

<size in bytes>CRLF
<data>CRLF

Exemplo:

3
foo

Os dados podem conter caracteres Unicode. Preciso enviá-los como UTF-8.

Estou procurando a maneira mais cruzada de navegadores de calcular o comprimento da string em bytes em JavaScript.

Eu tentei isso para compor minha carga útil:

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

Mas não me dá resultados precisos para os navegadores mais antigos (ou, talvez, as strings nesses navegadores em UTF-16?).

Alguma pista?

Atualizar:

Exemplo: o comprimento em bytes da string ЭЭХ! Naïve?em UTF-8 é 15 bytes, mas alguns navegadores relatam 23 bytes.

Alexander Gladysh
fonte
1
Possível duplicata? stackoverflow.com/questions/2219526/…
Eli
@Eli: nenhuma das respostas da pergunta que você vinculou funciona para mim.
Alexander Gladysh
Quando você fala sobre "ЭЭХ! Ingênuo?" você o colocou em uma forma normal particular? unicode.org/reports/tr15
Mike Samuel
@ Mike: Eu digitei no editor de texto aleatório (no modo UTF-8) e salvei. Assim como qualquer usuário da minha biblioteca faria. No entanto, parece que descobri o que estava errado - veja minha resposta.
Alexander Gladysh

Respostas:

89

Não há como fazer isso nativamente em JavaScript. (Veja a resposta de Riccardo Galli para uma abordagem moderna.)


Para referência histórica ou onde APIs de TextEncoder ainda não estão disponíveis .

Se você conhece a codificação de caracteres, pode calculá-la sozinho.

encodeURIComponent assume UTF-8 como a codificação de caracteres, então se você precisa dessa codificação, você pode fazer,

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

Isso deve funcionar devido à maneira como o UTF-8 codifica sequências de bytes múltiplos. O primeiro byte codificado sempre começa com um bit alto de zero para uma única sequência de bytes ou um byte cujo primeiro dígito hexadecimal é C, D, E ou F. O segundo e subsequentes bytes são aqueles cujos primeiros dois bits são 10 Esses são os bytes extras que você deseja contar em UTF-8.

A tabela na wikipedia torna isso mais claro

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

Se, em vez disso, você precisa entender a codificação da página, pode usar este truque:

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
fonte
Bem, como eu saberia a codificação de caracteres dos dados? Preciso codificar qualquer string de usuário (programador) fornecida à minha biblioteca JS.
Alexander Gladysh
@Alexander, ao enviar a mensagem para o servidor, você especifica a codificação do conteúdo do corpo da mensagem por meio de um cabeçalho HTTP?
Mike Samuel
1
@Alexander, legal. Se você estiver estabelecendo um protocolo, obrigar o UTF-8 é uma ótima ideia para a troca de texto. Uma variável a menos que pode resultar em incompatibilidade. UTF-8 deve ser a ordem de byte de rede das codificações de caracteres.
Mike Samuel
4
@MikeSamuel: A lengthInUtf8Bytesfunção retorna 5 para caracteres não BMP como str.lengthpara esses retornos 2. Escreverei uma versão modificada dessa função na seção de respostas.
Lauri Oherd
1
Esta solução é legal, mas utf8mb4 não é considerado. Por exemplo, encodeURIComponent('🍀')é '%F0%9F%8D%80'.
Albert
117

Anos se passaram e hoje você pode fazer isso nativamente

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

Observe que ainda não é compatível com o IE (ou Edge) (você pode usar um polyfill para isso).

Documentação MDN

Especificações padrão

Riccardo Galli
fonte
4
Que abordagem fantástica e moderna. Obrigado!
Con Antonakos
Observe que, de acordo com a documentação do MDN , o TextEncoder ainda não é compatível com o Safari (WebKit).
Maor
TextEncodesuporta apenas utf-8 desde o Chrome 53.
Jehong Ahn
1
Se você só precisa do comprimento, pode ser um exagero alocar uma nova string, fazer a conversão real, pegar o comprimento e, em seguida, descartar a string. Veja minha resposta acima para uma função que apenas calcula o comprimento de uma maneira eficiente.
lovasoa
66

Esta é uma versão muito mais rápida, que não usa expressões regulares, nem 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;
}

Aqui está uma comparação de desempenho .

Ele apenas calcula o comprimento em UTF8 de cada ponto de código Unicode retornado por charCodeAt () (com base nas descrições da wikipedia de UTF8 e caracteres substitutos de UTF16).

Ele segue a RFC3629 (em que os caracteres UTF-8 têm no máximo 4 bytes).

Lovasoa
fonte
46

Para codificação UTF-8 simples, com compatibilidade ligeiramente melhor do que TextEncoder, Blob resolve. Não funciona em navegadores muito antigos.

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

Esta função retornará o tamanho de byte de qualquer string UTF-8 que você passar para ela.

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

Fonte

Lauri Oherd
fonte
não funciona com a string 'ユ ー ザ ー コ ー ド', comprimento esperado de 14, mas 21
Clima VN
1
@MayWeatherVN seu ユーザーコードcomprimento errado em bytes é sempre 21, testei em diferentes ferramentas; seja mais gentil com seus comentários;)
Capitex
Essa seqüência Lembro-me testando em php é de 14
Maio Tempo VN
23

Outra abordagem muito simples usando Buffer(apenas para NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length
Iván Pérez
fonte
1
Você pode pular a criação de um buffer com Buffer.byteLength(string, 'utf8').
Joe
1
@Joe Obrigado pela sugestão, acabei de fazer uma edição para incluí-la.
Iván Pérez
5

Demorei um pouco para encontrar uma solução para o React Native, então vou colocá-la aqui:

Primeiro instale o bufferpacote:

npm install --save buffer

Em seguida, use o método do nó:

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

Na verdade, descobri o que há de errado. Para que o código funcione, a página <head>deve ter esta tag:

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

Ou, como sugerido nos comentários, se o servidor enviar o Content-Encodingcabeçalho HTTP , ele também deve funcionar.

Então, os resultados de diferentes navegadores são consistentes.

Aqui está um exemplo:

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

Observação: suspeito que a especificação de qualquer codificação (precisa) resolveria o problema de codificação. É apenas uma coincidência eu precisar do UTF-8.

Alexander Gladysh
fonte
2
A unescapefunção JavaScript não deve ser usada para decodificar Uniform Resource Identifiers (URI).
Lauri Oherd
1
@LauriOherd unescaperealmente nunca deve ser usado para decodificar URIs. No entanto, para converter texto em UTF-8 funciona bem
TS de
unescape(encodeURIComponent(...)).lengthsempre calcula o comprimento correto com ou sem meta http-equiv ... utf8. Sem uma especificação de codificação, alguns navegadores podem simplesmente ter um texto diferente (depois de codificar os bytes do documento em texto html real) cujo comprimento eles calcularam. Pode-se testar isso facilmente, imprimindo não apenas o comprimento, mas também o próprio texto.
TS de
3

Aqui está um método independente e eficiente para contar bytes UTF-8 de uma string.

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

Observe que o método pode gerar erro se uma string de entrada for UCS-2 malformada

Fuweichin
fonte
3

No NodeJS, Buffer.byteLengthé um método especificamente para este propósito:

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

Observe que, por padrão, o método assume que a string está na codificação UTF-8. Se uma codificação diferente for necessária, passe-a como o segundo argumento.

Boaz
fonte
É possível calcular strLengthInBytesapenas sabendo a 'contagem' de caracteres dentro da string? ie var text = "Hello World!; var text_length = text.length; // pass text_length as argument to some method?. E, apenas para referência, re Buffer- acabei de encontrar esta resposta que discute new Blob(['test string']).sizee, no nó Buffer.from('test string').length,. Talvez ajudem algumas pessoas também?
user1063287
1
@ user1063287 O problema é que o número de caracteres nem sempre é equivalente ao número de bytes. Por exemplo, a codificação UTF-8 comum é uma codificação de largura variável, na qual um único caractere pode ter 1 byte a 4 bytes de tamanho. É por isso que um método especial é necessário, bem como a codificação usada.
Boaz
Por exemplo, uma string UTF-8 com 4 caracteres pode ter pelo menos 4 bytes "de comprimento", se cada caractere tiver apenas 1 byte; e no máximo 16 bytes de "comprimento" se cada caractere tiver 4 bytes. Observe que em ambos os casos a contagem de caracteres ainda é 4 e, portanto, não é uma medida confiável para o comprimento dos bytes .
Boaz
1

Isso funcionaria para caracteres BMP e 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
fonte
0

Você pode tentar isto:

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

Funciona para mim.

anh tran
fonte
retorna 1 para "â" no cromo
Rick
o primeiro problema poderia ser corrigido alterando \ xff para \ x7f, mas isso não corrige o fato de que os codepoints entre 0x800-0xFFFF serão relatados como tendo 2 bytes, quando levam 3.
Rick