Engines
Dois jeitos de ler uma página, e um que escolhe por você. O que você paga acompanha o que a página realmente exigiu.
fetch
Uma requisição HTTP simples pelo proxy. Resolve sozinha uma página renderizada no servidor, uma resposta de API, um feed RSS ou um site de documentação. Ou seja, boa parte da web, ainda hoje.
Envie um formulário ou um JSON
{
"url": "https://example.com/search",
"method": "POST",
"headers": { "content-type": "application/x-www-form-urlencoded" },
"body": "q=paving"
}Algumas páginas de busca só respondem a um POST. Coloque method como "POST", ponha em body o que o formulário envia e informe o tipo em headers: o de formulário, como no exemplo, ou application/json para JSON. Como o Chrome, ele envia o cabeçalho Origin com o endereço do próprio site. Se o formulário do site fica em outra página, ponha essa página num cabeçalho referer, e o Origin acompanha. Volta a página de resultados, lida como qualquer outra.
Um POST sai uma vez só. Mandar de novo poderia repetir o que ele fez, então ele nunca é repetido por outro endereço nem tentado de outro jeito, e sempre usa a engine fetch. O body pode ter até 64 KB. Se o site redirecionar, um 301, 302 ou 303 abre a página seguinte com GET e sem body. Um 307 ou 308 reenvia o body só se o protocolo, o host e a porta continuarem os mesmos. Para qualquer outro endereço, mesmo de http para https ou de www para o domínio sem www, a chamada termina em UPSTREAM_BLOCKED e informa o novo endereço. Se confiar nele, mande o POST para lá você mesmo.
Baixe o arquivo original
import { writeFileSync } from "node:fs";
import { Scrapa } from "@scrapa/sdk";
const scrapa = new Scrapa(process.env.SCRAPA_API_KEY!);
const { raw } = await scrapa.scrape({ url: "https://example.com/report.pdf", formats: ["raw"] });
if (raw) writeFileSync("report.pdf", Buffer.from(raw.base64, "base64"));Peça raw e você recebe o arquivo exatamente como o site mandou: raw.base64 traz os bytes e raw.contentType diz o que eles são. Mais uma linha salva o arquivo no disco.
Arquivos de até 5 MB voltam inteiros. Um maior responde FILE_TOO_LARGE sem cobrança, nunca pela metade. Custa o mesmo que um fetch: 1 crédito, mais o tráfego a partir do segundo megabyte. O formato pdf é outra coisa: é o navegador imprimindo uma página.
Leia um PDF ou um arquivo do Word
import { Scrapa } from "@scrapa/sdk";
const scrapa = new Scrapa(process.env.SCRAPA_API_KEY!);
const page = await scrapa.scrape({ url: "https://example.com/report.pdf" });
console.log(page.markdown); // "# Annual report\n\n## Results\n\nRevenue grew in every region."
console.log(page.metadata.pages); // 12Aponte url para um PDF ou um arquivo do Word (.docx) e ele volta como texto, igual a uma página. Texto grande vira título e o espaço entre as linhas separa os parágrafos, então markdown, text, links e extract funcionam com ele. metadata.pages diz quantas páginas o PDF tem.
A gente lê arquivos de até 25 MB e, de um PDF, as primeiras 200 páginas e os primeiros 5 milhões de caracteres. Num PDF mais longo, metadata.truncated vem true. Custa o preço da engine que fez a leitura: 1 crédito no fetch, 5 no navegador, mais o tráfego a partir do segundo megabyte. A gente lê cada arquivo por no máximo 15 segundos, e para antes se o timeoutMs acabar. Aí a chamada responde DOCUMENT_TIMEOUT sem cobrança. Um arquivo do Word que passa de 32 MB depois de descompactado responde FILE_TOO_LARGE, e um link de arquivo que devolve uma página web curta no lugar do arquivo (muitas vezes uma verificação de que você é humano) responde UPSTREAM_BLOCKED. Nenhum dos dois é cobrado. O link quebrado é a exceção: a página 404 ou 410 é a resposta do site, então volta com esse status e é cobrada como qualquer página. Um PDF escaneado guarda fotos do texto, não o texto, então volta com pouco ou nada.
browser
Um Chrome de verdade (o build do próprio Google, não um shell headless) abre a página, roda os scripts e espera a rede se acalmar. É o que uma SPA, uma lista carregada sob demanda ou um aviso de cookies exigem, e é a única engine capaz de gerar screenshot ou PDF.
{
"url": "https://app.example.com/dashboard",
"engine": "browser",
"formats": ["markdown", "screenshot"]
}auto, o padrão
Primeiro o fetch. Se o corpo vier como uma casca de JavaScript (sem texto legível, com um ponto de montagem, com um aviso pedindo scripts), a mesma URL é relida no navegador e você paga a linha do navegador. Se o fetch já tinha respondido direito, você pagou um fetch.
Você nunca paga as duas engines. Uma página que respondeu no primeiro degrau é cobrada uma vez; uma escalada que não responde devolve os créditos e mantém a página do fetch, com metadata.needsJs explicando por que ela veio magra.
Screenshots e PDFs
Os dois voltam em base64 dentro da resposta, e os dois forçam a engine browser independentemente do que engine disser. Pedir screenshot e PDF juntos não custa mais do que pedir um: é o mesmo carregamento de página.
Dizendo ao navegador o que esperar
{
"url": "https://app.example.com/prices",
"engine": "browser",
"browser": {
"waitForSelector": ".price",
"viewport": { "width": 390, "height": 844 }
}
}Uma SPA desenha o conteúdo depois que a página carrega. browser.waitForSelector espera o elemento que você realmente precisa, o que é melhor do que chutar um tempo. Se ele nunca aparecer, você recebe NAV_TIMEOUT sem cobrança, não a página pela metade. browser.waitMs é o chute, para os scripts que nenhum seletor descreve. Os dois saem de dentro do timeout, não por cima dele.
browser.viewport define o tamanho da janela em que a página é renderizada. Ajuste quando o site entrega um layout diferente por largura. browser.fullPage tira o screenshot da página inteira, e não só da janela.
Clicar e digitar antes da leitura
{
"url": "https://news.example.com/article/42",
"browser": {
"actions": [
{ "type": "click", "selector": "#accept-cookies", "all": true },
{ "type": "scrape" }
]
}
}Algumas páginas escondem o que você quer atrás de um aviso de cookies, de um botão ou de uma caixa de busca. browser.actions é uma lista de passos que um Chrome de verdade roda um depois do outro, na ordem em que você listar, antes de ler a página. Um passo pode esperar, clicar, digitar, apertar uma tecla, rolar, tirar um screenshot, guardar o HTML ou rodar um script seu. Aqui o primeiro passo aceita os cookies e o segundo guarda o HTML da página, que volta em actions.scrapes. all: true clica em tudo o que combinar e não dá erro quando não há nada, porque muitos sites só mostram o aviso para parte dos visitantes. Os formatos que você pedir leem a página do jeito que o último passo deixou.
Passos sempre usam a engine browser, pelo preço do navegador: 5 créditos, sem nada a mais por passo. Você pode mandar até 50 passos, e todas as esperas somadas precisam caber no timeoutMs. Se um passo não funcionar, como um clique num seletor que não encontra nada, a chamada responde ACTION_FAILED, diz qual passo e por quê, e não custa nada. A exceção é um script. Depois que um começa a rodar, a chamada é cobrada mesmo que esse script dê erro, um passo seguinte falhe, a página trave ou dê timeout, o site bloqueie a página, o script leve a um endereço que a gente recusa, o seu próprio proxy não abra a conexão ou um seletor seu de includeTags ou excludeTags fique lento demais na página que ele montou, porque o script pode já ter lido a página. Se você cancelar uma chamada com script depois que ela já foi para o navegador, o preço do navegador é cobrado mesmo assim, porque a gente não tem como saber se o script já rodou. Se a falha for nossa (um 5xx), os créditos voltam para você.
Quando o site diz não
{ "url": "https://shop.example.com/item/42", "retry": 2 }Página bloqueada não custa nada, a não ser que um script seu tenha rodado nela antes (veja acima), então as duas formas de responder a uma são de graça para tentar. retry pede de novo por outro endereço, até duas vezes. A saída troca a cada tentativa, que é o que passa por um bloqueio no endereço e não em você.
{ "url": "https://shop.example.com/item/42", "engine": "fetch", "escalate": false }escalate: false faz o contrário: uma tentativa e o veredito do site. Use quando a rodada precisa sair barata e previsível, porque em auto é a subida que cobra a linha do navegador em vez da do fetch. Ele exige que você nomeie a engine, já que auto é a própria subida.
Quanto tempo a página tem
{ "url": "https://slow.example.com", "timeoutMs": 45000 }O tempo padrão é folgado para uma renderização lenta e limitado para que nada fique pendurado. Use timeoutMs por chamada quando você souber melhor. Uma página que nunca chega responde NAV_TIMEOUT, que não custa nada, a não ser que um script seu de browser.actions tenha rodado na página antes.
Leia a mesma página de novo, de graça
{ "url": "https://shop.example.com/item/42", "maxAge": 600000 }Com maxAge, uma página que a sua conta leu há pouco pode voltar da nossa cópia, sem ir ao site. O valor é a idade máxima da cópia que você aceita, em milissegundos, até 3600000 (uma hora); o exemplo aceita uma cópia de até 10 minutos. A cópia custa 0 créditos, e a resposta traz cached: true e, em cachedAt, quando a página foi lida. Se não houver uma cópia recente o bastante, a gente lê a página agora, cobra o preço normal e guarda essa cópia por até uma hora. Uma cópia pode ser apagada antes, para liberar espaço ou num deploy, e aí a próxima chamada lê a página de novo pelo preço normal.
A cópia é só sua: nenhuma outra conta recebe. Ela precisa bater com a URL, o país e a engine. Um lote guarda as próprias cópias, separadas das chamadas avulsas. O auto aceita os dois tipos de cópia, o fetch só a de uma requisição simples e o browser só a renderizada. O extract continua custando 5 créditos, porque o modelo lê a cópia de novo. Um POST, ou uma chamada com headers, com o seu próprio proxy, com opções de browser, com screenshot, PDF, raw ou escalate: false, sempre lê a página na hora. Página bloqueada nunca é guardada, então a próxima chamada pergunta ao site de novo, e página que ainda precisa de JavaScript (metadata.needsJs) também não.