Un rastreador SEO tiene un problema curioso: no tiene forma de saber si lo que extrae es correcto. Puede tener miles de tests, y todos verdes, porque los tests comparan su salida contra lo que su autor esperaba. Si el autor entendió mal algo, el test consagra el error.
La única salida es contrastar contra otra implementación. Lo hicimos, y encontramos un fallo que llevaba meses ahí.
Rastrear no sirve para comparar
Lo primero que se le ocurre a cualquiera es rastrear el mismo sitio con las dos herramientas y ver si dan lo mismo. No funciona.
Cada rastreador decide por dónde empieza, qué enlaces sigue, cuándo se detiene, cómo normaliza las URLs y qué hace con los parámetros. Dos rastreos del mismo sitio dan conjuntos distintos de URLs, y lo que acabas comparando son dos recorridos, no dos extracciones. Casi todas las diferencias que veas serán de alcance.
La forma correcta es el modo lista: un fichero con URLs y la instrucción de rastrear exactamente esas. Casi todas las herramientas serias lo tienen, y trae una ventaja añadida, que no depende de límites de licencia por número de URLs porque tú eliges cuántas pones.
El montaje
Sacamos 300 URLs de un rastreo anterior. Todas internas, todas con estado 200, todas HTML:
sqlite3 crawl.sqlite "SELECT url FROM urls
WHERE is_internal = 1 AND status_code = 200
AND content_type LIKE 'text/html%'
ORDER BY url LIMIT 300;" > lista.txt
El ORDER BY url no es cosmético: sin él, dos ejecuciones te dan conjuntos distintos y pierdes la
capacidad de repetir la prueba.
Después, las dos herramientas contra ese mismo fichero. La nuestra:
crawlforge list lista.txt --concurrency 4 --out cf.sqlite
Y la otra en modo headless, que suele existir aunque esté poco documentado. Dos cosas que nos
costaron tiempo: exige rutas absolutas, y el CSV que exporta lleva las cabeceras en el idioma de la
interfaz. Con la aplicación en español, las columnas se llaman Dirección y Título 1.
Comparar campo por campo
Seis campos: código de estado, título, meta description, H1, canonical e indexabilidad. Mil ochocientas comparaciones.
Normalizar los espacios antes de comparar es imprescindible. Una herramienta puede colapsar espacios consecutivos y otra no, y eso llena el resultado de diferencias que no lo son:
def norm(s):
return re.sub(r"\s+", " ", (s or "").strip())
La primera pasada nos dio 290 diferencias en el canonical, lo cual era demasiado bonito para ser
verdad. Y no lo era: estábamos leyendo una columna que no existía en ese CSV, así que comparábamos
todos los canonicals contra cadena vacía. La columna se llamaba Elemento de enlace canónico 1.
Corregido eso, el resultado real:
OK estado 0 de 300
OK título 0 de 300
OK descripción 0 de 300
≠ h1 1 de 300
OK canonical 0 de 300
OK indexabilidad 0 de 300
La diferencia
Una, en el H1 de la portada:
<h1>Agencia Especializada en WordPress<br />con +25 años de experiencia</h1>
Nosotros devolvíamos Agencia Especializada en WordPresscon +25 años de experiencia.
El <br /> parte el contenido del elemento en dos nodos de texto. Nuestro parser los acumulaba y
los pegaba sin nada en medio, así que las palabras de los extremos se juntaban. La otra herramienta
lo hacía bien.
El mismo fallo afectaba a los textos de enlace: <a>Leer<em>más</em>aquí</a> salía como
Leermásaquí.
Por qué 810 tests no lo vieron
Los fixtures que habíamos escrito para encabezados eran así:
<h1>Un título normal</h1>
<h2>Otro título</h2>
HTML de libro de texto. Ningún elemento dentro, ningún <br>, ningún <span> de estilo. Y como el
test comparaba nuestra salida contra lo que nosotros esperábamos, y esperábamos lo que el código
producía, todo cuadraba.
El arreglo fue insertar un separador al cerrar cada nodo de texto; el espacio sobrante lo limpia la normalización que ya se hacía después. Cuatro líneas.
Al escribir el test del caso cometimos el mismo error otra vez. La primera versión era:
<a href="/x">Leer <em>más</em></a>
Con el espacio delante del <em>. Ese test pasa con el fallo puesto, porque el espacio ya está en el
texto, así que no probaba nada. Quitándolo, falla como debe.
Lo que se puede afirmar después
Repetida la prueba tras el arreglo: cero diferencias en 1.800 comparaciones.
Eso no significa que nuestra extracción sea correcta. Significa que coincide con la de otra implementación madura, que es una cosa distinta y bastante más útil de lo que parece: si vas a cambiar de herramienta, lo primero que necesitas saber es que los datos no se van a mover debajo de tus informes.
Conviene decir también lo que la prueba no cubre. Son 300 URLs de un solo sitio, todas con estado 200 y todas HTML. No dice nada sobre redirecciones, errores de servidor, contenido que no sea HTML ni sitios que montan el contenido con JavaScript. Y coincidir en seis campos no garantiza coincidir en los demás.
Hazlo con la tuya
Si mantienes cualquier herramienta que extrae datos de páginas web, esta prueba cuesta veinte minutos y encuentra cosas. La receta entera:
- Saca una lista de 200-300 URLs reales, ordenada, de un sitio con HTML variado.
- Pásala por las dos herramientas en modo lista.
- Normaliza espacios antes de comparar.
- Comprueba el nombre exacto de cada columna antes de fiarte de un cero.
- Cuando encuentres una diferencia, ve al HTML original y decide quién tiene razón.
Ese último paso importa: no siempre la tienes tú, y tampoco siempre la tiene la otra. Nosotros estábamos equivocados esta vez, y saberlo valió mucho más que el rato que costó averiguarlo.