Voltar ao blogue
Guias
Mihnea-Octavian ManolacheLast updated on May 8, 202614 min read

Formulário de envio do Puppeteer: Guia do Node.js para 2026

Formulário de envio do Puppeteer: Guia do Node.js para 2026
Resumo: Use page.locator(selector).fill(value) para scripts de envio de formulários do Puppeteer rápidos e determinísticos e page.type() quando a página deteta teclas reais (preenchimento automático, anti-bot, validação em tempo real). Envie clicando no botão, premindo Enter ou chamando form.requestSubmit()e aguarde sempre por um sinal concreto de sucesso em vez de um tempo limite fixo.

Os formulários são a forma como a maioria das páginas úteis realmente funciona. Inícios de sessão, barras de pesquisa, fluxos de checkout, carregadores de ficheiros, assistentes de integração em várias etapas: se automatizar a web para testes ou scraping, mais cedo ou mais tarde terá de lidar com um formulário. Um fluxo de trabalho de envio de formulário do Puppeteer parece enganosamente simples à primeira vista, mas depois choca-se com as realidades de um site moderno: re-renderização de aplicações de página única, honeypots ocultos, campos de entrada apenas com rótulo, editores presos em iframes e JavaScript que discretamente descarta a sua entrada porque nunca viu um evento keydown .

Um formulário HTML é um <form> elemento que envolve <input>, <select>, <textarea>e controlos semelhantes, com um action atributo e um gatilho de envio que envia os dados recolhidos para processamento. Essa é a parte fácil. A parte difícil é fazer com que um script do Chrome sem interface gráfica se comporte de forma suficientemente semelhante a uma pessoa para que a página aceite efetivamente o envio e devolva uma resposta utilizável.

Este guia é a folha de referência que eu gostaria de ter tido quando comecei a enviar scripts do Puppeteer para produção. Vamos escolher a API certa para a tipagem, definir seletores estáveis, percorrer três estratégias de envio e quando cada uma falha, abordar todos os tipos de entrada comuns (incluindo seletores de ficheiros personalizados e editores de texto rico), aguardar o sinal de sucesso correto, validar o resultado e terminar com uma lista de verificação de depuração para a temida falha silenciosa.

Por que é que automatizar o envio de formulários com o Puppeteer é mais difícil do que parece

Os formulários controlam as partes mais valiosas da web moderna: criação de contas, resultados de pesquisa, painéis de controlo, downloads pagos. Eles também concentram todos os pontos críticos da automação de navegadores num único lugar. Um único script de envio de formulário do Puppeteer pode ter de lidar com entradas React ou Vue que ignoram uma value , validação que é acionada a cada tecla pressionada, rótulos apenas ARIA sem id, campos honeypot ocultos, elementos fora do ecrã nos quais não se pode clicar e sandboxes iframe para texto rico. Se assumir que um formulário é apenas HTML estático, o seu script irá falhar silenciosamente. Os padrões abaixo assumem que não é.

Configuração do projeto: Node.js, ESM e uma instalação funcional do Puppeteer

Crie uma pasta nova e execute npm init -y. Defina "type": "module" em package.json para que a sintaxe import funciona, depois instale o pacote completo com npm install puppeteer. Isso fornece um binário Chromium compatível, pelo que não necessita de um navegador separado. Utilize puppeteer-core em vez disso se pretender ligar-se a uma instalação existente do Chrome. Antes de escrever um único seletor, faça um teste rápido para verificar se tudo está ligado:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();

Se o título de uma página real for exibido, está tudo bem. Execute com headless: false enquanto depura e, depois, mude para 'new' assim que o script estiver estável.

Escolher o método de preenchimento certo: page.type vs Locator.fill vs injeção de valor bruto

O Puppeteer oferece três maneiras de inserir texto num campo, e a escolha tem consequências reais tanto para a velocidade quanto para a detecção de bots. Observe que, com base na documentação atual do Puppeteer no momento da redação deste artigo, não existe um método de nível superior page.fill() na Page classe, tal como o Playwright expõe; a ação equivalente encontra-se na API do Locator do Puppeteer através de page.locator(selector).fill(value).

Método

Eventos disparados

Velocidade

Quando recorrer a ela

page.type(selector, value)

keydown, keypress, input, keyup por personagem

Lento

Validação em tempo real, autocompletar, monitorização anti-bot, sugestões de pesquisa

page.locator(sel).fill(value)

input, change (única vez)

Rápido

Só precisa do valor final no campo

$eval(sel, el => el.value = ...)

Nenhum, a menos que os despache

Mais rápido

Formulários em massa onde a página não escuta as teclas pressionadas

Se seguir a rota $eval , despacha new Event('input', { bubbles: true }) depois, para que o React ou o Vue perceba realmente a alteração.

Selecionar campos de formulário com seletores estáveis

Um script de envio de formulário do Puppeteer só é executado se os seus seletores sobreviverem a uma reimplantação. Classifique as suas opções:

  1. #id quando um id existir e parecer estável.
  2. [name="..."] para qualquer <input name> que envie um POST para um backend, uma vez que o nome faz parte do contrato.
  3. [data-testid="..."] ou outros data-* hooks adicionados explicitamente para automação.
  4. aria-label e label[for] cadeias para interfaces de utilizador que privilegiam a acessibilidade.
  5. seletores de atributos CSS como input[type="email"] apenas quando o formulário tiver exatamente um campo desse tipo.
  6. XPath como último recurso, quando precisar de correspondência de texto, como //button[contains(., "Sign in")].

Evite nomes de classes gerados automaticamente, como .css-1q8r9j. Escolher CSS em vez de XPath geralmente compensa em termos de clareza e velocidade, mas o XPath é inestimável quando tem de se basear em texto visível.

Exemplo completo: pesquisar no Yelp por localização

A barra de pesquisa do Yelp usa duas entradas de texto: #find_desc para o que se procura e #dropperText_Mast para onde. Locator fill funciona bem aqui; o formulário não precisa de eventos por tecla.

await page.goto('https://www.yelp.com');
await page.locator('#find_desc').fill('coffee');
await page.locator('#dropperText_Mast').fill('Berlin, Germany');

await Promise.all([
  page.waitForNavigation({ waitUntil: 'networkidle2' }),
  page.click('button[type="submit"]'),
]);

await page.waitForSelector('h3 a.businessName__09f24__HG_pC', { timeout: 10000 });

O Promise.all padrão dispara o click e o ouvinte de navegação de forma atómica, por isso nunca perde o evento de navegação porque este foi resolvido antes de a espera ser registada.

Exemplo completo: iniciar sessão no GitHub com seletores mistos

A página de login do GitHub é um exercício útil porque os três campos usam estilos de seletor diferentes: id no nome de utilizador, name na palavra-passe e type no botão de envio.

await page.goto('https://github.com/login');
await page.type('input[id="login_field"]', process.env.GH_USER);
await page.type('input[name="password"]', process.env.GH_PASS);

await Promise.all([
  page.waitForNavigation(),
  page.click('input[type="submit"]'),
]);

Utilizo deliberadamente page.type aqui. As páginas de login tendem a identificar sessões que preenchem credenciais muito rapidamente, e as teclas digitadas caractere a caractere deixam um rasto com um aspeto mais humano. Nunca codifique credenciais de forma rígida; obtenha-as a partir de variáveis de ambiente.

Três métodos fiáveis do Puppeteer para enviar formulários (e quando cada um falha)

Assim que os campos estiverem preenchidos, tem três opções válidas:

  1. Clique no botão de envio com page.click('button[type="submit"]'). O padrão. Falha quando o botão está oculto, fora do ecrã ou coberto por um banner fixo. Resolva com page.waitForSelector(sel, { visible: true }) primeiro.
  2. Pressione Enter com await page.keyboard.press('Enter') depois de selecionar um campo. Funciona em quase todas as caixas de pesquisa e formulários de login. Falha quando a página intercepta a tecla Enter para autocompletar ou quando nenhum campo está selecionado.
  3. Chamar form.requestSubmit() through page.$eval('form', f => f.requestSubmit()). Ignora completamente os manipuladores de clique e executa a validação nativa, o que é útil quando o botão visível é renderizado de forma personalizada e não é fiável. Falha quando um manipulador JS personalizado interrompe o envio real e apenas escuta o clique.

Escolha com base no comportamento, não no hábito.

Lidando com todos os tipos de entrada comuns

Para além de simples campos de texto, os formulários reais combinam caixas de seleção, botões de opção, menus suspensos, controles deslizantes, datas, ficheiros e texto rico. Cada um tem um caminho ideal para o Puppeteer e algumas armadilhas. As próximas quatro subsecções abordam-nos com padrões que podem ser copiados e colados.

Caixas de seleção e botões de opção

Para caixas de seleção e botões de opção nativos, page.click é o seu aliado; alterna o estado e dispara os eventos certos. Trate os grupos de botões de opção pelo seu atributo, não apenas pela sua posição.

await page.click('input[type="checkbox"][name="newsletter"]');
await page.click('input[type="radio"][name="plan"][value="pro"]');

const isChecked = await page.$eval(
  'input[name="newsletter"]',
  el => el.checked,
)

Leia sempre checked de volta; os wrappers com estilo podem ignorar o clique sem alterar o estado.

Selecione menus suspensos (seleção única e múltipla)

Os <select> são o caso mais fácil. Use page.select, que aceita a opção value, e não o rótulo visível. Para seleções múltiplas, passe uma matriz. Os seletores de países podem ser enormes; um exemplo comum do tutorial do ScrapeOps usa uma lista de cerca de 248 opções de países, e a mesma assinatura de chamada lida com todas elas.

await page.select('select#country', 'DE');
await page.select('select#languages', 'en', 'de', 'fr');

Os menus suspensos JS personalizados (pense em <div role="listbox">) requerem uma sequência de cliques: clique no gatilho, aguarde o painel de opções, clique na opção correspondente pelo texto visível via XPath.

Seletores de data e controles deslizantes de intervalo

O <input type="date"> aceita YYYY-MM-DD e funciona bem com page.type ou Locator fill. Os widgets de calendário personalizados requerem uma sequência de cliques no pop-up. Para um controlador deslizante de intervalo, defina o valor através do DOM e dispache os eventos; caso contrário, a página nunca será re-renderizada. No exemplo do controlador deslizante abaixo, definimos o controlador para 85% antes de capturar a imagem:

await page.$eval('input[type="range"]', el => {
  el.value = 85;
  el.dispatchEvent(new Event('input', { bubbles: true }));
  el.dispatchEvent(new Event('change', { bubbles: true }));
});

Editores de texto rico baseados em Contenteditable e iframe

Os editores de texto rico apresentam-se em duas formas. Uma contenteditable div aceita o Locator fill diretamente. Editores hospedados em iframe, como o CKEditor ou o TinyMCE, estão em ambiente isolado; tem de mudar primeiro o contexto através de ElementHandle.contentFrame() antes de poder encontrar qualquer coisa no seu interior.

const frameHandle = await page.$('iframe.cke_wysiwyg_frame');
const frame = await frameHandle.contentFrame();
await frame.locator('body').fill('Hello from Puppeteer.');

Se um seletor devolver null dentro da página principal, suspeite de um iframe antes de suspeitar de um erro ortográfico.

Carregamento de ficheiros: campos de entrada visíveis vs. botões de navegação personalizados

Para um campo visível <input type="file">, obtenha o seu identificador nativo com page.$ e chame uploadFile com caminhos absolutos. Vários ficheiros são apenas argumentos adicionais. Aviso importante: uploadFile não verifica se o ficheiro existe realmente. Um erro de digitação no caminho falha silenciosamente, o formulário é enviado sem anexo e você passa duas horas a culpar os seus seletores. Valide os caminhos no código.

import { existsSync } from 'node:fs';
import { resolve } from 'node:path';

const file = resolve('./uploads/report.pdf');
if (!existsSync(file)) throw new Error(`Missing: ${file}`);

const input = await page.$('input[type="file"]');
await input.uploadFile(file);

Quando a interface visível for um botão «Procurar» personalizado que oculta o campo de entrada real, use page.waitForFileChooser. Registe primeiro o ouvinte e, em seguida, acione o clique que abre a caixa de diálogo do sistema operativo:

const [chooser] = await Promise.all([
  page.waitForFileChooser(),
  page.click('button.upload-trigger'),
]);
await chooser.accept([file]);

Estratégias de espera após o envio

setTimeout e page.waitForTimeout não são estratégias de espera; são ímanes de erros. Escolha um sinal de sucesso concreto:

  • waitForNavigation: recarregamento clássico de toda a página após o envio. Envolva com Promise.all para que possa antecipar o clique e a espera.
  • waitForResponse: POSTs SPA para uma API. Aguarde até que a URL ou o estado correspondente seja devolvido.
  • waitForSelector: um banner de sucesso, redirecionar o elemento de destino ou uma nova linha numa lista.
  • waitForNetworkIdle: o «catch-all» do Puppeteer moderno quando o sinal de sucesso é impreciso e a página simplesmente estabiliza.

Para um envio de pesquisa típico, observe o item de resultado; para um login, observe o elemento de navegação do painel. Ambos são sinais mais fortes do que uma alteração de URL.

Validar o sucesso ou a falha programaticamente

Um envio que retorna 200 não é o mesmo que um envio que funcionou. Leia a página a seguir.

  • Inspecione um contêiner de erro conhecido, por exemplo .error-message, e trate qualquer conteúdo de texto como uma falha grave: Epic sadface: Username is required é uma mensagem de validação real que verá no site de demonstração da Sauce Labs.
  • Execute a validação nativa através de el.checkValidity() via $eval para detetar campos que o utilizador preencheu incorretamente antes de clicar.
  • Compare page.url() antes e depois do envio quando esperar um redirecionamento.
  • Em caso de falha, faça uma captura de ecrã com await page.screenshot({ path: 'fail.png', fullPage: true }) para que tenha provas na CI.

Tratamento de caixas de diálogo, confirmações e alertas em JS no envio

Alguns formulários ainda lançam um confirm() antes do envio. O Puppeteer apresenta-os como dialog eventos, e deve registar o ouvinte antes do clique que aciona a caixa de diálogo; caso contrário, a caixa de diálogo irá bloquear a página.

page.on('dialog', async dialog => {
  console.log('dialog:', dialog.message());
  await dialog.accept();
});
await page.click('button#delete-account');

Use dialog.dismiss() para cancelar e dialog.message() para registar o que a página realmente solicitou.

Evitar bloqueios: anti-bot, honeypots e CAPTCHA em páginas de formulários

Os formulários de login e registo são onde a lógica anti-bot se concentra. Três ameaças reais:

  1. Honeypots. <input type="hidden"> ou visualmente ocultos que um utilizador real nunca toca. Se o seu script preencher cegamente todas as entradas, o servidor rejeita-o. Leia o estilo calculado do campo ou type e ignore tudo o que não estiver visível.
  2. Fingerprinting. O Vanilla Puppeteer revela navigator.webdriver = true e outros indícios. Com base em testes da comunidade no momento da redação deste artigo, puppeteer-extra-plugin-stealth corrige a maioria deles, embora os fornecedores de deteção continuem a iterar.
  3. CAPTCHA. De acordo com a documentação atual dos projetos relevantes, pode combinar puppeteer-extra com puppeteer-extra-plugin-recaptcha e um token pago do tipo 2captcha para lidar com reCAPTCHA e hCaptcha, mas a cobertura e a fiabilidade variam ao longo do tempo. Se continuar a perder esta batalha, a nossa API Scraper é uma solução mais rápida do que ajustar as flags de camuflagem semanalmente.

Receita de depuração: o que fazer quando um formulário se recusa a enviar

Quando um script de envio de formulário do Puppeteer não faz nada silenciosamente, siga esta lista por ordem:

  1. Execute com headless: false e slowMo: 100 para que possas ver o que o navegador realmente faz.
  2. Abra devtools: true e observe os separadores Rede e Consola para ver se há pedidos bloqueados ou erros gerados.
  3. Verifique required e pattern atributos, além de checkValidity() em cada campo; a validação nativa pode bloquear o envio antes de qualquer manipulador ser acionado.
  4. Verifique se há elementos fora do ecrã ou desativados; desloque-se para os visualizar com el.scrollIntoView() antes de clicar.
  5. Verifique se existe um wrapper iframe; se for o caso, mude de contexto com contentFrame().
  6. Ative a interceção de pedidos para registar todos os POST de saída e confirme se o pedido de envio chegou a sair do navegador.

Lista de verificação de produção para um script de envio de formulário do Puppeteer

Antes do lançamento:

  • Use seletores estáveis, dê preferência a id, namee data-testid.
  • Envolva cada navegação em Promise.all com uma espera concreta.
  • Defina valores timeout ; nunca defina o padrão como infinito.
  • Envolva a execução em tentativas de repetição com recuo exponencial.
  • Faça uma captura de ecrã em cada falha e envie-a para o seu repositório de registos.
  • Emita logs estruturados, execute headless: 'new'e alterne proxies para qualquer destino voltado para o público.

Conclusão e próximos passos

Escolha o método de digitação de acordo com o que a página deteta, selecione o caminho de envio que corresponda ao comportamento do formulário, aguarde um sinal de sucesso real e capture uma imagem de ecrã de cada falha. A partir daqui, aprofunde-se nos tutoriais relacionados do Puppeteer sobre downloads de ficheiros, noções básicas de navegadores headless e na escolha entre seletores XPath e CSS.

Pontos-chave

  • Use page.locator(selector).fill(value) para velocidade e page.type quando a página monitoriza as teclas digitadas (preenchimento automático, anti-bot, validação em tempo real).
  • Envie clicando no botão, premindo Enter ou chamando form.requestSubmit(); escolha de acordo com o comportamento do formulário, não com o hábito.
  • Combine sempre a ação de envio com uma espera concreta (waitForNavigation, waitForResponse, waitForSelectorou waitForNetworkIdle) dentro de Promise.all.
  • Para uploads de ficheiros, valide o caminho você mesmo; uploadFile não o fará, e um erro de digitação falha silenciosamente.
  • Quando um formulário se recusa silenciosamente a enviar, execute o headful com slowMo, verifique required/pattern a validação, procure por honeypots e verifique se há wrappers iframe.

Perguntas frequentes

O Puppeteer tem um método page.fill() como o Playwright?

Não na Page classe. Com base na documentação atual do Puppeteer no momento da redação deste artigo, a fill ação reside na API Locator, pelo que se chama await page.locator(selector).fill(value) em vez de await page.fill(selector, value). O Locator fill aparentemente suporta input, textarea, select, e checkbox , e aguarda que o elemento esteja pronto para ser acionado antes de atribuir o valor.

Como posso enviar um formulário do Puppeteer sem um botão de envio visível?

Use form.requestSubmit() através de page.$eval('form#login', f => f.requestSubmit()). Isso aciona a validação nativa do HTML5 e dispara o submit sem necessidade de um elemento clicável. Como alternativa, coloque o foco em qualquer campo dentro do formulário com page.focus() e chame await page.keyboard.press('Enter'), que a maioria dos formulários de pesquisa e de início de sessão aceita.

Como posso aguardar a conclusão do envio de um formulário numa aplicação de página única?

Aguarde a chamada da API subjacente em vez de uma navegação. Use await page.waitForResponse(res => res.url().includes('/api/submit') && res.status() === 200)ou page.waitForNetworkIdle({ idleTime: 500 }) se a SPA disparar várias solicitações paralelas. Combine qualquer uma delas com waitForSelector no elemento de sucesso para que também confirme que a IU apresentou o resultado.

Como faço para carregar vários ficheiros num único campo de entrada com o Puppeteer?

Passe cada caminho absoluto como um argumento separado para uploadFile: await input.uploadFile(file1, file2, file3). O destino <input type="file"> deve ter o multiple , caso contrário, o navegador mantém apenas a última entrada. Para botões de navegação personalizados, chame chooser.accept([file1, file2]) no seletor de ficheiros devolvido por waitForFileChooser.

O Puppeteer consegue preencher formulários dentro de um iframe?

Sim, mas deve primeiro mudar de contexto. Obtenha o elemento iframe com page.$('iframe#payment'), depois chame await handle.contentFrame() para obter um Frame objeto. A partir daí, todos os métodos que chamaria no page (type, click, locator, waitForSelector) está disponível no frame e é executado no âmbito do seu documento.

Conclusão

Um script fiável do Puppeteer para enviar formulários é, em grande parte, uma questão de preferência. Escolha o método de digitação que corresponda ao que a página está à espera, o caminho de envio que corresponda à forma como o formulário é efetivamente acionado e a espera que corresponda ao sinal de sucesso que pode realmente observar. A mecânica não é complexa; a disciplina está em não ignorar nenhuma dessas três escolhas.

Os padrões neste guia cobrem os casos que irá encontrar em 90% dos sites públicos. Os últimos 10% — páginas de login com identificação agressiva, checkout protegido por CAPTCHA, WAFs anti-bot que alteram o comportamento semanalmente — são um caso à parte. Ajustar sinalizadores de camuflagem na sua própria frota de navegadores é um verdadeiro trabalho de engenharia, e o custo de manutenção acumula-se.

Se preferir dedicar esse tempo ao fluxo de dados em vez de à camada de pedidos, dê uma vista de olhos na WebScrapingAPI. Ela gere a rotação de proxies, a identificação de navegadores e a resolução de CAPTCHAs por trás de um único ponto de acesso, para que o seu script Puppeteer possa manter a sua lógica de preenchimento de formulários e simplesmente delegar as partes que são mais difíceis de manter do que interessantes. Seja como for, crie agora o hábito de enviar e verificar e o seu eu futuro vai agradecer-lhe.

Sobre o autor
Mihnea-Octavian Manolache, Desenvolvedor Full Stack @ WebScrapingAPI
Mihnea-Octavian ManolacheDesenvolvedor Full Stack

Mihnea-Octavian Manolache é engenheiro Full Stack e DevOps na WebScrapingAPI, onde desenvolve funcionalidades do produto e mantém a infraestrutura que garante o bom funcionamento da plataforma.

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.