Resumo: Um fluxo de trabalho de download de ficheiros com o Puppeteer pode seguir quatro abordagens eficazes: clicar num botão e deixar o Chrome gravar numa pasta que controla, executar fetch() dentro da página e redirecione o base64 de volta para o Node, utilize o Protocolo Chrome DevTools com eventos de progresso de download, ou ignore o navegador e recupere o URL com o Axios usando cookies recolhidos da sessão do Puppeteer. Escolha com base no tamanho do ficheiro, na autenticação e na forma como o site expõe o link.Introdução
Se já tentou criar um script para um fluxo de download de ficheiros com o Puppeteer num site de produção real, já conhece o momento da verdade: o script clica no botão de download, a instância do Chrome sem interface gráfica reporta sucesso e o disco permanece vazio. Isso acontece porque o Chromium bloqueia downloads automatizados por predefinição no modo sem interface gráfica, e a solução não está na API de alto nível do Puppeteer. Encontra-se uma camada abaixo, no Protocolo Chrome DevTools.
Este guia destina-se a programadores Node.js de nível intermédio, engenheiros de controlo de qualidade e profissionais de scraping que já sabem como iniciar um navegador, navegar numa página e selecionar um elemento, e que agora precisam de capturar os bytes reais. Vamos percorrer quatro métodos delimitados, cada um com código completo, e seremos honestos sobre qual deles se adequa a cada situação.
Verá a mesma estrutura de base reutilizada em todo o lado: uma pasta de downloads criada com fs.mkdirSync, um User-Agent realista, uma janela de visualização de ambiente de trabalho e um padrão para aguardar até que o ficheiro esteja efetivamente no disco e não ainda a ser gravado. No final, terá uma receita de ficheiro de download do Puppeteer para downloads acionados por clique, downloads com autenticação, cargas binárias de grande dimensão e URLs conhecidos, além de uma tabela de decisão para escolher entre eles e uma lista de verificação de reforço de segurança para produção.
Por que é que descarregar ficheiros com o Puppeteer é mais complicado do que parece
Quando clicas page.click() um botão «Descarregar CSV» no Chrome, o ficheiro vai parar à sua pasta de Descarregamentos e você segue com o seu dia. Execute o mesmo script com headless: 'new' e nada acontece. O clique é acionado, o pedido de rede é enviado e o seu sistema de ficheiros permanece vazio. Isso não é um bug do Puppeteer. O Chromium trata intencionalmente os downloads automatizados como suspeitos, e a correção reside no Protocolo do Chrome DevTools, em vez de na API de superfície do Puppeteer. Até ativar essa opção, nenhum fluxo de download de ficheiros do Puppeteer irá deixar um único byte no disco.
Não existe uma única maneira ideal de lidar com isto. A abordagem correta depende de como o site expõe o ficheiro, do rigor da sua autenticação, do tamanho da carga útil e do nível de fiabilidade de que necessita. Quatro padrões cobrem quase todos os casos:
- Clique mais
setDownloadBehavior. Configure o diretório de downloads do navegador através do CDP, clique no botão e verifique se a transferência foi concluída. Ideal quando o download é acionado por JavaScript e não tem, ou não quer procurar, o URL subjacente. - In-page
fetch()mais base64. Executefetch()dentropage.evaluate(), codifique a resposta e envie-a de volta para o Node como base64. Ideal para SPAs, URLs de blobs e downloads controlados por cookies que só existem no contexto do navegador. - CDP puro com eventos de download. Abra uma sessão CDP, chame
Browser.setDownloadBehaviore escuteBrowser.downloadWillBegineBrowser.downloadProgress. Ideal quando precisa de progresso em tempo real, mapeamento de GUID para nome de ficheiro ou deteção de erros detalhada. - Passe a URL para o Axios ou
https. Use o Puppeteer para renderizar a página e extrair a URL real do ficheiro; em seguida, faça o download a partir do Node com os cookies e cabeçalhos que recolheu da sessão do Puppeteer. Ideal para ficheiros grandes, tarefas paralelas e sempre que o navegador estiver apenas a atrapalhar.
O resto deste guia é composto por uma secção por método, além de uma tabela de decisão, uma lista de verificação de fortificação e uma análise comparativa entre o Puppeteer e o Playwright no final.
Pré-requisitos e configuração do projeto
Antes de entrarmos nos métodos individuais, precisamos de um projeto que todos os quatro possam partilhar. A estrutura aqui é intencionalmente simples: uma pasta, um package.json, um diretório de downloads e um único launch.js ficheiro que iremos reutilizar em todos os exemplos. Manter a estrutura consistente permite-lhe trocar um método por outro sem alterar o resto do seu código, e torna as diferenças entre os métodos muito evidentes quando os compara lado a lado.
As notas de configuração referem-se ao Node.js 20 ou mais recente à data da redação; verifique as notas de lançamento atuais do Puppeteer se estiver a utilizar uma versão mais antiga, uma vez que a versão mínima suportada do Node.js muda com cada lançamento principal do Puppeteer.
Instalação do Puppeteer, noções básicas do Node.js e estrutura de pastas
Crie um projeto, inicialize o npm e instale o Puppeteer:
mkdir puppeteer-downloads
cd puppeteer-downloads
npm init -y
npm install puppeteerAbra package.json e adicione "type": "module" para que possamos usar import a sintaxe nos exemplos. Enquanto estiver lá, adicione algumas funcionalidades úteis para o desenvolvimento:
{
"type": "module",
"scripts": {
"method1": "node method1.js",
"method2": "node method2.js",
"method3": "node method3.js",
"method4": "node method4.js"
}
}O Puppeteer vem com o Chrome para testes e faz o download durante a instalação na maioria das plataformas, o que é suficiente para tudo neste guia. Se estiver a executar num contentor simplificado, confirme o comportamento da instalação nas notas de lançamento do Puppeteer para a versão que fixou, pois o comportamento do Chrome incluído mudou ao longo dos lançamentos.
Estrutura de pastas:
puppeteer-downloads/
downloads/ # files end up here
launch.js # shared harness
method1.js
method2.js
method3.js
method4.jsCrie a downloads/ pasta agora (mkdir downloads) ou deixe que o script de inicialização a crie na primeira execução.
Um script de inicialização básico com caminho de download, User-Agent e viewport
Todos os métodos deste guia partem da mesma estrutura. Coloque isto em launch.js:
// launch.js
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const DOWNLOAD_DIR = path.resolve(__dirname, 'downloads');
export async function launchBrowser({ headless = 'new' } = {}) {
// setDownloadBehavior requires an absolute path. Relative paths silently fail.
if (!fs.existsSync(DOWNLOAD_DIR)) {
fs.mkdirSync(DOWNLOAD_DIR, { recursive: true });
}
const browser = await puppeteer.launch({
headless,
args: [
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
],
});
return browser;
}
export async function newPage(browser) {
const page = await browser.newPage();
// Realistic desktop fingerprint. Some sites hide download buttons on mobile.
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
);
await page.setViewport({ width: 1366, height: 900 });
return page;
}Três coisas a ter em conta. Primeiro, setDownloadBehavior requer um caminho absoluto; se passar um caminho relativo, o Chrome ignora-o silenciosamente e não grava nada. Segundo, forçamos um User-Agent e uma janela de visualização de ambiente de trabalho porque alguns sites escondem links de download por trás de um layout móvel, e um cliente automatizado sem um User-Agent recebe frequentemente um que o Chrome considera não confiável. Terceiro, usamos headless: 'new' em vez de headless: 'shell'. O comportamento de download pode variar no shell modo, especialmente com downloads geridos pelo navegador, por isso mantemos a configuração padrão.
Pode alternar headless para false para depuração. Observar o clique a ocorrer no Chrome real é frequentemente a forma mais rápida de diagnosticar por que razão um fluxo de ficheiros de download do Puppeteer está a falhar silenciosamente. Assim que funcionar no modo headed e não no headless, sabe que o problema é a política de download e não o seu seletor.
Vale a pena fazer duas pequenas adições antes de reutilizar este conjunto de testes em todo o lado. Primeiro, defina um tempo limite de navegação padrão: page.setDefaultNavigationTimeout(60_000) em caches frias poupa muitas execuções de CI instáveis. Segundo, instale um console e pageerror listener para que qualquer erro na página durante o clique de download apareça nos seus logs do Node, em vez de ser ignorado pelo navegador. Ambos são comandos de uma linha e compensam o investimento na primeira vez que uma implementação falhar às 2 da manhã.
Este é também um local natural para incluir um link para um guia mais aprofundado sobre scraping com o Puppeteer, caso necessite de conhecimentos mais amplos sobre navegação, seletores e padrões de espera — que este artigo pressupõe que já possui.
Método 1: Clique no botão de download e aguarde o ficheiro
O Método 1 é o que mais se aproxima do "que um humano faria". Navegue até à página, clique no botão de download e deixe o Chrome gravar o ficheiro numa pasta à sua escolha. O truque é que o Chrome headless não grava em lado nenhum por predefinição; tem de lhe indicar explicitamente onde os downloads são permitidos e para onde devem ir, utilizando uma chamada do Protocolo Chrome DevTools. Depois de isso estar configurado, o resto do trabalho consiste em detetar quando o ficheiro está realmente concluído, porque page.click() o retorno ocorre muito antes de os bytes chegarem ao disco.
Este método é a escolha certa quando:
- O download é acionado por JavaScript, e não por um
<a href>, pelo que não é possível extrair facilmente o URL. - Não precisa de progresso em tempo real (apenas «já terminou?»).
- O ficheiro é suficientemente pequeno para que o armazenamento em buffer no disco seja adequado (normalmente menos de algumas centenas de MB).
É a escolha errada quando:
- O site requer autenticação complexa e cookies que só existem após várias interações SPA (o Método 2 é mais simples).
- Precisa de eventos de progresso ou deteção de interrupções (Método 3).
- O ficheiro é enorme e pretende fazer streaming diretamente para o S3 ou outro destino (Método 4).
Abaixo, definimos a pasta de download, clicamos no botão e verificamos a conclusão usando um .crdownload sentinela e uma verificação estável do tamanho do ficheiro, para que um ficheiro parcialmente gravado nunca seja devolvido como concluído.
Configurar a pasta de download com setDownloadBehavior
Existem duas chamadas CDP que irá encontrar em uso. A legada é Page.setDownloadBehavior, com âmbito limitado a uma única página:
const client = await page.target().createCDPSession();
await client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR, // absolute path
});Isto ainda funciona em muitas configurações, mas está oficialmente obsoleta, e as versões recentes do Chrome começaram a encaminhar os downloads através do destino CDP ao nível do navegador. Quando isso acontece, a sua Page.setDownloadBehavior chamada retorna sucesso e o ficheiro ainda vai parar em ~/Downloads (ou em lugar nenhum) porque a sessão da página já não é responsável pelos downloads. Se alguma vez passou uma tarde a olhar para um script que «funcionava» e que de repente deixou de gravar ficheiros após uma atualização automática do Chrome, esta é normalmente a razão.
A chamada compatível com versões futuras é Browser.setDownloadBehavior, com âmbito no navegador:
const session = await browser.target().createCDPSession();
await session.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR,
eventsEnabled: true, // required for Method 3 progress events
});Browser.setDownloadBehavior aplica-se a todas as páginas no navegador, não apenas àquela em que abriu a sessão, o que é exatamente o que pretende para um fluxo de trabalho de download com várias abas. Também permite ativar eventos de download com eventsEnabled: true, que o Método 3 utilizará intensamente. A equipa do Chrome DevTools documenta ambas as chamadas, e a referência do Protocolo do Chrome DevTools é a fonte de referência quando o comportamento muda entre versões do Chrome.
Conselho prático: prefira Browser.setDownloadBehavior para código novo. Guarde Page.setDownloadBehavior apenas como alternativa para versões muito antigas do Chrome que não possa atualizar. E passe sempre um caminho absoluto; os caminhos relativos não são apenas arriscados, falham silenciosamente.
Acionar o clique e verificar a conclusão
Chamar await page.click(selector) retorna no momento em que o evento de clique é disparado, o que é muito antes do momento em que os bytes são transferidos. Para saber quando o download está realmente concluído, precisamos de um auxiliar que monitore a pasta de downloads e ignore os ficheiros temporários do Chrome. O Chrome grava em something.pdf.crdownload enquanto o download está em andamento e, em seguida, renomeia o ficheiro para o seu nome final quando os bytes são confirmados. O nosso auxiliar aguarda tanto a renomeação como um intervalo de tamanho de ficheiro estável, o que protege contra ficheiros parciais em ligações lentas e sistemas de ficheiros anormais.
// waitForRealFile.js
import fs from 'fs/promises';
import path from 'path';
export async function waitForRealFile(dir, knownBefore, {
timeoutMs = 90_000,
stableChecks = 3,
intervalMs = 250,
} = {}) {
const deadline = Date.now() + timeoutMs;
let lastSize = -1;
let stable = 0;
let candidate = null;
while (Date.now() < deadline) {
const entries = await fs.readdir(dir);
const fresh = entries.filter(
(n) => !knownBefore.has(n) && !n.endsWith('.crdownload'),
);
if (fresh.length) {
candidate = path.join(dir, fresh[0]);
const { size } = await fs.stat(candidate);
if (size === lastSize && size > 0) {
if (++stable >= stableChecks) return candidate;
} else {
stable = 0;
lastSize = size;
}
}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`Download did not finish within ${timeoutMs}ms`);
}Os valores predefinidos de tempo limite de 90 segundos, três verificações de tamanho estável e um intervalo de sondagem de 250 ms são um ponto de partida razoável para ficheiros na ordem das dezenas de MB. Aumente o tempo limite para downloads maiores e reduza-o para pontos finais rápidos, onde prefere falhar rapidamente.
O fluxo no lado da chamada é o seguinte:
const before = new Set(await fs.readdir(DOWNLOAD_DIR));
await page.click('[data-testid="download-button"]');
const finalPath = await waitForRealFile(DOWNLOAD_DIR, before);
console.log('Downloaded:', finalPath);Uma nota sobre integridade: waitForRealFile é heurística. O Chrome pode renomear um ficheiro antes de este ser totalmente gravado em casos raros, especialmente em sistemas de ficheiros de rede. Se precisar de garantias mais fortes, combine este auxiliar com o evento CDP Browser.downloadProgress do Método 3, onde o state: 'completed' sinal é mais fiável (embora, como veremos, ainda não seja absoluto).
Script completo do Método 1 e modos de falha comuns
Juntando tudo em method1.js:
// method1.js
import fs from 'fs/promises';
import { launchBrowser, newPage, DOWNLOAD_DIR } from './launch.js';
import { waitForRealFile } from './waitForRealFile.js';
const TARGET_URL = 'https://example.com/reports';
const DOWNLOAD_SELECTOR = '[data-testid="download-report"]';
(async () => {
const browser = await launchBrowser();
const page = await newPage(browser);
const session = await browser.target().createCDPSession();
await session.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR,
eventsEnabled: false,
});
await page.goto(TARGET_URL, { waitUntil: 'networkidle2' });
const before = new Set(await fs.readdir(DOWNLOAD_DIR));
await page.click(DOWNLOAD_SELECTOR);
const finalPath = await waitForRealFile(DOWNLOAD_DIR, before);
console.log('Saved to:', finalPath);
await browser.close();
})();Algumas coisas que este script acerta e que a maioria dos tutoriais ignora:
- Ele usa
networkidle2para que o botão de download esteja no DOM e vinculado antes de clicarmos. Se clicar demasiado cedo, aciona o clique antes de o JavaScript que o processa ter carregado. - Ele captura um instantâneo do diretório antes de clicar, para que um ficheiro remanescente de uma execução anterior não seja reportado como o novo download.
- Ele fecha explicitamente o navegador; caso contrário, o processo Node pode ficar pendurado num Chrome ainda aberto.
Falhas comuns e o que verificar:
- Não se descarrega nada. Confirme
Browser.setDownloadBehaviorfoi executado antes da navegação e quedownloadPathseja absoluto. Um caminho relativo é a falha silenciosa mais comum. - O seletor é clicado, mas nada acontece. O "download" pode ser uma navegação em vez de um download. Observe a página no modo de cabeçalho; se o URL mudar em vez de acionar uma caixa de diálogo de gravação, mude para o Método 2 ou Método 4 para capturar os bytes diretamente.
- O download fica parado
.crdownload. Ou o servidor bloqueou, o seu tempo de espera é demasiado curto, ou a página fechou antes de o download terminar. AumentetimeoutMse certifique-se de que não chamabrowser.close()até quewaitForRealFileser resolvido. - O modo headless funciona localmente, mas não na CI. Os Chromes em contêineres às vezes são fornecidos sem permissões de gravação no caminho de download ou com políticas de sandbox mais restritas. Crie a pasta antecipadamente e passe
--no-sandboxapenas quando compreender as implicações de segurança.
Mais uma falha fácil de passar despercebida: um script do Método 1 que funciona na primeira vez e falha na segunda execução, porque a execução anterior deixou um report.pdf.crdownload na pasta e o novo clique está agora bloqueado ou o ficheiro foi renomeado para report (1).pdf. Limpe *.crdownload e quaisquer ficheiros de saída remanescentes no início de cada execução, para que o instantâneo do diretório esteja limpo antes de clicar. O before definido em waitForRealFile protege-o apenas contra ficheiros que já existiam no momento do instantâneo, não contra aqueles que o Chrome gerou para si com um nome de ficheiro deduplicado que não esperava.
Método 2: Obter o ficheiro dentro da página e encaminhá-lo para o Node.js
O Método 1 funciona desde que o Chrome esteja disposto a conduzir o download por si. Alguns sites não são assim tão educados. Geram o URL do ficheiro em JavaScript, bloqueiam-no atrás de cookies que só existem após um login SPA em várias etapas, ou fornecem-lhe um blob: URL que o próprio Chrome criou e que nenhum cliente HTTP externo consegue resolver. Em todos esses casos, o único local que pode buscar o ficheiro é a própria página, porque a página já tem a sessão correta.
O Método 2 é executado fetch() dentro page.evaluate(), lê o corpo da resposta no navegador e envia os bytes de volta para o Node através da camada de serialização do Puppeteer. Como page.evaluate() só pode devolver valores serializáveis em JSON, os dados binários têm de ser codificados, e a resposta universal é o base64. O Node descodifica-os, grava um Buffer no disco e obtém o seu ficheiro.
Este método destaca-se para:
- SPAs autenticadas, onde cookies e cabeçalhos são mais fáceis de “pegar emprestado” dentro da página do que coletar e reproduzir.
- Ficheiros servidos através de URLs de blob, URLs de objeto ou geração na memória (relatórios PDF criados em JavaScript são um exemplo clássico).
- Pontos de extremidade compatíveis com CORS, onde a própria página tem permissão para descarregar o ficheiro.
Tem dificuldades com:
- Ficheiros muito grandes, porque o base64 aumenta a carga útil em cerca de 33% e o seu processamento através do V8 consome muitos recursos de CPU e memória.
- Pontos finais não-CORS que a página não tem permissão para buscar (as regras do navegador ainda se aplicam).
Abaixo, abordamos primeiro o padrão de ficheiros de tamanho pequeno a médio e, em seguida, uma variante fragmentada que lida com casos de várias centenas de MB sem sobrecarregar o seu processo Node.
Usar page.evaluate com fetch para ler a resposta como um Blob
No interior de page.evaluate(), fetch() comporta-se exatamente como um fetch normal do navegador. Inclui cookies para pedidos da mesma origem, segue redirecionamentos e respeita o CORS. É isso que o torna poderoso neste contexto: se a página consegue ver o ficheiro, o seu script também consegue.
const base64 = await page.evaluate(async (fileUrl) => {
const res = await fetch(fileUrl, { credentials: 'include' });
if (!res.ok) {
throw new Error(`Fetch failed: ${res.status} ${res.statusText}`);
}
const buf = await res.arrayBuffer();
// Convert ArrayBuffer to base64 inside the browser.
let binary = '';
const bytes = new Uint8Array(buf);
const chunkSize = 0x8000; // 32 KB stride to avoid stack issues
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, i + chunkSize),
);
}
return btoa(binary);
}, fileUrl);Dois detalhes de implementação que vale a pena compreender. Primeiro, String.fromCharCode.apply(null, bigArray) satura a pilha de chamadas se passar dezenas de megabytes de uma só vez, razão pela qual percorremos o buffer em incrementos de 32 KB antes de chamar btoa. Segundo, credentials: 'include' é o que torna isto um padrão de «Puppeteer fetch download» em primeiro lugar; sem ele, perde-se os cookies de sessão e a solicitação deixa de estar autenticada.
Podes adaptar o mesmo padrão para um caso de utilização de download de PDF do Puppeteer em que a URL é construída dinamicamente na SPA: extrai a URL do atributo data- ou de um callback JS, passe-a para page.evaluate()e deixe a página fazer a recuperação. Os bytes que são devolvidos são apenas bytes; o formato de origem não importa para o Node.
Se fetch() falhar com um erro CORS, isso significa que o navegador está a indicar que a página não tem permissão para ler o corpo da resposta. Tem duas opções: mudar para o Método 1 e deixar o Chrome conduzir o download (o CORS não se aplica a navegações ou downloads), ou mudar para o Método 4 e repetir a solicitação a partir do Node, onde a política de mesma origem não se aplica.
Devolver o base64 ao Node e gravar o buffer no disco
Assim que o base64 estiver de volta ao Node, o resto é fácil. Buffer.from(base64, 'base64') decodifica-o, fs.writeFile coloca-o no disco e Buffer.byteLength permite-lhe verificar se o tamanho corresponde ao Content-Length que tenha obtido anteriormente:
import fs from 'fs/promises';
import path from 'path';
import { launchBrowser, newPage, DOWNLOAD_DIR } from './launch.js';
const TARGET_URL = 'https://example.com/report-page';
const FILE_URL_SELECTOR = 'a#download-link';
(async () => {
const browser = await launchBrowser();
const page = await newPage(browser);
await page.goto(TARGET_URL, { waitUntil: 'networkidle2' });
const fileUrl = await page.$eval(FILE_URL_SELECTOR, (a) => a.href);
const base64 = await page.evaluate(async (url) => {
const res = await fetch(url, { credentials: 'include' });
const buf = await res.arrayBuffer();
let binary = '';
const bytes = new Uint8Array(buf);
for (let i = 0; i < bytes.length; i += 0x8000) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, i + 0x8000),
);
}
return btoa(binary);
}, fileUrl);
const buffer = Buffer.from(base64, 'base64');
console.log('Bytes from page.evaluate:', buffer.byteLength);
const outPath = path.join(DOWNLOAD_DIR, 'report.pdf');
await fs.writeFile(outPath, buffer);
console.log('Saved to:', outPath);
await browser.close();
})();Numa execução real com um PDF pequeno, este script regista algo como Bytes from page.evaluate: 3672808 e, em seguida, grava o ficheiro num único fs.writeFile. A contagem de bytes é um indicador útil: se esperava 5 MB e obteve 80 KB, é quase certo que recebeu uma página de erro HTML em vez de um PDF, e deve inspecionar os primeiros bytes do buffer para confirmar antes de guardar.
Este padrão funciona bem até cerca de 50 MB. Acima disso, a própria cadeia base64 começa a dominar a pilha do Node (cada caractere tem dois bytes no V8), e começará a ver JavaScript heap out of memory falhas. É isso que a próxima subsecção resolve.
Transmissão de ficheiros grandes com base64 fragmentado
Para ficheiros com várias centenas de MB, devolver uma única cadeia base64 a partir de page.evaluate() é uma receita para uma falha por falta de memória. A solução consiste em ler a resposta como um fluxo dentro do navegador, dividi-la em blocos de cerca de 1 MB, codificar cada bloco como base64 e enviá-los de volta para o Node um de cada vez. No lado do Node, descodifica-se cada bloco para um Buffer e anexa-se a um fluxo de escrita, de modo que o ficheiro inteiro nunca fica retido na RAM.
O padrão utiliza expose function para dar ao navegador uma forma de fazer uma chamada de retorno para o Node, além de ReadableStream.getReader() para percorrer o corpo da resposta pedaço a pedaço:
import fs from 'fs';
import path from 'path';
import { launchBrowser, newPage, DOWNLOAD_DIR } from './launch.js';
const FILE_URL = 'https://example.com/big-archive.zip';
const OUT_PATH = path.join(DOWNLOAD_DIR, 'big-archive.zip');
(async () => {
const browser = await launchBrowser();
const page = await newPage(browser);
const out = fs.createWriteStream(OUT_PATH);
let written = 0;
await page.exposeFunction('onChunk', async (b64) => {
const buf = Buffer.from(b64, 'base64');
written += buf.byteLength;
if (!out.write(buf)) {
// Apply backpressure if the write stream is saturated.
await new Promise((r) => out.once('drain', r));
}
});
await page.exposeFunction('onDone', () => {
out.end();
console.log('Total bytes:', written);
});
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.evaluate(async (url) => {
const res = await fetch(url, { credentials: 'include' });
const reader = res.body.getReader();
const CHUNK = 1 << 20; // 1 MB target
let pending = new Uint8Array(0);
const flush = (bytes) => {
let binary = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, i + 0x8000),
);
}
return window.onChunk(btoa(binary));
};
while (true) {
const { value, done } = await reader.read();
if (done) break;
const merged = new Uint8Array(pending.length + value.length);
merged.set(pending, 0);
merged.set(value, pending.length);
pending = merged;
while (pending.length >= CHUNK) {
await flush(pending.subarray(0, CHUNK));
pending = pending.subarray(CHUNK);
}
}
if (pending.length) await flush(pending);
await window.onDone();
}, FILE_URL);
await browser.close();
})();Algumas coisas a internalizar. page.exposeFunction adiciona uma variável global na página que, quando chamada, aguarda um manipulador do lado do Node. Usamo-la para enviar pedaços base64 diretamente para um fluxo de escrita, de modo que os bytes nunca se acumulem na memória do V8. Também respeitamos a contrapressão: se out.write() retornar false, esperamos por 'drain' antes de continuar. Sem isso, uma rede rápida e um disco lento acabariam por armazenar todo o ficheiro na memória do Node de qualquer forma, contrariando o objetivo.
O tamanho de fragmento de 1 MB é um equilíbrio. Fragmentos menores significam mais idas e voltas entre a página e o Node e mais sobrecarga base64 por chamada. Fragmentos maiores reduzem a sobrecarga, mas ocupam mais memória no navegador. Um MB é um ponto de partida razoável; ajuste de acordo com a sua carga de trabalho.
Quando a recuperação na página é a escolha certa (autenticação, SPA, URLs de blob)
O Método 2 é a resposta certa quando o ficheiro apenas «existe» dentro da sessão do navegador e o Método 1 não consegue aceder-lhe por uma de três razões.
A primeira é a autenticação baseada em cookies ou tokens, que é resistente à repetição. Alguns sites associam a sessão a impressões digitais (User-Agent mais IP mais um token CSRF num armazenamento que não seja de cookies), e reproduzir isso fora do navegador é frágil. A recuperação na página contorna isso completamente porque o pedido provém da página que detém a sessão.
A segunda razão são os downloads gerados por SPA. Um clique num botão executa JavaScript que cria um Blob, passa-o para URL.createObjectURLe aciona um download através de um <a download> . A URL é algo como blob:https://app.example.com/abc-123 e apenas a página de origem pode resolvê-la. O Método 1 pode capturar o download resultante se setDownloadBehavior estiver implementado, mas o Método 2 é mais determinístico: recrie você mesmo a mesma recuperação, codifique o resultado e ignore completamente o fluxo de download do Chrome.
O terceiro são os pontos de extremidade de exportação dinâmicos. As APIs que recebem uma carga JSON, geram um CSV ou PDF na hora e o retornam inline são fáceis de programar com page.evaluate() porque pode JSON.stringify a carga útil, enviar um POST e ler a resposta como um fluxo.
Quando a busca na própria página não é adequada: ficheiros muito grandes (abordados acima), ficheiros protegidos por CORS que a página não tem permissão para ler e qualquer caso em que uma simples solicitação Axios a partir do Node simplesmente funcionaria. Use a ferramenta mais simples que consiga os bytes.
Método 3: Gerar downloads com o Protocolo Chrome DevTools
O Método 1 usa o CDP nos bastidores, mas trata-o como uma etapa de configuração. O Método 3 torna o CDP o protagonista. Quando precisar de progresso em tempo real, quando estiver a executar downloads paralelos e precisar de mapear cada um deles de volta ao clique que o iniciou, ou quando quiser detetar interrupções antecipadamente, vai querer os eventos CDP ao nível do navegador: Browser.downloadWillBegin e Browser.downloadProgress. Estes fornecem-lhe um GUID por download, o nome de ficheiro sugerido, o total de bytes, se conhecido, os bytes recebidos até ao momento e uma máquina de estados de inProgress, completed, e canceled.
Este é o mesmo protocolo que o próprio painel DevTools do Chrome utiliza, e está mais próximo de uma API de download «real» do que qualquer coisa que o Puppeteer exponha nativamente. O senão é que reside uma camada abaixo page.click(), pelo que tem de o ligar explicitamente e ficar à escuta de eventos na sessão CDP, em vez de esperar por uma promessa do Puppeteer.
Quando escolher o Método 3:
- Precisa de mostrar o progresso a um utilizador ou enviá-lo para uma fila de tarefas.
- Está a executar tarefas simultâneas de download de ficheiros no Puppeteer e precisa de mapear nomes de ficheiros para o contexto.
- Quer um sinal claro de «este download foi cancelado», em vez de ter de adivinhar a partir do sistema de ficheiros.
- Quer uma solução de download sem interface gráfica do Puppeteer fiável que não dependa do legado
Page.setDownloadBehavior.
Quando ignorá-lo:
- Só precisa de um ficheiro de cada vez e o Método 1 é suficiente.
- Pode obter o URL e usar o Axios; a complexidade da infraestrutura do CDP raramente vale a pena nesse caso.
Abrir uma sessão CDP com page.createCDPSession
Existem duas sessões CDP à escolha no Puppeteer: com âmbito de página e com âmbito do navegador. Para o Método 3, queremos a sessão com âmbito do navegador, porque os eventos de download são emitidos ao nível do navegador e Browser.setDownloadBehavior é um método ao nível do navegador.
const session = await browser.target().createCDPSession();Compare isso com await page.createCDPSession(), que é de âmbito da página. As sessões de página ainda funcionam para chamadas de navegação, rede e tempo de execução com âmbito de uma página, mas não irão ver downloads ao nível do navegador se o Chrome as encaminhar através do destino do navegador (o que é a tendência nas versões recentes).
Um modelo mental útil: uma sessão CDP é um websocket tipado para um destino. browser.target() é o destino do navegador, page.target() é um destino de página, e cada um recebe eventos diferentes. Confundi-los é uma fonte frequente de bugs do tipo «o meu ouvinte nunca dispara» no Método 3. Se o seu Browser.downloadProgress ouvinte estiver em silêncio, verifique se abriu a sessão no browser.target(), e não na página.
Pode ter várias sessões CDP abertas ao mesmo tempo, incluindo uma por página mais uma no navegador. Para tarefas de download, basta uma única sessão ao nível do navegador.
Browser.setDownloadBehavior e escuta de downloadWillBegin / downloadProgress
Com a sessão do navegador em mãos, configure o comportamento de download e subscreva os eventos:
const downloads = new Map(); // guid -> { filename, totalBytes, received, state }
await session.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR,
eventsEnabled: true, // turn on downloadWillBegin / downloadProgress
});
session.on('Browser.downloadWillBegin', (event) => {
// event: { guid, url, suggestedFilename, frameId }
downloads.set(event.guid, {
filename: event.suggestedFilename,
received: 0,
totalBytes: 0,
state: 'inProgress',
});
console.log(`Starting download: ${event.suggestedFilename}`);
});
session.on('Browser.downloadProgress', (event) => {
// event: { guid, totalBytes, receivedBytes, state }
const entry = downloads.get(event.guid);
if (!entry) return;
entry.totalBytes = event.totalBytes;
entry.received = event.receivedBytes;
entry.state = event.state;
if (event.totalBytes > 0) {
const pct = ((event.receivedBytes / event.totalBytes) * 100).toFixed(1);
process.stdout.write(` ${entry.filename}: ${pct}%\r`);
}
if (event.state === 'completed') {
console.log(`\nFinished: ${entry.filename}`);
} else if (event.state === 'canceled') {
console.warn(`\nCanceled: ${entry.filename}`);
}
});Alguns padrões que vale a pena absorver:
- O
guidcampo é a sua chave para rastrear downloads paralelos. O Chrome atribui um GUID novo por download, e osuggestedFilenameé o nome que o ficheiro terá no disco (salvo colisões, em que o Chrome acrescenta(1),(2), etc.). totalBytespode ser0se o servidor não enviar umContent-Length. Nesse caso, não é possível mostrar uma percentagem, apenas uma contagem de bytes em execução. Planeie a sua interface de utilizador em conformidade.state: 'completed'é um forte indício de que o download está concluído, mas não é uma garantia absoluta de que o ficheiro foi totalmente gravado no disco. O Chrome pode reportar a conclusão um pouco antes da renomeação ou da gravação final, por isso, uma breve verificação do tamanho estável continua a ser uma boa ideia além do evento.state: 'canceled'inclui downloads cancelados pelo utilizador (raro em modo headless) e downloads abortados (falha de rede, desligamento do servidor). Trate ambos da mesma forma: tente novamente ou indique claramente a falha.
Se não definir eventsEnabled: true, obtém o download mas não os eventos, o que o coloca de volta no território da sondagem do Método 1. Opte sempre pelo Método 3.
Para uma verificação mais rigorosa de que «o ficheiro está realmente no disco», combine o 'completed' evento com um pequeno waitForFileStable , semelhante ao do Método 1, mas mais rigoroso (tempo limite de 30 segundos, três verificações estáveis):
async function waitForFileStable(filePath, {
timeoutMs = 30_000,
stableChecks = 3,
intervalMs = 200,
} = {}) {
const deadline = Date.now() + timeoutMs;
let last = -1, stable = 0;
while (Date.now() < deadline) {
try {
const { size } = await fs.stat(filePath);
if (size === last && size > 0) {
if (++stable >= stableChecks) return size;
} else {
stable = 0; last = size;
}
} catch {}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`File never stabilized: ${filePath}`);
}Agora tem ambos os sinais: o CDP diz «concluído» e o sistema de ficheiros confirma.
Script completo do Método 3 com registo do progresso
// method3.js
import path from 'path';
import { launchBrowser, newPage, DOWNLOAD_DIR } from './launch.js';
import { waitForFileStable } from './waitForFileStable.js';
const TARGET_URL = 'https://example.com/reports';
const SELECTOR = '[data-testid="download-report"]';
(async () => {
const browser = await launchBrowser();
const page = await newPage(browser);
const session = await browser.target().createCDPSession();
await session.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR,
eventsEnabled: true,
});
let resolveDone, rejectDone;
const done = new Promise((r, j) => { resolveDone = r; rejectDone = j; });
let lastFilename = null;
session.on('Browser.downloadWillBegin', (e) => {
lastFilename = e.suggestedFilename;
console.log('Begin:', e.guid, '->', e.suggestedFilename);
});
session.on('Browser.downloadProgress', async (e) => {
if (e.state === 'completed') {
const finalPath = path.join(DOWNLOAD_DIR, lastFilename);
try {
await waitForFileStable(finalPath);
resolveDone(finalPath);
} catch (err) { rejectDone(err); }
} else if (e.state === 'canceled') {
rejectDone(new Error('Download canceled'));
}
});
await page.goto(TARGET_URL, { waitUntil: 'networkidle2' });
await page.click(SELECTOR);
const finalPath = await done;
console.log('Saved to:', finalPath);
await browser.close();
})();O que este script oferece em relação ao Método 1: conclusão determinística (sabe exatamente quando o download começa e termina através de eventos, não por adivinhação), progresso em tempo real (o downloadProgress manipulador é acionado a cada poucas centenas de KB) e tratamento explícito de cancelamento. Ele também se generaliza de forma clara para N downloads paralelos: mantenha uma Map<guid, Promise>, resolva cada promessa dentro do manipulador e Promise.all tudo o resto.
Em produção, normalmente é aconselhável envolver done num limite de tempo para que um download bloqueado não pare o seu worker para sempre. Um limite máximo de 5 a 10 minutos é razoável para ficheiros típicos. Se o exceder, registe o GUID, encerre a página e tente novamente. O CDP dá-lhe a visibilidade para tomar essa decisão; o sistema de ficheiros por si só não o faz.
Um segundo padrão que vale a pena conhecer para o Método 3: promessas por download. Em vez de uma única done promessa, mantenha uma Map<guid, { resolve, reject }> e crie uma entrada dentro Browser.downloadWillBegin. O Browser.downloadProgress manipulador chama então resolve ou reject na entrada que corresponde ao guid. Com isso em vigor, pode disparar N cliques consecutivos, recolher N promessas e Promise.all resolvê-las. O mesmo código do manipulador funciona para um ficheiro ou para cinquenta, e obtém-se um relatório de erros por ficheiro claro, em vez de um único tempo limite global que oculta qual o download que realmente falhou.
Método 4: Ignore o navegador, passe a URL para o Axios ou https
Às vezes, a melhor estratégia de download de ficheiros com o Puppeteer é quase não usar o Puppeteer. Se o site expor uma URL real e estável para o ficheiro (mesmo que tenha de renderizar a página e clicar por aí para a descobrir), pode renderizar com o Puppeteer apenas o tempo suficiente para extrair essa URL mais o estado de autenticação e, em seguida, fazer o download com axios ou o comando integrado do Node https. O resultado é mais rápido do que o Método 1, mais eficiente em termos de memória do que o Método 2 e facilmente paralelizável de uma forma que a execução de N instâncias do Chrome não é.
Este é também o método mais «aborrecido», no bom sentido. Assim que a URL estiver em mãos, o download é apenas um HTTP GET. Não há regressão do modo headless para acompanhar, nem desvio de versão do CDP, nem .crdownload sentinela para consultar. Passa-se a URL e alguns cabeçalhos para o Axios, canaliza-se a resposta para um fluxo de escrita e o ficheiro fica no disco.
Escolha o Método 4 quando:
- O ficheiro de destino estiver numa URL estável que pode extrair do DOM, de uma resposta de rede ou de uma variável JS.
- O ficheiro é grande e pretende um verdadeiro streaming para o disco sem buffering através do V8.
- Precisa de executar muitos downloads em simultâneo. Um conjunto de pedidos Axios é muito mais económico do que um conjunto de Chromes headless.
Ignore o Método 4 quando:
- A URL de download for de uso único, assinada ou vinculada a um token da sessão do navegador de uma forma que não possa ser reproduzida.
- O site impõe desafios de JavaScript ou verificações de impressão digital que o Axios não consegue passar sem um esforço significativo.
Quando o segundo caso se aplica, normalmente troca o Axios por uma camada de pedidos que lida com essas verificações, mas a estrutura do script não muda.
Extrair cookies e cabeçalhos do Puppeteer para autenticar a solicitação
O objetivo de um fluxo híbrido é herdar a sessão do Puppeteer. Executa o login do SPA ou qualquer outro ritual que o site exija e, em seguida, transfere os cookies e alguns cabeçalhos essenciais para o Axios.
async function buildAxiosHeaders(page) {
const cookies = await page.cookies(); // current page's cookies
const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
const userAgent = await page.evaluate(() => navigator.userAgent);
const referer = page.url();
return {
Cookie: cookieHeader,
'User-Agent': userAgent,
Referer: referer,
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9',
};
}Os quatro cabeçalhos acima cobrem a grande maioria das verificações de CDN e WAF. Cookie transporta a sessão, User-Agent corresponde ao que a página já comprovou, Referer corresponde ao que o navegador enviaria ao clicar no link de download, e Accept-Language é uma pequena indicação de que um navegador real esteve ali recentemente. Se o site verificar Sec-Ch-Ua ou outras dicas do cliente, copie-as também com page.evaluate(() => navigator.userAgentData).
Duas armadilhas. Primeiro, page.cookies() retorna cookies para o URL atual por padrão. Se o ficheiro estiver hospedado num subdomínio diferente, passe esse URL explicitamente: page.cookies(fileUrl). Caso contrário, os cookies que enviar não serão transmitidos. Segundo, alguns sites definem HttpOnly ou Secure sinalizadores que o Axios respeita sem problemas, mas os cookies com escopo de caminho (Path=/api) são ignorados, a menos que os preserve ao construir o cabeçalho. A solução mais simples é obter os cookies da origem exata que irá aceder e juntar apenas os cookies cujo path sejam um prefixo do caminho da URL do ficheiro.
Se quiser evitar fazer isto manualmente, existem adaptadores axios-cookiejar maduros que recebem os cookies do Puppeteer e permitem que o Axios os gerencie por solicitação. Para o caso comum, basta um único Cookie é suficiente. Para um contexto mais aprofundado sobre como tornar as chamadas do Axios mais resistentes à deteção, um guia interno do axios-headers complementa naturalmente esta secção.
Transmissão da resposta com axios responseType: stream
O download em si é simples quando se usa responseType: 'stream'. O Axios devolve o corpo da resposta como um stream Node, e você canaliza-o para um stream de escrita. O ficheiro completo nunca fica retido na RAM:
import axios from 'axios';
import fs from 'fs';
import { pipeline } from 'stream/promises';
async function downloadToFile(url, outPath, headers) {
const res = await axios.get(url, {
headers,
responseType: 'stream',
timeout: 30_000,
maxRedirects: 5,
validateStatus: (s) => s >= 200 && s < 400,
});
await pipeline(res.data, fs.createWriteStream(outPath));
}stream.pipeline (ou a sua versão promise, usada aqui) é a primitiva certa porque propaga erros de ambos os lados e limpa os fluxos corretamente em caso de falha. Um res.data.pipe(write) ignora os erros do fluxo de gravação, o que faz com que se acabe com um ficheiro parcialmente gravado e sem nenhuma exceção.
Algumas opções de nível de produção:
- Tempos de espera.
timeout: 30_000é um tempo limite para o estabelecimento da solicitação. Para downloads longos, envolva também o pipeline num watchdog para que um fluxo lento não fique pendurado para sempre. - Repetidas tentativas. Envolva a chamada num pequeno auxiliar de repetição com recuo exponencial, limitado a três tentativas. A maioria das falhas transitórias (504, ECONNRESET) são corrigidas pela repetição.
- Evite gravações simultâneas no mesmo caminho. Duas tarefas paralelas a sobrescrever
report.pdfé um bug de corrupção silencioso. Use um nome de ficheiro temporário e renomeie, ou use nomes de ficheiro únicos por tarefa.
Para paralelismo, um pequeno pool é o padrão mais seguro. Três a cinco downloads Axios simultâneos é um limite razoável, e um for...of await é a base mais segura se não tiver a certeza sobre os limites de taxa do lado do servidor. Acima de cinco tarefas simultâneas, deve medir em vez de adivinhar.
Downloads de URL puros sem o Puppeteer no ciclo
Depois de descobrir o padrão de URL, muitas vezes pode dispensar o Puppeteer por completo. Uma execução híbrida típica usa o Puppeteer para extrair uma grelha de resultados de pesquisa, extrair um URL de página de detalhes por resultado e, em seguida, visitar cada página de detalhes para obter o URL do ficheiro ou, se o padrão de URL for previsível, derivá-lo diretamente da lista.
Um fluxo de ponta a ponta representativo que descarrega cinco ficheiros de imagem tem a seguinte forma:
import axios from 'axios';
import fs from 'fs';
import path from 'path';
async function downloadAll(items, headers, outDir) {
for (let i = 0; i < items.length; i++) {
const url = items[i].downloadUrl;
const out = path.join(outDir, `image-${String(i + 1).padStart(3, '0')}.jpg`);
await downloadToFile(url, out, headers);
console.log('Saved', out);
}
}Execute isso numa lista de cinco URLs extraídas e obtém image-001.jpg através image-005.jpg no disco, sem qualquer processo do Chrome associado à transferência propriamente dita. Se as URLs forem públicas e não assinadas, pode ignorar completamente o Puppeteer em execuções subsequentes e aceder diretamente às URLs. Essa é frequentemente a decisão certa para atualizações diárias de um conjunto de dados conhecido; só paga o custo do Puppeteer na primeira vez, enquanto descobre o formato da URL.
A lição mais importante: pense no Puppeteer como uma ferramenta de descoberta e autenticação, não como uma ferramenta de download. A função do navegador é descobrir onde os bytes se encontram e comprovar a sessão correta; o download em si pode quase sempre ser feito por um cliente menor e mais rápido.
Dois padrões operacionais ampliam isto. Primeiro, armazene em cache o padrão de URL descoberto num pequeno ficheiro JSON ou numa base de dados indexada por site, e só volte a executar a etapa de descoberta do Puppeteer quando uma solicitação do Axios começar a devolver 404 ou HTML inesperado. As URLs de ficheiros da maioria dos sites seguem um modelo estável (/exports/{id}/{filename}.csv), e uma vez que tenha o modelo, as atualizações diárias não precisam de nenhum navegador. Em segundo lugar, quando a URL estiver assinada, mas a lógica de assinatura for reproduzível (HMAC numa carga de pedido, por exemplo), faça a engenharia reversa da assinatura uma vez e ignore o Puppeteer permanentemente para esse alvo. A abordagem de ficheiro de download do Puppeteer justifica-se no primeiro contacto; tudo o que se segue é HTTP simples.
Escolher o método certo de ficheiro de download do Puppeteer: um guia de decisão
Quatro métodos são mais do que o SERP normalmente apresenta, e é essa a questão: cada um tem o seu nicho. Aqui está uma tabela de decisão que associa algumas perguntas de sim/não ao método certo, além de uma tabela comparativa que pode manter aberta enquanto lê este guia.
Comece pelas perguntas:
- Tem um URL de ficheiro estável e reproduzível? Se sim, passe para a pergunta 2. Se não (o URL é de utilização única, gerado por JS ou válido apenas dentro da sessão da página), está no território do Método 1 ou do Método 2.
- O ficheiro está protegido por autenticação que sobrevive fora do navegador? Se conseguir eliminar os cookies e reproduzir o pedido, o Método 4 é quase sempre a escolha certa. Se a autenticação estiver ligada ao navegador (tokens CSRF armazenados na memória JS, com impressão digital da sessão), use o Método 2.
- O ficheiro é muito grande (mais de ~100 MB) ou está a executar muitos em paralelo? O Método 4 ganha. O streaming com Axios é mais económico do que executar N instâncias do Chrome, e as idas e voltas de base64 no Método 2 não são escaláveis.
- Precisa de eventos de progresso ou de um sinal de cancelamento claro? O Método 3 é o único que lhe oferece ambos diretamente a partir do Chrome.
- O download é acionado por um clique cuja URL não pode ser facilmente inspecionada? O Método 1 é a resposta mais simples e geralmente é suficiente.
|
Método |
Ideal para |
Evite para |
Perfil de memória |
Modelo de autenticação |
|---|---|---|---|---|
|
Transferências acionadas por JS, URLs desconhecidas |
Ficheiros muito grandes, interface de progresso |
Baixo (o Chrome transfere para o disco) |
O que quer que o clique veja |
|
SPAs, URLs de blobs, autenticação ligada ao navegador |
Ficheiros com várias centenas de MB |
Elevado sem fragmentação |
Cookies do navegador, automático |
|
Tarefas paralelas, progresso, cancelamento |
Ficheiros pequenos pontuais |
Baixo (o Chrome transfere para o disco) |
O que quer que o clique veja |
|
Ficheiros grandes, pipelines paralelos, URLs conhecidos |
URLs assinadas de uso único |
Baixo (streaming verdadeiro) |
Cookies + cabeçalhos reproduzidos |
Uma regra geral: prefira o método que utilize o mínimo de Puppeteer e que ainda funcione. O Método 4 é o padrão se a URL for conhecida. O Método 1 é o padrão se não for. O Método 3 é o que o Método 1 deveria ter sido quando é necessário paralelismo ou progresso. O Método 2 é a saída de emergência para tudo o resto.
Em caso de dúvida, crie primeiro um protótipo do Método 4. Se funcionar, ficará contente por não ter executado um Chrome para cada ficheiro. Se não funcionar, saberá em poucos minutos se o problema é a autenticação (Método 2) ou se é o URL (Método 1).
Reforço de produção: tempos de espera, novas tentativas e verificações de integridade
Um script de download de ficheiros do Puppeteer que funciona no seu portátil e falha em produção quase sempre falha por uma de quatro razões: um tempo limite que se esqueceu de definir, uma nova tentativa que se esqueceu de escrever, um .crdownload sentinela que se esqueceu de limpar ou um ficheiro parcial que tratou como completo. Aqui está a lista de verificação pela qual passamos os scripts antes de os colocarmos em produção.
Limites de tempo em todas as camadas. Defina timeout em page.goto (o padrão é 30s, muitas vezes demasiado curto em caches frias), um tempo limite explícito no seu waitForRealFile helper, um Axios timeout para o Método 4 e um limite de tempo real para todo o trabalho. Os travamentos de CI geralmente são causados pela ausência de um desses elementos, não pela presença de um bug real.
Repetidas com backoff. Envolva a chamada que envolve a rede num helper de repetição, com backoff exponencial limitado a três tentativas, e uma falha definitiva na última tentativa. Repita em ECONNRESET, ETIMEDOUT, respostas 5xx e qualquer coisa que pareça transitória. Não tente novamente em 401, 403 ou 404, pois esses códigos indicam erros no seu código.
Limpe .crdownload os ficheiros entre execuções. O Chrome deixa-os por aí quando um download é cancelado ou o processo termina prematuramente. Se voltar a executar o script, o seu waitForRealFile pode detetar o sentinela obsoleto e reportar o ficheiro errado como novo. Limpe .crdownload, .tmpe os seus próprios ficheiros de trabalho no início de cada execução.
Verifique a integridade, não apenas a existência. Três camadas de verificação são razoáveis para cargas importantes: o ficheiro existe, o tamanho do ficheiro corresponde ao esperado Content-Length (quando o servidor fornece um) e uma soma de verificação, se a fonte a publicar. Uma comparação rápida de MD5 ou SHA-256 crypto.createHash('sha256') é rápido em ficheiros de vários GB e deteta truncamentos que uma verificação de existência simplista não detecta.
Limite a simultaneidade, não se limite apenas a paralelizar. Três a cinco downloads simultâneos é um padrão sensato; além disso, começa a competir consigo mesmo pelo disco e pela largura de banda, e muitos sites restringem os limites de taxa. Um p-limit pool de estilo mais limites de concorrência por host é uma pequena quantidade de código que evita muitos relatórios de incidentes.
Registe mapeamentos de GUID para nome de ficheiro (Método 3) ou mapeamentos de URL para saída (Método 4). Quando algo corre mal às 3 da manhã, um registo estruturado do tipo «esta URL produziu este ficheiro com esta contagem de bytes e este estado» é o que o salva. Guarde os registos.
Coloque os ficheiros parciais em quarentena. Se um download falhar a meio, os bytes parciais são perigosos. Mova-os para um partial/ diretório, não os deixe onde a próxima etapa do seu pipeline possa lê-los como se estivessem completos. Um ficheiro parcial que parece completo é o tipo de bug mais dispendioso na automatização de downloads.
Evitar bloqueios durante downloads automatizados
Mesmo quando o fluxo de ficheiros de download do Puppeteer é à prova de bala na camada de tratamento de ficheiros, o próprio pedido pode ser bloqueado antes mesmo de produzir bytes. CDNs, WAFs e fornecedores de soluções anti-bot analisam as mesmas impressões digitais, quer esteja a extrair HTML ou a descarregar um CSV de 200 MB, pelo que se aplicam as mesmas defesas.
O reforço de segurança mais barato e eficaz reside em três cabeçalhos e numa decisão de IP:
- User-Agent realista. Use um UA de desktop do Chrome atual que corresponda à versão do Chrome for Testing incluída, e não o padrão do Puppeteer. Alguns hosts bloqueiam o UA padrão assim que o detectam.
- Viewport correspondente. Um viewport de 1366x900 corresponde a uma sessão de desktop real. Um viewport de 800x600 grita «automatização».
- Referer. Defina
Refererpara a página que continha o link para o ficheiro. Os WAFs frequentemente devolvem um erro 403 em acessos diretos a recursos sem referer, especialmente para PDFs e imagens. - IP razoável. Os IPs de centros de dados de fornecedores de nuvem comuns são pré-marcados pela maioria dos fornecedores de soluções anti-bot. Se os seus downloads estão a receber erros 403 em navegadores reais, mas passam quando utiliza uma VPN para uma ligação residencial, tem um problema de IP, não um problema de script.
Algumas medidas adicionais ajudam em casos mais difíceis. Adicione um pequeno slowMo (50 a 200 ms) para espaçar os cliques. Use page.waitForTimeout após goto para permitir que as verificações de bots baseadas em JavaScript se estabilizem. Escalone tarefas com vários ficheiros para não efetuar N acessos no mesmo segundo.
Quando tiver feito tudo o que foi mencionado acima e o site ainda o bloquear, a medida certa é delegar a camada de pedidos em vez de continuar a ajustar os cabeçalhos. Ferramentas como a nossa rede de proxies residenciais otimizada para scraping ou o nosso endpoint da API Scraper na WebScrapingAPI tratam da rotação de proxies, da reputação de IP e das verificações de fingerprinting mais complexas por trás de uma única solicitação, para que o seu código Puppeteer possa manter-se focado em controlar a página. Esse também é o lugar certo para procurar se precisar de downloads específicos por país ou tiver de fazer scraping por trás de páginas de desafio.
Este é também um bom momento para refletir se precisa mesmo de um navegador headless completo. Vale a pena ler a visão geral do navegador headless, cujo link se encontra noutro local do site, se ainda estiver a decidir entre um harness Puppeteer personalizado e uma alternativa hospedada.
Puppeteer vs Playwright para downloads de ficheiros
Resposta honesta: o Playwright tem uma API mais agradável para downloads, o Puppeteer tem uma ligação mais direta com o funcionamento interno do Chrome, e qualquer um deles funciona bem em produção.
O Playwright expõe page.waitForEvent('download'), que devolve um Download objeto com auxiliares como download.path(), download.saveAs(path)e download.suggestedFilename(). Não é necessário tocar no CDP para o caso básico. Isso é genuinamente mais curto do que a configuração equivalente do Puppeteer e funciona da mesma forma no Chromium, Firefox e WebKit, o que é a maior vantagem em conjuntos de testes entre navegadores. Se estiver a começar do zero e a sua pilha ainda não se baseia no Puppeteer, um fluxo de trabalho de download do Playwright tem aproximadamente metade do código.
A força do Puppeteer é que ele está mais próximo do Chrome DevTools Protocol. Se precisar de eventos CDP brutos, chamadas de protocolo personalizadas ou comportamentos que ainda não foram encapsulados numa API de nível superior, o Puppeteer chega lá com uma camada a menos de indireção. O Método 3 neste guia é um bom exemplo. O mesmo padrão no Playwright também funciona (o Playwright expõe uma sessão CDP), mas as expressões do Puppeteer parecem nativas porque toda a biblioteca foi moldada em torno do CDP.
Para um pipeline de ficheiros de download do Puppeteer que já esteja em funcionamento, nada disto constitui motivo para migrar. O Método 1, mais Browser.setDownloadBehavior corresponde às funcionalidades do Playwright waitForEvent('download') em funcionalidades quase na íntegra; basta escrever mais algumas linhas. Migre para o Playwright quando a compatibilidade entre navegadores for a verdadeira vantagem, e não apenas por causa dos downloads. Temos um guia mais extenso sobre web scraping com o Playwright no site, caso queira uma comparação completa.
Pontos-chave
- Não existe um único método ideal para o download de ficheiros no Puppeteer. Adapte o método à restrição que mais o prejudica: URL desconhecida (Método 1), autenticação ligada ao navegador (Método 2), tarefas paralelas com progresso (Método 3) ou URL conhecida com cookies reproduzíveis (Método 4).
setDownloadBehavioré inegociável. O Headless Chrome bloqueia downloads por padrão. Use o método de nível do navegadorBrowser.setDownloadBehaviorcom um caminho absoluto; a chamada ao nível da página está obsoleta e falha de forma imprevisível.- Aguarde por ficheiros reais, não por eventos de clique. Faça um instantâneo da pasta de downloads, ignore
.crdownloade exija uma janela de tamanho de ficheiro estável antes de reportar sucesso. - Ignore o navegador sempre que possível. Uma combinação de Puppeteer e Axios é mais rápida, mais leve e mais fácil de escalar do que executar N instâncias do Chrome para downloads paralelos.
- Reforce a camada de pedidos separadamente do script. User-Agent realista, correspondência de viewport, referer, IPs residenciais e concorrência limitada evitam a maioria dos incidentes de «403 misterioso».
Perguntas frequentes
Algumas perguntas surgem em todos os projetos de ficheiros de download do Puppeteer, geralmente depois de o primeiro script funcionar mais ou menos no modo headed e falhar na CI. As respostas abaixo ignoram a recapitulação dos quatro métodos, que se encontram acima, e concentram-se nas decisões operacionais: como escolher rapidamente quando não é possível prototipar os quatro, o que fazer quando os ficheiros se recusam a terminar, como é na prática o caminho mais limpo sem navegador, onde se situa o Playwright em relação ao Puppeteer para downloads e como lidar com a autenticação vinculada à sessão sem perder um fim de semana com isso.
Como escolher o melhor método para descarregar um ficheiro com o Puppeteer?
Trabalhe a partir de uma lista curta. Se conseguir extrair um URL estável e a autenticação for reproduzível, use o Axios com cookies recolhidos da sessão do Puppeteer. Se o URL for gerado por JavaScript ou for válido apenas dentro da página, execute fetch() dentro page.evaluate() e devolva o base64. Se tiver apenas um alvo de clique e precisar de uma conclusão básica, configure Browser.setDownloadBehavior e clique. Se precisar de progresso ou segurança paralela, conduza tudo através de eventos CDP. Adapte o método à restrição que mais prejudica.
Por que é que o meu download do Puppeteer fica preso num ficheiro .crdownload ou nunca termina?
A causa mais comum é o script sair antes de o Chrome descarregar o ficheiro, por isso feche sempre o navegador apenas depois de um auxiliar de sondagem confirmar que o nome de ficheiro final existe com um tamanho estável. Outros suspeitos: um downloadPath (deve ser absoluto), o clique a desencadear uma navegação em vez de um download, ou um bloqueio do servidor que o Chrome reporta como cancelado. Observe a execução no modo «headed» uma vez e a causa torna-se normalmente óbvia em segundos.
Posso descarregar ficheiros sem iniciar o Chrome de todo?
Sim, e muitas vezes é a escolha certa. Se o URL do ficheiro for público, ou se os cookies e cabeçalhos necessários para o obter puderem ser reproduzidos por um cliente HTTP, ignore o navegador e use axios ou a função de gravação em streaming integrada do Node https com uma gravação em streaming. As únicas vezes em que precisa de um navegador são quando o JavaScript constrói a URL, quando a autenticação está ligada à sessão do navegador de uma forma que não consegue reproduzir, ou quando uma camada de deteção de bots bloqueia especificamente clientes que não sejam navegadores nessa URL.
Como é que o Puppeteer se compara ao Playwright no que diz respeito a downloads de ficheiros?
O Playwright envolve os downloads numa API de eventos de alto nível (page.waitForEvent('download')) que devolve um Download objeto com saveAs() e path() auxiliares, o que é mais curto do que a configuração equivalente do Puppeteer com CDP. O Puppeteer obriga-o a ligar Browser.setDownloadBehavior e a sondar o sistema de ficheiros ou a ouvir eventos CDP. Ambas são fiáveis em produção. Escolha com base na biblioteca que a sua pilha já utiliza, e não apenas pela API de download.
Como posso descarregar ficheiros que requerem um login ou um cookie de sessão?
Duas opções simples. Ou faça o login no Puppeteer, extraia os cookies com page.cookies()e reproduzir o pedido de ficheiro do Axios com um Cookie cabeçalho e User-Agent e Referer. Ou execute a recuperação do ficheiro dentro de page.evaluate() para que a solicitação herde a sessão automaticamente. A primeira opção é mais rápida e fácil de escalar; a segunda é mais robusta quando a autenticação está vinculada a tokens na memória ou impressões digitais que não sobrevivem à reprodução.
Conclusão e próximos passos
Um fluxo de trabalho fiável de download de ficheiros com o Puppeteer tem menos a ver com o Puppeteer e mais com a escolha de onde os bytes realmente se movem. Use o Método 1 quando um clique for tudo o que tiver. Recorra ao Método 2 quando a sessão da página for a única coisa capaz de buscar o ficheiro. Opte pelo Método 3 quando precisar de progresso, paralelismo ou sinais de cancelamento claros. Use o Método 4 por padrão assim que puder reproduzir a URL e trate o Puppeteer como uma ferramenta de descoberta, em vez de uma ferramenta de download.
Envolva cada script com os princípios básicos de fortalecimento de produção: caminhos de download absolutos, tempos de espera em camadas, tentativas com recuo, verificações de integridade além da mera existência e concorrência limitada. Detete .crdownload sentinelas, limpe-as entre execuções e nunca deixe um ficheiro parcial fluir para a fase seguinte como se estivesse completo.
Se os seus downloads estão a ser bloqueados em vez de falharem, o problema já não está no seu script, está na camada de pedidos. É aí que uma infraestrutura de scraping gerida mostra o seu valor. A API do navegador WebScrapingAPI oferece-lhe navegadores na nuvem totalmente hospedados que pode controlar com o mesmo código Puppeteer (ou Playwright), além de uma rede de proxies residenciais e desbloqueio integrado para os alvos mais difíceis, para que possa manter o manual de quatro métodos acima e apenas trocar o local de origem das solicitações. A partir daí, escalar um pipeline de ficheiros de download do Puppeteer é uma alteração de configuração, em vez de uma reestruturação da arquitetura.
Escolha o método certo para o ficheiro de hoje, fortaleça-o uma vez e siga em frente.




