Skip to content
Codificación de caracteres para developers — ASCII, UTF-8, UTF-16

Codificación

Codificación de caracteres para developers — ASCII, UTF-8, UTF-16

La codificación de caracteres confunde a la gente décadas después de estar resuelta. El modelo mental: code points, encodings, BOMs, y por qué UTF-8 casi siempre gana.

La codificación de caracteres debería ser un problema resuelto en 2026, y para la mayoría lo es — casi todo es UTF-8. Pero el modelo mental sigue importando porque el 5% de casos en que algo falla (BOMs, exports legacy, diálogos de archivo de Windows, librerías cliente de BD) te come horas a la semana si no conoces la forma del problema. Esta guía es la base.

Los dos conceptos separados

Fuente de confusión principal: “Unicode” y “UTF-8” son cosas distintas.

  • Unicode es un estándar que asigna a cada carácter un entero único llamado code point, escrito U+XXXX. A es U+0041. é es U+00E9. El emoji de caca es U+1F4A9. Hay más de un millón de code points posibles; unos 150.000 están asignados a fecha de Unicode 16.
  • Un encoding es una forma de convertir code points en bytes. UTF-8, UTF-16, UTF-32 y encodings legacy como Latin-1, Shift-JIS, Windows-1252 todos encodean (algún subconjunto de) caracteres en bytes.

Unicode es el set de caracteres. UTF-8 es un encoding de ese set. Puedes tener Unicode encodeado en UTF-8, Unicode encodeado en UTF-16, o los mismos caracteres como bytes Windows-1252 legacy. Mismos caracteres, bytes distintos en disco.

ASCII, el ancestro común

ASCII es un encoding de 7 bits definido en 1963. Cubre 128 code points: letras latinas, dígitos, puntuación, caracteres de control. Cada byte cabe en 7 bits; el octavo es cero.

Todo encoding moderno es compatible hacia atrás con ASCII a nivel de bytes para esos 128 code points. Un archivo que solo contiene ASCII es simultáneamente ASCII válido, UTF-8, Latin-1 y Windows-1252. Por eso herramientas de los 80 siguen funcionando para texto solo en inglés.

UTF-8 — el default universal

UTF-8 es un encoding de longitud variable. Cada code point encodea a 1-4 bytes:

Rango de code pointBytesPatrón binario
U+0000 a U+007F10xxxxxxx
U+0080 a U+07FF2110xxxxx 10xxxxxx
U+0800 a U+FFFF31110xxxx 10xxxxxx 10xxxxxx
U+10000 a U+10FFFF411110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Tres consecuencias a interiorizar:

  1. Los code points ASCII son un byte cada uno, bit a bit idénticos a ASCII. A en UTF-8 es 0x41, el mismo byte que en ASCII.
  2. Cualquier byte que empiece en 10 es byte de continuación, nunca inicio de carácter. Por eso UTF-8 es self-synchronizing: puedes caer en mitad de un archivo y encontrar el siguiente inicio de carácter en máximo 3 bytes.
  3. Los caracteres no-ASCII ocupan 2-4 bytes. é son 2 bytes (0xC3 0xA9). son 3. Un emoji de caca son 4.

UTF-8 es ya el encoding dominante en la web (más del 98% de páginas), en la mayoría de bases de datos, en sistemas de archivos Linux, y en el I/O por defecto de la mayoría de lenguajes.

# Ver los bytes detrás de un carácter
printf "é" | xxd
# 00000000: c3a9       .. 
printf "A" | xxd
# 00000000: 41         A
printf "你" | xxd
# 00000000: e4bda0     ...

Para explorar interactivamente la relación entre caracteres y su representación binaria, las herramientas text-to-binary y binary-to-text te permiten ver el layout de bytes carácter a carácter.

UTF-16 — por qué persiste

UTF-16 encodea code points en 2 o 4 bytes. Era el encoding “obvio” cuando Unicode tenía 16 bits de ancho (pre-1996). Unicode creció más allá, así que UTF-16 añadió “pares surrogate” — dos unidades de 16 bits que juntas encodean un solo code point por encima de U+FFFF.

UTF-16 sigue apareciendo en tres sitios:

  • APIs Windows internamente. Las versiones W de funciones Win32 (CreateFileW, MessageBoxW) toman wide strings UTF-16.
  • Strings Java y JavaScript internamente. El string.length de JavaScript cuenta unidades de código UTF-16, no caracteres — por eso "💩".length es 2, no 1.
  • Algunos servicios XML y SOAP antiguos que emiten UTF-16 con BOM.

Para casi cualquier I/O nuevo, UTF-8 es lo correcto. UTF-16 no tiene ventaja de almacenamiento para texto en inglés (lo duplica), no es self-synchronizing, y tiene un problema de orden de bytes que UTF-8 no tiene.

// Indexación de strings en JavaScript es UTF-16, no code points
const s = "💩abc";
s.length;        // 4 (2 code units para 💩 + 3)
[...s].length;   // 4? No — 4 (el spread itera code points)
s.charAt(0);     // "\uD83D" — surrogate solo, no un carácter
[...s][0];       // "💩" — iterando con spread se usan code points

Byte Order Mark — un peligro histórico

Un Byte Order Mark (BOM) es un U+FEFF opcional al inicio de un archivo que indica el orden de bytes de UTF-16 o UTF-32. En UTF-16LE es FF FE; en UTF-16BE es FE FF.

Para UTF-8 no hay ambigüedad de orden — bytes son bytes — pero Microsoft Notepad y otras tools escriben un BOM UTF-8 de tres bytes (EF BB BF) al inicio. Es legal por la spec Unicode pero desaconsejado. Rompe:

  • Scripts de shell (el BOM aparece antes del #!/bin/bash).
  • Parsers CSV que esperan que el primer campo empiece en el byte 0.
  • Parsers YAML y JSON en algunas librerías antiguas.
  • git diff de un archivo que de repente tiene BOM.

Si tu export CSV desde Excel abre con caracteres raros antes del primer nombre de campo, tiene un BOM UTF-8. Quítalo con sed -i '1s/^\xEF\xBB\xBF//' file.csv o lee el archivo como utf-8-sig en Python.

Encodings legacy que siguen en circulación

EncodingQué esDónde lo encuentras
Latin-1 / ISO-8859-1Caracteres europeos 1 byteDefault HTTP antiguo, dumps de BD antiguos
Windows-1252Latin-1 + algo de puntuaciónExcel, programas Windows viejos
Shift-JISJaponés 1-2 bytesSistemas japoneses legacy
GB18030Chino (continental)Sistemas gubernamentales chinos
UTF-16LEInterno JavaScript / WindowsSerializado como formato ocasionalmente

El carácter é es U+00E9 en Unicode, byte 0xE9 en Latin-1, bytes 0xC3 0xA9 en UTF-8. Cuando ves é en un navegador, estás viendo bytes UTF-8 siendo decodeados como Latin-1: los dos bytes UTF-8 de é se interpretan como dos caracteres Latin-1 separados. Este síntoma único te dice: “algo en la pipeline no es UTF-8-aware”.

Grapheme clusters — lo que va más allá de code points

Un carácter percibido por el usuario no siempre es un code point. 👨‍👩‍👧 es un emoji familia hecho de cinco code points unidos por zero-width joiners. é puede ser o U+00E9 (un code point) o U+0065 + U+0301 (dos code points: e + acute combinante). Para contar “caracteres” como los ve un usuario necesitas segmentación de grapheme clusters, que es el trabajo de Intl.Segmenter en JavaScript o el crate grapheme en Rust.

Por eso "💩".length === 2 no es la respuesta correcta a “cuántos caracteres tiene este string” — y por eso el word counter usa conteo grapheme-aware para resultados correctos en input con muchos emojis.

Reglas prácticas

  1. Usa UTF-8 en todos lados. Input, output, nombres de archivo, encoding de columna BD, headers HTTP. Todos.
  2. Declara el encoding en todo formato que lo soporte. <meta charset="utf-8"> en HTML, # -*- coding: utf-8 -*- en Python viejo, Content-Type: application/json; charset=utf-8 en respuestas HTTP.
  3. Strip BOMs en ingesta. Si aceptas archivos, maneja un BOM opcional. No emitas BOMs en salida.
  4. Nunca confíes en que “un string tiene una longitud” — sabe si cuentas bytes, unidades de código (UTF-16), code points, o grapheme clusters. Son cuatro números distintos.

Conclusiones

Unicode es el set de caracteres. UTF-8 es el encoding. Default a UTF-8 para todo. Los BOMs son peligro histórico; manéjalos en input, no los emitas en output. La longitud de un string son cuatro números distintos según qué quieras. Para el encoding de transporte vecino, mira la guía de Base64 en producción; para cómo el encoding de caracteres se cruza con URLs, la guía de URL encoding.

Herramientas relacionadas

Por ·