Scrapling, auditado: 92.848 estrellas, 1,9 incidencias por cada 1.000 estrellas y un emparejador adaptativo que falla con un 47,08 % de confianza

Scrapling tiene 92.848 estrellas y solo 179 incidencias. Su benchmark falla hoy con selectolax 1.0 y el emparejador adaptativo devolvió 12 elementos erróneos.

Scrapling es un framework de scraping en Python que, en el momento de escribir esto, tiene 92.848 estrellas en GitHub, 54 versiones publicadas y un README que empieza con “The BEST Web Scraping framework”. La versión 0.5 llegó el 17 de octubre de 2026 y, según sus propias palabras, “upgrades almost everything: fetching, parsing, spiders, browser automation, AI tools, and docs in ten languages”, acompañada de una advertencia: “this release introduces a lot of breaking changes that don’t fit here”.

Hay tres afirmaciones de esa versión que merecen medirse en vez de repetirse: que el parser es mucho más rápido que sus rivales, que el seguimiento adaptativo de elementos sobrevive a un rediseño del sitio y que el fetcher de navegador es sigiloso. Las tres se pueden comprobar en una máquina Linux de 2 vCPU en menos de una hora, así que lo hice.

El resultado es mixto de una forma interesante. La librería es real, se usa a escala y hace varias cosas bien. Dos de sus cifras de cabecera no sobreviven a la reproducción, y la función adaptativa —lo único que ofrece el README y que ningún competidor tiene— se comporta de una manera que corrompe un conjunto de datos en silencio mucho antes de lanzar cualquier error.

Qué es Scrapling en la práctica

Un solo paquete que sustituye la pila habitual de requests + BeautifulSoup/parsel + playwright, con cuatro capas que se instalan por separado:

CapaInstalaciónQué aporta
Parserpip install scraplingSelector: CSS/XPath/texto/regex sobre lxml, con emparejador adaptativo
Fetcher HTTPpip install "scrapling[fetchers]"Fetcher / FetcherSession sobre curl_cffi (suplantación de TLS)
Fetcher de navegadorlo anterior + scrapling installStealthyFetcher / StealthySession sobre patchright
Crawlerlo anteriorSpiders tipo Scrapy, ProxyRotator, AutoThrottle, pausa y reanudación

El pip install scrapling a secas entrega solo el parser: from scrapling.fetchers import Fetcher lanza ModuleNotFoundError. Está documentado y es correcto, y es relevante para la sección de benchmarks, porque las tablas publicadas son cifras de parser comparadas contra pilas de scraping completas.

Forma del código, medido sobre un clon superficial de main:

  • 61 archivos Python, 16.108 líneas dentro de scrapling/
  • 99 archivos de test, 25.529 líneas dentro de tests/: 1,58 líneas de test por línea de código
  • Python al 99,7 % de los bytes, licencia BSD-3-Clause, Development Status :: 4 - Beta
  • 41 contribuidores, de los cuales D4Vinci (Karim Shoair) firma 1.683 de 1.780 commits (94,6 %); 37 de los 41 tienen 8 commits o menos

Una proporción de tests superior a 1 no es habitual en un proyecto personal, y además el repositorio incluye AI_POLICY.md, que exige declarar el uso de IA en las contribuciones y se reserva la etiqueta AI-Slop para el código generado sin declarar. La gobernanza aquí es más madura de lo que sugiere el contador de incidencias.

Las estrellas y las incidencias no se mueven juntas

Las estrellas de GitHub son un botón de marcador, y este proyecto ilustra muy bien hasta dónde pueden separarse del resto de señales. Todas las cifras se leyeron de la API de GitHub el 3 de noviembre de 2026:

RepositorioEstrellasWatchersWatchers/estrellasIncidencias totalesIncidencias por 1.000 estrellas
D4Vinci/Scrapling92.8483270,35 %1791,9
scrapy/scrapy65.1871.7432,67 %3.32050,9
microsoft/playwright-python15.0861561,01 %1.560103,4
lexiforest/curl_cffi6.813420,62 %57183,8
apify/crawlee-python9.638440,46 %63465,8

Scrapling tiene 1,4 veces las estrellas de Scrapy y una vigesimoséptima parte de su tasa de incidencias. Solo hay cuatro abiertas, y la proporción de forks sobre estrellas es del 10,5 % (9.778 forks), alta incluso para una librería que la gente incorpora a su propio código.

La lectura obvia —que las estrellas son falsas— no es lo que dicen los demás datos. Las descargas de PyPI cuentan otra historia:

MétricaScraplingscrapy
Descargas, 1 de noviembre de 202645.75558.247
Descargas, octubre de 2026 (mes natural)2.259.645~4,3 M (cifra de 30 días)
Estrellas92.84865.187

El uso real es aproximadamente una quinta parte del de Scrapy, mientras que las estrellas son 1,4 veces las de Scrapy. Eso da una tasa de estrellas por descarga unas 6,7 veces mayor, coherente con un proyecto cuyo público llega por canales sociales y no por evaluación técnica. El propio repositorio empuja una invitación a Discord y una cuenta de X en su descripción; no hay pestaña de Discussions.

La señal más clara está en Hacker News: el envío con más puntos de Scrapling en el índice de Algolia es del 13 de octubre de 2024, con 4 puntos y 1 comentario. Una herramienta para desarrolladores con 92.000 estrellas que nunca ha tenido una discusión en portada no está siendo descubierta por el público que abre incidencias. Nada de esto es una falta: significa que “92.000 estrellas” no dice casi nada sobre cuánta revisión ha recibido el código, y por eso existen las dos secciones siguientes.

Afirmación 1: la tabla de benchmarks, repetida

El README publica esta tabla bajo el título “Scrapling isn’t just powerful - it’s also blazing fast”:

#LibreríaTiempo (ms)vs Scrapling
1Scrapling1,991,0x
2Parsel/Scrapy2,061,035
3Raw Lxml2,561,286
4PyQuery23,98~12x
5Selectolax197,02~99x
6MechanicalSoup1545,15~776,5x
7BS4 con Lxml1562,1~785,0x
8BS4 con html5lib3412,73~1714,9x

El enlace de metodología apunta a benchmarks.py en el repositorio, así que todo es reproducible. No lo es:

$ python benchmarks.py
File ".../selectolax/parser.py", line 13, in <module>
    raise ImportError(DEPRECATION_MESSAGE)
ImportError: Modest backend is deprecated since selectolax 1.0. The engine has not
been updated since 2021, while lexbor has been available since 2021 and is actively
developed. Modest is outdated, not maintained, contains bugs and does not follow
modern HTML5 standards. Please use lexbor backend instead

El script importa from selectolax.parser import HTMLParser, el backend Modest, que ya no existe en selectolax actual. Es decir: la fila publicada “Selectolax: 197,02 ms, ~99x más lento” se midió contra un motor HTML que sus propios mantenedores describen como obsoleto, sin mantenimiento, con errores y anterior a HTML5. Esa cifra no se puede regenerar hoy sin editar el archivo del benchmark.

Cambié exactamente una línea —from selectolax.lexbor import LexborHTMLParser as HTMLParser— y bajé las repeticiones de 100 a 10 para que cupiera en un descanso (las proporciones se mantienen; el argumento no depende de eso):

LibreríaTabla del repo (ms)Repetición, Lexbor, n=10 (ms)vs Scrapling (repetición)
Scrapling1,992,541,0x
Parsel/Scrapy2,062,500,98x
Raw Lxml2,563,101,22x
PyQuery23,9827,0210,6x
Selectolax197,02155,6261,3x
BS4 con Lxml1562,11365,24537x
MechanicalSoup1545,151502,74592x
BS4 con html5lib3412,733431,271351x

Cambiaron dos cosas. Parsel queda marginalmente por delante de Scrapling en lugar de un 3,5 % por detrás, lo cual era esperable porque el Selector de Scrapling se construye sobre el mismo árbol lxml y el mismo traductor de cssselect (“This project includes code adapted from: Parsel”). Y los multiplicadores de cabecera se comprimen un tercio, aunque no lo suficiente para explicarse solos.

Lo que los explica es el fixture. large_html son 5.000 elementos <div class="item"> anidados, así que hay 5.000 nodos coincidentes y el más interno contiene 5.000 niveles de descendientes. Una extracción de texto que recorre todo el subárbol en cada coincidencia hace trabajo O(n²) sobre ese documento; una que lee solo el texto propio del nodo hace trabajo O(n). El benchmark compara esas dos operaciones distintas y presenta la diferencia como velocidad del parser:

  • test_scrapling usa .css(".item::text"): solo texto directo
  • test_parsel también usa ::text: solo texto directo
  • test_lxml usa e.text: solo texto directo
  • test_selectolax usa node.text(): todo el subárbol
  • test_bs4_* usa e.text: todo el subárbol
  • test_pyquery usa e.text(): todo el subárbol

Para ver cuánto de la brecha publicada es semántica y no de implementación, medí las mismas librerías sobre ambos fixtures y ambos modos de extracción:

Librería / modoFixture anidado (el del repo)5.000 hermanos planos
selectolax, text(deep=False)29,23 ms3,58 ms
selectolax, text() (profundo)155,32 ms3,62 ms
Raw lxml, e.text2,86 ms8,14 ms
Scrapling, .css(".item::text")2,43 ms13,29 ms
parsel, .css(".item::text")2,44 ms13,90 ms
BS4, texto hijo directo51,76 ms67,84 ms
BS4, e.text (profundo)1338,44 ms73,37 ms

Compárese la primera fila con la tabla del repositorio: el 99x se convierte en 5,3x cambiando un argumento dentro de la misma librería sobre la misma página, y en una lista plana de 5.000 elementos —la forma de una página de resultados real— selectolax es 3,7 veces más rápido que Scrapling, no 99 veces más lento. El 785x de BS4 se reduce a 26x con la misma sustitución.

Esto no es un ataque a la librería: el parser de Scrapling es genuinamente rápido, está a un 2 % de parsel y supera a lxml puro en el fixture anidado. Es un ataque a la tabla, que usa un documento sintético patológico y una semántica de extracción inconsistente para producir multiplicadores que harían que cualquier lector juzgara mal la disyuntiva.

La segunda tabla tiene el problema inverso. La documentación reporta AutoScraper 12,58 ms, 5,47x más lento; el mismo script en la misma máquina dio 3,88 ms para Scrapling y 220,08 ms para AutoScraper: 56,7x, diez veces la proporción publicada. Ambas mediciones llaman a build() sobre la misma página, así que la diferencia es deriva de versiones de la dependencia, no de la dirección de la afirmación.

Afirmación 2: el seguimiento adaptativo, medido

La función adaptativa es la razón por la que la gente elige Scrapling en lugar de parsel. Conviene leer el mecanismo en scrapling/parser.py antes de fiarse de la frase de marketing “Smart Element Tracking: Relocate elements after website changes using intelligent similarity algorithms”:

  1. Con auto_save=True serializa el elemento coincidente en un diccionario: tag, attributes limpios, element.text (el texto propio del nodo, es decir lo que precede a su primer hijo), path (una tupla de nombres de etiquetas ancestras, sin índices), parent_name, parent_attribs, parent_text y siblings (nombres de etiquetas de los hermanos, también sin posiciones).
  2. Con adaptive=True, cuando el selector no devuelve nada, relocate() recorre todos los elementos de la página, puntúa cada candidato contra ese diccionario y devuelve los elementos empatados en la puntuación máxima, siempre que supere percentage, que por defecto es 40.
  3. La puntuación es la media de varios ratios de difflib.SequenceMatcher más una comprobación binaria, multiplicada por 100: igualdad de etiqueta, similitud de texto, similitud del diccionario de atributos (claves y valores), similitud separada de class/id/href/src, similitud de path, similitud del padre y de los hermanos. Dividir entre el número de comprobaciones significa que un candidato que acierta en las señales baratas puede pasar de 40 sin acertar en las que identifican el elemento.

Como path y siblings no contienen posiciones, y como element.text está vacío en cualquier tarjeta cuyo título viva en un nodo hijo, las tarjetas de una lista idéntica son casi indistinguibles para esta función.

Construí una lista de 12 tarjetas de producto y ejecuté cuatro escenarios. El script completo está en la sección de reproducción:

EscenarioLo que el selector encontró el día 1Puntuación máximaQué devolvió el modo adaptativo
Clases renombradas (.product .ptitle → .item-x .ttl)12 tarjetas61,9 %1 elemento (Widget-01)
Lo mismo, más reordenación y un título cambiado1261,9 %1 elemento
Atributo desplazado a un hijo (<h3 class="ptitle">)12—1 elemento
Títulos eliminados de la página1247,08 %12 elementos equivocados (<span class="price">$9.99</span>)
Títulos eliminados, percentage=801247,08 %0 elementos y un aviso

Tres hallazgos, ordenados por lo que le costarán:

La coincidencia correcta puntúa 61,9 % y la incorrecta 47,08 %. El margen entre “encontró el elemento correcto” y “encontró la etiqueta de precio en lugar del título” es de 14,8 puntos porcentuales, y el umbral por defecto de 40 está 7 puntos por debajo de la respuesta equivocada. Si el selector pierde su objetivo, la librería no devuelve vacío: devuelve basura con seguridad.

Nada avisa con la verbosidad por defecto. La única señal es log.debug("Highest probability was 61.9%"), invisible salvo que se configure el registro en DEBUG. La rama de fallo sí avisa correctamente, y merece la pena ver cómo es ese aviso, porque demuestra que la librería sabe que está adivinando:

WARNING: Adaptive relocation found no element above the 80% threshold
(top score: 47.08%). Lower `percentage` if this is the right element.

El recuento cambia en silencio. Un selector que devolvía 12 filas devuelve 1 tras la reubicación. Esto sí está documentado, pero solo en “Known Issues”, al final de la página de adaptativo: “In the adaptive save process, only the unique properties of the first element in the selection results are saved. So if the selector you are using selects different elements on the page in other locations, adaptive will return the first element to you only when you relocate it later.” La página de la función, encima de esa nota, y el README hablan de rastrear elementos en plural.

También hay una curva de coste. Como relocate() puntúa cada nodo, el precio es lineal con el tamaño de la página pero con una constante grande: cada candidato cuesta una docena de llamadas a SequenceMatcher:

TarjetasNodos del DOMLlamada adaptativa (relocate + lectura del almacén)
501538,1 ms
5001.50390,7 ms
2.0006.003553,9 ms

A 0,09 ms por nodo, una página de listado de 40.000 nodos cuesta unos 3,7 segundos por llamada adaptativa, y el adaptativo es justo la ruta a la que se recurre cuando un sitio ha cambiado y se está rastreando cada página. Usa percentage de forma deliberada, limita el adaptativo a objetivos de un solo elemento (un precio, un indicador de stock) y verifica cardinalidad y un hash del contenido después de cada reubicación.

Dónde vive la base de datos

Selector(..., adaptive=True) escribe en Path(scrapling/parser.py).parent / "elements_storage.db", es decir dentro de site-packages. En este entorno virtual la ruta concreta fue:

/opt/data/scripts2/recon/scrapling_venv/lib/python3.13/site-packages/scrapling/elements_storage.db

Si ese directorio es de solo lectura, que es lo que ocurre en una imagen de contenedor endurecida, el guardado falla con un segundo error engañoso encima del primero:

OperationalError: attempt to write a readonly database
Exception ignored in: <function SQLiteStorageSystem.__del__>
AttributeError: 'SQLiteStorageSystem' object has no attribute 'cursor'

El AttributeError viene de __del__ llamando a close() sobre una conexión que nunca se abrió. Quien depure esa traza mirará en el sitio equivocado. Pasa storage_args={"storage_file": ...} y pon el archivo en un volumen escribible.

La clave del almacén es el dominio base, resuelto con tld, o la cadena literal "default" cuando no se pasó url=: analizar cadenas HTML de varios sitios sin url= los mete a todos en el mismo espacio de nombres, donde el mismo identificador de selector sobrescribe el registro anterior. La documentación lo dice sin rodeos —“The save process overwrites previous data, and the adaptive feature uses only the latest saved properties”— pero basta olvidar un argumento para contaminar dos scrapers entre sí.

Afirmación 3: obtención y sigilo

La capa de obtención es menos polémica y en general cumple. Seis páginas, misma máquina, en secuencia, reutilizando el proceso del navegador. Es la misma disyuntiva que medí ayer con un navegador headless que prescinde de Chromium: la vía HTTP es barata, la de renderizado no, y solo una de las dos ve el contenido.

MétodoTiempo totalRSS máximoPágina con JS (quotes.toscrape.com/js/)
Fetcher (curl_cffi, HTTP)1,90 s57,5 MB200, 5.808 B, 0 citas
StealthyFetcher (patchright)—55,0 MB200, 8.985 B, 10 citas, 1.428 ms
Playwright Chromium2,42 s45,3 MB*200, 8.940 B, 10 citas, 372 ms

* La cifra de Playwright cuenta solo el proceso Python; el navegador es un proceso hijo, así que la memoria no es comparable entre filas. El tiempo sí.

La lección útil está en la tercera columna: el fetcher HTTP devuelve HTTP 200 con el <title> correcto y cero contenido, porque la página renderiza sus citas en JavaScript. Un pipeline que comprueba response.status == 200 registrará esa página como rastreada con éxito y escribirá una fila vacía. Es la misma clase de fallo que el emparejador adaptativo: sin excepción, sin aviso, datos equivocados.

Para la afirmación de “indetectable” serví una página local que informa la superficie clásica de huellas y la pedí por las dos vías de navegador:

SeñalPlaywright ChromiumStealthyFetcher
navigator.webdriver"true""false"
navigator.userAgentHeadlessChrome/156.0.8078.4Chrome/156.0.0.0
navigator.plugins.length05
navigator.languagesen-USen-US,en
Notification.permissiondeniedgranted
window.chrome.runtimeausenteausente
hardwareConcurrency22
Zona horariaUTCUTC

Patchright corrige las tres señales más baratas y deja dos. Un navegador que dice ser Chrome 156 con cinco plugins y permiso de notificaciones concedido pero sin window.chrome.runtime es incoherente, y 2 núcleos en UTC es un servidor. Que eso importe depende por completo del detector del objetivo, y por eso la frase del README “can easily bypass all types of Cloudflare’s Turnstile/Interstitial” necesita tus números, no los míos. Probé nowsecure.nl, la página de prueba habitual de Cloudflare, y obtuve 200 con ~180 KB de los tres métodos desde esta IP de centro de datos: esa prueba no distinguió nada y no tengo evidencia en ningún sentido. Mide tu propia tasa de éxito por objetivo antes de montar un negocio encima.

Un valor por defecto que conviene conocer: robots_txt_obey es False. En la 0.5, activarlo también cambió el comportamiento: un fallo al obtener robots.txt bloquea ahora todo el dominio durante el rastreo, donde antes permitía el acceso.

Actualizar a la 0.5 no es un cambio de número

La versión 0.5 elimina por completo el backend Playwright simple. DynamicFetcher, DynamicSession, AsyncDynamicSession y fetchers.chrome.PlayWrightFetcher desaparecen; todo lo que usa navegador es Patchright ahora. Si tu código importa tipos de playwright.sync_api en un callback, debe importar patchright.sync_api.

Los cambios incompatibles con más probabilidad de alterar datos sin lanzar error:

CambioQué hace en silencio
response.follow() ya no hereda la peticiónUn POST seguido se convierte en GET simple; se descartan params, data, json, files, multipart
capture_xhr eliminadoUsa StealthySession(record_requests=True) + session.network.search(url_pattern=...)
None en datos de formularioQueda como la cadena literal "None" en lugar de descartarse
Salida MarkdownSe escribe en UTF-8 sin importar el charset de la página, con líneas en blanco entre elementos: rompe hashes guardados
Selectores generadosAhora incluyen :nth-of-type(1) cuando hace falta
Huella de peticiónLos parámetros de query se funden en la clave y los cuerpos JSON vacíos se conservan: los checkpoints y cachés antiguos ya no coinciden
Evaluación JSPatchright ejecuta en contexto aislado por defecto; page.evaluate no ve variables globales de la página sin isolated_context=False
Historial de red100 coincidencias por búsqueda, 1.000 peticiones retenidas, 1 MiB por cuerpo, 20 MiB en total

El consejo de actualización del propio proyecto es el correcto y sorprendentemente directo: “Run a small crawl before your full job. Check the extracted data, followed URLs, sent request bodies, saved file encoding, and any cache or checkpoint behaviour you depend on.”

Qué haría yo con esto

  • Usa el parser. Selector es rápido, la API es un superconjunto de la de parsel y poder sustituir parsel dentro de un pipeline de Scrapy es la función que ningún competidor tiene. Fija la versión.
  • No uses el adaptativo como mecanismo de corrección. Úsalo como alerta: ejecuta el selector normal y, cuando devuelva cero, ejecuta la ruta adaptativa y falla el trabajo si cambiaron la puntuación, el recuento o un hash del contenido. Trata una coincidencia con 47 % de confianza como un informe de error, no como una fila.
  • Pasa siempre url= y storage_args. Si no, todos los sitios comparten el espacio de nombres default y la base de datos vive dentro de site-packages.
  • Fija percentage explícitamente: 70 o más para cualquier cosa que alimente una base de datos, y registra tú la puntuación en INFO, porque la librería la registra en DEBUG.
  • Separa la vía HTTP de la del navegador. Fetcher para HTML estático (1,9 s para seis páginas, 57 MB), StealthyFetcher para lo que se renderiza, y verifica sobre el contenido extraído, nunca sobre status == 200. Para un crawler puramente HTTP construido sobre la misma idea, el enfoque de MediaCrawler se lee bien en paralelo.
  • Prueba la actualización antes de fiarte. De 0.4.15 a 0.5 hay una reescritura de la capa de navegador con media docena de cambios silenciosos en la salida; un diff de un rastreo de 50 páginas cuesta diez minutos y los detecta todos.

Reprodúcelo en una hora

python3.13 -m venv venv && ./venv/bin/pip install "scrapling[fetchers]"
export PLAYWRIGHT_BROWSERS_PATH=/ruta/con/permiso/de/escritura
./venv/bin/python -m patchright install chromium   # solo para StealthyFetcher

El escenario adaptativo, reducido a lo esencial: las cuatro filas de la tabla anterior solo cambian en el HTML que recibe el tercer constructor.

from scrapling import Selector

def page(names, cls="product", tclass="ptitle"):
    return "<html><body>" + "".join(
        f'<div class="{cls}"><span class="{tclass}">{n}</span>'
        f'<span class="price">$9.99</span></div>' for n in names
    ) + "</body></html>"

names = [f"Widget-{i:02d}" for i in range(1, 13)]
url = "https://shop.example.com/list"

# día 1: guardar
saved = Selector(page(names), url=url, adaptive=True).css(".product .ptitle", auto_save=True)
print(len(saved))                                  # 12

# día 2: clases renombradas -> adaptativo
moved = Selector(page(names, "item-x", "ttl"), url=url, adaptive=True)
print(len(moved.css(".product .ptitle", adaptive=True)))   # 1, no 12

# día 3: títulos eliminados -> el adaptativo inventa una respuesta
gone = ("<html><body>" + "".join(
    '<div class="item-x"><span class="x">n/a</span><span class="price">$9.99</span></div>'
    for _ in names) + "</body></html>")
hit = Selector(gone, url=url, adaptive=True).css(".product .ptitle", adaptive=True)
print(len(hit), hit[0].html_content)   # 12 <span class="price">$9.99</span>

Pon logging.getLogger().setLevel(logging.DEBUG) sobre scrapling.core.utils.log para ver la puntuación (Highest probability was 47.08%). La reproducción del benchmark es benchmarks.py del repositorio con el import de selectolax cambiado a selectolax.lexbor.LexborHTMLParser y repeat=10.

Preguntas frecuentes

¿Son falsas las 92.848 estrellas de Scrapling? No encontré evidencia de ello, y dos datos apuntan en contra: 2.259.645 descargas en PyPI durante octubre de 2026 y 310 repositorios que lo declaran como dependencia. Lo que sí muestran las cifras es que las estrellas y la revisión técnica se han desacoplado: 327 watchers, 179 incidencias en total y un máximo de 4 puntos en Hacker News. Trata el contador de estrellas como alcance, no como revisión de código.

¿Es realmente más rápido que BeautifulSoup? Sí, por mucho, pero no 785 veces. Sobre el propio fixture anidado del repositorio, la diferencia honesta entre extracción de texto directo en Scrapling y extracción profunda en BS4 es de unas 26 veces (2,43 ms frente a 51,76 ms); el 785x sale de comparar operaciones distintas y de un backend obsoleto de selectolax. Contra parsel hay empate (2,50 frente a 2,54 ms de mediana en mi repetición).

¿Funciona el scraping adaptativo? Funciona en el caso para el que se diseñó: un elemento y un umbral que tú fijas. Encontró la tarjeta correcta tras renombrar las clases, con 61,9 %. También devolvió 12 elementos equivocados con 47,08 % cuando el objetivo desapareció, sin aviso con el registro por defecto, y colapsa una coincidencia de 12 filas a 1. Verifica recuentos y contenido, no solo si está vacío.

¿Necesito un navegador para usarlo? No. pip install "scrapling[fetchers]" sin scrapling install te da el fetcher HTTP, que resolvió seis páginas en 1,9 segundos en total. El navegador solo hace falta para StealthyFetcher, y cada arranque cuesta alrededor de un segundo extra.

¿Está listo para producción? Los metadatos del paquete dicen Development Status :: 4 - Beta, el changelog describe la 0.5 como llena de cambios incompatibles y la capa de almacenamiento escribe dentro de site-packages por defecto. El parser es de nivel producción; el crawler y la capa adaptativa son beta en el sentido literal. Fija versiones, controla tu ruta de almacenamiento y compara tu salida después de cada actualización.

¿Puede saltarse Cloudflare? El README afirma que “can easily bypass all types of Cloudflare’s Turnstile/Interstitial”. No pude verificarlo: la página de prueba estándar devolvió 200 también a un Playwright sin modificar desde esta IP, así que la prueba no demostró nada. La tabla de huellas de arriba muestra tres señales corregidas y dos que quedan (window.chrome.runtime, 2 núcleos en UTC). Mide sobre tus propios objetivos.

La forma del hallazgo

Una librería puede ser buena y su marketing puede estar equivocado a la vez, y la distancia entre ambos es donde está el trabajo real. El parser de Scrapling está a un 2 % de parsel; su fetcher HTTP es cuatro veces más rápido que un navegador en páginas estáticas; su suite de tests es mayor que su código. Nada de eso necesita un gráfico de 92.848 estrellas ni un benchmark de 785x.

Las dos cifras que deberían cambiar tu implementación son las que nadie publicó: 47,08 % de similitud bastan para que el emparejador adaptativo devuelva el elemento equivocado doce veces, y 61,9 % es lo que puntúa una coincidencia correcta: 14,8 puntos de separación entre un scraper que funciona y una corrupción silenciosa de datos. Fija tú el umbral y verifica lo que volvió.