Unicode sin folclore

Unicode es un sistema de caracteres. UTF-8 es una forma de convertir ese sistema en bytes. Casi todos los "bugs de Unicode" pasan porque el software esconde el momento en que los bytes se vuelven texto y luego adivina en esa frontera. Deja de adivinar: declara la codificación, decodifica una sola vez, mantén el texto como texto y codifica una sola vez al salir.

🎙️ Publicado y grabado: ·

01Separa caracteres, code points y bytes

Un carácter es lo que una persona piensa como unidad escrita. Un code point de Unicode es una entrada numerada, como U más cero cero cuatro uno para la A, o U más uno F seis cero cero para la cara sonriente. Los bytes son los números que se guardan o se transmiten. Esas capas coinciden en el ASCII simple y se separan en todo lo interesante.

Text seen:       A        é        😀
Code point:      U+0041   U+00E9   U+1F600
UTF-8 bytes:     41       C3 A9    F0 9F 98 80
Byte count:      1        2        4

# Python makes the boundary visible
s = "é"
ord(s)                  # 233
s.encode("utf-8").hex()  # 'c3a9'

"Carácter Unicode" suele ser demasiado vago para depurar. Pregunta qué code points existen, qué bytes llegaron y qué codificación se usó. La salida en hexadecimal resuelve discusiones que ninguna captura de pantalla resuelve.

02Usa UTF-8 salvo que una restricción real diga lo contrario

UTF-8 representa cada code point de Unicode con uno a cuatro bytes. ASCII conserva sus valores familiares de un byte, y eso hizo fácil adoptar UTF-8. El texto no ASCII usa varios bytes. Es de ancho variable pero autosincronizante: los bytes de continuación tienen un patrón reconocible, lo que ayuda a los decodificadores a encontrar los límites.

U+0000..U+007F      0xxxxxxx                         1 byte
U+0080..U+07FF      110xxxxx 10xxxxxx                2 bytes
U+0800..U+FFFF      1110xxxx 10xxxxxx 10xxxxxx       3 bytes
U+10000..U+10FFFF   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  4 bytes

"café".encode("utf-8") → 63 61 66 C3 A9

Mi postura por defecto es directa: elige UTF-8 y déjalo declarado. No elijas una codificación heredada para ahorrar unos bytes de texto local. La compresión maneja mejor la repetición, mientras que la ambigüedad de codificación crea bugs que cruzan sistemas y sobreviven a los respaldos.

03Decodifica bytes una vez; codifica texto una vez

Decodificar convierte bytes en texto usando una codificación. Codificar convierte texto en bytes. Los archivos, sockets, drivers de base de datos y subprocesos son fronteras. Dentro de la aplicación, mantén los valores como cadenas Unicode. Una codificación no es algo que "le apliques a una cadena por seguridad"; es el contrato que se usa al cruzar una frontera de bytes.

# explicit file boundaries in Python
with open("names.txt", "r", encoding="utf-8") as f:
    text = f.read()             # bytes → text

payload = text.encode("utf-8") # text → bytes
round_trip = payload.decode("utf-8")
Fallo real de Python

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x96 in position 42: invalid start byte suele significar que el archivo es Windows-1252, donde el byte noventa y seis es una raya corta. Primero, conserva los bytes originales. Segundo, identifica quién produjo el archivo o revisa una muestra representativa; no adivines a partir de un solo carácter. Tercero, decodifica con la codificación de origen confirmada, por ejemplo encoding="cp1252". Cuarto, escribe un archivo nuevo en UTF-8 y arregla al productor. No uses errors="ignore"; el borrado silencioso convierte una corrupción visible en datos que faltan.

04El mojibake es evidencia del decodificador equivocado

Mojibake es basura con apariencia de texto, causada por decodificar bytes con la codificación de caracteres equivocada. Que la palabra café se vuelva café es el caso clásico: bytes UTF-8 interpretados como Windows-1252 o Latin-1. El carácter de reemplazo, �, significa que un decodificador no pudo mapear los bytes y sustituyó por U más F F F D. Una vez que los originales se reemplazan, la información puede estar ya perdida.

Original text:              café
Correct UTF-8 bytes:        63 61 66 C3 A9
Wrong Windows-1252 decode:  café

Danger: decode with replacement → save → original bad bytes are gone
Repair: recover original bytes → identify encoding → decode once
Síntoma real: rombos con signos de interrogación

Una importación de CSV muestra Jos�. Primero, detén la importación antes de que sobrescriba registros limpios. Segundo, revisa el archivo de origen como bytes y consigue la codificación de exportación declarada. Tercero, vuelve a exportar como UTF-8 o decodifica con la codificación heredada real. Cuarto, verifica nombres con acentos, escrituras no latinas y emoji antes de la importación completa. Reemplazar � por é a mano es inventar: el carácter perdido pudo haber sido cualquiera.

05El BOM es metadato que a veces se cuela en los datos

La marca de orden de bytes servía para indicar el orden de bytes en UTF-16 y UTF-32. UTF-8 no tiene problema de orden de bytes, pero algunas herramientas anteponen los tres bytes E F B B B F como firma de UTF-8. Algunos lectores se los comen. Otros exponen U más F E F F como primer carácter, y eso rompe encabezados, JSON y scripts de consola.

UTF-8 BOM bytes: EF BB BF
Visible mojibake if decoded wrongly: 
Python reader that consumes it: encoding="utf-8-sig"
Plain UTF-8 output:              encoding="utf-8"

# Diagnose, do not blindly strip every file
raw = open("data.csv", "rb").read(4)
print(raw.hex())  # efbbbf...
Fallos reales:  y BOM inesperado

Un encabezado de CSV se vuelve name, o un parser reporta JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig). Primero, revisa los primeros bytes buscando EF BB BF. Segundo, confirma que el archivo no fue decodificado ya con una codificación heredada; un  literal delata ese error. Tercero, configura el lector para que consuma un BOM UTF-8 confirmado, o exporta UTF-8 plano. Cuarto, compara exactamente el primer campo después de parsear. No hagas un reemplazo global de texto: un U más F E F F legítimo en otro lugar es dato.

06Normaliza cuando la equivalencia importa

El mismo texto visible puede usar secuencias distintas de code points. La letra é puede ser un único code point precompuesto, o la letra e seguida de un acento agudo combinante. La normalización Unicode convierte secuencias equivalentes a una forma elegida. NFC es un buen valor por defecto para almacenar y comparar. NFKC también cambia caracteres de compatibilidad, así que úsalo solo cuando ese plegado semántico sea lo que quieres.

import unicodedata

a = "é"             # U+00E9
b = "e\u0301"        # U+0065 + U+0301
a == b                      # False
unicodedata.normalize("NFC", a) == \
unicodedata.normalize("NFC", b)  # True

Normaliza en una frontera definida, no al azar antes de cada comparación. Conserva el original cuando la ortografía exacta tenga valor legal o forense. La normalización no es plegado de mayúsculas, ni transliteración, ni quitar acentos, ni validación de seguridad; esas son decisiones aparte con pérdidas aparte.

07La gente edita clústeres de grafemas, no code points

Un clúster de grafemas es, más o menos, un carácter tal como lo percibe el usuario. Puede contener un code point, una base más marcas combinantes, una bandera hecha de indicadores regionales, o una secuencia de emoji unida por controles invisibles. El emoji de familia puede contener varias personas y varios joiners y verse como un solo glifo. Por eso indexar por code point no es un modelo de cursor seguro.

Visible unit       Possible code points
é                  U+00E9  or  U+0065 U+0301
🇯🇵                 U+1F1EF U+1F1F5
👩🏽‍💻                woman + skin tone + joiner + laptop

JavaScript:
"😀".length             // 2 UTF-16 code units
[..."😀"].length        // 1 code point
// Still use Intl.Segmenter for grapheme clusters.

Usa una implementación de segmentación Unicode mantenida, como el iterador de grafemas de la plataforma o Intl.Segmenter. No escribas una regex de emoji sacada de un blog y lo des por resuelto. Unicode evoluciona, y las banderas, modificadores, selectores de variación y secuencias con joiner derrotan a los rangos simples.

08Define qué significa "longitud" antes de contar

Longitud puede significar bytes para un límite de almacenamiento, unidades de código para una API del runtime, code points para reglas de protocolo, clústeres de grafemas para un editor, o ancho de visualización para una terminal. Ninguna es universalmente correcta. Nombra la unidad en la variable y en el mensaje de error. "Longitud máxima cien" es un requisito incompleto.

LIMIT                         MEASURE
Database byte column          encoded bytes in database charset
Username policy               normalized code points or graphemes, documented
Text input counter            grapheme clusters users perceive
Terminal table                display columns, with a width library
Network packet                encoded bytes

Never: encoded[:10].decode("utf-8")
Instead: truncate text at a supported boundary, then encode
Fallo real: una cadena cortada no decodifica

UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 8-9: unexpected end of data significa que el recorte partió una secuencia multibyte. Primero, vuelve a los bytes sin cortar o al texto original. Segundo, decodifica la entrada completa. Tercero, segmenta el texto en la unidad que el producto promete, de preferencia clústeres de grafemas para entradas visibles. Cuarto, recorta segmentos completos y codifica después. Si el límite duro es en bytes, ve sumando segmentos completos hasta que el siguiente segmento codificado se pase.

09Haz explícitos los contratos de codificación entre sistemas

Los nombres de archivo son cadenas Unicode en muchas APIs, pero la normalización y el manejo de mayúsculas cambian según el sistema de archivos. Las bases de datos tienen codificaciones o collations de servidor, base, tabla, columna y conexión. HTTP envía bytes; el media type y las reglas del protocolo dicen cómo decodificarlos. Prueba el camino completo, no solo un literal de cadena dentro de un proceso.

FILES      open(path, encoding="utf-8") for text content
DATABASE   UTF-8-capable schema + UTF-8 client connection
MYSQL      prefer utf8mb4, not historical three-byte utf8
HTTP       Content-Type: text/plain; charset=utf-8
JSON       send UTF-8 bytes; do not guess from rendered text
TEST       "José · 東京 · हिंदी · 😀 · e\u0301" round trip
Fallo real de base de datos

MySQL puede reportar ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x98\x80' for column 'name' at row 1 cuando un emoji de cuatro bytes llega a una columna o conexión que usa el viejo charset utf8 de tres bytes. Primero, revisa los charsets de la columna, la tabla, la base y la conexión del cliente. Segundo, migra el esquema relevante a utf8mb4 con un collation apropiado. Tercero, configura la conexión del driver en utf8mb4. Cuarto, repite una prueba de ida y vuelta y vuelve a leer el valor. Cambiar solo la columna puede dejar la conexión corrompiendo la entrada.

10Flujo de depuración y cheat sheet de Unicode

No repares bugs de codificación llamando encode y decode una y otra vez hasta que la pantalla se vea bien. Congela la entrada, encuentra la primera frontera rota y demuestra cada transformación.

DEBUG IN ORDER
1. Preserve original bytes; stop destructive imports or saves.
2. Locate the first boundary where correct text becomes wrong.
3. Print bytes as hex and text as escaped code points.
4. Find the declared encoding at the producer and transport.
5. Decode exactly once with that encoding.
6. Normalize only if the product's comparison rules require it.
7. Keep text internally; encode once at output.
8. Round-trip accents, combining marks, CJK, emoji, and empty text.

SYMPTOM                         LIKELY CAUSE
café                           UTF-8 decoded as Windows-1252/Latin-1
�                               invalid bytes replaced; possible data loss
 at the start                BOM bytes decoded as legacy text
Unexpected UTF-8 BOM            reader expects plain UTF-8
Incorrect string value \xF0…    database/connection lacks four-byte support
Different strings look equal    normalization mismatch
Broken emoji after truncation   code-unit, code-point, or byte split

DEFAULTS
Text encoding: UTF-8 · normalization when needed: NFC
MySQL Unicode: utf8mb4 · user-visible slicing: grapheme clusters

Unicode no es misterioso; las fronteras escondidas sí. Haz visibles los bytes, exige contratos en UTF-8 y elige la unidad correcta para comparar y recortar. La peor solución es la que hace que la muestra de hoy se vea bien mientras destruye los bytes originales de mañana.

Tell me what missed

A correction is more useful than a compliment. This goes straight to the person who writes SwiftGrasp.

Was this page useful?
0/1000

Please do not include passwords, private keys, or personal information.