El arte del scrapping: primera parte

¿Alguna vez entraron a un sitio de comics y dijeron "vaya, qué comics tan buenos, pero con este sitio pasar de una página a otra tarda como diez segundos", o "wow, estos comics están geniales pero el botón de siguiente página es muy pequeño"?

... ¿No?, bueno, yo sí. Y fue tal mi odio hacia ciertos webcomics y sus sistemas web con 30 toneladas de Javascript, que decidí descargar únicamente las imágenes en mi disco duro. ¿El problema? La mayoría de ellos poseen más de 3000 publicaciones, cada una en una página individual, y cada cómic posee un distinto programa web con sus propios formatos de indexación. Por suerte, las excepciones a este último factor son muy pocas, pero mortales para la mente de un programador.

Fue así como empecé con el web scrapping, que es básicamente descargar contenido web con scripts, generalmente escritos desde cero, y les voy a contar por qué. En lo personal, no me gusta mucho estar todo el tiempo con el navegador abierto (ya saben los recursos que consumen al no hacer nada), teniendo que hacer clic en un botón bien chiquito (no en sentido literal, puedo hacer clic en él, pero tener que mover el mouse hacia ahí ya me parece mucho laburo) para pasar a la siguiente página. En cambio, prefiero usar un visor de imágenes (vamos los irfanviueros todavíiiiia), que al hacer clic en cualquier lado de la pantalla, o mejor aún, presionar una sola tecla, y pasar directamente a la siguiente página. Aparte, el zoom en el navegador se hace más complicado (Ctrl+ruedita) y agranda cosas que no quiero agrandar ni ver.

En resumen, al menos para mí, para ver cómics es necesario verlos de una forma especial, y para verlos de una forma especial necesito tener todas las imágenes en mi disco duro, ordenadas de cierto modo para seguir la línea de tiempo (si es que hay una, de otro modo no importa mucho). Ahí entró mi deseo de automatizar esta tarea, para ahorrarme tener que repetir los mismos comandos mil veces decidí escribir un script que probablemente me cueste horas y decenas de intentos en planificar y escribir correctamente. Es una hazaña que va mejorando con el tiempo, y cada vez aparecen más formas de hacer las cosas que pueden llegar a facilitar muchísimo trabajo. El camino a la eficacia, lo llamo yo.

Nivel fácil: Questionable Content

Para mi suerte, este es de los primeros webcomics que me interesé (después de Homestuck, pero ese sitio es una aventura de texto y el HTML es esencial para leer), y a la vez el que necesita menos complejidad para descargar sus publicaciones.

Gracias a Jeph Jacques (el autor del webcomic), el sistema de indexado es una sencilla secuencia conjunta ascendente de números arábigos. Es decir, 1 2 3 4 5 ... n, donde n es el último cómic que publicó hasta la fecha. (Una forma más elegante de definirlo que "empieza en el 1 y termina en el último número", creo yo.)

Bien, tenemos el indexado. Ahora... ¿dónde están las imágenes? Fácil, en un solo directorio. (En serio, Jeph, si llegás a leer esto, sos un crack, y tu sitio es el mejor para webcomics que vi en mi vida.) Es más, el nombre del archivo coincide perfectamente con el número de la publicación correspondiente, al igual que con el número que aparece en la URL.

Así que, en simples términos, hace falta solo una variable independiente, que va a aumentar 1 por ciclo, y en cada ciclo se descarga la imagen de dicho número de página. No parece tan complicado.

Esta fue la primera versión de mi "script" (en realidad es un for loop en Batch de una sola línea):

>for /L %i in (1, 1, 3920) do wget https://questionablecontent.net/comics/%i.png

Con eso debería bastar, uno?

Vamos a verificarlo. Si descargamos 3920 imágenes, deberíamos tener 3920 archivos en el directorio.

>dir
                                    ...
                                    ...
24/10/2007  03:43           229.710 998.png
25/10/2007  11:33           182.504 999.png
            3818 archivos    612.371.468 bytes

... Bueno, solo se descargaron 3818 archivos de 3920. Algo no anda bien.

Después de un poco de investigación, descubrí que nuestro querido Jeph no nos la hizo tan fácil como pensábamos. De hecho, sus cómics pueden aparecer en 3 formatos de imagen distintos: del N°1 al N°3920, las imágenes pueden ser de formato PNG (97.4%), JPG (1,86%) o GIF (0,74%).

Aunque, mirando los números, no es una cifra tan grande la del resto de formatos. Podría simplemente no complicarme la vida descargando otros tipos de archivos; pero la verdad ya me compliqué la vida haciendo esta mierda, así que no voy a dejar mi trabajo hecho a la mitad.

Fue muy gracioso lo que hice ese preciso momento en el que me enteré de este error en mi script. En mi cabeza había algo así como "bueno, no sé cuál de todos los números son GIF/JPG, pero sí sé cuáles son PNG, así que solo tengo que averiguar qué números no son PNG, y de ahí descargar los GIFs. Luego, repetir el mismo paso, pero esta vez sabiendo solo cuáles son los GIFs y los PNGs, así que ya tengo sabido cuáles son los JPGs".

Y lo hice. Dios, qué retorcido estaba hace dos meses.

Lo primero que hice fue hacer el mismo for loop, pero esta vez escribiendo un número a un archivo si dicho número no era un PNG:

>for /L %i in (1, 1, 3920) do if not exist %i.png echo %i >> nopng.txt

Y en efecto, el archivo tenía 102 líneas con todos los números de las imágenes que no eran PNG. Así que procedí a descargar todos los GIFs:

>for /F "tokens=*" %i in (nopng.txt) do
wget https://questionablecontent.net/comics/%i.gif

Obviamente, no fue un comando perfecto, hubieron 29 imágenes que no pude descargar.

... Y de vuelta a hacer lo mismo, pero esta vez para los JPGs, y esta vez eligiendo a los números que no tienen un respectivo PNG ni GIF.

>for /L %i in (1, 1, 3920) do if not exist *.png if not exist *.gif
(wget https://questionablecontent.net/comics/%i.jpg)

¿Por qué hice todo en una sola línea de comandos la última vez y no la primera? No me pregunten a mí, pregúntenle al Bru de hace dos meses.

Cuestión que luego de tanto tiempo (en mi perspectiva) de haber hecho toda esa malandrada de comandos que tenía que escribir a mano, descubrí curl. Seh, no hace mucho estoy con todo esto del scripting, recién me entero de varios programitas de GNU (ando usando MSYS2, por si se preguntan). Así que, para su deleite, reescribí el script entero (y esta vez en bash, que hace muchísimo más fácil manejar variables):

#!/usr/bin/bash
for i in $(seq 1 3920); do
  img=`curl https://questionablecontent.net/$i | grep -Eo "comics.$i...."`
  wget https://questionablecontent.net/$img
done

¿Qué es lo que hice acá? Bueno, en lugar de tratar de adivinar si el número de página era PNG, GIF o JPG, decidí averiguarlo con curl, que, como muchos sabrán, se puede descargar fácilmente el HTML de un sitio web, y con grep, seleccionar el contenido que se desee. Puse "comics.$i....", evidentemente porque en el sitio de Jeph la imagen no está escrita en un directorio absoluto, sino uno relativo: ./comics/.... Ese pequeño pero importantísimo dato es guardado en la variable $img, que se renueva cada ciclo y es posteriormente usado con wget para descargar la imagen deseada. Así no hay posibilidad de pérdidas, ni siquiera si a Jeph se le ocurre cambiar el directorio de la imagen a uno absoluto (siempre va a seleccionar solamente comics/...).

Igualmente, hay un tema. El cómic todavía no dejó de publicar páginas (por suerte), y si queremos descargar las imágenes más nuevas hay que cambiar el rango del for loop (los números del seq), y eso ya es tener que cambiar el código cada vez que queramos ejecutarlo. Y eso es tremendo dolor de huevos.

No se alarmen, tranquilos. Hay una solución, pero requiere más complejidad.

Podemos reemplazar el 1 del rango —es decir, el inicio del loop— con una variable calculada con la cantidad de imágenes + 1. ¿Por qué se le suma 1? Pues, porque sino descargaremos otra vez más el último archivo; si hay 3920 archivos y empezamos el loop con (3920, ...) descargaremos 3920 por segunda vez. Claro, hay una opción de wget para omitirlo, pero lo encuentro muy revoltoso cuando simplemente podemos usar la aritmética a nuestro favor.

Ok, tenemos el inicio del rango, pero... ¿y el final? ¿Cómo podemos saber cuál es la última página publicada?

Bueno, en realidad hay una forma bien sencilla, y es usando el mismo comando de $img pero sin colocarle el número de página en la URL (y parseando un poco mejor el número en comics/...).

#!/usr/bin/bash
let "s=`ls *.png *.gif *.jpg | wc -l`+1"
e=`curl https://questionablecontent.net/
 | grep -Eo "comics.*" | grep -Eo "[0-9]*"`
for i in $(seq $s $e); do
  img=`curl https://questionablecontent.net/$i | grep -Eo "comics.$i...."`
  wget https://questionablecontent.net/$img
done

Es una manera de hacerlo. Sin embargo, no es mi favorita por diversas razones. Primero, el comando ls *.png *.gif *.jpg en sí tarda varios segundos, llega hasta 20 en mi computadora. Además, con la copia de $img estoy descargando 1 URL extra para hacer el trabajo. Creo que podría hacerlo mejor.

Y lo hice. (Vaya, un déjà vu.)

Hay una forma más sencilla de sumar 1 a la cantidad de archivos de un directorio, que muy estúpidamente no me había dado cuenta... Añadir otro archivo. En esta explicación no se tomaba en cuenta que en el mismo directorio de las imágenes está el archivo de script, por lo que al ejecutar

>ls | wc -l

nos sale exactamente el número que estamos buscando, ¡y muchísimo más rápido y eficiente!

Ahora, con el tema del último número del rango, encontré una mejor solución y es un tanto irónica. La manera de conseguir el último número es equivocándose, y descargando de más.

Espera, espera, espera. Tengo una explicación a esa aparente contradicción: hay una manera de detener al loop en el momento que wget reciba un error 404—es decir, que no haya encontrado ninguna imagen de ningún formato. Es ese el momento en el que el ciclo se detiene, y el programa habrí descargado con éxito todos los comics nuevos, listos para leerse.

Y como el loop no tiene fin, en lugar de un for loop decidí usar un while loop infinito, en donde cada ciclo suma $s 1. Teóricamente es lo mismo, pero más fácil de escribir y más bonito de leer.

#!/usr/bin/bash
let "s=`ls | wc -l`"
while true; do
  img=`curl https://questionablecontent.net/$s | grep -Eo "comics.$s...."`
  wget https://questionablecontent.net/$img || break
  let "s=$s+1"
done

... Que, en teoría, si guardas el archivo .sh en la misma carpeta donde vas a descargar los cómics, funciona incluso si no tienes imagen alguna. Así que esta es mi versión más reciente del script.

Muy pronto nos veremos con la segunda parte de esta serie de publicaciones, en la que veremos un peor rival que el tierno Jeph Jacques de hoy. Nos veremos otro día.