Yoink
Yoink es un crawler de datos públicos, respetuoso por defecto, que construimos para trabajos que duran más de lo que un solo proceso debería durar con confianza: lo suficiente como para que la red falle, el host se reinicie y un crawler ingenuo pierda todo lo que tenía. Python asíncrono, ~3,200 líneas, 134 pruebas y un modelo de checkpoints reanudables que significa que puedes matarlo y reiniciarlo sin reconstruir el estado.
Líneas de Python
~3,200
Pruebas
134
Comportamiento por defecto
Respetuoso
Por qué construir otro crawler
La mayoría de los crawlers de propósito general caen en dos bandos. Uno es el kit académico —flexible, pero tú escribes tu propia programación, los reintentos, el parser de robots.txt y la lógica de checkpoints. El otro es el scraper SaaS —rápido de empezar, pero pierdes la capacidad de correrlo localmente, versionar la especificación o auditar lo que realmente descargó. Necesitábamos algo intermedio: un solo paquete de Python que instalas, apuntas a un objetivo y en el que confías para que se comporte bien en el sitio de alguien más durante horas sin supervisión.
El trabajo específico que lo detonó fue recolectar unos cuantos millones de páginas de datos públicos estructurados para análisis. Los scrapers en la nube pedían seis cifras al año. Escribir el nuestro desde cero habría sido una semana de preparativos antes de que aterrizara la primera fila. Yoink fue la tercera opción.
Respetuoso por defecto
La decisión de diseño que lo define fue hacer del comportamiento respetuoso el camino por defecto, no una opción que activas. Yoink lee y respeta robots.txt antes de tocar cualquier otra cosa; respeta el crawl-delay; limita la tasa por host (no por proceso), así un trabajo repartido entre workers no puede martillar por accidente el mismo origen; y anuncia un User-Agent real que apunta a una dirección de contacto. Nada de esto es novedoso —es lo estándar—, pero la mayoría de los crawlers lo dejan apagado hasta que el desarrollador se acuerda, y la mayoría de los desarrolladores no se acuerda.
El costo es que Yoink es más lento que un scraper agresivo. En un objetivo con un crawl-delay de 10 segundos, un trabajo de Yoink es 10× más lento que una herramienta que ignora la directiva. Decidimos que eso era lo correcto: quienes operan los sitios que rastreamos son los mismos que escriben los datos que queremos indexar, y ser un buen ciudadano es el precio de entrada.
Checkpoints reanudables
La otra decisión de diseño que lo define fue el sistema de checkpoints. Yoink escribe un checkpoint estructurado en S3 en cada transición de URL —solicitud, respuesta, resultado del parseo, errores—, identificado por un ID de crawl determinista. Si el proceso muere, la siguiente ejecución lee el checkpoint, omite todo lo ya descargado y se reanuda en la frontera exacta donde se quedó. No hay una base de datos aparte, ninguna cola en memoria que perder, ninguna pregunta de "¿seguro que terminó?" al final de una corrida de ocho horas.
La implementación es poco llamativa a propósito. Los checkpoints son JSON delimitado por líneas en un prefijo de S3; reanudar un crawl es una lectura en streaming de ese prefijo. La opción aburrida sobrevive a las fallas de operación que la opción ingeniosa no —y rastrear a escala es, en su mayoría, fallas de operación.
Renderizado de JS, pero solo cuando se pide
Cerca de un tercio de los objetivos que necesitábamos requería JavaScript para renderizar los datos —apps de una sola página, secciones con carga diferida, contenido que aparece en la hidratación. Yoink puede manejar un navegador headless cuando se le configura, pero nunca por defecto. La diferencia de costo entre fetch-and-parse y renderizar con headless es de aproximadamente dos órdenes de magnitud, tanto en tiempo como en dinero; hacerlo opcional por objetivo mantiene fáciles los casos fáciles. La configuración vive en una especificación YAML tipada que se versiona junto con el trabajo.
Cómo aguantó
Corrimos Yoink contra el trabajo que lo detonó —la recolección de unos cuantos millones de páginas— y la falla que más nos preocupaba, una corrida de ocho horas muriéndose a la mitad, resultó ser un no-evento. El proceso fue terminado y reiniciado más de una vez a lo largo de la vida del crawl, por nosotros y porque el host se reiniciaba debajo de él; cada vez, leyó su checkpoint y se reanudó en la frontera sin volver a descargar una página ni perder una fila. Ningún origen nos limitó la tasa ni nos bloqueó, que es la prueba silenciosa de que los valores por defecto de cortesía valieron la pena.
No tenemos una métrica de conversión bonita que presumir —es un crawler, no un embudo, y desconfiamos de adornarlo. El resultado honesto es el que el diseño predijo: un trabajo demasiado largo para vigilarlo corrió sin supervisión, sobrevivió a las fallas operativas que matan a los crawlers ingenuos y terminó con un registro completo y auditable de qué descargó y cuándo. Esa era la meta que nos propusimos alcanzar al construirlo, y es la meta sobre la que lo defenderíamos.
Qué cambiaríamos
Dos cosas, si empezáramos de nuevo. Primero, el formato de los checkpoints sería Parquet o un binario con prefijo de longitud en lugar de JSONL —el parseo de JSON dominaba el tiempo real en la ruta de reanudación una vez que el checkpoint pasaba de unos cuantos cientos de miles de entradas. Segundo, construiríamos antes el bucle de descubrimiento basado en dirección de contacto y sitemap; tratar sitemap.xml como la fuente principal de URLs (en vez del descubrimiento por enlaces HTML) recorta tanto el tiempo hasta la primera fila como la carga sobre el host por un factor significativo en sitios que publican uno.
¿Tienes algo difícil de construir?
Cuéntanos en qué estás trabajando. Te regresamos con el alcance, los riesgos y lo que haríamos primero, en un día hábil.