Volver al blog

PixelRAG + Scrapeless: ¿Qué se rompe cuando el mosaico de capturas de pantalla se vuelve remoto?

Daniel Kim
Daniel Kim

Lead Scraping Automation Engineer

07-Aug-2026

TL;DR:

  • Visual RAG indexa páginas como mosaicos de captura de pantalla en lugar de texto analizado, por lo que la geometría del mosaico no es cosmética: decide lo que el modelo de incrustación ve realmente.
  • El renderizador de PixelRAG establece un área de captura de 875 por 8192 con Emulation.setDeviceMetricsOverride, y luego recorta cada captura de pantalla a 875 de ancho.
  • En un navegador en la nube que anula, los informes de éxito no aplican nunca el ancho solicitado. A través de sesiones, el ancho del diseño resultante ha oscilado entre 945 y 5120 — a veces sin cambios, generalmente más ancho, nunca 875.
  • El recorte se respeta exactamente, lo que hace que esto sea peligroso: obtienes un mosaico del tamaño correcto que contiene la porción izquierda de una página dispuesta mucho más ancha.
  • En una captura real, un mosaico de 875 de ancho de un diseño de 1920 de ancho tenía una columna de producto cuando la página dispone cuatro, con la tarjeta cortada a través de su botón.
  • La ruta rápida rawFilePath se acepta sin error contra un navegador remoto, devuelve datos en línea en su lugar, y no escribe ningún archivo local, por lo que un pipeline que lee ese archivo no obtiene nada mientras que cada llamada parece exitosa.
  • Compara el ancho que pediste contra innerWidth después de la anulación antes de confiar en un solo mosaico.
  • Comienza en el plan gratuito de Scrapeless y mide tu propia geometría de captura.

El Navegador de Scraping Scrapeless es un navegador en la nube que controlas a través de un WebSocket CDP, y cada medición a continuación se tomó contra él con las mismas llamadas CDP que usaría un renderizador local.

La generación aumentada por recuperación visual omite el análisis de HTML. En lugar de convertir una página a texto e incrustar los fragmentos, renderiza la página en imágenes, incrusta las imágenes, y deja que un modelo de visión lea la respuesta de los píxeles. Tablas, gráficos y diseños de múltiples columnas sobreviven, porque nada se ha aplanado en un flujo de texto.

Eso solo funciona si la imagen es correcta. Un renderizador de mosaicos hace una promesa específica: la página está dispuesta a un ancho fijo, luego se corta en mosaicos de altura fija, y un índice visual solo es tan bueno como esa promesa. PixelRAG es el ejemplo más claro actual, y su renderizador merece ser leído de cerca porque la suposición que hace es una que cada pipeline de captura de pantalla hace. Esta publicación mide lo que sucede con esa suposición en un navegador que no lanzaste.

Cómo Se Supone Que Debe Funcionar El Mosaico

El renderizador rápido de PixelRAG habla CDP en bruto a través de un WebSocket en lugar de pasar por una biblioteca de controladores. Establece dos constantes en la parte superior de render/src/pixelrag_render/backends/fast_cdp.py:

python Copy
VIEWPORT_WIDTH = 875
TILE_HEIGHT = 8192

Luego, una vez por trabajador, aplica eso como el área de captura:

python Copy
await conn.cdp("Emulation.setDeviceMetricsOverride", {
    "width": VIEWPORT_WIDTH,
    "height": tile_height,
    "deviceScaleFactor": 1,
    "mobile": False,
})

Y para cada mosaico captura un rectángulo recortado al mismo ancho, bajando por la página:

python Copy
await conn.cdp("Page.captureScreenshot", {
    "clip": {"x": 0, "y": t * tile_height, "width": VIEWPORT_WIDTH,
             "height": clip_h, "scale": 1},
})

La lógica es sólida y estándar. Emulation.setDeviceMetricsOverride es la llamada CDP a la que la especificación del viewport de CSS se mapea, y es lo que llaman las bibliotecas de controladores por debajo cuando estableces un viewport. Disponer la página a 875, cortarla en tiras de 8192 de alto, incrustar las tiras. En un navegador que lanzaste tú mismo, eso es exactamente lo que sucede.

Requisitos Previos

  • Python 3.10 o posterior y el paquete websockets.
  • Una clave API de Scrapeless del panel de control, exportada como SCRAPELESS_KEY.
  • Sin navegador local. Cada llamada a continuación va al navegador en la nube a través de una conexión WebSocket.
bash Copy
pip install "websockets==15.0.1"
export SCRAPELESS_KEY="your_api_key_here"

Mide la Anulación en lugar de Confiar en Ella

La anulación devuelve un objeto de resultado sin error, que es todo el problema: nada sobre la respuesta sugiere que la solicitud no fue respetada. Lee el ancho del diseño directamente de la página en su lugar.

python Copy
import asyncio, json, os

import websockets

CDP = ("wss://browser.scrapeless.com/api/v2/browser"
       f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
WIDTH, HEIGHT = 875, 8192


async def call(ws, state, method, params=None, session=None):
    state["id"] += 1
    msg = {"id": state["id"], "method": method}
    if params:
        msg["params"] = params
    if session:
        msg["sessionId"] = session
    await ws.send(json.dumps(msg))
    while True:
        reply = json.loads(await ws.recv())
        if reply.get("id") == state["id"]:
            return reply


async def one_session(run):
    async with websockets.connect(CDP, max_size=50 * 1024 * 1024) as ws:
        state = {"id": 0}
        target = await call(ws, state, "Target.createTarget", {"url": "about:blank"})
        tid = target["result"]["targetId"]
        attached = await call(ws, state, "Target.attachToTarget",
                              {"targetId": tid, "flatten": True})
        sid = attached["result"]["sessionId"]
        await call(ws, state, "Page.enable", session=sid)
        await call(ws, state, "Page.navigate",
                   {"url": "https://books.toscrape.com/"}, session=sid)
        await asyncio.sleep(2.5)

        async def inner_width():
            r = await call(ws, state, "Runtime.evaluate",
                           {"expression": "innerWidth", "returnByValue": True},
                           session=sid)
            return r["result"]["result"]["value"]

        before = await inner_width()
        result = await call(ws, state, "Emulation.setDeviceMetricsOverride",
                            {"width": WIDTH, "height": HEIGHT,
                             "deviceScaleFactor": 1, "mobile": False}, session=sid)
        await asyncio.sleep(1)
        after = await inner_width()
        print(f"run{run}: error={result.get('error')} before={before} "
              f"after={after} requested={WIDTH} applied={after == WIDTH}")
        await call(ws, state, "Target.closeTarget", {"targetId": tid})
        return after


async def main():
    widths = [await one_session(run) for run in range(1, 4)]
    print(f"distinct widths: {sorted(set(widths))} · requested {WIDTH} ever applied: "
          f"{WIDTH in widths}")


asyncio.run(main())
text Copy
run1: error=None before=945 after=1280 requested=875 applied=False
run2: error=None before=945 after=1600 requested=875 applied=False
run3: error=None before=945 after=945 requested=875 applied=False
distinct widths: [945, 1280, 1600] · requested 875 ever applied: False

Ejecuta de nuevo y la columna after tiene números diferentes. Esa inestabilidad es el resultado en lugar de ruido a su alrededor, así que la línea que vale la pena señalar es la última: el ancho solicitado nunca es el ancho aplicado.

Tres cosas vale la pena separar aquí. La llamada no falla. El ancho en el que se encuentra difiere de sesión a sesión — en ejecuciones separadas, el valor ha sido de 945, 1240, 1280, 1400, 1440, 1600, 1680, 1920, 2560 y 5120. Y en ninguna de esas sesiones fue el 875 que se pidió.

Nota la ejecución 3 anterior: a veces el ancho no se mueve en absoluto, y 945 antes se convierte en 945 después. Así que la anulación no es confiablemente una no-op y tampoco es un cambio de manera confiable. La única propiedad que se ha mantenido en cada sesión es la útil: el número que solicitas no es el número que obtienes.
La lectura práctica es que el navegador remoto posee su propia ventana, y el anulación vuelve a asignar el diseño en función de esa ventana en lugar de tus números. Localmente, tú posees el proceso, por lo que la misma llamada se comporta.

Lo que Realmente Contiene el Azulejo

Ahora captura un azulejo de la manera en que lo hace un renderizador de azulejos, con un recorte explícito en el ancho que creías haber establecido.

python Copy
import asyncio, base64, json, os, pathlib, struct

import websockets

CDP = ("wss://browser.scrapeless.com/api/v2/browser"
       f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
WIDTH = 875


async def call(ws, state, method, params=None, session=None):
    state["id"] += 1
    msg = {"id": state["id"], "method": method}
    if params:
        msg["params"] = params
    if session:
        msg["sessionId"] = session
    await ws.send(json.dumps(msg))
    while True:
        reply = json.loads(await ws.recv())
        if reply.get("id") == state["id"]:
            return reply


async def main():
    async with websockets.connect(CDP, max_size=100 * 1024 * 1024) as ws:
        state = {"id": 0}
        tid = (await call(ws, state, "Target.createTarget",
                          {"url": "about:blank"}))["result"]["targetId"]
        sid = (await call(ws, state, "Target.attachToTarget",
                          {"targetId": tid, "flatten": True}))["result"]["sessionId"]
        await call(ws, state, "Page.enable", session=sid)
        await call(ws, state, "Page.navigate",
                   {"url": "https://books.toscrape.com/"}, session=sid)
        await asyncio.sleep(2.5)
        await call(ws, state, "Emulation.setDeviceMetricsOverride",
                   {"width": WIDTH, "height": 8192,
                    "deviceScaleFactor": 1, "mobile": False}, session=sid)
        await asyncio.sleep(1)

        layout = await call(ws, state, "Runtime.evaluate",
                            {"expression": "innerWidth", "returnByValue": True},
                            session=sid)
        shot = await call(ws, state, "Page.captureScreenshot",
                          {"format": "png", "captureBeyondViewport": True,
                           "clip": {"x": 0, "y": 0, "width": WIDTH,
                                    "height": 1200, "scale": 1}}, session=sid)
        image = base64.b64decode(shot["result"]["data"])
        png_w, png_h = struct.unpack(">II", image[16:24])
        print(f"layout width: {layout['result']['result']['value']}")
        print(f"tile size:    {png_w}x{png_h} (requested {WIDTH}x1200)")
        pathlib.Path("tile.png").write_bytes(image)
        await call(ws, state, "Target.closeTarget", {"targetId": tid})


asyncio.run(main())
text Copy
layout width: 1920
tile size:    875x1200 (requested 875x1200)

El ancho y la altura impresos aquí se leen directamente del encabezado de la imagen en lugar de asumirse, ya que la especificación PNG los coloca ambos en el primer fragmento del archivo. El azulejo es exactamente del tamaño solicitado. La página detrás de él está diseñada considerablemente más ancha, y el ancho en el que aterriza cambia por sesión, por lo que la fracción de la página que capturas tampoco está fija.

El tile.png guardado de esta ejecución muestra lo que eso cuesta. A un ancho de diseño de 1920, el azulejo sostiene una columna de producto donde la página diseña cuatro, la tarjeta visible está cortada a través de su botón "Agregar al carrito", y el texto del encabezado se sale del borde derecho. Justo más de la mitad del ancho de la página está fuera de la imagen.

Un raspador de texto no le importaría, porque lee el DOM y el DOM está completo. Un índice visual se preocupa completamente: el azulejo es la entrada, y aproximadamente la mitad de la página nunca alcanza el modelo de incrustación. La recuperación se degrada sin un solo error en ninguna parte de la tubería, por lo que comparar el ancho solicitado contra innerWidth vale la pena hacerlo una vez por entorno.

¿Quieres comprobar tu propia geometría de captura contra un navegador en la nube? Crea una cuenta gratuita de Scrapeless y ejecuta los dos bloques anteriores.

El Camino de Captura en Crudo Desaparece

El truco de rendimiento de PixelRAG es omitir la codificación PNG en el navegador. Pide a Chrome que vacíe los píxeles en bruto directamente a la memoria compartida con un parámetro rawFilePath, luego los comprime en un grupo de procesos separado. El repositorio envía un archivo de parche de Chromium para exactamente esto, y el trabajador de compresión lee el archivo nuevamente con open(raw_path, "rb").

Contra un navegador remoto, ese parámetro es aceptado y silenciosamente ignorado.

python Copy
import asyncio, json, os, pathlib

import websockets

CDP = ("wss://browser.scrapeless.com/api/v2/browser"
       f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
RAW = pathlib.Path("/dev/shm/tile_probe.raw")


async def call(ws, state, method, params=None, session=None):
    state["id"] += 1
    msg = {"id": state["id"], "method": method}
    if params:
        msg["params"] = params
    if session:
        msg["sessionId"] = session
    await ws.send(json.dumps(msg))
    while True:
        reply = json.loads(await ws.recv())
        if reply.get("id") == state["id"]:
            return reply


async def main():
    RAW.unlink(missing_ok=True)
    async with websockets.connect(CDP, max_size=100 * 1024 * 1024) as ws:
        state = {"id": 0}
        tid = (await call(ws, state, "Target.createTarget",
                          {"url": "about:blank"}))["result"]["targetId"]
        sid = (await call(ws, state, "Target.attachToTarget",
                          {"targetId": tid, "flatten": True}))["result"]["sessionId"]
        await call(ws, state, "Page.enable", session=sid)
        await call(ws, state, "Page.navigate",
                   {"url": "https://books.toscrape.com/"}, session=sid)
        await asyncio.sleep(2.5)

        shot = await call(ws, state, "Page.captureScreenshot",
                          {"fromSurface": True, "optimizeForSpeed": True,
                           "rawFilePath": str(RAW),
                           "clip": {"x": 0, "y": 0, "width": 875,
                                    "height": 1000, "scale": 1}}, session=sid)
        print("error:", shot.get("error"))
        print("inline data returned:", len(shot["result"].get("data", "")))
        print("local file exists:", RAW.exists())
        await call(ws, state, "Target.closeTarget", {"targetId": tid})


asyncio.run(main())
text Copy
error: None
inline data returned: 245576
local file exists: False

Sin error, datos de imagen reales en la respuesta, y nada en el disco. El camino es la pista: /dev/shm se refiere a la máquina que ejecuta el navegador, y esa no es la máquina que ejecuta tu código. Un trabajador que abre el archivo esperado genera FileNotFoundError para cada azulejo, y si esas fallas se cuentan en lugar de elevarse, la ejecución informa azulejos escritos mientras el directorio de salida permanece vacío.

Este es el hallazgo más agudo de los dos, porque no es una regresión de calidad. Es una tubería que no produce nada y dice que funcionó.

Qué Hacer en Cambio

Ninguno de los hallazgos argumenta en contra del renderizado remoto. Argumentan en contra de llevar suposiciones locales a ello.

Lee la geometría en lugar de establecerla. innerWidth después de la anulación es el número que tus matemáticas de azulejos deberían usar, no el número que pasaste. Si tus azulejos deben tener un ancho fijo para un modelo que fue entrenado a ese ancho, escala o rellena la imagen capturada después del hecho en lugar de depender de que el navegador se diseñe con tu número.

Toma la imagen codificada sobre el camino en crudo. Perder rawFilePath cuesta tiempo de codificación PNG o JPEG y devuelve los bytes a tu proceso, que es el único lugar donde son útiles cuando el navegador es remoto.

Trata la geometría del azulejo como algo que afirmar. Una verificación que compara el ancho solicitado con el ancho medido, ejecutada una vez cuando cambia el entorno, captura ambos de estos antes de que se construya un índice sobre ellos. Ninguno aparece como un error, y ambos cambian lo que ve el modelo.

Conclusión

El azuleo de captura de pantalla asume que el renderizador honra la ventana gráfica que estableciste, y en un navegador que lanzaste, esa suposición es segura. En un navegador en la nube, la anulación tiene éxito y el ancho del diseño aterriza en otro lugar—medido aquí como seis valores diferentes en seis sesiones, ninguno de ellos el 875 solicitado—mientras que el recorte de la captura de pantalla se honra hasta el píxel. El resultado es un azulejo del tamaño correcto de la región equivocada.

El camino de captura en crudo falla de manera más limpia y más severa: aceptado, ignorado, sin archivo, sin error. Verifica la cantidad de azulejos y el ancho medido antes de confiar en un índice visual, y lee la geometría en lugar de asumirla.
La información más profunda sobre el protocolo se encuentra en ¿Qué es el Protocolo de Chrome DevTools?, la conexión en sí se cubre en la guía de Playwright y Scraping Browser, los detalles del plan están en la página de precios de Scrapeless, y los parámetros de sesión están en la documentación de Scrapeless.

FAQ

Q: ¿Significa esto que PixelRAG no puede usar un navegador en la nube?

Para nada — significa que su renderizador asume uno local hoy en día. La lógica de mosaico, el modelo de incrustación y el índice no se ven afectados; lo que necesita cambiar es el paso de captura, que debería leer el ancho del diseño y usar la respuesta de imagen codificada en lugar de la ruta de archivo sin procesar.

Q: ¿Por qué el anulado cambia el ancho en absoluto si no aplica mi valor?

Porque desencadena un nuevo diseño, solo que contra la ventana del navegador en lugar de las métricas que solicitaste. El ancho antes de la llamada era 945 en cada sesión medida y algo diferente después, así que la llamada tiene un efecto — es el destino que no es tuyo para elegir.

Q: ¿Es confiable el parámetro de clip?

Sí, y eso es lo que hace que la discrepancia sea fácil de pasar por alto. Cada captura regresó exactamente a las dimensiones de píxeles solicitadas. El tamaño de salida correcto no es evidencia de que la página de fondo estaba diseñada a ese tamaño.

Q: ¿Evitaría esto una biblioteca de controladores como Playwright?

No, porque el controlador emite el mismo comando CDP por debajo. Una medición anterior en este punto final encontró que el ajustador de viewport de Playwright dejaba las dimensiones reportadas completamente sin cambios, así que el síntoma difiere ligeramente por cliente mientras que la causa es la misma. La especificación WebDriver BiDi es el esfuerzo de estándares para dar un contrato definido al control remoto del navegador, que es donde el comportamiento como este eventualmente se definiría.

Q: ¿Cómo puedo verificar rápidamente mi propia configuración?

Establece el viewport, luego evalúa innerWidth y compáralo con lo que solicitaste. Una línea, un viaje de ida y vuelta, y te dice inmediatamente si tus cálculos de mosaico están operando en un número real o en uno esperanzador.

Q: ¿Un recorte de ancho fijo realmente perjudica la recuperación?

Cambia la distribución de entrada. Un modelo de incrustación ajustado en capturas de pantalla de páginas dispuestas a un ancho determinado recibe recortes de diseños más anchos en su lugar, con contenido faltante a la derecha. El mosaico aún se incrusta y aún devuelve vecinos, por lo que nada emerge como roto — los resultados simplemente empeoran en páginas cuyo contenido útil se encuentra fuera del recorte.

Q: ¿Cuál debería ser sessionTTL para una ejecución de mosaico?

Lo suficientemente largo como para cubrir toda la página, ya que el mosaico es secuencial y una página alta necesita muchas capturas en una sesión. Dimensiona en función de tu página más lenta esperada en lugar de una promedio, y cierra el objetivo cuando la ejecución termine.

En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.

Artículos más populares

Catalogar