<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Calidad De Datos el josiete.com</title><link>https://www.josiete.com/tags/calidad-de-datos/</link><description>Contenido reciente en Calidad De Datos el josiete.com</description><generator>Hugo</generator><language>es-es</language><lastBuildDate>Mon, 28 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.josiete.com/tags/calidad-de-datos/index.xml" rel="self" type="application/rss+xml"/><item><title>Cuando los bytes se disfrazan de texto: Unicode, Python y errores de codificación</title><link>https://www.josiete.com/posts/errores-codificacion-unicode/</link><pubDate>Mon, 28 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.josiete.com/posts/errores-codificacion-unicode/</guid><description>&lt;p&gt;&lt;img src="https://www.josiete.com/images/encoding-errors-unicode.webp" alt="Lluvia de caracteres verdes sobre fondo negro, con las palabras decode y encode resaltadas en el centro"&gt;&lt;/p&gt;&#10;&lt;p&gt;Un fichero contiene &lt;code&gt;José&lt;/code&gt;, pero después de pasar por un pipeline aparece &lt;code&gt;JosÃ©&lt;/code&gt;. Otro conserva las tildes, aunque una búsqueda de &lt;code&gt;Jose&lt;/code&gt; devuelve también &lt;code&gt;José&lt;/code&gt;. Y en un tercero los emojis se convierten en &lt;code&gt;?&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Los tres casos parecen problemas de texto, pero conviene investigar causas distintas: cómo interpretamos los bytes, cómo comparamos las cadenas y qué información se ha perdido durante el recorrido. Cambiar a UTF-8 sin localizar el fallo puede limitarse a guardar correctamente un texto que ya estaba estropeado.&lt;/p&gt;&#10;&lt;h2 id="qué-es-una-codificación-y-por-qué-la-necesitamos"&gt;Qué es una codificación y por qué la necesitamos&lt;/h2&gt;&#10;&lt;p&gt;Un fichero no almacena una &lt;code&gt;ñ&lt;/code&gt; dibujada: almacena bytes. Para convertir esos números en texto necesitamos un acuerdo que establezca qué secuencias representan cada carácter. Ese acuerdo es una &lt;strong&gt;codificación de caracteres&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Podemos imaginarla como las reglas de escritura y lectura de un mensaje. Emisor y receptor deben compartirlas: recibir los mismos bytes no garantiza leer el mismo texto si cada lado utiliza reglas diferentes.&lt;/p&gt;&#10;&lt;p&gt;Las codificaciones nacen de esa necesidad de representar y transmitir texto con máquinas. El espacio disponible, los idiomas y la compatibilidad con equipos existentes condicionaron las soluciones. El &lt;a href="https://www.unicode.org/reports/tr17/"&gt;modelo de codificación de Unicode&lt;/a&gt; distingue el repertorio de caracteres, sus códigos y las formas de llevarlos a unidades de almacenamiento.&lt;/p&gt;&#10;&lt;p&gt;Un ejemplo verificable en Python muestra por qué los bytes necesitan contexto:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&lt;span style="color:#e6db74"&gt;&amp;#34;ñ&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;hex()) &lt;span style="color:#75715e"&gt;# c3b1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&lt;span style="color:#e6db74"&gt;&amp;#34;ñ&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(&lt;span style="color:#e6db74"&gt;&amp;#34;latin-1&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;hex()) &lt;span style="color:#75715e"&gt;# f1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ambas secuencias representan la misma letra bajo su codificación correspondiente. Ninguna lleva una etiqueta que diga, por sí sola, cómo hay que leerla.&lt;/p&gt;&#10;&lt;h2 id="de-ascii-a-un-mundo-con-muchos-alfabetos"&gt;De ASCII a un mundo con muchos alfabetos&lt;/h2&gt;&#10;&lt;p&gt;Aunque asociemos ASCII a los ordenadores de los ochenta, viene de los años sesenta. El &lt;a href="https://www.rfc-editor.org/rfc/rfc20"&gt;RFC 20, publicado en 1969&lt;/a&gt;, ya lo proponía para intercambiar información en red. Sus siete bits permiten 128 valores: letras latinas sin tildes, cifras, signos y caracteres de control. La &lt;code&gt;ñ&lt;/code&gt;, la &lt;code&gt;á&lt;/code&gt; y los ideogramas chinos quedan fuera.&lt;/p&gt;&#10;&lt;p&gt;Para cubrir más idiomas aparecieron otras tablas, como ISO-8859-1 y Windows-1252, junto a codificaciones para otros sistemas de escritura. En las tablas de un byte había poco espacio y cada entorno lo aprovechaba de manera distinta. Lo que servía para un idioma podía resultar insuficiente para otro; intercambiar ficheros entre sistemas exigía conocer la tabla de origen.&lt;/p&gt;&#10;&lt;p&gt;ISO-8859-1, conocido como Latin-1, y Windows-1252 no son intercambiables: difieren en el rango &lt;code&gt;0x80–0x9F&lt;/code&gt;. Por ejemplo, &lt;code&gt;0x80&lt;/code&gt; representa &lt;code&gt;€&lt;/code&gt; en Windows-1252 y un control en Latin-1. Python incluye ambos códecs por separado en su &lt;a href="https://docs.python.org/3/library/codecs.html#standard-encodings"&gt;lista de codificaciones&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Unicode surgió para construir un repertorio común. Sus primeras conversaciones comenzaron en 1987 entre ingenieros de Xerox y Apple, y el consorcio se constituyó en 1991, según su &lt;a href="https://www.unicode.org/history/"&gt;historia oficial&lt;/a&gt;. La intención era poder mezclar idiomas sin cambiar de tabla cada vez.&lt;/p&gt;&#10;&lt;h2 id="unicode-utf-8-y-utf-16-son-conceptos-relacionados"&gt;Unicode, UTF-8 y UTF-16 son conceptos relacionados&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Unicode asigna puntos de código a los caracteres&lt;/strong&gt;. Por ejemplo, &lt;code&gt;ñ&lt;/code&gt; corresponde a &lt;code&gt;U+00F1&lt;/code&gt;. &lt;strong&gt;UTF-8 y UTF-16 especifican cómo representar ese texto&lt;/strong&gt; mediante unidades que se pueden almacenar y transmitir. No hay una secuencia histórica «ASCII, después UTF-8, después UTF-16, después Unicode»: las dos UTF son formas de representar Unicode.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Concepto&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Función&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Ejemplo para &lt;code&gt;ñ&lt;/code&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unicode&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Identificar el carácter con un punto de código&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;U+00F1&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UTF-8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Usar de uno a cuatro bytes por valor escalar Unicode&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;C3 B1&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UTF-16LE&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Usar una o dos unidades de 16 bits, con bytes en orden little-endian&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;F1 00&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;UTF-8 conserva los valores ASCII. UTF-16 necesita un par de unidades para caracteres como &lt;code&gt;😀&lt;/code&gt;: no significa «dos bytes para cualquier carácter». Al serializar UTF-16 también importa el orden de los bytes, indicado por LE, BE o, según el formato, una marca BOM. La &lt;a href="https://www.unicode.org/faq/utf_bom.html"&gt;FAQ de Unicode sobre las UTF&lt;/a&gt; explica estas diferencias.&lt;/p&gt;&#10;&lt;h2 id="por-qué-unicode-importa-en-python"&gt;Por qué Unicode importa en Python&lt;/h2&gt;&#10;&lt;p&gt;En Python 3, &lt;code&gt;str&lt;/code&gt; representa texto Unicode y &lt;code&gt;bytes&lt;/code&gt; representa bytes. Mantener esa frontera clara permite procesar nombres, idiomas y símbolos sin convertirlos continuamente a una tabla local. Una cadena &lt;code&gt;str&lt;/code&gt; no es un fichero UTF-8 en miniatura. La &lt;a href="https://docs.python.org/3/howto/unicode.html"&gt;guía Unicode de Python&lt;/a&gt; desarrolla esta separación.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;texto &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;España 😀&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# str&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;datos &lt;span style="color:#f92672"&gt;=&lt;/span&gt; texto&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;# bytes&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;recuperado &lt;span style="color:#f92672"&gt;=&lt;/span&gt; datos&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;# str&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; recuperado &lt;span style="color:#f92672"&gt;==&lt;/span&gt; texto&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; len(texto) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; len(datos) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;La longitud de &lt;code&gt;str&lt;/code&gt; cuenta puntos de código; la de &lt;code&gt;bytes&lt;/code&gt;, bytes. Tampoco equivale siempre al número de símbolos visibles: una letra con un acento combinante o un emoji compuesto puede ocupar varios puntos de código. La &lt;a href="https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str"&gt;referencia de los tipos de Python&lt;/a&gt; describe estas secuencias.&lt;/p&gt;&#10;&lt;p&gt;La regla práctica es &lt;strong&gt;decodificar al entrar, trabajar con texto y codificar al salir&lt;/strong&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bytes de origen ── decode(origen) ──► str de Python&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;str de Python ── encode(destino) ─► bytes de destino&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;decode&lt;/code&gt; lleva los bytes a texto; &lt;code&gt;encode&lt;/code&gt; lleva el texto a bytes. Representar ese texto en pantalla es trabajo del sistema de salida y del renderizador. Si una librería ya devuelve &lt;code&gt;str&lt;/code&gt;, esa frontera puede estar resuelta: comprueba el tipo antes de añadir otra conversión.&lt;/p&gt;&#10;&lt;h2 id="cómo-lidiar-con-un-error-recuperar-el-origen-e-interpretar-bien"&gt;Cómo lidiar con un error: recuperar el origen e interpretar bien&lt;/h2&gt;&#10;&lt;p&gt;Ante bytes sin interpretar, necesitamos &lt;strong&gt;&lt;code&gt;decode&lt;/code&gt;, con la codificación real de origen&lt;/strong&gt;. Volver a la representación binaria original ayuda a investigar, pero no hay una «codificación binaria más básica» que elimine la necesidad de saber cómo se escribieron esos bytes.&lt;/p&gt;&#10;&lt;p&gt;Supongamos que recibimos un nombre de un sistema que exporta Windows-1252 y debemos entregarlo en UTF-8:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origen &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;b&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Jos&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\xe9&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# bytes recibidos del sistema antiguo&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;texto &lt;span style="color:#f92672"&gt;=&lt;/span&gt; origen&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;&amp;#34;cp1252&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;# &amp;#34;José&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;salida &lt;span style="color:#f92672"&gt;=&lt;/span&gt; texto&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;# b&amp;#39;Jos\xc3\xa9&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Primero interpretamos el origen; después serializamos para el destino. Si el fichero es grande, podemos dejar que la entrada y salida de texto hagan esa conversión:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; open(&lt;span style="color:#e6db74"&gt;&amp;#34;entrada.csv&amp;#34;&lt;/span&gt;, encoding&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;cp1252&amp;#34;&lt;/span&gt;, errors&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;strict&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; entrada:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; open(&lt;span style="color:#e6db74"&gt;&amp;#34;salida.csv&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;w&amp;#34;&lt;/span&gt;, encoding&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;, errors&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;strict&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; salida:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; linea &lt;span style="color:#f92672"&gt;in&lt;/span&gt; entrada:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; salida&lt;span style="color:#f92672"&gt;.&lt;/span&gt;write(linea)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Especificar &lt;code&gt;encoding&lt;/code&gt; evita depender de los valores por defecto del entorno. Los modos de texto y binario están documentados en &lt;a href="https://docs.python.org/3/library/functions.html#open"&gt;&lt;code&gt;open&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="si-ya-tenemos-josã"&gt;Si ya tenemos &lt;code&gt;JosÃ©&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Este fenómeno se llama &lt;em&gt;mojibake&lt;/em&gt;: unos bytes se han interpretado con reglas equivocadas. Aquí podemos reproducir una causa concreta:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;original &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;José&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mal &lt;span style="color:#f92672"&gt;=&lt;/span&gt; original&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;&amp;#34;latin-1&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; mal &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;JosÃ©&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Solo porque conocemos la conversión equivocada y no hubo pérdida:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bytes_recuperados &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mal&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(&lt;span style="color:#e6db74"&gt;&amp;#34;latin-1&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bien &lt;span style="color:#f92672"&gt;=&lt;/span&gt; bytes_recuperados&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; bien &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;José&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;En esta reparación aparece primero un &lt;code&gt;encode&lt;/code&gt; para deshacer una decodificación errónea conocida. No contradice la regla de entrada: estamos reconstruyendo bytes que ya se habían convertido a texto. &lt;strong&gt;Aplicar &lt;code&gt;encode(...).decode(...)&lt;/code&gt; indiscriminadamente no es una solución general.&lt;/strong&gt; Si el error utilizó Windows-1252, habría que estudiar esa transformación, no asumir Latin-1.&lt;/p&gt;&#10;&lt;p&gt;Tampoco basta con que una decodificación no lance una excepción. Latin-1 asigna un carácter a cada valor de byte: puede aceptar datos que producen un texto absurdo. Los detectores automáticos ofrecen hipótesis; el contrato de la fuente, sus metadatos y muestras conocidas ofrecen mejores evidencias.&lt;/p&gt;&#10;&lt;p&gt;Si se sustituyeron caracteres por &lt;code&gt;?&lt;/code&gt; o &lt;code&gt;�&lt;/code&gt;, o se eliminaron con &lt;code&gt;errors=&amp;quot;ignore&amp;quot;&lt;/code&gt;, puede haberse perdido información. Para recuperar el valor original habrá que volver a una copia anterior. En un pipeline, propongo fallar o poner el registro en cuarentena antes que aceptar una modificación silenciosa. Esa es una decisión de calidad de datos que debe quedar explícita.&lt;/p&gt;&#10;&lt;h2 id="en-una-base-de-datos-almacenamiento-y-collation"&gt;En una base de datos: almacenamiento y collation&lt;/h2&gt;&#10;&lt;p&gt;La codificación responde a «¿cómo represento el texto?». Una &lt;strong&gt;collation&lt;/strong&gt; responde a «¿cómo ordeno y comparo este texto?». Define reglas lingüísticas y sensibilidades, como distinguir mayúsculas o acentos.&lt;/p&gt;&#10;&lt;p&gt;Un nombre puede estar almacenado perfectamente y aun así producir resultados inesperados en &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;DISTINCT&lt;/code&gt; o una restricción &lt;code&gt;UNIQUE&lt;/code&gt;. Si la comparación considera dos nombres equivalentes, pueden agruparse o colisionar aunque sus caracteres difieran. El efecto depende del motor y de la collation concreta.&lt;/p&gt;&#10;&lt;p&gt;En MySQL, los sufijos &lt;code&gt;ci&lt;/code&gt; y &lt;code&gt;cs&lt;/code&gt; indican insensibilidad y sensibilidad a mayúsculas; &lt;code&gt;ai&lt;/code&gt; y &lt;code&gt;as&lt;/code&gt;, a acentos. Así lo define su &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/charset-collation-names.html"&gt;documentación sobre nombres de collation&lt;/a&gt;. Este ejemplo corresponde a MySQL 8.0/8.4:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _utf8mb4&lt;span style="color:#e6db74"&gt;&amp;#39;José&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;COLLATE&lt;/span&gt; utf8mb4_0900_ai_ci &lt;span style="color:#f92672"&gt;=&lt;/span&gt; _utf8mb4&lt;span style="color:#e6db74"&gt;&amp;#39;jose&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; iguales; &lt;span style="color:#75715e"&gt;-- 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _utf8mb4&lt;span style="color:#e6db74"&gt;&amp;#39;José&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;COLLATE&lt;/span&gt; utf8mb4_0900_as_cs &lt;span style="color:#f92672"&gt;=&lt;/span&gt; _utf8mb4&lt;span style="color:#e6db74"&gt;&amp;#39;jose&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; iguales; &lt;span style="color:#75715e"&gt;-- 0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Para admitir todo el repertorio Unicode en MySQL, utiliza &lt;code&gt;utf8mb4&lt;/code&gt;, no el antiguo &lt;code&gt;utf8mb3&lt;/code&gt;, limitado a caracteres representables en hasta tres bytes. La &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-utf8mb4.html"&gt;documentación de utf8mb4&lt;/a&gt; explica por qué un emoji como &lt;code&gt;😀&lt;/code&gt; necesita el primero. Comprueba también la configuración de la conexión, no solo la columna.&lt;/p&gt;&#10;&lt;p&gt;En SQL Server, la collation también determina la página de códigos de &lt;code&gt;varchar&lt;/code&gt; cuando no usa UTF-8. Desde SQL Server 2019, las collations con &lt;code&gt;_UTF8&lt;/code&gt; permiten UTF-8 en &lt;code&gt;varchar&lt;/code&gt;; &lt;code&gt;nvarchar&lt;/code&gt; utiliza UCS-2 o UTF-16 según la collation y el soporte de caracteres suplementarios. Los literales Unicode se escriben como &lt;code&gt;N'José'&lt;/code&gt;. Véase &lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/collations/collation-and-unicode-support"&gt;collation y soporte Unicode en SQL Server&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="cómo-elegir-la-collation-adecuada"&gt;Cómo elegir la collation adecuada&lt;/h3&gt;&#10;&lt;p&gt;Parte de las reglas del producto. Un buscador de nombres puede necesitar encontrar &lt;code&gt;José&lt;/code&gt; al escribir &lt;code&gt;jose&lt;/code&gt;. Un identificador puede exigir que &lt;code&gt;ABC&lt;/code&gt; y &lt;code&gt;abc&lt;/code&gt; sean distintos. La búsqueda y la unicidad no tienen por qué compartir exactamente las mismas reglas.&lt;/p&gt;&#10;&lt;p&gt;Prueba nombres reales de los idiomas admitidos: &lt;code&gt;José/Jose&lt;/code&gt;, &lt;code&gt;ABC/abc&lt;/code&gt;, &lt;code&gt;Peña/Pena&lt;/code&gt;, además de casos de turco o alemán si corresponden. &lt;strong&gt;No asumas que todas las collations tratan la &lt;code&gt;ñ&lt;/code&gt; como una &lt;code&gt;n&lt;/code&gt; con un acento prescindible&lt;/strong&gt;: las reglas lingüísticas importan.&lt;/p&gt;&#10;&lt;p&gt;Antes de cambiarla, revisa duplicados bajo la nueva comparación, índices, restricciones y consultas. Aplicar &lt;code&gt;COLLATE&lt;/code&gt; en una consulta puede resolver una comparación puntual, pero conviene comprobar su plan de ejecución. Y cambiar la collation no reconstruye un &lt;code&gt;José&lt;/code&gt; que ya se guardó como &lt;code&gt;JosÃ©&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="cómo-depurar-el-fallo-dentro-de-un-pipeline"&gt;Cómo depurar el fallo dentro de un pipeline&lt;/h2&gt;&#10;&lt;p&gt;La pregunta útil es: &lt;strong&gt;¿en qué primera frontera cambia el valor?&lt;/strong&gt; Sigue una muestra conocida durante todo el recorrido:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CSV → lectura en Python → transformación → base de datos → API → navegador&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Conserva los bytes de origen y registra, para una muestra sintética, el tipo y la representación exacta antes y después de cada frontera. Este pequeño inspector evita depender de cómo dibuja los caracteres una consola:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;inspeccionar&lt;/span&gt;(valor):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; isinstance(valor, bytes):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;tipo&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;bytes&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;longitud&amp;#34;&lt;/span&gt;: len(valor), &lt;span style="color:#e6db74"&gt;&amp;#34;hex&amp;#34;&lt;/span&gt;: valor&lt;span style="color:#f92672"&gt;.&lt;/span&gt;hex()}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; isinstance(valor, str):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;tipo&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;str&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;longitud&amp;#34;&lt;/span&gt;: len(valor),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;escaped&amp;#34;&lt;/span&gt;: ascii(valor),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;puntos&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;U+&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;ord(c)&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;04X&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; c &lt;span style="color:#f92672"&gt;in&lt;/span&gt; valor],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TypeError&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Se esperaba str o bytes&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(inspeccionar(&lt;span style="color:#e6db74"&gt;&amp;#34;José&amp;#34;&lt;/span&gt;))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(inspeccionar(&lt;span style="color:#e6db74"&gt;&amp;#34;José&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;En producción, limita y protege estas muestras: no hace falta volcar datos personales en los logs. Añade el origen, el códec utilizado y la etapa para poder reproducir la transformación.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Síntoma&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Hipótesis que comprobar&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;JosÃ©&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UTF-8 leído como otra tabla o una transformación repetida&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;UnicodeDecodeError&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Códec equivocado, bytes dañados o una secuencia multibyte incompleta&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;UnicodeEncodeError&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;La codificación de salida no admite algún carácter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;�&lt;/code&gt; o &lt;code&gt;?&lt;/code&gt; inesperados&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Sustitución con pérdida en alguna etapa&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Cuadrados vacíos, pero puntos de código correctos&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Falta de glifos en la fuente&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Datos correctos, resultados de búsqueda inesperados&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Collation o reglas de normalización&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Comprueba el BOM: &lt;code&gt;utf-8-sig&lt;/code&gt; permite consumir una marca UTF-8 si el origen la incluye. Si el contrato dice UTF-16, revisa su orden de bytes. Una cabecera HTTP que anuncia un charset distinto de los bytes enviados también merece atención. En JSON, &lt;code&gt;&amp;quot;Jos\u00e9&amp;quot;&lt;/code&gt; puede ser una representación escapada perfectamente válida; compara el valor después de parsear, no solo el fichero a simple vista. El &lt;a href="https://www.rfc-editor.org/rfc/rfc8259"&gt;RFC 8259&lt;/a&gt; define los escapes y el uso de UTF-8 para intercambiar JSON entre sistemas.&lt;/p&gt;&#10;&lt;p&gt;Si lees en bloques, un carácter puede quedar partido entre dos lecturas. Usa un lector de texto o un &lt;a href="https://docs.python.org/3/library/codecs.html#incrementaldecoder-objects"&gt;decodificador incremental&lt;/a&gt;, que conserva los bytes pendientes:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; codecs&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;decoder &lt;span style="color:#f92672"&gt;=&lt;/span&gt; codecs&lt;span style="color:#f92672"&gt;.&lt;/span&gt;getincrementaldecoder(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;)(errors&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;strict&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; decoder&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;b&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\xc3&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; decoder&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;b&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\xb1&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, final&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;ñ&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No interpretes un bloque incompleto como prueba de que el fichero utiliza otra codificación.&lt;/p&gt;&#10;&lt;h2 id="prevenirlo-con-pruebas-de-datos"&gt;Prevenirlo con pruebas de datos&lt;/h2&gt;&#10;&lt;p&gt;Una prueba con &lt;code&gt;Alice&lt;/code&gt; o &lt;code&gt;Madrid&lt;/code&gt; apenas ejercita estas fronteras. Propongo un pequeño corpus con tildes, &lt;code&gt;ñ&lt;/code&gt;, &lt;code&gt;€&lt;/code&gt;, comillas tipográficas, alfabetos no latinos, emojis y caracteres combinantes. Estas pruebas unitarias sirven como base y se pueden ejecutar con &lt;code&gt;pytest&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; unicodedata&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pytest&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@pytest.mark.parametrize&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;texto&amp;#34;&lt;/span&gt;, [&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;José&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;España&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;€&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;“hola”&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;東京&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;العربية&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;😀&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;e&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\u0301&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;])&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_utf8_sin_perdida&lt;/span&gt;(texto):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; texto&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; texto&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_entrada_legacy_con_valor_esperado&lt;/span&gt;():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; &lt;span style="color:#e6db74"&gt;b&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Jos&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\xe9&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;&amp;#34;cp1252&amp;#34;&lt;/span&gt;) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;José&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_utf8_invalido_se_rechaza&lt;/span&gt;():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; pytest&lt;span style="color:#f92672"&gt;.&lt;/span&gt;raises(&lt;span style="color:#a6e22e"&gt;UnicodeDecodeError&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;b&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\xff&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&lt;span style="color:#e6db74"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;, errors&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;strict&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_normalizacion_si_el_contrato_exige_nfc&lt;/span&gt;():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; unicodedata&lt;span style="color:#f92672"&gt;.&lt;/span&gt;normalize(&lt;span style="color:#e6db74"&gt;&amp;#34;NFC&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;e&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\u0301&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;é&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;La normalización Unicode resuelve representaciones equivalentes, como &lt;code&gt;é&lt;/code&gt; y &lt;code&gt;e&lt;/code&gt; más acento combinante; no repara mojibake. &lt;a href="https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize"&gt;&lt;code&gt;unicodedata.normalize&lt;/code&gt;&lt;/a&gt; permite aplicar NFC cuando el contrato lo exige. No elimines acentos del dato original para facilitar una búsqueda.&lt;/p&gt;&#10;&lt;p&gt;El &lt;em&gt;round trip&lt;/em&gt; UTF-8 es una comprobación básica: también lo pasaría &lt;code&gt;JosÃ©&lt;/code&gt;. La prueba decisiva recorre &lt;strong&gt;el pipeline real&lt;/strong&gt;: leer un fichero de prueba con bytes conocidos, ejecutar la transformación, insertar con el mismo driver y esquema que producción, recuperar mediante la API y comparar con valores esperados independientes. Incluye la ruta de streaming y sus límites entre bloques si existe.&lt;/p&gt;&#10;&lt;p&gt;Añade casos de búsquedas y unicidad bajo la collation elegida, rechazo o cuarentena de entradas inválidas y ausencia de sustituciones introducidas por el proceso. Para el origen, documenta codificación y política de BOM; para el procesamiento, mantén texto Unicode; para cada salida, fija la serialización. Cuando esos acuerdos están escritos y probados, el próximo &lt;code&gt;JosÃ©&lt;/code&gt; deja de ser un misterio y se convierte en una frontera concreta que podemos corregir.&lt;/p&gt;&#10;</description></item></channel></rss>