PixelRAG + Scrapeless: O que quebra quando a montagem de capturas de tela se torna remota
Lead Scraping Automation Engineer
TL;DR:
- Os índices visuais RAG indexam páginas como azulejos de captura de tela em vez de texto analisado, então a geometria do azulejo não é cosmética — ela decide o que o modelo de incorporação realmente vê.
- O renderizador do PixelRAG define uma área de captura de 875 por 8192 com
Emulation.setDeviceMetricsOverride, e depois recorta cada captura de tela para 875 de largura. - Contra um navegador em nuvem que substitui, relata sucesso e nunca aplica a largura solicitada. Ao longo das sessões, a largura do layout resultante variou de 945 a 5120 — às vezes inalterada, geralmente mais larga, nunca 875.
- O recorte é honrado exatamente, o que torna isso perigoso: você obtém um azulejo com o tamanho correto contendo a fatia esquerda de uma página disposta de forma muito mais ampla.
- Em uma captura real, um azulejo de 875 de largura de um layout de 1920 de largura continha uma coluna de produto onde a página dispunha quatro, com o cartão cortado através de seu botão.
- O caminho rápido
rawFilePathé aceito sem erro contra um navegador remoto, retorna dados em linha em vez disso e não grava nenhum arquivo local — então um pipeline que lê esse arquivo não obtém nada enquanto cada chamada parece ser bem-sucedida. - Compare a largura que você pediu com
innerWidthapós a substituição antes de confiar em um único azulejo. - Comece no plano gratuito do Scrapeless e meça sua própria geometria de captura.
O Scrapeless Scraping Browser é um navegador em nuvem que você opera através de um WebSocket CDP, e cada medição abaixo foi feita contra ele com as mesmas chamadas CDP que um renderizador local usaria.
A geração aumentada de recuperação visual ignora a análise HTML. Em vez de converter uma página em texto e incorporar os pedaços, ela renderiza a página em imagens, incorpora as imagens e deixa um modelo de visão ler a resposta dos pixels. Tabelas, gráficos e layouts de várias colunas sobrevivem, porque nada foi achatado em um fluxo de texto.
Isso só funciona se a imagem estiver certa. Um renderizador de azulejos faz uma promessa específica — a página é disposta em uma largura fixa, depois cortada em azulejos de altura fixa — e um índice visual é tão bom quanto essa promessa. O PixelRAG é o exemplo atual mais claro, e seu renderizador vale a pena ser lido de perto porque a suposição que faz é uma que todo pipeline de captura de tela faz. Este post mede o que acontece com essa suposição em um navegador que você não lançou.
Como o Tiling Deve Funcionar
O renderizador rápido do PixelRAG fala CDP bruto através de um WebSocket em vez de passar por uma biblioteca de driver. Ele define duas constantes no topo de render/src/pixelrag_render/backends/fast_cdp.py:
python
VIEWPORT_WIDTH = 875
TILE_HEIGHT = 8192
Então, uma vez por trabalhador, aplica isso como a área de captura:
python
await conn.cdp("Emulation.setDeviceMetricsOverride", {
"width": VIEWPORT_WIDTH,
"height": tile_height,
"deviceScaleFactor": 1,
"mobile": False,
})
E para cada azulejo captura um retângulo recortado na mesma largura, descendo pela página:
python
await conn.cdp("Page.captureScreenshot", {
"clip": {"x": 0, "y": t * tile_height, "width": VIEWPORT_WIDTH,
"height": clip_h, "scale": 1},
})
A lógica é sólida e padrão. Emulation.setDeviceMetricsOverride é a chamada CDP que a especificação da viewport CSS conceitos mapeiam, e é o que as bibliotecas de driver chamam por trás quando você define uma viewport. Distribua a página em 875, corte-a em tiras de 8192 de altura, incorpore as tiras. Em um navegador que você lançou, é exatamente isso que acontece.
Pré-requisitos
- Python 3.10 ou posterior e o pacote
websockets. - Uma chave API do Scrapeless do painel, exportada como
SCRAPELESS_KEY. - Nenhum navegador local. Cada chamada abaixo vai para o navegador em nuvem através de uma conexão WebSocket.
bash
pip install "websockets==15.0.1"
export SCRAPELESS_KEY="your_api_key_here"
Meça a Substituição em vez de Confiar Nela
A substituição retorna um objeto de resultado sem erro, que é todo o problema — nada na resposta sugere que o pedido não foi honrado. Leia a largura do layout de volta da página em vez disso.
python
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
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
Execute novamente e a coluna after contém números diferentes. Essa instabilidade é o resultado em vez de ruído ao seu redor, então a linha que vale a pena fixar é a última: a largura solicitada nunca é a largura aplicada.
Três coisas valem a pena separar aqui. A chamada não falha. A largura que ela atinge difere de sessão para sessão — ao longo de execuções separadas, o valor variou de 945, 1240, 1280, 1400, 1440, 1600, 1680, 1920, 2560 e 5120. E em nenhuma dessas sessões foi a 875 que foi pedida.
Observe a execução 3 acima: às vezes a largura não se move de forma alguma, e 945 antes se torna 945 depois. Portanto, a substituição não é confiavelmente uma operação nula e também não é confiavelmente uma mudança. A única propriedade que se manteve em todas as sessões é a útil — o número que você solicita não é o número que você obtém.
A leitura prática é que o navegador remoto possui sua própria janela, e a substituição realinha em relação a essa janela em vez dos seus números. Localmente, você possui o processo, então a mesma chamada se comporta.
O que o Tile Realmente Contém
Agora capture um tile da maneira que um renderizador de azulejos faz, com um clipe explícito na largura que você achava que tinha definido.
python
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
layout width: 1920
tile size: 875x1200 (requested 875x1200)
A largura e a altura impressas aqui são lidas diretamente do cabeçalho da imagem em vez de serem supostas, uma vez que a especificação PNG coloca ambos no primeiro bloco do arquivo. O tile tem exatamente o tamanho solicitado. A página atrás dele é disposta consideravelmente mais larga, e a largura em que ele cai muda a cada sessão, portanto a fração da página que você captura também não é fixa.
O tile.png salvo desta execução mostra quanto isso custa. Em uma largura de layout de 1920, o tile contém uma coluna de produto onde a página dispõe quatro, o cartão visível é cortado pelo seu botão "Adicionar ao carrinho", e o texto do cabeçalho sai da borda direita. Pouco mais da metade da largura da página está fora da imagem.
Um extrator de texto não se importaria, pois lê o DOM e o DOM está completo. Um índice visual se importa completamente: o tile é a entrada, e aproximadamente metade da página nunca chega ao modelo de incorporação. A recuperação degrada sem um único erro em qualquer lugar no pipeline, que é exatamente por que comparar a largura solicitada com innerWidth vale a pena fazer uma vez por ambiente.
Quer verificar sua própria geometria de captura em relação a um navegador na nuvem? Crie uma conta gratuita no Scrapeless e execute os dois blocos acima.
O Caminho de Captura Bruta Desaparece
O truque de throughput do PixelRAG é pular a codificação PNG no navegador. Ele pede ao Chrome para despejar pixels brutos diretamente na memória compartilhada com um parâmetro rawFilePath, e depois os comprime em um pool de processos separado. O repositório envia um arquivo de patch do Chromium especificamente para isso, e o trabalhador de compressão lê o arquivo de volta com open(raw_path, "rb").
Contra um navegador remoto, esse parâmetro é aceito e ignorado silenciosamente.
python
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
error: None
inline data returned: 245576
local file exists: False
Sem erro, dados de imagem reais na resposta e nada no disco. O caminho é a pista: /dev/shm refere-se à máquina que executa o navegador, e essa não é a máquina que executa seu código. Um trabalhador que abre o arquivo esperado levanta FileNotFoundError para cada tile, e se essas falhas forem contadas em vez de levantadas, a execução reporta tiles gravados enquanto o diretório de saída permanece vazio.
Esta é a descoberta mais aguda das duas, porque não é uma regressão de qualidade. É um pipeline que não produz nada e diz que funcionou.
O Que Fazer em Vez
Nenhuma descoberta argumenta contra a renderização remota. Elas argumentam contra levar suposições locais para isso.
Leia a geometria de volta em vez de configurá-la. innerWidth após a substituição é o número que suas matemáticas de azulejos devem usar, não o número que você passou. Se seus tiles devem ter uma largura fixa para um modelo que foi treinado nessa largura, escale ou preencha a imagem capturada depois do fato em vez de depender do navegador para disposicionar sua largura.
Leve a imagem codificada sobre o caminho bruto. Perder rawFilePath custa tempo de codificação PNG ou JPEG e retorna os bytes ao seu processo, que é o único lugar onde eles são úteis quando o navegador é remoto.
Trate a geometria do tile como algo a ser afirmado. Uma verificação comparando a largura solicitada com a largura medida, executada uma vez quando o ambiente muda, captura ambas essas situações antes que um índice seja construído sobre elas. Nenhuma delas aparece como um erro, e ambas mudam o que o modelo vê.
Conclusão
A tiling de captura de tela assume que o renderizador respeita a viewport que você definiu, e em um navegador que você lançou essa suposição é segura. Em um navegador na nuvem, a substituição é bem-sucedida e a largura do layout cai em algum lugar diferente — medida aqui como seis valores diferentes em seis sessões, nenhum deles a largura solicitada de 875 — enquanto o clipe da captura de tela é respeitado até o pixel. O resultado é um tile de tamanho correto da região errada.
O caminho de captura bruta falha de forma mais limpa e severa: aceito, ignorado, nenhum arquivo, nenhum erro. Verifique a contagem de tiles e a largura medida antes de confiar em um índice visual, e leia a geometria de volta em vez de apenas supô-la.
Informações mais detalhadas sobre o protocolo estão em O que é o Protocolo do Chrome DevTools, a conexão em si é abordada no guia do Playwright e do navegador de scraping, detalhes do plano estão na página de preços do Scrapeless, e os parâmetros da sessão estão na documentação do Scrapeless.
FAQ
Q: Isso significa que o PixelRAG não pode usar um navegador na nuvem?
De forma nenhuma — significa que seu renderizador assume um local hoje. A lógica de mosaico, o modelo de incorporação e o índice não são afetados; o que precisa ser alterado é a etapa de captura, que deve ler a largura do layout de volta e usar a resposta de imagem codificada em vez do caminho de arquivo bruto.
Q: Por que a substituição muda a largura se ela não aplica meu valor?
Porque desencadeia uma nova disposição, apenas contra a própria janela do navegador em vez das métricas solicitadas. A largura antes da chamada era 945 em cada sessão medida e algo diferente depois, então a chamada tem um efeito — é o destino que não é seu para escolher.
Q: O parâmetro clip é confiável?
Sim, e é isso que torna a discrepância fácil de perder. Cada captura retornou exatamente nas dimensões de pixel solicitadas. O tamanho de saída correto não é evidência de que a página subjacente foi disposta nesse tamanho.
Q: Uma biblioteca de driver como o Playwright evitaria isso?
Não, porque o driver emite o mesmo comando CDP por baixo. Uma medição anterior neste endpoint descobriu que o definidor de viewport do Playwright deixava as dimensões relatadas completamente inalteradas, então o sintoma difere ligeiramente por cliente, enquanto a causa é a mesma. A especificação WebDriver BiDi é o esforço de rastreamento de padrões para dar controle remoto do navegador um contrato definido, que é onde comportamentos como este acabariam sendo esclarecidos.
Q: Como posso verificar rapidamente minha própria configuração?
Defina o viewport, em seguida, avalie innerWidth e compare com o que você solicitou. Uma linha, uma viagem de ida e volta, e isso diz imediatamente se sua matemática de mosaico está operando em um número real ou em um número esperançoso.
Q: Um corte de largura fixa realmente prejudica a recuperação?
Isso muda a distribuição de entrada. Um modelo de incorporação ajustado em capturas de tela de páginas dispostas em uma largura dada recebe cortes de layouts mais largos, com conteúdo faltando à direita. O mosaico ainda embute e ainda retorna vizinhos, então nada aparece como quebrado — os resultados simplesmente ficam piores em páginas cujo conteúdo útil fica fora do corte.
Q: Qual deve ser o sessionTTL para uma execução de mosaico?
Longo o suficiente para cobrir toda a página, uma vez que o mosaico é sequencial e uma página longa precisa de muitas capturas em uma sessão. Dimensione-o contra sua página mais lenta esperada em vez de uma média, e feche o alvo quando a execução terminar.
Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.


