Tecnologias e proteções 16 min de leitura

Scraping de sites dinâmicos: duas abordagens

Duas formas de fazer scraping de sites dinâmicos: engenharia reversa das requisições XHR à API e navegadores headless — vantagens, desvantagens e critérios de escolha.

EW
Equipe Web-Scraping.biz
Coleta de dados para as demandas do negócio
Publicado: 2 março 2025

Os sites modernos raramente entregam o HTML já pronto com os dados. O catálogo de produtos, o feed, os preços, as avaliações — quase tudo costuma ser carregado depois que a página já abriu, via JavaScript. Para um scraper, isso significa que uma simples requisição GET da página e o parsing posterior do HTML vão devolver um esqueleto vazio, sem dados.

Há duas formas radicalmente diferentes de resolver esse problema:

  1. Interceptar as requisições à API — localizar as requisições com as quais o próprio navegador obtém os dados e repeti-las diretamente, sem navegador.
  2. Emulação completa do navegador — iniciar um navegador real (ou headless), deixar que ele renderize o JS e operar sobre a página como um usuário: clicar, rolar, arrastar elementos.

A primeira abordagem é mais rápida e consome menos recursos; a segunda é mais universal e mais robusta diante de lógicas fora do comum. Na prática, é frequente combinar as duas.


Abordagem 1. Interceptar e emular as requisições à API

A ideia

Quando uma página «completa» os seus dados na hora, quase sempre dispara requisições HTTP em segundo plano (XHR/fetch) a uma API interna que devolve JSON. Se você localizar esse endpoint e reproduzir a requisição com os cabeçalhos, cookies e tokens corretos, poderá obter os dados diretamente, sem passar pela renderização. É dezenas de vezes mais rápido e não exige navegador.

Como localizar a API

  1. Abra o DevTools (F12) → aba Network.
  2. Filtre por Fetch/XHR.
  3. Role a página, clique em «Mostrar mais», troque de categoria — enfim, provoque o carregamento de dados.
  4. Localize a requisição em cuja resposta esteja o JSON que interessa (produtos, preços etc.).
  5. Examine-a: URL, método, parâmetros da requisição, cabeçalhos, corpo e cookies.
  6. Clique com o botão direito → Copy → Copy as cURL — um ótimo ponto de partida: dá para importá-lo no Postman ou traduzi-lo direto para código.

Tokens: CSRF, sessões e autenticação

A principal dificuldade dessa abordagem é que a requisição quase nunca vai «nua». O servidor espera um conjunto de dados de validação e, sem eles, devolve 401, 403 ou 419.

Token CSRF (Cross-Site Request Forgery). Proteção contra a falsificação de requisições entre sites. O servidor emite um token aleatório que o cliente precisa devolver nas requisições que alteram dados (e, às vezes, também nas de leitura). Onde costuma ficar:

  • em <meta name="csrf-token" content="...">, dentro do HTML da página;
  • em um campo oculto do formulário <input type="hidden" name="_token" value="...">;
  • em um cookie (muitas vezes XSRF-TOKEN) que depois precisa ser duplicado no cabeçalho X-CSRF-Token ou X-XSRF-TOKEN.

O esquema é o seguinte: primeiro carrega-se a página normal, extraem-se o token e os cookies de sessão e depois se incorporam eles à requisição à API.

Cookies de sessão. No primeiro acesso, o servidor envia Set-Cookie (por exemplo, sessionid, PHPSESSID, laravel_session). É preciso preservá-los entre requisições; para isso usa-se um objeto de sessão (requests.Session, httpx.Client), que faz isso automaticamente.

Autenticação (Bearer / JWT / chave de API). Se os dados estão atrás de um login, o cabeçalho costuma levar Authorization: Bearer <token>. Os tokens JWT são obtidos pelo endpoint de login e depois anexados a cada requisição.

Outros campos de proteção. X-Requested-With: XMLHttpRequest (muitas vezes obrigatório nos endpoints AJAX), Referer, Origin e, às vezes, parâmetros assinados (signature, nonce, timestamp) gerados pelo JavaScript do frontend.

Quando essa abordagem falha. Se o token ou a assinatura da requisição forem gerados por um JavaScript ofuscado no próprio navegador (ou em WASM), reproduzi-lo do lado do servidor fica muito difícil. É o sinal de que convém passar para a segunda abordagem — a emulação do navegador, onde o JS se executa sozinho.

Exemplo: Python + requests (com CSRF e paginação)

python
import re
import requests

session = requests.Session()
BASE = "https://example-shop.com"

# 1. Carregamos a página para obter o token CSRF e os cookies de sessão
resp = session.get(f"{BASE}/catalog", headers={
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
})

# O CSRF pode estar em uma tag meta...
m = re.search(r'name="csrf-token"\s+content="([^"]+)"', resp.text)
csrf = m.group(1) if m else session.cookies.get("XSRF-TOKEN")

headers = {
    "X-CSRF-Token": csrf,
    "X-Requested-With": "XMLHttpRequest",
    "Referer": f"{BASE}/catalog",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    "Accept": "application/json",
}

# 2. Consultamos a API interna página a página
all_items = []
page = 1
while True:
    r = session.get(
        f"{BASE}/api/products",
        params={"category": "phones", "page": page, "per_page": 48},
        headers=headers,
    )
    r.raise_for_status()
    payload = r.json()
    items = payload.get("items", [])
    if not items:
        break
    all_items.extend(items)
    page += 1

for it in all_items:
    print(it["title"], it["price"])

Exemplo: Python + httpx (async, mais rápido com grandes volumes)

python
import asyncio
import httpx

async def fetch_page(client, page):
    r = await client.get("/api/products", params={"page": page, "per_page": 48})
    return r.json().get("items", [])

async def main():
    async with httpx.AsyncClient(base_url="https://example-shop.com",
                                 headers={"X-Requested-With": "XMLHttpRequest"}) as client:
        tasks = [fetch_page(client, p) for p in range(1, 11)]
        results = await asyncio.gather(*tasks)
    items = [x for chunk in results for x in chunk]
    print(len(items))

asyncio.run(main())

Exemplo: Node.js + fetch

javascript
const csrf = "..."; // extraído do HTML/cookies de antemão

const res = await fetch("https://example-shop.com/api/products?page=1&per_page=48", {
  headers: {
    "X-CSRF-Token": csrf,
    "X-Requested-With": "XMLHttpRequest",
    "Accept": "application/json",
    "Cookie": "sessionid=abc123; XSRF-TOKEN=" + csrf,
  },
});

const data = await res.json();
data.items.forEach(item => console.log(item.title, item.price));

Abordagem 2. Emulação completa do navegador

A ideia

Iniciamos um motor real (Chromium, Firefox, WebKit): ele baixa a página, executa todo o JS e renderiza o DOM. A partir daí, operamos sobre a página igual a uma pessoa: esperamos os elementos aparecerem, clicamos, rolamos, arrastamos controles deslizantes. Todos os tokens, as assinaturas e os scripts anti-bot se executam sozinhos — não precisamos reproduzi-los.

Desvantagens: é uma ordem de grandeza mais lento, voraz em CPU/RAM e mais fácil de ser detectado pelos sistemas anti-bot (embora isso se combata com «modos stealth» específicos).

Com o que emular

  • Selenium — o padrão mais veterano; suporta Python, Java, C#, JavaScript e Ruby. Controla navegadores reais por meio do WebDriver.
  • Playwright — um framework moderno da Microsoft. Python, JavaScript/TS, .NET, Java. Chromium, Firefox e WebKit «prontos para usar», autoespera inteligente de elementos e uma prática interceptação de requisições de rede.
  • Puppeteer — Node.js; na origem, só Chromium (há suporte experimental para Firefox). Muito rápido e maduro para o Chrome.

Ações sobre a página

A seguir — as mesmas quatro ações (clique, rolagem, rolagem até um elemento e manter o botão do mouse pressionado + movimento) em diferentes tecnologias. Manter pressionado + mover é a base do drag-and-drop, dos controles deslizantes e dos captchas de slider.

Playwright (Python)

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://example-shop.com/catalog")

    # CLIQUE
    page.click("button.load-more")

    # ROLAGEM com a roda do mouse
    page.mouse.wheel(0, 2000)

    # ROLAGEM até um elemento específico
    page.locator("footer").scroll_into_view_if_needed()

    # MANTER pressionado + MOVIMENTO (drag / control deslizante)
    box = page.locator(".slider-handle").bounding_box()
    start_x = box["x"] + box["width"] / 2
    start_y = box["y"] + box["height"] / 2
    page.mouse.move(start_x, start_y)
    page.mouse.down()                                   # botão pressionado
    page.mouse.move(start_x + 200, start_y, steps=25)   # deslocamento suave (25 passos)
    page.mouse.up()                                     # soltamos

    browser.close()

Playwright (JavaScript/Node)

javascript
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto('https://example-shop.com/catalog');

  // CLIQUE
  await page.click('button.load-more');

  // ROLAGEM
  await page.mouse.wheel(0, 2000);

  // ROLAGEM até o elemento
  await page.locator('footer').scrollIntoViewIfNeeded();

  // MANTER PRESSIONADO + MOVIMENTO
  const box = await page.locator('.slider-handle').boundingBox();
  await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
  await page.mouse.down();
  await page.mouse.move(box.x + 200, box.y, { steps: 25 });
  await page.mouse.up();

  await browser.close();
})();

Selenium (Python) — com ActionChains

python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example-shop.com/catalog")
wait = WebDriverWait(driver, 10)

# CLIQUE (esperando que fique clicável)
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.load-more")))
btn.click()

# ROLAGEM
driver.execute_script("window.scrollBy(0, 2000)")

# ROLAGEM até o elemento
footer = driver.find_element(By.CSS_SELECTOR, "footer")
driver.execute_script("arguments[0].scrollIntoView({block:'center'})", footer)

# MANTER PRESSIONADO + MOVIMENTO
handle = driver.find_element(By.CSS_SELECTOR, ".slider-handle")
(ActionChains(driver)
    .click_and_hold(handle)     # botão pressionado
    .move_by_offset(200, 0)     # deslocamos 200 px para a direita
    .pause(0.3)
    .release()                  # soltamos
    .perform())

driver.quit()

Selenium (Java)

java
WebDriver driver = new ChromeDriver();
driver.get("https://example-shop.com/catalog");

// CLIQUE
driver.findElement(By.cssSelector("button.load-more")).click();

// ROLAGEM
((JavascriptExecutor) driver).executeScript("window.scrollBy(0, 2000)");

// MANTER PRESSIONADO + MOVIMENTO
WebElement handle = driver.findElement(By.cssSelector(".slider-handle"));
new Actions(driver)
    .clickAndHold(handle)
    .moveByOffset(200, 0)
    .release()
    .perform();

Puppeteer (Node.js)

javascript
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  await page.goto('https://example-shop.com/catalog');

  // CLIQUE
  await page.click('button.load-more');

  // ROLAGEM
  await page.evaluate(() => window.scrollBy(0, 2000));

  // ROLAGEM até o elemento
  await page.$eval('footer', el => el.scrollIntoView());

  // MANTER PRESSIONADO + MOVIMENTO
  const handle = await page.$('.slider-handle');
  const box = await handle.boundingBox();
  await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
  await page.mouse.down();
  await page.mouse.move(box.x + 200, box.y, { steps: 25 });
  await page.mouse.up();

  await browser.close();
})();

Modos de mascaramento (stealth): escapar da detecção da automação

Um navegador headless executado «como está» se distingue com facilidade de um real. Os sistemas anti-bot (Cloudflare, DataDome, PerimeterX/HUMAN, Akamai etc.) verificam dezenas de sinais e, se pelo menos parte deles denunciar a automação, chega o captcha, o challenge ou o bloqueio. O modo stealth é um conjunto de patches e técnicas que mascaram esses indícios.

Por quais indícios detectam

  • navigator.webdriver === true — a flag mais evidente, que o navegador controlado por WebDriver/CDP ativa automaticamente.
  • Artefatos de headless. Ausência de window.chrome, lista de plugins vazia (navigator.plugins), valores atípicos de navigator.languages, um renderizador WebGL do tipo SwiftShader/Google Inc. em vez de uma placa de vídeo real.
  • Fingerprinting. Canvas, WebGL, AudioContext e o conjunto de fontes geram uma «impressão digital» estável do ambiente; em um headless padrão, ela fica suspeitamente genérica.
  • Impressão digital TLS/JA3. No nível da própria conexão HTTP, o «aperto de mãos» de um cliente Python ou Node difere do do Chrome — e isso é detectado antes mesmo de o JS ser executado (também vale para a abordagem 1).
  • Comportamento. Cliques instantâneos sem movimento do mouse, tempos perfeitamente regulares, saltar direto para uma página interna sem navegação prévia — nada disso é humano.
  • Reputação do IP. As faixas de data centers (AWS, Hetzner etc.) estão marcadas; os endereços residenciais e móveis levantam menos suspeitas.

Ferramentas já disponíveis

  • puppeteer-extra + puppeteer-extra-plugin-stealth (Node) — o pacote mais conhecido; oculta navigator.webdriver, corrige WebGL/plugins/languages e dezenas de outros «vazamentos».
  • playwright-extra com o mesmo plugin stealth — o equivalente para o Playwright em Node.
  • undetected-chromedriver (Python, sobre o Selenium) — um ChromeDriver com patches que supera muitas verificações do Cloudflare. Sua evolução é o nodriver (sem o protocolo webdriver, puramente via CDP).
  • SeleniumBase no modo UC (--uc) — um invólucro sobre o Selenium com antidetecção integrada.
  • rebrowser-patches — patches de baixo nível para o runtime do Puppeteer/Playwright que fecham vazamentos de CDP mais sutis.

Importante: nenhum plugin stealth oferece garantias. Os sistemas anti-bot se atualizam o tempo todo, e o que passava ontem pode ser detectado amanhã. É uma «corrida armamentista», não uma configuração de uma única vez.

Exemplos

Python — undetected-chromedriver:

python
import undetected_chromedriver as uc

options = uc.ChromeOptions()
options.add_argument("--lang=pt-BR")
# para a antidetecção normalmente NÃO se usa headless, ou se usa o modo novo:
# options.add_argument("--headless=new")

driver = uc.Chrome(options=options)
driver.get("https://example-shop.com/catalog")
print(driver.title)
driver.quit()

Node — puppeteer-extra + stealth:

javascript
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

(async () => {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  // UA verossímil e cabeçalhos coerentes
  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.goto('https://example-shop.com/catalog');
  await browser.close();
})();

Node — playwright-extra + stealth:

javascript
const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();

chromium.use(stealth);

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto('https://example-shop.com/catalog');
  await browser.close();
})();

Técnicas manuais (além dos plugins ou no lugar deles)

  • Ocultar webdriver e ajustar o ambiente. Via CDP/init script antes de a página carregar:
python
# Playwright (Python): executa em cada documento novo ANTES dos scripts do site
page.add_init_script(
    "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
)
  • Novo modo headless. Nas versões recentes do Chrome, a flag --headless=new fica mais perto de um navegador normal do que o headless antigo; às vezes sai melhor rodar direto no modo não headless sob uma tela virtual (Xvfb).
  • Perfil persistente. Iniciar com user_data_dir conserva os cookies e o «aquecimento» da sessão entre execuções — fica menos parecido com um bot recém-criado.
  • Comportamento humano. Pausas aleatórias, movimento do mouse seguindo uma curva (Bézier), rolagem em pequenos trechos em vez de um único salto. Para gerar trajetórias existem bibliotecas como pyautogui/bezier ou o parâmetro steps embutido no mouse.move.
  • Proxies. Os proxies residenciais e móveis com rotação reduzem bastante a proporção de challenges frente aos IPs de data centers.

Mascaramento (stealth) para a abordagem 1 (sem navegador)

A interceptação da API também é detectada — pela impressão digital TLS do cliente HTTP. Para que a requisição pareça, já no «aperto de mãos», um Chrome real, usam-se clientes que falseiam a impressão digital TLS:

python
# curl_cffi sabe imitar o TLS/JA3 de um navegador específico
from curl_cffi import requests

r = requests.get(
    "https://example-shop.com/api/products?page=1",
    impersonate="chrome124",   # falseamos o aperto de mãos do Chrome 124
)
print(r.json())

Alternativas: tls-client (Python/Go), curl-impersonate (um binário do sistema). Isso costuma resolver o problema quando um requests «nu» recebe um 403 enquanto no navegador essa mesma página abre.


O que cada biblioteca sabe fazer

A linha divisória principal: se a ferramenta executa JavaScript e se sabe imitar as ações do mouse. Os clientes HTTP e os parsers de HTML não fazem nenhuma das duas coisas — só servem para sites estáticos ou para a primeira abordagem (a emulação da API).

Biblioteca Linguagem Renderização de JS Ações (clique/scroll/drag) Uso
requests / httpx Python Não Não Cliente HTTP
aiohttp Python Não Não HTTP assíncrono
BeautifulSoup / lxml Python Não Não Parsing de HTML
Scrapy Python Não (requer o plugin Splash/Playwright) Não Framework de crawling
Selenium Python/Java/C#/JS/Ruby Sim Sim Controle do navegador
Playwright Python/JS/.NET/Java Sim Sim Controle do navegador
Puppeteer Node.js Sim (Chromium) Sim Controle do navegador
Cypress JS Sim Sim (mas voltado a testes e2e) Testes
axios / fetch / got Node.js Não Não Cliente HTTP
cheerio Node.js Não Não Parsing de HTML (estilo jQuery)
Colly Go Não Não Framework de crawling
chromedp / rod Go Sim Sim Controle do navegador
HtmlUnit Java Parcial/instável Limitadas Navegador headless
jsoup Java Não Não Parsing de HTML

Conclusão em resumo:

  • Só precisa repetir uma requisição à APIrequests/httpx (Python), fetch/got (Node), Colly (Go).
  • Fazer o parsing de um HTML já obtidoBeautifulSoup/lxml, cheerio, jsoup.
  • Precisa de renderização de JS e ações com o mousePlaywright, Selenium, Puppeteer, chromedp/rod.
  • HtmlUnit dá conta de um pouco de JS, mas tropeça com frequência nas SPAs modernas — para uma renderização séria, recorre-se ao Playwright/Selenium.

Caso prático: monitoramento de lojas online e rolagem infinita

É, provavelmente, a tarefa prática mais comum. Nos catálogos, os produtos raramente aparecem todos de uma vez: aplica-se lazy loading / rolagem infinita — as novas fichas são carregadas à medida que a página é rolada (ou ao clicar em «Mostrar mais»). Uma simples requisição do HTML só devolve a primeira «leva».

Há dois caminhos, e ambos são muito usados no monitoramento de preços e sortimento:

Caminho A (preferencial): interceptar a API de paginação

Ao rolar, a loja quase sempre chama algo parecido com /api/catalog?page=2&offset=48. Se for esse o caso — esqueça o navegador e colete os dados página a página de forma direta (veja a abordagem 1). É rápido, estável e escala para milhares de produtos. É assim que se constrói a maioria dos monitoramentos industriais: o navegador é usado uma única vez — para explorar a estrutura da API e os tokens, e a coleta em si é feita com um cliente HTTP.

Caminho B: renderização + rolagem quando a API está fechada

Se o endpoint estiver protegido por uma assinatura fora do trivial ou os dados forem gerados exclusivamente no cliente, não resta senão rolar com o navegador e recolher as fichas do DOM. O algoritmo: rolamos para baixo → esperamos o carregamento → contamos as fichas → repetimos enquanto a quantidade crescer.

python
from playwright.sync_api import sync_playwright

def scrape_catalog(url):
    products = []
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(url)

        prev_count = -1
        stable_rounds = 0

        while stable_rounds < 2:  # 2 rolagens «vazias» seguidas = chegamos ao fim
            # rolamos até o fundo
            page.mouse.wheel(0, 4000)
            page.wait_for_timeout(1500)  # damos tempo para carregar

            cards = page.locator(".product-card")
            count = cards.count()

            if count == prev_count:
                stable_rounds += 1
            else:
                stable_rounds = 0
            prev_count = count

        # recolhemos todas as fichas depois da rolagem completa
        cards = page.locator(".product-card")
        for i in range(cards.count()):
            card = cards.nth(i)
            products.append({
                "title": card.locator(".title").inner_text(),
                "price": card.locator(".price").inner_text(),
                "url":   card.locator("a").get_attribute("href"),
            })

        browser.close()
    return products

data = scrape_catalog("https://example-shop.com/catalog/phones")
print(f"Produtos coletados: {len(data)}")

A mesma técnica para o botão «Mostrar mais» — clicamos enquanto o botão existir:

python
while page.locator("button.load-more").count() > 0:
    page.click("button.load-more")
    page.wait_for_timeout(1200)

Híbrido: o melhor dos dois mundos

Playwright e Puppeteer sabem escutar o tráfego de rede. Dá para abrir a página no navegador (para que passem todos os tokens e as verificações anti-bot), mas obter os dados não do DOM, e sim das respostas dessa mesma API que o navegador chama ao rolar:

python
def handle_response(response):
    if "/api/products" in response.url:
        data = response.json()
        # guardamos o JSON já pronto: não é preciso fazer o parsing do HTML
        save(data["items"])

page.on("response", handle_response)
page.goto("https://example-shop.com/catalog")
# depois é só rolar: os dados «chegam» sozinhos ao manipulador

Costuma ser a opção ideal para o monitoramento: a robustez da emulação de navegador + um JSON limpo e estruturado em vez da frágil análise do layout.

Dicas práticas para o monitoramento

  • Espere pelos dados, não pelo relógio. Em vez de «dormir 1,5 segundo», use a espera pelo surgimento de um elemento (wait_for_selector) ou pela rede entrar em silêncio (wait_for_load_state("networkidle")) — é mais confiável e, muitas vezes, mais rápido.
  • Deduplicação. Com a rolagem infinita, algumas fichas podem ser lidas em duplicidade — colete por um id/url único.
  • Limite a frequência. Requisições agressivas demais sobrecarregam o site e provocam um bloqueio rápido. Adicione atrasos e use os pools de proxies com cuidado e dentro da lei.
  • Coloque a exploração prévia em cache. Descubra a estrutura da API e os tokens uma única vez; em produção, rode uma coleta HTTP leve e mantenha o navegador pesado como reserva.

Como escolher a abordagem

Critério Interceptar a API Emulação de navegador
Velocidade Muito alta Baixa
Consumo de recursos Mínimo Alto (CPU/RAM)
Complexidade de configuração Maior (engenharia reversa de tokens) Menor (tudo «como um usuário»)
Robustez diante de mudanças de layout Alta (depende da API) Média (depende dos seletores)
Superar tokens/assinaturas do cliente Difícil Automático
Escala por volume Excelente Limitada

Regra prática: verifique sempre primeiro se basta interceptar a API — é mais rápido, mais barato e mais estável. Passe para a emulação de navegador só quando a API estiver oculta atrás de criptografia do lado do cliente, protegida por uma lógica anti-bot complexa, ou quando for preciso reproduzir uma interação fora do trivial (drag-and-drop, controles deslizantes, formulários em etapas).