Página 1 de 1

Codificacion en HTTPS GET

NotaPublicado: 2017-10-06 22:20 @972
por Superdri
¡Saludos a todos los foreros!

Ando haciendo una aplicación web que recoja datos de una API que funciona con peticiones HTTPS y JSON, pero tengo un problema: cuando interpreto los datos que recibo con el módulo JSON de Perl se me va todo al traste y no consigo la codificación correcta para meterlo en MySQL :twisted:

Os dejo un trozo de código por si alguno ve el fallo.
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. my $response = $ua->request($req);
  2.  
  3. if ($response->is_success) {
  4.         my $respuesta = $response->decoded_content;
  5.         $json_obj = new JSON;
  6.         $json_obj->utf8();
  7.         $json = $json_obj->decode($respuesta);
  8.         print Dumper($json);
  9. }
  10.  
Coloreado en 0.002 segundos, usando GeSHi 1.0.8.4

Un trocito del Dumper:
Sintáxis: [ Descargar ] [ Ocultar ]
Using text Syntax Highlighting
$VAR1 = {
          'classes' => [
                         {
                           'id' => 1,
                           'powerType' => 'rage',
                           'name' => 'Guerrero',
                           'mask' => 1
                         },
                         {
                           'mask' => 2,
                           'powerType' => 'mana',
                           'name' => "Palad\x{ed}n",
                           'id' => 2
                         }
 
Coloreado en 0.000 segundos, usando GeSHi 1.0.8.4


¡Hasta luego!

Re: Codificacion en HTTPS GET

NotaPublicado: 2017-10-06 22:41 @987
por explorer
Bienvenido a los foros de Perl en Español, Superdri.

Normalmente los JSON se guardan en UTF-8, así que yo veo como correcto el uso que haces del método utf8().

El tema es que Perl no sabe qué hacer con esa información si queremos sacarla "fuera" del programa. No sabe si tiene que sacarla tal cual o convertirla a una codificación en particular.

Al ejecutar el método utf8() le estamos diciendo que la codificación del archivo JSON que recibimos es esa, y Perl traduce los caracteres acentuados a la codificación interna, que normalmente será la Unicode.

Si a Perl no se le dice nada, toda esa información la sacará en codificación iso-8859-1, la codificación por defecto, pero nuestra terminal o el archivo donde vamos a escribirlo es posible que estén en otra distinta.

Por eso vemos, en el volcado, que Paladín está codificado de esa manera, en iso-8859-1 (un byte 0xed para representar 'í').

¿Qué codificación está esperando MySQL?

Podemos pasar de una codificación a otra usando las funciones del módulo Encode, por ejemplo.

Re: Codificacion en HTTPS GET

NotaPublicado: 2017-10-07 14:11 @633
por Superdri
Muchas gracias por la respuesta.

La codificación para la base de datos es UTF-8.

He leído un articulo publicado en Metacpan y entiendo que primero hay que descodificar lo que se recibe para luego codificar lo que envías, así que cambié un poco el código pero sigue sin funcionar.

Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1.         my $respuesta = $response->decoded_content;
  2.         $respuesta = decode('UTF-8', $respuesta);
  3.         print Dumper($respuesta);
  4.         $json_obj = new JSON;
  5.         $json = $json_obj->decode($respuesta);
  6.         print Dumper($json);
  7.  
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4


Más adelante codifico el nombre antes de meterlo en la base de datos MySQL.

Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1.         $name = encode('UTF-8', $name);
  2.  
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4


Pequeña muestra del dumper
Sintáxis: [ Descargar ] [ Ocultar ]
Using text Syntax Highlighting
$VAR1 = "{\"classes\":[{\"id\":1,\"mask\":1,\"powerType\":\"rage\",\"name\":\"Guerrero\"},{\"id\":2,\"mask\":2,\"powerType\":\"mana\",\"name\":\"Palad\x{ed}n\"}
Coloreado en 0.000 segundos, usando GeSHi 1.0.8.4

Pero aun así sigue sin meterme de manera correcta la cadena en MySQL :twisted:

¡Hasta luego!

Re: Codificacion en HTTPS GET

NotaPublicado: 2017-10-08 11:27 @519
por explorer
Si vas a sacar algo por el STDOUT (por ejemplo, el Dumper por medio del print) lo recomendable es que la terminal también esté en UTF-8, por lo que también deberás configurar la salida estándar para que use UTF-8.

Al principio del programa pones

use open qw(:std :utf8);

y a partir de ese momento, STDIN, STDOUT y STDERR y todos los open() de archivos que hagas, tendrán por defecto la codificación UTF-8. (Más información en el primer mensaje del hilo 44 recetas para trabajar con Unicode en Perl).

Si trabajas de forma regular con UTF-8, es recomendable el uso del módulo utf8::all que activa todo el soporte de UTF-8 en Perl.


Una vez que tengas el terminal y el programa configurado para UTF-8, a la hora de sacar texto en pantalla o a los archivos estarás seguro de qué es lo que está generando Perl. Si aún estás en una terminal que no es UTF-8, debes indicarle al "use open" en qué codificación estás. Si es una latin1 o ISO-8859-1 o ISO-8859-15 (la que incorpora el €), no necesitas nada de esto porque ISO-8859-1 es la de por defecto de Perl. Pero en los últimos años prácticamente todas las terminales están trabajando ya en UTF-8.

Vayamos por partes...

Primero haces un decoded_content() de lo que recibes.

Este método tomará el contenido y lo decodificará según lo que indique la cabecera HTTP "Content-Encoding" y "charset" (en "Content-Type") (más información en perldoc HTTP::Message) a la codificación interna Unicode de Perl. Si "Content-Encoding" indica que el contenido está comprimido, lo descomprimirá según el método indicado, y si el "charset" no es algo sencillo, como "us-ascii" o "iso-8859-1", usará Enconde::decode para hacer la decodificación. Extracto de HTTP::Message:
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1.                 require Encode;
  2.                 eval {
  3.                     $content_ref = \Encode::decode($charset, $$content_ref,
  4.                          ($opt{charset_strict} ? Encode::FB_CROAK() : 0) | Encode::LEAVE_SRC());
  5.                 };
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4

Si el "charset" es UTF-8, observa entonces que decoded_content() ya está llamando a decode('UTF-8',...) por ti. Y con la línea siguiente, donde llamas a decode(), pues sobra. Más bien, estás decodificando dos veces, con lo que el resultado es un conocido error (el de "doble de/codificación").

En caso de dudas, siempre es bueno poner Dumper después de cada línea para ver si estamos haciendo las conversiones adecuadas, como tienes puesto en tu código, pero recordando que debemos ajustar la salida a la codificación de la terminal (o usar un hexdump para ver el flujo de bytes).

En el caso de MySQL, se trata de un tema delicado. Siempre ha habido problemas con el almacenamiento de texto en UTF-8. Se supone que a partir de la v4.1 de MySQL, se trabaja por defecto en UTF-8, pero... no es así. Por defecto, incluso para la v5.7, siempre trabaja en latin1 y latin1_swedish_ci para el conjunto de caracteres y el cotejo (ordenación).

Puedes comprobarlos haciendo la siguiente consulta:
SHOW VARIABLES LIKE 'character_set%';

Para que una configuración permanente, en my.cnf deben aparecer estas líneas:
Sintáxis: [ Descargar ] [ Ocultar ]
Using text Syntax Highlighting
[client]
default-character-set=utf8
[mysqld]
character-set-server = utf8
Coloreado en 0.000 segundos, usando GeSHi 1.0.8.4

Pero... resulta que ese utf8, NO es el UTF-8. Realmente, es un subconjunto de UTF-8 que permite codificar caracteres de hasta un máximo de 3 bytes (dicho de otra manera: los caracteres más exóticos, codificados en 4 o más bytes, quedan fuera).

Para tener un soporte completo de UTF-8 (en el caso de que tengas que almacenar caracteres exóticos), debes usar el conjunto de caracteres utf8mb4.

Re: Codificacion en HTTPS GET

NotaPublicado: 2017-10-08 13:10 @590
por Superdri
Gracias por responder, explorer :)

Bueno, he puesto la terminal de Windows en utf8 con el comando chcp 65001 y quitado el decode() que sobraba y ahora me imprime por consola las tildes bien. Ahora el problema viene a la hora de insertarlo en MySQL.

He cambiado la configuración del my.ini de XAMPP. Le añadí la línea
Sintáxis: [ Descargar ] [ Ocultar ]
  1. character-set-server = utf8 
en el bloque [mysqld] y añadí al bloque [client] la línea
Sintáxis: [ Descargar ] [ Ocultar ]
  1. default-character-set=utf8 
de modo que las variables de la MySQL han quedado:
Sintáxis: [ Descargar ] [ Ocultar ]
Using text Syntax Highlighting
character_set_client            utf8mb4
character_set_connection        utf8mb4
character_set_database          utf8
character_set_filesystem        binary
character_set_results           utf8mb4
character_set_server            utf8
character_set_system            utf8
character_sets_dir              C:\xampp\mysql\share\charsets\
 
Coloreado en 0.000 segundos, usando GeSHi 1.0.8.4

pero al insertar los datos en utf8 siguen apareciendo caracteres extraños. También he probado a crear la tabla con utf8mb4 de la siguiente manera:
Sintáxis: [ Descargar ] [ Ocultar ]
Using sql Syntax Highlighting
  1. CREATE TABLE `blizzardrankings`.`wow_classes_es` (`id` INT(2) NOT NULL, `name` VARCHAR(32) CHARACTER SET utf8mb4 NOT NULL, `power_type` VARCHAR(32) CHARACTER SET utf8mb4 NOT NULL, `mask` INT(32) NOT NULL) ENGINE = InnoDB CHARSET=utf8mb4
  2.  
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4


Os dejo el programa completo porque ya no sé qué hacer :<
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. use open qw(:std :utf8);
  2. use utf8::all;
  3. use IO::All;
  4. use JSON;
  5. use Data::Dumper;
  6. use DBD::mysql;
  7. use Encode;
  8.  
  9. my ($dbname,$dbhost,$dbuser,$dbpass) = ('blizzardrankings', 'localhost', 'user', 'pass');
  10.  
  11. my $contenido < io('https://eu.api.battle.net/wow/data/character/classes?locale=es_ES&apikey=5gdrgfafqdnkryj8tqafqxsdr4qamdcq');
  12.  
  13. my $json = JSON->new->utf8->decode($contenido);
  14. print Dumper($json);
  15.  
  16. my $db = DBI->connect("DBI:mysql:$dbname:$dbhost", "$dbuser", "$dbpass") or die "Imposible conectar con la DB";
  17. $db->{'mysql_enable_utf8'} = 1;
  18.  
  19. my $sth = $db->prepare("DROP TABLE `wow_classes_es`");
  20. $sth->execute() or die "imposible borrar la tabla";
  21. $sth->finish;
  22.  
  23. $sth = $db->prepare("CREATE TABLE `blizzardrankings`.`wow_classes_es` (`id` INT(2) NOT NULL, `name` VARCHAR(32) CHARACTER SET utf8mb4 NOT NULL, `power_type` VARCHAR(32) CHARACTER SET utf8mb4 NOT NULL, `mask` INT(32) NOT NULL) ENGINE = InnoDB CHARSET=utf8mb4");
  24. $sth->execute() or die "imposible crear la tabla";
  25. $sth->finish;
  26.  
  27. my @classes = @{$json->{'classes'}};
  28.  
  29. foreach my $indice (keys(@classes)) {
  30.         my $name = $json->{'classes'}->[$indice]->{'name'};
  31.         my $power = $json->{'classes'}->[$indice]->{'powerType'};
  32.         print "$name\n";
  33.         $sth = $db->prepare("INSERT INTO wow_classes_es VALUES ('$json->{'classes'}->[$indice]->{'id'}', '$name', '$json->{'classes'}->[$indice]->{'mask'}', '$power')");
  34.         $sth->execute() or die "imposible crear la tabla";
  35.         $sth->finish;
  36. }
  37.  
  38. $db->disconnect;
  39.  
Coloreado en 0.002 segundos, usando GeSHi 1.0.8.4


Gracias de nuevo, ¡un saludo!

Re: Codificacion en HTTPS GET

NotaPublicado: 2017-10-08 15:20 @680
por Superdri
Bueno, por fin me funciona, le puse la siguiente sentencia sql antes de agregar los datos en utf8:
Sintáxis: [ Descargar ] [ Ocultar ]
Using sql Syntax Highlighting
  1. SET character_set_results = 'utf8', character_set_client = 'utf8', character_set_connection = 'utf8', character_set_database = 'utf8', character_set_server = 'utf8'
  2.  
Coloreado en 0.000 segundos, usando GeSHi 1.0.8.4

Gracias por toda la ayuda :)

Re: Codificacion en HTTPS GET

NotaPublicado: 2017-10-08 17:36 @775
por explorer
Enhorabuena por encontrar la solución.

Te cuento lo que he hecho.

He instalado la base de datos, he puesto las variables que te indico antes en el archivo my.cnf. Volver a arrancar, y con eso, esas variables que indicas ya deberían aparecer a utf8.

Ahora el programa.

Uno de los temas peliagudos es el módulo IO::All. Parece que ahorra mucho trabajo, pero... es un poco monstruoso.

Te pongo un código parecido al tuyo, pero reduciendo al mínimo lo que necesitas, para que veas que tienes más opciones.
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. #!/usr/bin/env perl
  2. use v5.18;
  3. use utf8;                               # usamos caracteres utf8 en este código fuente
  4. use open qw(:std :utf8);                # las entradas/salidas estándar en utf8
  5.  
  6. use LWP::Simple;
  7. use Encode;
  8. use JSON;
  9. use DBI;
  10.  
  11. use Data::Dumper;
  12.  
  13. ## Recuperamos información
  14. # Está en utf8, pero Perl la recibe como una cadena de bytes, sin saber qué codificación es. Cree que es latin1
  15. my $contenido = get(
  16.     'https://eu.api.battle.net/wow/data/character/classes?locale=es_ES&apikey=5gdrgfafqdnkryj8tqafqxsdr4qamdcq'
  17. );
  18. #say "[$contenido]";
  19. #say (utf8::is_utf8($contenido) ? "sí":"no");          # no
  20.  
  21. ## Conversión a JSON
  22. # Además de pasarlo a JSON, indicamos que está en UTF-8. El resultado es una estructura, con caracteres en utf8
  23. my $json = decode_json $contenido;                      # suponemos $contenido es texto JSON codificado en UTF8
  24.  
  25. #print Dumper($json);                                   # ¡cuidado! Data::Dumper saca caracteres escapados latin1
  26. #my $name = $json->{'classes'}[1]{'name'};              # Prueba
  27. #say "[$name]";                                         # Paladín
  28. #say (utf8::is_utf8($name) ? "sí":"no");                       # sí
  29.  
  30. ## Conexión a la base de datos
  31. my ($dbname,$dbhost,$dbuser,$dbpass) = ('blizzardrankings', 'localhost', 'user', 'pass');
  32.  
  33. my $db = DBI->connect("DBI:mysql:$dbname:$dbhost", "$dbuser", "$dbpass",
  34.     {
  35.         mysql_enable_utf8mb4 => 1,
  36.     }
  37. ) or die "Imposible conectar con la DB";
  38.  
  39. ## Destrucción de la tabla, si existe
  40. my $sth;
  41. $sth= $db->prepare("DROP TABLE IF EXISTS `wow_classes_es`");
  42. $sth->execute() or die "imposible borrar la tabla";
  43. $sth->finish;
  44.  
  45. ## Creacion de la tabla, si no existe
  46. $sth = $db->prepare("
  47.    CREATE TABLE IF NOT EXISTS `blizzardrankings`.`wow_classes_es` (
  48.         `id`            INT(2)          NOT NULL,
  49.         `name`          VARCHAR(32)     NOT NULL,
  50.         `power_type`    VARCHAR(32)     NOT NULL,
  51.         `mask`          INT(32)         NOT NULL
  52.    ) ENGINE = InnoDB CHARSET=utf8mb4;
  53. ");
  54. $sth->execute() or die "imposible crear la tabla";
  55. $sth->finish;
  56.  
  57. ## Ingreso en la base de datos
  58. my @classes = @{$json->{'classes'}};
  59.  
  60. for my $clase_ref (@classes) {
  61.     my $name  = $clase_ref->{'name'     };
  62.     my $power = $clase_ref->{'powerType'};
  63.  
  64.     say "$name\t$power\t", (utf8::is_utf8($name) ? "sí":"no"); # sí
  65.    
  66.     $sth = $db->prepare("
  67.         INSERT INTO wow_classes_es VALUES (
  68.             '$clase_ref->{'id'}'        ,
  69.             '$name'                     ,
  70.             '$power'            ,
  71.             '$clase_ref->{'mask'}'
  72.         )"
  73.     );
  74.     $sth->execute() or die "imposible hacer la consulta";
  75.     $sth->finish;
  76. }
  77.  
  78. ## Desconexión de la BD
  79. $db->disconnect;
  80. say "";
  81.  
  82. ## Conexión a la base de datos
  83. $db = DBI->connect("DBI:mysql:$dbname:$dbhost", "$dbuser", "$dbpass",
  84.     {
  85.         mysql_enable_utf8mb4 => 1,
  86.     }
  87. ) or die "Imposible conectar con la DB";
  88.  
  89. ## Preparar la consulta
  90. $sth = $db->prepare("
  91.    SELECT * FROM `wow_classes_es`;
  92. ");
  93. $sth->execute() or die "imposible hacer la consulta";
  94.  
  95. $" = "\t|";
  96.  
  97. #binmode STDOUT, ':encoding(latin1)';                   # método alternativo. Ver más abajo
  98.  
  99. while (my @fila = $sth->fetchrow_array) {
  100.  
  101.     # Perl no sabe que esta recibiendo datos en utf8, asi que los decodificamos
  102.     $fila[1] = decode_utf8($fila[1]) ;  # `name`
  103.     $fila[2] = decode_utf8($fila[2]) ;  # `power`
  104.  
  105.     say "@fila";                        # la salida será vuelta a codificar a UTF8 por acción del "use open"
  106.  
  107.     # Parece un poco raro todo esto, ¿no?
  108.     #
  109.     # Estamos recibiendo datos en UF8, los decodificamos a Unicode, y luego en la salida, "use open", vuelven a
  110.     # codificarse en UTF8 para la terminal.
  111.     #
  112.     # Hay otra forma, desde luego:
  113.     #
  114.     # Quitamos o comentamos la línea del "use open".
  115.     # Perl seguirá sacando el contenido de las variables como si fuese un flujo de bytes.
  116.     # Ahora comentamos las líneas decode_utf8() de este while() para no hacer ninguna codificación.
  117.     # Lo que sale por el say() son los datos en bruto de la base de datos. Como YA están en UTF8, pues
  118.     # eso es justo lo que recibe la terminal.
  119. }
  120. $sth->finish;
  121.  
  122. ## Desconexión de la BD
  123. $db->disconnect;
Coloreado en 0.003 segundos, usando GeSHi 1.0.8.4

Este programa saca esto:
Sintáxis: [ Descargar ] [ Ocultar ]
Using text Syntax Highlighting
Guerrero        rage    no
Paladín        mana    sí
Cazador focus   no
Pícaro energy  sí
Sacerdote       mana    no
Caballero de la Muerte  runic-power     no
Chamán mana    sí
Mago    mana    no
Brujo   mana    no
Monje   energy  no
Druida  mana    no
Cazador de demonios     fury    no

1       |Guerrero       |rage   |1
2       |Paladín       |mana   |2
3       |Cazador        |focus  |4
4       |Pícaro        |energy |8
5       |Sacerdote      |mana   |16
6       |Caballero de la Muerte |runic-power    |32
7       |Chamán        |mana   |64
8       |Mago   |mana   |128
9       |Brujo  |mana   |256
10      |Monje  |energy |512
11      |Druida |mana   |1024
12      |Cazador de demonios    |fury   |2048
Coloreado en 0.000 segundos, usando GeSHi 1.0.8.4

Puntos importantes:

* En vez de IO::All voy a usar LWP::Simple. El resultado es exactamente igual: se baja el json, sin decodificar nada

* El módulo JSON tiene una función llamada decode_json que hace todo: decodifica desde UTF8 y pasa el json a estructura Perl

* Luego hacemos la conexión con la opción mysql_enable_utf8mb4 => 1 en el connect()

* Hacemos el DROP y el CREATE. ¡Ojo! Fíjate que lo he simplificado bastante

* Luego hacemos el INSERT. Como los datos que hay dentro de la estructura ya están en UTF8, se los pasamos a la base de datos directamente. Supongo que ya te has dado cuenta que $power no estaba en su sitio

* Cerramos la BD, y la abrimos otra vez para ver cómo acceder a ella

* Hacemos la consulta y luego el bucle, recuperando fila por fila, y aquí es donde se pone la cosa interesante...

* Ves en el código que lo que recibo de la BD lo decodifico de UTF8 a Unicode interno. Y luego, por medio del say(), los datos se vuelven a codificar a UTF8 (por efecto del "use open" al principio del programa).

Como te pongo en los comentarios, esto parece raro. ¿Decodificamos y luego a la salida los datos vuelven a codificarse?

Bueno, tenemos que hacerlo así porque Perl no sabe que lo que recibe de la BD está en UTF8.

Podríamos... hacer un atajo: si la salida estándar STDOUT no estuviera en :utf8, entonces podríamos enchufar los datos que recibimos de la BD a la terminal (que suponemos en UTF8).

Esto lo podemos hacer de varias formas, pero depende de lo que necesitemos en todo el programa. Es decir, si el "use open" no nos sirve para mucho, pues lo podemos quitar, por lo que entonces ya no necesitamos los decode_utf8() del bucle while(), y mandamos los datos a terminal de forma directa.

Si pensamos que "use open" no es muy útil (y en realidad así lo es), podemos crear un atajo, justo antes del bucle while():

binmode STDOUT, ':encoding(latin1)';

De esta manera, le decimos a Perl que la salida estándar vuelve a ser latin1, que es lo mismo que decir que no queremos que haga ninguna transformación de los datos. Comentamos las líneas decode_utf8(), y ya está: los datos con UTF8 desde la BD sale a la terminal directamente. Si la terminal está en UTF8, pues ya los vemos bien.

Re: Codificacion en HTTPS GET

NotaPublicado: 2017-10-08 20:12 @883
por Superdri
Magnífico :D

Muy útil lo de dropear la tabla solo si existe. Yo tenía que andar comentando las 3 líneas que la crean cuando la tabla no existía :mrgreen:

El power estaba mal colocado, así es ;)

Al final lo hice con utf8, no utfmb4. El caso es que usaba la sentencia del DBI
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. $db->{'mysql_enable_utf8'} = 1;
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4
y con las variables de la configuración de MySQL que me dijiste y no insertaba en utf8 (algo estaba haciendo mal seguro), aunque prefiero hacerlo con la solución que puse antes porque si subo la web a un alojamiento seguramente no pueda tocar el archivo de configuración de MySQL.

Tendré en cuenta tu atajo sobre la salida de datos para no tener que decodificar de utf8 a Unicode para luego volver a codificar.

Guardaré el código que has pasado para tenerlo todo clarito. Mil gracias por la ayuda, ahora entiendo un poco más cómo funcionan Perl con las codificaciones de caracteres.

Un saludo y ¡hasta otra!