jueves, 24 de agosto de 2017

Codificación de Datos: Una Guía UTF-8 para PHP y MySQL

Como desarrollador PHP o MySQL, una vez que pasas más allá de los confines de los cómodos conjuntos de caracteres sólo en inglés, te encuentras rápidamente enredado en el maravillosamente y extraño mundo de UTF-8.
Una Mirada Rápida UTF-8 Primer

Unicode es un estándar de la industria de computación ampliamente utilizado, que define un mapeo completo de valores únicos de códigos numéricos a los caracteres de la mayoría de los conjuntos de caracteres escritos hoy en día, para ayudar con la interoperabilidad de los sistemas y el intercambio de datos. 

UTF-8 es una codificación de amplitud variable (variable-width encoding) que puede representar todos los caracteres en el conjunto de caracteres Unicode. Fue diseñado para mantener la retrocompatibilidad con ASCII y para evitar las complicaciones con Endianness y marcas de orden de bytes en UTF-16 y UTF-32. UTF-8 se ha convertido en la codificación de caracteres dominante para la World Wide Web, lo que representa más de la mitad de todas las páginas Web. 

UTF-8 codifica cada carácter utilizando de uno a cuatro bytes. Los primeros 128 caracteres de Unicode corresponden uno a uno con ASCII, haciendo válido el texto ASCII, al igual que el texto con codificación UTF-8. Es por esta razón que los sistemas que están limitados al uso del conjunto de caracteres en inglés, están aislados de las complejidades que de lo contrario pueden surgir con UTF-8. 

Por ejemplo, el código hexadecimal Unicode para la letra A es U + 0041, que en UTF -8 simplemente está codificado con el byte único 41. En comparación, el código hexadecimal Unicode para el carácter  es U+233B4, que en UTF-8 se codifica con los cuatro bytes F0, A3, B4, 8E.



























En un trabajo previo a éste, comenzamos a encontrar problemas de codificación de datos al mostrar biografías de artistas de todo el mundo. Pronto se hizo evidente que había problemas con los datos almacenados ya que a veces los datos se codifican correctamente y otras veces no.
Esto llevó a los programadores a implementar una mezcla de parches, a veces con JavaScript, a veces con etiquetas meta charset HTML, a veces con PHP, y así sucesivamente. Pronto, terminamos con una lista de 600.000 biografías de los artistas, con la información codificada al doble o triple, con datos almacenados en diferentes formas, dependiendo de quién había programado la característica o aplicado el parche. Un clásico nido de ratas técnico.
De hecho, navegar por problemas UTF-8 relacionados con codificación de datos, puede ser una experiencia frustrante. Este post proporciona un “libro de cocina” conciso para abordar estos problemas cuando se trabaja con PHP y MySQL particularmente, basado en la experiencia práctica y las lecciones aprendidas (y con agradecimientos, en parte, a la información descubierta aquí y aquí en el camino).
En concreto, vamos a cubrir lo siguiente en este post:
  • Mods que tendrás que hacer a tu archivo php.ini y código PHP.
  • Mods que tendrás que hacer a tu archivo my.ini y otros problemas relacionados con MySQL que se deben tener en cuenta (incluyendo mods de configuración, necesarias si estás utilizando Sphinx )
  • Cómo migrar datos de una base de datos MySQL previamente codificada en latin1 en lugar de utilizar una codificación UTF-8

PHP y la Codificación UTF-8 - Modificaciones en el Archivo php.ini:

Lo primero que debes hacer es modificar tu archivo ‘php.ini’ para utilizar UTF-8 como el conjunto de caracteres por defecto:
default_charset = "utf-8";
(Nota: Puedes utilizar posteriormente phpinfo()para verificar que éste se haya ajustado correctamente).
Bien, ahora PHP y UTF-8 deberían funcionar bien juntos. ¿Verdad?
Bueno, no exactamente. De hecho, ni están cerca de hacerlo.
Si bien este cambio se asegurará de que PHP siempre de salida a UTF-8 como codificación de caracteres (en los encabezados tipo–contenido de respuesta de navegador), todavía tienes que hacer una serie de modificaciones en tu código PHP, para asegurarte de que procesa y genera caracteres UTF-8 correctamente.

PHP y la Codificación UTF-8 - Modificaciones a tu Código:

Para asegurarte de que tu código PHP se maneje bien en el sandbox de codificación de datos UTF-8, aquí están las cosas que debes hacer:
  • Ajusta UTF-8 como el conjunto de caracteres para todas las salidas de los encabezados por tu código PHP.
    En cada encabezado de salida PHP, especifica UTF-8 como la codificación:
    header(‘Content-Type: text/html; charset=utf-8’);
  • Especifica UTF-8 como el tipo de codificación para XML
      <?xml version="1.0" encoding="UTF-8"?>
    
  • Elimina caracteres no compatibles de XML
Dado que no todos los caracteres UTF-8 se aceptan en un documento XML, necesitas eliminar cualquier tipo de caracteres de cualquier XML que generes. Una función útil para hacer esto (la cual encontré aquí) es la siguiente:
    function utf8_for_xml($string)
    {
      return preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u',
                          ' ', $string);
    }
He aquí cómo puedes utilizar esta función en tu código:
    $safeString = utf8_for_xml($yourUnsafeString); 
  • Especifica UTF-8 como el conjunto de caracteres para todo el contenido HTML
    Para el contenido HTML, especifica UTF-8 como la codificación:
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">  
    
    En formularios HTML, especifica UTF-8 como la codificación:
     <form accept-charset="utf-8">
    
  • Especifica UTF-8 como la codificación de todas las llamadas a htmlspecialchars
    Por ejemplo:
     htmlspecialchars($str, ENT_NOQUOTES, "UTF-8")
    
Nota: A partir de PHP 5.6.0, el valor default_charset se utiliza por defecto. A partir de PHP 5.4.0, UTF-8 venía por defecto, pero antes de PHP 5.4.0, se usó la norma ISO-8859-1 como predeterminado. Por lo tanto, es una buena idea especificar siempre explícitamente a UTF-8, para estar seguros, a pesar de que éste argumento es técnicamente opcional.
También ten en cuenta que, para UTF-8, htmlspecialchars htmlentities se pueden utilizar indistintamente.
  • Ajusta UTF-8 como el conjunto de caracteres por defecto para todas las conexiones de MySQL
Especifica UTF-8 como el conjunto de caracteres por defecto para usar al intercambiar datos con la base de datos MySQL, utilizando mysql_set_charset:
$link = mysql_connect('localhost', 'user', 'password');
mysql_set_charset('utf8', $link);
Ten en cuenta que, a partir de PHP 5.5.0, mysql_set_charset está en desuso, y mysqli::set_charsetse debe utilizar en su lugar:
  $mysqli = new mysqli("localhost", "my_user", "my_password", "test");

  /* check connection */
    if (mysqli_connect_errno()) {
        printf("Connect failed: %s\n", mysqli_connect_error());
        exit();
    }
    
    /* change character set to utf8 */
    if (!$mysqli->set_charset("utf8")) {
        printf("Error loading character set utf8: %s\n", $mysqli->error);
    } else {
        printf("Current character set: %s\n", $mysqli->character_set_name());
    }
    
    $mysqli->close();
  • Usa siempre versiones compatibles de las funciones de manipulación de cadenas UTF-8
Hay varias funciones de PHP que pueden fallar, o al menos no comportarse como se esperaba si la representación del carácter necesita más de 1 byte (como lo hace UTF-8). Un ejemplo es la función strlen, que devolverá el número de bytes en lugar de la cantidad de caracteres.
Hay dos opciones disponibles para hacer frente a esto:
  • Las funciones iconv que están disponibles por defecto con PHP, proporcionan versiones compatibles de varios bytes de muchas de estas funciones (por ejemplo, iconv_strlen, etc.). Sin embargo, recuerda que las cadenas que suministres a estas funciones deben a su vez ser codificadas correctamente.
  • También existe la extensión mbstring a PHP (información sobre la activación y configuración está disponible aquí). Esta extensión proporciona un conjunto completo de funciones que responden adecuadamente por la codificación multibyte.

MySQL y la Codificación UTF-8 - Modificaciones en el Archivo my.ini:

En el lado de MySQL / UTF-8 de las cosas, modificaciones al archivo my.ini son requeridas de la siguiente manera:
  • Establece los siguientes parámetros de configuración después de cada etiqueta correspondiente: [client] default-character-set=UTF-8
      [mysql]
      default-character-set=UTF-8
        
      [mysqld]
      character-set-client-handshake = false #force encoding to uft8
      character-set-server=UTF-8
      collation-server=UTF-8_general_ci
        
      [mysqld_safe]
      default-character-set=UTF-8
    
  • Después de hacer los cambios anteriores en tu archivo my.ini, reinicia el MySQL daemon.
  • Para comprobar que todo ha sido configurado correctamente para utilizar la codificación UTF-8, ejecuta la siguiente consulta:
      mysql> show variables like 'char%';
    
El resultado debe ser algo asi:
        | character_set_client        | UTF-8                       
        | character_set_connection    | UTF-8                       
        | character_set_database      | UTF-8                       
        | character_set_filesystem    | binary                    
        | character_set_results       | UTF-8                       
        | character_set_server        | UTF-8                       
        | character_set_system        | UTF-8                       
        | character_sets_dir          | /usr/share/mysql/charsets/
Si por el contrario ves latin1 enumerado para cualquiera de estos, comprueba tu configuración y asegúrate de haber reiniciado correctamente el MySQL Daemon.

MySQL y la Codificación UTF-8 - Otras Cosas a Considerar:

  • MySQL UTF-8 es en realidad una aplicación parcial del conjunto de caracteres UTF-8. En concreto, la codificación de datos MySQL UTF-8, utiliza un máximo de 3 bytes, mientras que se requieren 4 bytes para codificar el conjunto completo de caracteres UTF-8. Esto está bien para todos los caracteres del idioma, pero si necesitas sostener símbolos astrales (cuyos puntos de código oscilan entre U + 010000 a U + 10FFFF), estos requieren una codificación de cuatro bytes que no se puede sostener en MySQL UTF-8. En MySQL 5.5 0.3, esto se discutió con la adición de apoyo al conjunto de caracteres utf8mb4, que utiliza un máximo de cuatro bytes por carácter y por lo tanto sostiene el conjunto completo de caracteres UTF-8. Así que, si estás utilizando MySQL 5.5.3 o posterior, utiliza utf8mb4 en lugar de UTF-8 como conjunto de caracteres de base de datos / tabla / fila. Más información disponible aquí.
  • Si el cliente que se conecta no tiene ninguna forma de especificar la codificación para su comunicación con MySQL, una vez establecida la conexión, puede que tengas que ejecutar el siguiente comando / consulta:
      set names UTF-8;
    
  • Al determinar el tamaño de los campos varchar al modelar la base de datos, no te olvides que los caracteres UTF-8 pueden requerir hasta 4 bytes por carácter.
MySQL y la Codificación UTF-8 - Si Utilizas Sphinx:
  • En el archivo de configuración Sphinx (es decir, sphinx.conf):
    • Establece tu definición del índice para tener:
      charset_type = utf-8
    • Agrega lo siguiente a tu definición de fuente:
      sql_query_pre = SET CHARACTER_SET_RESULTS=UTF-8 sql_query_pre = SET NAMES UTF-8
  • Reinicia el motor y vuelve a hacer todos los índices.
  • Si deseas configurar la Sphynx de modo que letras como C c Ć ć Ĉ ĉ Ċ ċ Č č sean tratadas como iguales a efectos de búsqueda, tendrás que configurar una charset_table (también conocido como plegado de caracteres) que es esencialmente un mapeo entre los caracteres. Más información está disponible aquí.

MySQL - Migración de Datos de una Base de Datos Que ya Están Codificados en latin1 a UTF-8

Si tienes una base de datos existente ya codificada en latin1, aquí te muestro cómo convertir los latin1 a UTF-8:
  1. Asegúrate que realizaste todas las modificaciones a los ajustes de configuración en tu archivo my.ini, como se describió anteriormente.
  2. Ejecuta el siguiente comando:
    ALTER SCHEMA `your-db-name` DEFAULT CHARACTER SET UTF-8;
    
  3. A través de la línea de comandos, comprueba que todo está configurado correctamente para UTF-8
    mysql> show variables like 'char%';
    
  4. Crea un archivo de volcado con la codificación latin1 para la tabla que deseas convertir:
    mysqldump -u USERNAME -pDB_PASSWORD --opt --skip-set-charset --default-character-set=latin1
              --skip-extended-insert DATABASENAME --tables TABLENAME >
              DUMP_FILE_TABLE.sql
    
    Ejemplo:
    mysqldump -u root --opt --skip-set-charset  --default-character-set=latin1
              --skip-extended-insert artists-database --tables tbl_artist >
              tbl_artist.sql
    
  5. Haz una búsqueda y reemplazo global del conjunto de caracteres en el fichero de volcado de latin1 a UTF-8:
    Por ejemplo, usando Perl:
    perl -i -pe 's/DEFAULT CHARSET=latin1/DEFAULT CHARSET=UTF-8/' DUMP_FILE_TABLE.sql
    
Nota para los usuarios de Windows: Esta cadena de reemplazo del conjunto de caracteres (de latin1 a UTF-8) también se puede hacer usando buscar y reemplazar en WordPad (o algún otro editor de texto, como vim). Asegúrate de guardar el archivo tal como es (no como archivo de texto Unicode!).
  1. A partir de este punto, vamos a empezar a jugar con los datos de base de datos, por lo que probablemente sería prudente hacer una copia de seguridad de la base de datos, si no lo has hecho todavía. A continuación, restaura el volcado dentro de la base de datos:
    mysql> source "DUMP_FILE_TABLE.sql";
    
  2. Busca cualquier registro que no se haya convertido correctamente y corrígelo. Dado que los caracteres que no son ASCII, son múlti-bytes por diseño, los podemos encontrar mediante la comparación de la longitud de bytes con la longitud de caracteres (es decir, para identificar las filas que pueden contener caracteres UTF-8 de doble-codificado que deben ser corregidos).
    • Ve si hay algún registro con caracteres de varios bytes (si ésta consulta devuelve cero, entonces no debe haber ningún registro con caracteres de varios bytes en la tabla y se puede proceder al Paso 8).
       mysql> select count(*) from MY_TABLE where LENGTH(MY_FIELD) != CHAR_LENGTH(MY_FIELD);
      
    • Copia filas con caracteres de varios bytes en una tabla temporal:
       create table temptable (
           select * from MY_TABLE where
           LENGTH(MY_FIELD) != CHAR_LENGTH(MY_FIELD));
      
    • Convierte caracteres UTF-8 de doble-codificado a caracteres UTF-8 adecuados.
    Esto es en realidad un poco complicado. Una cadena de doble-codificación es aquella que ha sido codificada adecuadamente como UTF-8. Sin embargo, MySQL luego nos hizo el favor erróneo de convertirla (de lo que pensó era latin1) a UTF-8 de nuevo, cuando fijamos la columna a codificación UTF-8. La resolución de éste, por lo tanto, requiere de un proceso de dos pasos a través del cual “engañamos” a MySQL con el fin de evitar que nos haga este “favor”.
En primer lugar, fijamos de nuevo el tipo de codificación para la columna a latin1, eliminando de este modo la doble codificación:
Ejemplo:
  alter table temptable modify temptable.ArtistName varchar(128) character set latin1;
Nota: Asegúrate de utilizar el tipo de campo correcto para tu tabla. En el ejemplo anterior, para nuestra tabla, el tipo de campo correcto para ArtistName era varchar (128), pero el campo de la tabla podría ser texto o cualquier otro tipo. Asegúrate de especificarlo correctamente.
El problema es que ahora, si fijamos la codificación de la columna de nuevo a UTF-8, MySQL ejecutará el latin1 a la codificación de datos UTF-8 de nuevo, y volveremos al punto de partida. Para evitar esto, se cambia el tipo de columna a blob y luego se fija a UTF-8. Esto explota el hecho de que MySQL no intentará codificar un blob. Y así, podemos “engañar” a la conversión del conjunto de caracteres de MySQL, para evitar el problema de doble codificación.
Ejemplo:
  alter table temptable modify temptable.ArtistName blob;
  alter table temptable modify temptable.ArtistName varchar(128) character set UTF-8;
(Una vez más, como se señaló anteriormente, asegúrate de usar el tipo de campo adecuado para tu tabla.)
  • Elimina filas con sólo caracteres de un solo byte pertenecientes a la tabla temporal:
          delete from MY_TABLE where LENGTH(MY_FIELD) = CHAR_LENGTH(MY_FIELD);
    
  • Vuelve a insertar las filas fijas en la tabla original (antes de hacer esto, deberías ejecutar algunas selects en la tabla temporal para verificar que ha sido corregida de forma adecuada, solo por precaución).
          replace into MY_TABLE (select * from temptable);
    
  1. Verifica los datos restantes y, si es necesario, repite el proceso del paso 7 (esto podría ser necesario, por ejemplo, si los datos fueron codificados al triple). Más errores, si se encuentran, pueden ser más fáciles de resolver de forma manual.

Código Fuente y Archivos de Recursos

Otra cosa a recordar y comprobar es que los archivos de código fuente, archivos de recursos y así sucesivamente, sean guardados correctamente con codificación de datos UTF-8. De lo contrario, todos los caracteres “especiales” en estos archivos tal vez no sean manejados correctamente.
En Netbeans, por ejemplo, puedes hacer clic derecho sobre tu proyecto, selecciona propiedades y luego en “Fuentes”, encontrarás la opción de codificación de datos (por lo general por defecto es UTF-8, pero es mejor comprobar).
O en Windows Notepad, utiliza la opción “Guardar como…” en el menú de Archivos y selecciona la opción de codificación UTF-8 en la parte inferior del cuadro de diálogo. (Ten en cuenta que la opción “Unicode” que ofrece Notepad es en realidad UTF-16, y eso no es lo quieres.)

Para Terminar

Aunque puede ser algo tedioso, tomarse el tiempo para revisar estos pasos para abordar sistemáticamente tus problemas de codificación de datos MySQL y PHP UTF-8 puede ahorrarte una gran cantidad de tiempo. A la larga, este tipo de enfoque metódico es muy superior a la común tendencia de remendar el sistema.
Espero que ésta guía destaque la importancia de tomar en consideración la definición del conjunto de datos al configurar un entorno de proyecto inicialmente y el trabajar en un entorno de proyecto de software que tiene en cuenta la codificación de caracteres en su manipulación de texto y cadenas.
Articulo originalmente publicado en Toptal.

No hay comentarios:

Publicar un comentario

El administrador se reserva el derecho de publicar los mensajes