Por Qué tu ñ Aparece Como ñ
Dev Tools

Por Qué tu ñ Aparece Como ñ

El Síntoma: un Nombre que Antes Decía Otra Cosa

Un cliente llamado "Muñoz" se guarda, se exporta o se envía por correo, y lo que vuelve es "Muñoz". Un símbolo de grados se convierte en "°". Una comilla tipográfica se transforma en una fila de cuadraditos o de signos de interrogación. Nada de esto es un archivo corrupto en el sentido de datos perdidos o desordenados — normalmente cada byte original sigue ahí, intacto, en el disco. Lo que falla es la suposición sobre a qué alfabeto pertenecen esos bytes. Este tipo concreto y reconocible de texto ilegible tiene nombre propio, tomado del japonés: mojibake, literalmente "transformación de caracteres". No es un término genérico para "el texto se ve mal" — describe un fallo muy preciso: bytes escritos bajo una codificación y leídos después bajo otra distinta.

Vale la pena aprender a reconocer el mojibake a simple vista, en lugar de arreglarlo a base de prueba y error, porque el resultado ilegible no es aleatorio. Es una función determinista de los bytes originales y de la codificación equivocada que se les aplicó. Una vez que conoces esa función, puedes mirar "ñ" y deshacer el camino hasta el carácter original con total seguridad, igual que descifrarías cualquier otra transformación reversible.

La Aritmética Detrás de ñ

Empecemos por la letra en sí. La ñ española lleva asignado un identificador fijo dentro de Unicode, el punto de código U+00F1. Ese identificador no es en sí mismo un valor de byte; es sencillamente el número que el estándar reservó para esta letra, y convertirlo en bytes guardados es una tarea aparte, a cargo de una codificación.

Codifica U+00F1 en UTF-8 y la regla es directa: cualquier punto de código entre U+0080 y U+07FF ocupa exactamente dos bytes, repartidos en cinco bits para el primero y seis para el segundo. U+00F1 son 241 en decimal, que en binario de 8 bits es 11110001. Rellenado hasta los 11 bits que usa la forma de dos bytes, esa cadena queda 00011110001: los 5 bits superiores son 00011 y los 6 bits inferiores son 110001. El primer byte es el prefijo fijo 110 más esos 5 bits: 11000011, que es 0xC3. El segundo byte es el prefijo fijo 10 más los 6 bits restantes: 10110001, que es 0xB1. Así que la ñ, bien codificada, queda como los bytes 0xC3 seguido de 0xB1 — un par que, dondequiera que se guarde, ya es exactamente correcto. Todavía no ha pasado nada malo.

La corrupción llega un paso después, cuando algo lee esos dos bytes y da por hecho que representan Latin-1 (ISO 8859-1) en lugar de UTF-8. Latin-1 es una codificación de un solo byte: cada valor de byte corresponde exactamente a un carácter, sin secuencias de varios bytes. Busca el byte 0xC3 en la tabla Latin-1 y obtienes à (A mayúscula con virgulilla), valor de byte 195. Busca el 0xB1 y obtienes ± (el signo de más-menos), valor de byte 177. Dos bytes pensados para leerse juntos, como una sola letra, se leen por separado, como dos letras sin ninguna relación entre sí — y "ñ" es lo que aparece en la pantalla. Los bytes nunca cambiaron. Lo único que cambió fue la tabla usada para interpretarlos.

La  Fantasma que Aparece Delante de las Cosas

Hay un patrón relacionado que aparece constantemente y que, a primera vista, parece un fallo sin relación con el anterior: una  suelta se cuela justo antes de un símbolo de grados, un espacio de no separación o un símbolo de moneda, como en "25°C" en vez de "25°C". Es exactamente el mismo mecanismo, disparado por un rango de puntos de código distinto.

Todo punto de código entre U+0080 y U+00BF se codifica en UTF-8 con el mismo primer byte: 0xC2. Toma el símbolo de grados, U+00B0 (176 en decimal, 10110000 en binario). Rellenado a 11 bits queda 00010110000: los 5 bits superiores 00010 dan un primer byte de 11000010 = 0xC2, y los 6 bits inferiores 110000 dan un segundo byte de 10110000 = 0xB0. Así que el ° en UTF-8 son los bytes 0xC2 0xB0. Lee esos dos bytes como Latin-1: 0xC2 es Â, y 0xB0 resulta que ya es ° en la propia tabla Latin-1 — así que lo que se ve es "°", una letra fantasma pegada delante de un símbolo que, por lo demás, se ve bien. La misma cuenta explica una  suelta antes de un espacio de no separación (U+00A0, que se codifica como 0xC2 0xA0): el espacio en sí es invisible en cualquiera de los dos casos, así que lo único que notas es la  huérfana donde no debería haber nada.

Cuatro Sitios Donde Vive Realmente Este Desajuste

El mojibake es un síntoma, no un diagnóstico. Para arreglarlo hay que encontrar la única capa, de entre varias posibles, donde chocaron una suposición de UTF-8 y una suposición de Latin-1. En la práctica, casi siempre es uno de estos cuatro puntos.

  • La columna de la base de datos y el charset de la conexión. Una columna de tabla puede estar declarada con un juego de caracteres (por ejemplo, latin1) mientras la aplicación envía bytes que cree que son UTF-8 a través de una conexión a la que nadie le ha dicho lo contrario. En MySQL, ejecuta SHOW FULL COLUMNS FROM tu_tabla para ver el charset declarado de cada columna, y SHOW VARIABLES LIKE 'character_set%' para ver qué está negociando la conexión actual. Una columna atascada en latin1 que recibe bytes UTF-8 los reinterpretará en silencio al entrar, al salir, o en ambos momentos, según en qué punto esté el desajuste.
  • La cabecera HTTP Content-Type. Un cuerpo de respuesta codificado en UTF-8 pero servido con Content-Type: text/html; charset=ISO-8859-1 — o sin ningún parámetro de charset, obligando al navegador a adivinar — le dice al extremo receptor que decodifique con la tabla equivocada antes incluso de examinar un solo byte. Compruébalo directamente con curl -sI https://ejemplo.com/pagina y lee la línea Content-Type, o abre el panel de red de tu navegador e inspecciona las cabeceras de la petición real.
  • Un archivo leído sin indicar una codificación explícita. Muchos entornos de ejecución recurren a un valor por defecto de la plataforma cuando no se indica ninguna codificación — el open() de Python usa la codificación preferida del locale a menos que le pases encoding="utf-8"; los constructores de FileReader de Java que no reciben charset leen con Charset.defaultCharset(), que seguía al locale de la plataforma hasta que JEP 400 impuso UTF-8 como valor por defecto de toda la JVM en JDK 18 (marzo de 2022) — así que en un JDK actual este caso por sí solo ya no muerde, pero sigue mordiendo en un runtime más antiguo, o en cualquier JDK 18 o posterior arrancado con -Dfile.encoding=COMPAT para recuperar el comportamiento anterior. Un archivo UTF-8 leído bajo un valor por defecto silencioso distinto se malinterpreta desde el mismo momento en que se carga, antes de que tu código haya hecho nada más con él.
  • Un CSV abierto directamente en una hoja de cálculo. Las versiones antiguas de Excel, e incluso las actuales con un simple doble clic, adivinan la codificación de un archivo en lugar de preguntar, y esa suposición suele ser la página de códigos regional del sistema y no UTF-8. La solución es importar el archivo de forma deliberada — Datos → Desde texto/CSV en Excel — y elegir UTF-8 explícitamente en el asistente, o guardar la exportación con una marca de orden de bytes UTF-8 (los tres bytes 0xEF 0xBB 0xBF al principio del archivo), que la mayoría de las hojas de cálculo detectan y respetan automáticamente.
Por Qué tu ñ Aparece Como ñ

Confirmar el Diagnóstico Mirando los Bytes

Adivinar solo a partir del texto ilegible en pantalla invita a errores, porque más de una combinación de codificación equivocada puede producir un ruido con aspecto parecido. La forma fiable de confirmar qué ha pasado es dejar de mirar el texto renderizado y mirar en su lugar los bytes en bruto. En la línea de comandos, xxd archivo_sospechoso.txt | head u od -An -tx1 archivo_sospechoso.txt vuelca los valores de byte reales del archivo en hexadecimal, sin depender de cómo decida mostrarlos un editor o una terminal en concreto.

Con los bytes en bruto delante, la comprobación es aritmética, no una suposición: si esperabas una ñ y ves el par de bytes c3 b1, el archivo está correctamente codificado en UTF-8 y la corrupción ocurre más adelante, al mostrarlo o renderizarlo. Si en cambio ves un solo byte como f1, el archivo se guardó como Latin-1 (o Windows-1252) y nunca llegó a ser UTF-8 — un problema realmente distinto, con un arreglo distinto. Pegar el texto sospechoso en una herramienta que muestre, byte a byte, cómo queda codificado cada carácter en UTF-8 — nuestro propio conversor ASCII y Unicode lo lista carácter a carácter junto al punto de código — da la misma respuesta sin necesidad de terminal, y permite comparar lo que escribiste con lo que realmente devolvió una base de datos o una API.

Por Qué la Doble Codificación no Siempre se Puede Deshacer

A veces la secuencia de dos bytes 0xC3 0xB1 no solo se muestra mal — se guarda mal, de forma permanente, por un segundo paso a través del mismo error. Si un sistema lee bytes UTF-8 como si fueran Latin-1, produciendo los dos caracteres à y ±, y esa cadena de dos caracteres se codifica a su vez como UTF-8 y se vuelve a escribir en el almacenamiento, ahora tienes cuatro bytes en disco donde antes había dos: la à (U+00C3) se convierte en 0xC3 0x83, y el ± (U+00B1) se convierte en 0xC2 0xB1, dejando la secuencia guardada 0xC3 0x83 0xC2 0xB1. Esto es UTF-8 "doblemente codificado", y en principio se puede deshacer: decodifica los cuatro bytes como UTF-8 para recuperar los dos caracteres à y ±, toma sus puntos de código — 0xC3 y 0xB1 — trátalos de nuevo como simples valores de byte, y decodifica ese par como UTF-8 para volver a caer en la ñ.

Pero esa vuelta atrás solo funciona cuando cada byte del camino corresponde a un carácter que Latin-1 (o su prima cercana Windows-1252, que en la práctica usan muchos decodificadores que dicen "Latin-1") define realmente. Windows-1252 tiene cinco valores de byte — 0x81, 0x8D, 0x8F, 0x90 y 0x9D — que no corresponden a ningún carácter en su tabla. Un byte UTF-8 que, durante una mala decodificación, caiga justo en una de esas posiciones sin definir, se sustituye habitualmente por el carácter de reemplazo de Unicode, U+FFFD, en lugar de pasar tal cual. Esa sustitución no es reversible, porque U+FFFD se usa para representar muchos bytes distintos que no se pueden representar de otra forma — una vez que varios valores originales distintos han quedado colapsados en el mismo carácter de relleno, no hay forma de saber, solo con ese relleno, cuál era el original. La información se ha perdido de verdad, no solo se ha mostrado mal, y esa es la diferencia real entre un fallo de visualización que se puede arreglar después y un fallo de pérdida de datos que no se puede.

Una Lista de Comprobación Práctica para la Próxima Vez

  • Reconoce primero la forma: una à o una  repetida seguida de un símbolo sin relación es, casi siempre, bytes UTF-8 leídos como Latin-1 o Windows-1252 — no un problema de tipografía, ni un vago problema de "caracteres especiales".
  • Confírmalo con un volcado hexadecimal o una herramienta a nivel de byte antes de cambiar nada. Si los bytes guardados ya están mal (un solo byte donde debería haber una secuencia de dos), el arreglo va en el punto donde se escribe, no en el punto donde se muestra.
  • Comprueba por separado el charset de la columna de la base de datos y el de la conexión activa — pueden no coincidir aunque ambos parezcan razonables por separado.
  • Comprueba la cabecera HTTP real de la respuesta, no solo lo que tu motor de plantillas dice que produce, porque un parámetro de charset ausente o equivocado se impone sobre todo lo que viene después.
  • Nunca abras un CSV exportado con un simple doble clic si necesita sobrevivir a un ida y vuelta por una hoja de cálculo; usa en su lugar la vía de importación que permite elegir la codificación.
  • Si el texto ya pasó por una recodificación con pérdidas y en algún punto muestra el carácter de reemplazo de Unicode, trata esa parte como irrecuperable en lugar de perder tiempo intentando deshacerla.

Para el razonamiento más profundo sobre puntos de código, UTF-8 y por qué existe este tipo de fallo, consulta ASCII, Unicode y UTF-8: planos, normalización y orden alfabético. Para el primo invisible de este problema — bytes que no se muestran como la letra equivocada sino que se comportan como la instrucción equivocada — consulta nuestro artículo hermano sobre LF, CR y NUL, y qué le hace cada uno a un archivo.

← Volver al Blog