El arte del scrapping: segunda parte

Ok, esta es la segunda parte de mis series sobre cómo empecé a hacer web scrapping de páginas de cómics, y qué se necesita para automatizar esta tarea.

Si no leíste la primera parte, te recomiendo hacerlo, pues la mayoría de conceptos que usé anteriormente volverán a ser usados, solo que con distintas técnicas y scripts.

Siguiente nivel: Calvin and Hobbes (GoComics)

¿Conocen Calvin and Hobbes? Seguramente no, es un cómic que no salió mucho de Estados Unidos. Solía ser una tira cómica que salía de lunes a sábados en los diarios, muy al estilo de Mafalda, pero con la diferencia de tener un intervalo de tiempo bastante mayor—21 años consecutivos publicando cómics hasta la fecha. Haciendo suficiente matemática, podemos llegar a concluir que el total de cómics publicados (hasta el día de la publicación de este post) es de más de 8000 publicaciones.

¡Es de más de 8000!

Y como buen scrapper, no quise perder la oportunidad de descargar esa severidad de cómics, y tenerlos siempre a mi disposición, incluso si no tengo conexión a Internet (que sucede bastante a menudo).

Empecemos con lo técnico.

CaH (escrito así de ahora en adelante porque me da paja escribirlo siempre) está archivado y se actualiza diariamente de lunes a sábados en el sitio web GoComics, con un formato de indexado constante, y con las imágenes en el mismo lugar. Se parece mucho a la forma de funcionar de Jeph Jacques en el post anterior, ¿no?

No. Es muchísimo peor.

Para empezar, el formato de indexado funciona en fechas. En. Putas. Fechas. A ver si alguien puede responderme cómo carajo hacer un loop usando fechas del calendario gregoriano. Es una locura si queremos hacerlo con un solo archivo usando solamente for loops. Necesitamos otra manera.

La primera vez que quise hacer esto, lo hice de la forma más cavernaria posible: escribí todas las fechas, desde el primer hasta el último cómic, en Vim. Claro, usé macros y repeticiones para agilizar un poco todo, pero definitivamente no es la mejor opción tener un calendario al lado del editor y tener que repetir 30, 31 o 28/29 veces cada mes, para cada cómic que quieras hacer.

Un par de días después, recordé un clásico sistema de numeración arábica del tiempo: el tiempo de Unix, o tiempo epoch.

Para los que no sepan qué carajo es esto, si están en un sistema operativo basado en Unix, abran una terminal y escriban:

$ date +%s

Debería aparecerles un número entero de 10 cifras. Ese número, amigos míos, es el intervalo de tiempo exacto desde el 1 de enero de 1970 00:00:00 (UTC) hasta el segundo en el que ingresaron dicho comando. De hecho, este es el sistema que usan absolutamente todas las computadoras de 32 bits para calcular el tiempo sin usar cantidades excesivas de memoria.

Además de imprimir la fecha actual en cualquier formato que querramos (aquí se describen todos los formatos posibles, sólo colóquenlos en el último argumento con un "+" detrás de ellos), date también puede imprimir la fecha que le pidamos, y eso es exactamente lo que yo estaba buscando para este tan dilemático script.

date, al convertir el tiempo en epoch, nos devuelve un número entero, sin decimales, así que el mínimo tiempo que se puede conseguir es un segundo (créanme, ya intenté dividirlo en milisegundos y terminé haciendo una máquina devoradora de RAM). Sabemos que un día tiene 86.400 segundos. Así que, si queremos, por ejemplo, imprimir todos los días desde el 18/11/1985 (primer cómic de CaH) hasta el 21/01/2019 (fecha de ejemplo), sólo habría que escribir un for loop que imprima la primera fecha, y que en cada ciclo dicha fecha aumente 86.400 segundos, es decir, un día. O, en lenguaje de bash, un while loop que se detenga cuando la variable independiente x sea menor o igual a la variable y. Para esto, evidentemente, hay que convertir todas las fechas a epoch para realizar la aritmética necesaria.

El formato de post de CaH es https://www.gocomics.com/calvinandhobbes/1985/11/18/, o en formato de date, %Y/%m/%d. Para nuestra suerte, este formato de URL es perfectamente válido:

#!/usr/bin/bash
x=$(date -d '1985/11/18' +%s)
y=$(date -d '2019/01/21' +%s)
while [ $x -le $y ]; do
  date -d @$x +https://www.gocomics.com/calvinandhobbes/%Y/%m/%d/
  # Ese @ es para aclarar que es en tiempo epoch
  let "x=x+86400"
done

Impresionante, ¿no? Con un sencillo script de 7 líneas (no veo para qué contar la del comentario), parece que tenemos resuelta la parte del indexado.

... Bueno, sí y no. ¿Recuerdan cuando les dije que el indexado de CaH es constante? Bueno, no es tan así. Hay un pequeño (gran) intervalo de tiempo en el que, o no se archivaron los cómics, o no se publicó nada históricamente (no he investigado al respecto). Este intervalo es desde el 1996/01/01 hasta el 2006/12/31. 2 años, 11 meses y 30 días sin ninguna imagen. Evidentemente, si intentamos scrappear CaH de esta forma, habría que esperar un intervalo nulo por aprox. dos horas. Y nuestro tiempo es valioso. Así que tendremos que modificar un poco la estrategia.

Hay dos formas de resolver esto: la primera, que es la que yo hice, fue imprimir todo el intervalo en un archivo de texto y remover las fechas que no tuvieran cómics—nuevamente, con Vim. Un método muy arcaico y artesanal, para mi gusto, que es justamente lo que estoy buscando evitar. Por esto, la segunda forma resulta más elegante y precisa.

En lugar de usar fechas exactas en el script, guardémoslo en un archivo .sh y reemplacemos las fechas de $x y $y por argumentos de línea de comandos ($1 y $2), y ejecutémoslo dos veces seguidas; la primera desde el 18/11/1985 al 31/12/1995, y la segunda desde el 01/01/2007 a la fecha actual (21/01/2019, en este caso).

#!/usr/bin/bash
x=$(date -d $1 +%s)
y=$(date -d $2 +%s)
while [ $x -le $y ]; do
  date -d @$x \+https://www.gocomics.com/calvinandhobbes/%Y/%m/%d/
  let "x=x+86400"
done

Y en la línea de comandos quedaría así:

$ ./epoch.sh '1985/11/18' '1995/12/31'; ./epoch.sh '2007/01/01' '2019/01/21'

De manera que conseguimos el resultado que estábamos buscando. En el caso de esta explicación, lo guardaré a un archivo de texto, llamado cah.txt.

Uf. Bien, ya terminamos... la parte del indexado. Ahora, vayamos con los archivos.

Ahora que tenemos todas las páginas de comics, tenemos que saber dónde residen los archivos de imágenes. Con este comando, se pueden sacar los formatos de URL de cada página:

$ curl https://www.gocomics.com/calvinandhobbes/1985/11/18/ | grep -m 1 -Eo "https.*assets.amuniversal.*\/[a-z0-9]+"
    ...
    ...
https://assets.amuniversal.com/cc713730deb701317193005056a9545d

... Fuck.

Empecemos a contar los problemas que hay con este sistema. Primero y principal, las URLs no contienen la extensión de archivo, y segundo, la "identifiación" del archivo está en Base32, un hash totalmente aleatorio e imposible de ordenar alfanuméricamente.

Si abrimos un archivo llamado cc713730deb701317193005056a9545d, es evidente que el visor de imágenes no sabrá qué formato usar y nos saltará un error. Claro, podemos abrirlo en el navegador y renombrarlo con la extensión correspondiente. Pero como les decía, hacer eso unas 8000 veces no es muy productivo.

Además del terrible tema de la extensión, el nombre de archivo no es muy útil para seguir una línea cronológica (que CaH sí tiene). Como mucho, los hashes se ordenan bien en un máximo de 7 veces consecutivas. Y nosotros necesitamos más de 8000 ordenamientos correctos...

Para la extensión, no podemos conseguir la información desde el nombre de archivo, pero sí desde el archivo en sí. La herramienta que viene en GNU coreutils, file, hace exactamente lo que necesitamos. Porque claro, como en Unix no se estaba acostumbrado a usar extensiones de archivos (recuerden que eran los 80s), debían tener alguna herramienta para averiguar qué tipo de archivo era, sin alterar la filosofía de Unix (aunque luego Microsoft se pasó todo eso por el culo).

Así que, si queremos renombrar las imágenes con su extensión correspondiente, sería guardar la extensión en una variable, procesarla y usarla en un comando mv.

$ wget https://assets.amuniversal.com/cc713730deb701317193005056a9545d
$ ext=`file -b cc713730deb701317193005056a9545d | cut -c -3 | tr '[:upper:]' '[:lower:]'`
$ mv cc713730deb701317193005056a9545d cc713730deb701317193005056a9545d.$ext

Pero espera, si el archivo es JPG, file imprime "JPEG [...]", y al procesarlo queda "jpe". No pasa nada. Con un pequeño if de una sola línea podemos cambiar eso:

 $ [ $ext == "jpe" ] && ext=jpg

No lo puse en un shell script porque todavía tenemos que ver el otro tema: el nombre del archivo. Los que ya lo dedujeron, felicidades, tienen la mente de un scrapper. La solución para ordenar cronológicamente las imágenes es, en efecto, usar la fecha del índice. Así es, lo que nos había roto tanto las pelotas viene a salvarnos la vida para no destruirnos la cabeza con un muro de concreto.

Entonces, suponiendo que todas las fechas están en el archivo cah.txt, y tenemos las operaciones para procesar el nombre y extensión de cada archivo, ya podríamos escribir un script que funcione relativamente bien.

Además, como en el caso del "jpe", voy a poner un mismo comando, que detiene el ciclo cuando encuentra un archivo que ya se descargó.

#!/usr/bin/bash
for i in $(cat cah.txt); do
  nm2=$(echo $i | grep -Eo "[0-9]+.*[0-9]"|sed -e "s/\//-/g") # "YYYY-MM-DD"
  [ -e $nm2* ] && continue # Si ya existe, termina el ciclo y continúa con la siguiente iteración
  im=`curl $(echo $i) | grep -m 1 -Eo "https.*assets.amuniversal.*\/[a-z0-9]+"`
  nm=$(echo $im | cut -c 32-) # Guarda los 32 últimos caracteres de $im
  wget $im
  # Pide el tipo de archivo, agarra los 3 primeros caracteres
  # y los convierte a minúsculas>
  ext=$(file -b $nm | cut -c -3 | tr '[:upper:]' '[:lower:]')
  [[ $ext = "jpe" ]] && ext=jpg # Si es "jpe", lo corrige en "jpg"
  mv $nm $nm2.$ext
done

Y bam. Ejecuten ese script (e instalen los programas que necesitan y no tienen, obviamente), y paso a paso les va a descargar—en este caso—todos los comics de CaH que les provean. Pero no solo se limita a esa franquicia; pueden descargar cualquier cómic entero con solo modificar el archivo del loop. Fácilmente puedo reemplazar cah.txt y hacer todos los .txt's que quiera con los scripts que mostré anteriormente.

Espero que hayan aprendido algo sobre todo esto, y es que aunque no encuentren justo ahora la solución a esto, tarde o temprano la encontrarán. Yo tardé dos días en descubrir lo del epoch, y otro día más en usar el índice para el nombre de archivo. Tardé en terminar esto, pero lo hice. Y eso es lo que tienen que dejarse grabado en las cabezas. No crean que Linus Torvalds terminó Linux en un día.

En fin, esta fue la segunda parte. La siguiente va a ser un sitio que muchos de ustedes probablemente usen o desconozcan totalmente de su existencia, sin puntos medios. Nos vemos.