El pipeline que jubiló a mi scraper de consultas DGT

· 5 min de lectura

# El pipeline que jubiló a mi scraper de consultas DGT

Las consultas que no se mantenían solas

En la base de datos de Conversor hay una materia prima que no se puede inventar: las consultas vinculantes de la Dirección General de Tributos, la DGT. Cuando Hacienda responde a una pregunta sobre cómo se clasifica una actividad económica, esa respuesta tiene fuerza vinculante. Para mí es oro: me dice, con criterio oficial, qué epígrafe del IAE le corresponde a una actividad concreta. Cruzar ese criterio con mi correspondencia IAE↔CNAE es lo que le da contexto a una tabla que, sin él, sería una lista de códigos muertos.

Traerlas a la base, además, no es un trabajo de una sola vez. La DGT publica consultas con regularidad, y cada publicación nueva cambia el estado del conjunto: lo que era la respuesta más reciente ayer deja de serlo hoy. Mantener esa capa al día es tan importante como montarla.

El problema es que esas consultas no llegan solas. Hay que traerlas, y hay que traerlas bien, y hay que seguir trayéndolas cuando se publican otras nuevas. Es un flujo de datos que vive en una web pública de la administración, y mi trabajo era convertirlo en algo que la base pudiera consultar de forma fiable.

Mi primer camino fue el que cualquier ingeniero toma en caliente: un scraper. Lo escribí en scripts/ingest-dgt-consultas.ts. Le puse hasta mote: "el de Petete".

El intento: raspar el HTML y confiar

La idea parecía impecable. La DGT publica las consultas en una web abierta, cualquiera puede leerlas desde el navegador. Si yo podía leerlas, un script también. Bastaba con entrar, recorrer el HTML y extraer el texto de cada consulta. Sin pedir credenciales, sin estudiar una API, sin esperar a nadie.

Y funcionaba. El scraper traía las consultas y las metía en la base. Cumplía su parte. Pero cumplía con dos defectos que me fui guardando en el bolsillo.

El primero era la fragilidad. Un scraper se sostiene sobre la estructura del HTML ajeno. Un cambio en un div, en una clase, en el orden de los elementos, y el parser deja de encontrar lo que buscaba. Y no falla con un error limpio: falla en silencio, guardando campos vacíos o dejando de guardar, y te enteras días después, cuando algo no cuadra.

Y aquí está el detalle que separa un dato fiscal de un dato cualquiera: si una consulta entra mal, no es un píxel fuera de sitio. Es un criterio de clasificación que otro ingeniero va a usar para decidir qué epígrafe asignar. El margen de error no existe.

El segundo era más sutil y, mirando atrás, más caro. El scraper hacía de todo: ingería el dato y, de paso, tocaba columnas de posicionamiento. El título optimizado para el buscador y el resumen. Dos trabajos distintos metidos en el mismo script.

El error: un plan A que era, en realidad, una deuda

La confesión quedó escrita en el propio proyecto: el scraper se conserva solo para re-ingesta manual de emergencia. Leído despacio, eso significa una cosa. La vía automática fallaba lo bastante como para que yo tuviera un botón de pánico preparado para lanzar el script a mano.

Ese "solo para emergencia" era el diagnóstico disfrazado de comentario. Un scraper no es un plan A: es una deuda que se paga en mantenimiento. Cada cambio de la web de la DGT era un ticket nuevo, y cada ticket era una tarde mirando HTML de otra persona para adivinar qué había cambiado esta vez.

Y la mezcla de responsabilidades era el segundo agujero. El código que traía el dato era el mismo que intentaba escribir el título bonito. Cambiaba la lógica de ingesta y temblaba el SEO. Ajustaba el resumen y temblaba la ingesta. Dos responsables distintos escribiendo en la misma tabla, sin frontera entre ellos.

No fue un fallo puntual el que me empujó al cambio. Fue darme cuenta de que el sistema, tal y como estaba, iba a necesitar mi atención para siempre. Un flujo de datos que no corre solo no escala, por bueno que sea el script que lo mueve.

La solución: un pipeline que hace una sola cosa

Lo sustituí por lib/normativa-api/sync.ts, con la función syncDgtConsultas. El cambio de fondo no es de nombre de archivo: es que la ingesta ahora va por la Normativa API, no por el HTML de la web pública. En cuanto existe un canal oficial y estructurado, rascar la web deja de tener sentido como vía principal.

Y la ejecución dejó de ser cosa mía. Un cron nocturno, /api/cron/normativa-sync, corre la ETL cada noche. Las consultas nuevas llegan solas, se actualizan solas, y el scraper queda donde debe estar: como plan B para una emergencia puntual, no como el camino de cada día.

La decisión que más me costó, y de la que más orgulloso estoy, es una línea que parece un detalle de implementación y no lo es. syncDgtConsultas nunca escribe titulo_seo ni resumen. Esas dos columnas no son suyas. Las gestiona el tooling de SEO, aparte, con su propio ciclo y su propia lógica.

Ahí está la frontera que antes no existía. La ETL ingiere el dato crudo y punto. El SEO decide cómo se presenta. Antes, un solo script intentaba ser las dos cosas a la vez y no era ninguna bien.

Qué aprendí

Un scraper no es un pipeline: es un atajo que se paga en mantenimiento. En cuanto la fuente ofrece una API, el scraper debe bajar a plan de emergencia, nunca quedarse como plan A.

Y la lección que me llevo para cualquier proyecto: una función debe saber cuál es su trabajo y negarse a hacer el de los demás. syncDgtConsultas ingiere; no hace SEO. Esa línea clara es lo que convirtió un arreglo frágil en algo que corre de noche, solo, sin que yo mire.

Si quieres ver las consultas de la DGT ya cruzadas con el IAE:

https://www.conversoriaecnae.es/consultas-dgt

Brian Mena

Brian Mena

Ingeniero informatico construyendo productos digitales rentables: SaaS, directorios y agentes de IA. Todo desde cero, todo en produccion.

LinkedIn