Scraping por linguagem 21 min de leitura

Web scraping em Java: guia completo

Guia completo de web scraping em Java: Jsoup, HtmlUnit, Selenium, multithreading e arquitetura industrial de scrapers e crawlers.

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

Guia abrangente sobre como extrair dados de páginas web em Java — do download simples de uma única página a um rastreador (crawler) multithread com proxy, TOR e a solução dos problemas mais comuns. A estrutura segue a ordem «do simples ao complexo»: cada seção se apoia na anterior.


Sumário

  1. Introdução: o que é o web scraping e seu marco legal
  2. Como baixamos a página: clientes HTTP
  3. Bibliotecas para o parsing do conteúdo
  4. Solução de problemas com acentos e cedilha (codificações)
  5. Obtenção do status da resposta e dos cabeçalhos
  6. Trabalho com cookies
  7. Trabalho com HTTPS/SSL
  8. Uso de proxies
  9. Scraping através do TOR
  10. Multithreading
  11. Renderização de JavaScript: sites dinâmicos
  12. Proteção anti-bot: User-Agent, atrasos, novas tentativas
  13. Armazenamento de URLs e filas
  14. Frameworks de crawling prontos para usar
  15. Vantagens e desvantagens da implementação em Java

1. Introdução

O web scraping é a extração automatizada de dados de páginas web. O processo se divide, em linhas gerais, em duas fases que convém não confundir:

  • Download (fetching / crawling) — obter o HTML (ou JSON/XML) via HTTP. Disso cuida o cliente HTTP.
  • Análise (parsing / extraction) — converter o HTML «bruto» em uma estrutura e selecionar os dados de interesse por meio de seletores. Disso cuida o parser.

Marco legal e ético

Antes de escrever uma única linha de código, convém ter em mente alguns pontos; não é aconselhamento jurídico, e sim uma higiene mínima:

  • robots.txt — arquivo na raiz do site (https://site.com/robots.txt) em que o proprietário indica o que os robôs podem rastrear. Quase nunca tem valor jurídico, mas ignorá-lo é falta de educação e um gatilho comum de bloqueios.
  • Os termos de uso (ToS) do site podem proibir expressamente a coleta automática. Descumpri-los representa um risco contratual e, em algumas jurisdições, algo mais sério.
  • Dados pessoais. A coleta de dados pessoais é regulada por lei (no Brasil, a LGPD; na UE, o GDPR). Aja com cautela.
  • Carga. Um scraping agressivo equivale a um DoS contra o servidor alheio. Adicione atrasos, limite o número de threads e respeite o cabeçalho Retry-After e os códigos 429/503.

Tecnicamente, Java permite quase tudo. A responsabilidade sobre «o quê» extrair e «com que finalidade» é sua.


2. Como baixamos a página

É o alicerce: sem uma resposta obtida corretamente, não há o que analisar. Java oferece várias opções de cliente HTTP.

2.1. java.net.http.HttpClient integrado (Java 11+)

O cliente padrão moderno. Não exige dependências e suporta HTTP/2 e os modos síncrono e assíncrono.

java
import java.net.URI;
import java.net.http.*;
import java.time.Duration;

HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .followRedirects(HttpClient.Redirect.NORMAL)
        .version(HttpClient.Version.HTTP_2)
        .build();

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com"))
        .timeout(Duration.ofSeconds(15))
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
        .header("Accept-Language", "pt-BR,pt;q=0.9")
        .GET()
        .build();

HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println("Status: " + response.statusCode());
String html = response.body();

Um detalhe importante sobre a codificação: BodyHandlers.ofString() sem argumento decodifica o corpo como UTF-8. Se o site usa outra codificação (por exemplo, ISO-8859-1), aparecerão caracteres ilegíveis (mojibake) — disso trata a seção 4. Para evitá-lo, é comum obter os bytes e determinar a codificação separadamente:

java
HttpResponse<byte[]> resp =
        client.send(request, HttpResponse.BodyHandlers.ofByteArray());
byte[] raw = resp.body();   // decodificamos depois, conhecendo o charset

2.2. OkHttp

Biblioteca de terceiros muito popular (Square). API confortável, pools de conexões, interceptors e um manejo simples de proxies e cookies. Uma boa opção «padrão» para o scraping sério.

java
// build.gradle: implementation("com.squareup.okhttp3:okhttp:4.12.0")
import okhttp3.*;

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(Duration.ofSeconds(10))
        .readTimeout(Duration.ofSeconds(15))
        .build();

Request request = new Request.Builder()
        .url("https://example.com")
        .header("User-Agent", "Mozilla/5.0 ...")
        .build();

try (Response response = client.newCall(request).execute()) {
    int code = response.code();
    byte[] bytes = response.body().bytes();   // de novo: melhor os bytes
}

2.3. Apache HttpClient 5

Biblioteca madura, potente e um pouco mais verbosa. Permite um ajuste fino de conexões, pools e autenticação. Comum em ambientes corporativos.

java
// org.apache.httpcomponents.client5:httpclient5:5.x
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
    HttpGet httpGet = new HttpGet("https://example.com");
    httpclient.execute(httpGet, response -> {
        int status = response.getCode();
        byte[] body = EntityUtils.toByteArray(response.getEntity());
        return body;
    });
}

2.4. Download direto com jsoup

O jsoup (veja a seção 3) pode baixar a página sozinho. É prático para protótipos, mas seu motor HTTP integrado é menos flexível (proxies, pools, ajuste fino); para tarefas de produção, a combinação usual é: baixar com um cliente robusto → parsear com jsoup.

java
Document doc = Jsoup.connect("https://example.com")
        .userAgent("Mozilla/5.0 ...")
        .timeout(15_000)
        .get();

Qual escolher

Cliente Quando usar
java.net.http.HttpClient Sem vontade de adicionar dependências, Java 11+, HTTP/2
OkHttp Opção universal; proxies/cookies/interceptors práticos
Apache HttpClient 5 Ambiente corporativo, controle fino, autenticação complexa
jsoup .connect() Protótipos e tarefas simples de «baixar e parsear»

3. Bibliotecas para o parsing

Uma vez obtido o HTML, é preciso extrair os dados. Não convém analisá-lo com expressões regulares: HTML não é uma linguagem regular, e um parser desses quebra diante de qualquer marcação fora do padrão.

3.1. jsoup: o burro de carga

O jsoup (versão atual em 2026: 1.22.2) implementa a especificação WHATWG HTML5 e constrói o mesmo DOM que os navegadores. Suporta seletores CSS e XPath e tolera o HTML «sujo».

java
// implementation("org.jsoup:jsoup:1.22.2")
import org.jsoup.Jsoup;
import org.jsoup.nodes.*;
import org.jsoup.select.Elements;

Document doc = Jsoup.parse(html, "https://example.com"); // 2º arg — baseUri para links absolutos

// seletores CSS
Elements links = doc.select("a[href]");
for (Element link : links) {
    String text = link.text();
    String absUrl = link.absUrl("href");  // URL absoluta
}

// seleção pontual
Element title = doc.selectFirst("h1.article-title");
String price = doc.select("span.price").text();

// atributos
String img = doc.selectFirst("img").attr("src");

Algumas técnicas comuns com seletores:

java
doc.select("div.product");              // por classe
doc.select("#main-content");            // por id
doc.select("ul.menu > li");             // filhos diretos
doc.select("a[href^=https]");           // o atributo começa com
doc.select("table tr:nth-child(2n)");   // pseudosseletores
doc.select("p:contains(Preço)");        // por texto

3.2. HtmlUnit: o «navegador sem interface»

O HtmlUnit é um navegador sem interface gráfica (GUI-less) escrito em Java. Executa JavaScript (de forma limitada), o que às vezes permite obter dados de sites dinâmicos sem recorrer ao pesado Selenium. É mais lento que o jsoup, porém mais poderoso.

3.3. Quando o HTML não é necessário

Com muita frequência, os dados da página são carregados por uma requisição à parte a uma API interna (JSON). Abra as DevTools → aba Network, localize o XHR/fetch correspondente e parseie o JSON puro com Jackson ou Gson. É muito mais confiável e rápido do que analisar o HTML. Verifique sempre essa via primeiro.

java
// com.fasterxml.jackson.core:jackson-databind
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jsonString);
String name = root.path("data").path("name").asText();

4. Acentos, cedilha e codificações

A dor de cabeça mais comum na web lusófona é o mojibake (João, coração ou ���). A causa é sempre a mesma: os bytes foram decodificados com a codificação errada.

Por que acontece

HTTP devolve bytes. Para obter uma string, é preciso decodificá-los com a codificação correta. Ela pode estar indicada:

  1. no cabeçalho HTTP Content-Type: text/html; charset=ISO-8859-1;
  2. em <meta charset="..."> ou <meta http-equiv="Content-Type"> dentro do HTML;
  3. em lugar nenhum (então é preciso adivinhá-la).

Se o corpo for lido como UTF-8, mas o site usar ISO-8859-1 (um clássico da web lusófona antiga), o resultado será lixo.

Regra: trabalhe com bytes e determine a codificação de forma explícita

O jsoup resolve isso quase automaticamente se receber bytes ou um InputStream em vez de uma string já montada — ele mesmo lê o charset do cabeçalho ou do <meta>:

java
// Correto: o jsoup detecta a codificação por meta/charset
byte[] bytes = response.body();  // do HttpClient/OkHttp como byte[]
InputStream in = new ByteArrayInputStream(bytes);
Document doc = Jsoup.parse(in, null, "https://example.com");
//                              ^^^^ null = detecção automática do charset

Se o charset for conhecido de antemão, indique-o de forma explícita:

java
Document doc = Jsoup.parse(in, "ISO-8859-1", "https://example.com");

Decodificação manual

Quando não for parsear com o jsoup, decodifique os bytes por conta própria:

java
import java.nio.charset.Charset;

// se a codificação for conhecida
String html = new String(bytes, Charset.forName("ISO-8859-1"));

// para UTF-8
String html2 = new String(bytes, StandardCharsets.UTF_8);

Detecção automática da codificação

Se a codificação não estiver indicada em lugar nenhum, ajudam as bibliotecas detectoras: juniversalchardet (port do universalchardet da Mozilla) ou ICU4J (CharsetDetector).

java
// org.apache.tika:tika-core também sabe detectar o charset
import org.apache.tika.parser.txt.CharsetDetector;

CharsetDetector detector = new CharsetDetector();
detector.setText(bytes);
String charset = detector.detect().getName(); // p. ex. "ISO-8859-1"
String html = new String(bytes, charset);

Checklist de acentos e cedilha

  • Nunca converta a resposta em string como UTF-8 «às cegas».
  • Passe ao jsoup um InputStream/byte[], não um String.
  • Salve os arquivos com uma codificação explícita: Files.write(path, html.getBytes(StandardCharsets.UTF_8)).
  • No console também aparecem artefatos — defina -Dfile.encoding=UTF-8 e verifique a codificação do terminal.

5. Status da resposta e cabeçalhos

Não convém sair analisando o corpo direto — confira primeiro se o servidor devolveu de fato a página, e não um redirecionamento, um captcha ou um erro.

java
HttpResponse<byte[]> resp = client.send(request, BodyHandlers.ofByteArray());

int status = resp.statusCode();
HttpHeaders headers = resp.headers();

// leitura de cabeçalhos específicos
String contentType = headers.firstValue("Content-Type").orElse("");
String server      = headers.firstValue("Server").orElse("");
long length        = headers.firstValueAsLong("Content-Length").orElse(-1);

// todos os cabeçalhos
headers.map().forEach((k, v) -> System.out.println(k + ": " + v));

O que convém vigiar:

Código Significado Reação do scraper
200 OK parseamos
301/302 redirecionamento seguimos o Location (ou ativamos follow-redirects)
403 acesso negado provavelmente anti-bot — trocamos UA/proxy
404 não encontrado marcamos a URL como morta
429 requisições demais esperamos o Retry-After, reduzimos o ritmo
5xx erro do servidor nova tentativa com atraso exponencial

Cabeçalhos de resposta úteis: Content-Type (tipo e codificação), Set-Cookie (veja a seção 6), Location (redirecionamento), Retry-After (pausa diante de 429/503), ETag/Last-Modified (para requisições condicionais e rastreamento incremental).

No OkHttp é análogo: response.code(), response.header("Content-Type"), response.headers().


Os cookies são necessários para as sessões, a autenticação, a superação das «verificações anti-bot» e a preservação do estado entre requisições.

6.1. HttpClient integrado + CookieManager

java
import java.net.*;

CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);

HttpClient client = HttpClient.newBuilder()
        .cookieHandler(cookieManager)   // os cookies agora são salvos automaticamente
        .build();

// depois de várias requisições, inspecionar o armazenamento:
CookieStore store = cookieManager.getCookieStore();
store.getCookies().forEach(c ->
        System.out.println(c.getName() + "=" + c.getValue()));

Agora os cookies são enviados sozinhos entre as requisições — algo prático para o login: primeiro um POST com usuário e senha e, depois, requisições às páginas protegidas com o mesmo client.

6.2. OkHttp + CookieJar

java
// O caminho mais simples: um PersistentCookieJar pronto ou um CookieJar próprio em memória
OkHttpClient client = new OkHttpClient.Builder()
        .cookieJar(new JavaNetCookieJar(cookieManager))
        .build();

6.3. jsoup: reenvio manual

O jsoup guarda os cookies no objeto Connection.Response, e eles podem ser reenviados manualmente:

java
Connection.Response login = Jsoup.connect("https://site.com/login")
        .data("username", "user", "password", "pass")
        .method(Connection.Method.POST)
        .execute();

Map<String, String> cookies = login.cookies();

Document page = Jsoup.connect("https://site.com/profile")
        .cookies(cookies)      // passamos a sessão
        .get();

Uma armadilha: no scraping multithread, uma única sessão compartilhada pode «arrastar» o estado entre as threads. Para contas ou proxies distintos, crie armazenamentos de cookies independentes para cada thread ou worker.


7. HTTPS/SSL

A maioria dos sites usa HTTPS. Em geral, o HttpClient integrado ou o OkHttp cuidam de tudo: validam o certificado contra as CAs raiz confiáveis do JDK. Só é preciso código adicional em dois casos.

7.1. Certificados autoassinados e «problemáticos»

Às vezes o site-alvo tem um certificado autoassinado ou vencido. Surge a tentação de desativar a validação:

java
// ⚠️ PERIGOSO: desativa toda a validação SSL — apenas para testes ou redes confiáveis!
TrustManager[] trustAll = new TrustManager[]{
    new X509TrustManager() {
        public void checkClientTrusted(X509Certificate[] c, String a) {}
        public void checkServerTrusted(X509Certificate[] c, String a) {}
        public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
    }
};
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAll, new SecureRandom());

HttpClient client = HttpClient.newBuilder()
        .sslContext(sc)
        .build();

Isso não deve ser feito em produção — abre a porta para ataques MITM. O correto é adicionar o certificado específico a um truststore personalizado:

bash
keytool -import -alias mysite -file mysite.crt -keystore custom.jks
java
KeyStore ks = KeyStore.getInstance("JKS");
try (InputStream in = Files.newInputStream(Path.of("custom.jks"))) {
    ks.load(in, "changeit".toCharArray());
}
TrustManagerFactory tmf =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, tmf.getTrustManagers(), null);

7.2. Impressão digital TLS (JA3): anti-bot avançado

As proteções sérias (Cloudflare e outras) distinguem os clientes pela impressão digital TLS (JA3/JA4): o conjunto de cifras e extensões do ClientHello. A do cliente Java padrão «não parece de navegador», então fica fácil identificar o bot. Falsificá-la por completo com um JDK puro é complicado; na prática, recorre-se a wrappers do tipo curl-impersonate ou utls, ou o tráfego é roteado através de um navegador real (seção 11). É um tema de nicho, mas importante para os sites «difíceis».


8. Proxies

Os proxies servem para repartir a carga entre vários IPs, contornar restrições geográficas e reduzir o risco de bloqueio por IP no scraping em massa. Tipos: datacenter (baratos, fáceis de detectar), residenciais (caros, parecem um usuário real) e móveis (os mais «limpos»).

8.1. HttpClient integrado

java
import java.net.*;

HttpClient client = HttpClient.newBuilder()
        .proxy(ProxySelector.of(new InetSocketAddress("proxy.host", 8080)))
        .build();

Com autenticação de proxy por usuário e senha:

java
Authenticator auth = new Authenticator() {
    @Override protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication("user", "pass".toCharArray());
    }
};
HttpClient client = HttpClient.newBuilder()
        .proxy(ProxySelector.of(new InetSocketAddress("proxy.host", 8080)))
        .authenticator(auth)
        .build();

Um detalhe do JDK: para o uso de proxy pode ser necessária a propriedade de sistema -Djdk.http.auth.tunneling.disabledSchemes=""; caso contrário, a autenticação Basic sobre o túnel CONNECT falha em silêncio.

8.2. OkHttp (muitas vezes mais prático para proxies)

java
Proxy proxy = new Proxy(Proxy.Type.HTTP,
        new InetSocketAddress("proxy.host", 8080));

OkHttpClient client = new OkHttpClient.Builder()
        .proxy(proxy)
        .proxyAuthenticator((route, response) -> {
            String credential = Credentials.basic("user", "pass");
            return response.request().newBuilder()
                    .header("Proxy-Authorization", credential)
                    .build();
        })
        .build();

O OkHttp também suporta Proxy.Type.SOCKS — algo de que vamos precisar para o TOR.

8.3. Rotação de proxies

Para o scraping em massa, mantém-se um pool de proxies que vão sendo alternados de forma cíclica ou quando chega um bloqueio. A rotação mais simples:

java
List<Proxy> pool = loadProxies();
AtomicInteger idx = new AtomicInteger();

Proxy next() {
    return pool.get(idx.getAndIncrement() % pool.size());
}

Na prática, acrescentam-se: verificação de «vitalidade» dos proxies, uma lista negra dos «queimados», a associação da sessão ou dos cookies a um proxy específico e, para cada proxy, seu próprio OkHttpClient (eles saem baratos se compartilham o pool de conexões, mas convém reutilizá-los).


9. Scraping através do TOR

O TOR oferece anonimato e troca de IP gratuita, mas é lento, e muitos sites bloqueiam seus nós de saída. Serve para tarefas pontuais, não para um scraping de alta velocidade.

9.1. Preparação

  1. Instale o TOR (o pacote tor ou o Tor Browser).
  2. O TOR sobe um proxy SOCKS5 local, normalmente em 127.0.0.1:9050 (no Tor Browser, 9150).
  3. Para trocar de circuito (novo IP), usa-se o ControlPort (9051) com o comando NEWNYM.

Um torrc mínimo:

code
SocksPort 9050
ControlPort 9051
CookieAuthentication 1

9.2. Requisições através do TOR (SOCKS5)

java
Proxy torProxy = new Proxy(Proxy.Type.SOCKS,
        new InetSocketAddress("127.0.0.1", 9050));

OkHttpClient client = new OkHttpClient.Builder()
        .proxy(torProxy)
        .build();

Request req = new Request.Builder()
        .url("https://check.torproject.org/api/ip")  // verificação de que estamos no TOR
        .build();

try (Response resp = client.newCall(req).execute()) {
    System.out.println(resp.body().string()); // {"IsTor":true,...}
}

Importante: com SOCKS5, a resolução DNS deve passar pelo proxy (DNS remoto); caso contrário, a consulta DNS «vaza» a partir do seu IP real. O OkHttp com Proxy.Type.SOCKS leva isso em conta; se usar o JDK puro, fique atento aos vazamentos de DNS.

9.3. Troca de IP com o comando NEWNYM

java
import java.io.*;
import java.net.Socket;

void newTorIdentity() throws IOException {
    try (Socket s = new Socket("127.0.0.1", 9051);
         PrintWriter out = new PrintWriter(s.getOutputStream(), true);
         BufferedReader in = new BufferedReader(
                 new InputStreamReader(s.getInputStream()))) {

        out.println("AUTHENTICATE \"\"");   // ou a senha, se estiver definida
        in.readLine();                       // esperamos o 250 OK
        out.println("SIGNAL NEWNYM");        // novo circuito
        in.readLine();
    }
}

Após o NEWNYM, convém esperar um pouco (o TOR leva alguns segundos para construir o novo circuito) e não invocar o comando com frequência excessiva.


10. Multithreading

O scraping é uma tarefa limitada por E/S (I/O-bound): a maior parte do tempo esperamos pela rede, então a paralelização traz um ganho enorme. O essencial é não «derrubar» o site-alvo nem ganhar um bloqueio.

10.1. ExecutorService: o clássico

java
import java.util.concurrent.*;

ExecutorService pool = Executors.newFixedThreadPool(10);
List<Future<String>> futures = new ArrayList<>();

for (String url : urls) {
    futures.add(pool.submit(() -> fetchAndParse(url)));
}

for (Future<String> f : futures) {
    try {
        String result = f.get(30, TimeUnit.SECONDS);
        // salvar o resultado
    } catch (Exception e) {
        // log + nova tentativa/descartar
    }
}
pool.shutdown();

10.2. Threads virtuais (Java 21+): ideais para E/S

As threads virtuais permitem milhares de threads «baratas» que ficam esperando pela rede — exatamente o que o scraping precisa:

java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (String url : urls) {
        executor.submit(() -> fetchAndParse(url));
    }
} // close() espera todas as tarefas terminarem

10.3. CompletableFuture + HttpClient assíncrono

java
List<CompletableFuture<HttpResponse<byte[]>>> calls = urls.stream()
        .map(u -> client.sendAsync(buildRequest(u), BodyHandlers.ofByteArray()))
        .toList();

CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();

10.4. O que é preciso levar em conta sem falta

  • Limite de concorrência por domínio. 200 threads contra um mesmo site equivalem a um DoS e a um bloqueio imediato. Limite-as, por exemplo com um Semaphore por domínio.
  • Throttling. Adicione atrasos e jitter entre as requisições (veja a seção 12).
  • Segurança entre threads. Melhor não compartilhar um Document do jsoup entre threads; para coleções compartilhadas, use ConcurrentHashMap ou ConcurrentLinkedQueue; para contadores, AtomicInteger/LongAdder.
  • Backpressure. Não despeje um milhão de tarefas na fila de uma vez — use uma fila limitada (ArrayBlockingQueue) e CallerRunsPolicy para que o produtor se freie.
  • Um único OkHttpClient/HttpClient para toda a aplicação. Eles são thread-safe e mantêm o pool de conexões; criar um para cada requisição é um antipadrão.

11. Renderização de JavaScript

O jsoup só enxerga o HTML que veio na resposta. Se o site é uma SPA (React/Vue/Angular) e os dados são desenhados com JavaScript no navegador, eles não estarão no HTML «bruto». Opções:

  1. Encontrar a API interna (veja 3.3) — quase sempre o melhor caminho: mais rápido, mais estável e sem navegador.
  2. Selenium WebDriver — controle de um navegador real (Chrome/Firefox via WebDriver). Pesado e lento, mas enxerga tudo o que o usuário vê.
  3. Playwright for Java — a alternativa moderna ao Selenium, da Microsoft: mais rápida, com uma API mais confortável e um bom manejo do conteúdo dinâmico e das esperas.
  4. HtmlUnit — um navegador leve embutido em Java; executa JS de forma limitada, mas sem binários externos.
java
// Selenium (Chrome headless)
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
WebDriver driver = new ChromeDriver(options);
driver.get("https://spa-site.com");

// esperar o elemento aparecer
new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".item")));

String renderedHtml = driver.getPageSource();
// depois dá para passar ao jsoup para uma análise confortável:
Document doc = Jsoup.parse(renderedHtml);
driver.quit();

Os motores de navegador resolvem de quebra parte dos problemas anti-bot (impressão digital TLS correta, execução dos desafios JS), mas pagam por isso em recursos: uma única instância do Chrome consome centenas de megabytes de RAM, então escalá-las sai caro.


12. Proteção anti-bot

Quanto mais massivo o scraping, maior a probabilidade de bloqueio. «Cortesia» básica e camuflagem:

  • User-Agent. Use um UA realista de um navegador de verdade; para tarefas em massa, rotacione uma lista de UAs. O UA padrão do Java é um sinal de alerta para o site.
  • Jogo completo de cabeçalhos. Um navegador real envia Accept, Accept-Language, Accept-Encoding, Referer, Sec-Fetch-*. Imite-os.
  • Atrasos e jitter. Uma pausa aleatória (por exemplo, de 1 a 5 s) entre as requisições em vez de uma «metralhadora» uniforme.
  • Novas tentativas com atraso exponencial diante de 429/5xx, respeitando o Retry-After.
  • Rotação de IP (seção 8) — a principal ferramenta contra os bloqueios por IP.
  • Captcha. Se aparecer um captcha, as opções são: reduzir o ritmo, trocar de IP ou usar serviços de resolução (2Captcha/Anti-Captcha), embora isso já seja uma zona cinzenta; avalie os riscos.
  • Cloudflare e desafios JS. Muitas vezes só se superam com um navegador real (seção 11) ou com bibliotecas especializadas em contorná-los.

Esqueleto de novas tentativas:

java
int attempts = 0;
while (attempts < MAX_RETRIES) {
    HttpResponse<byte[]> r = client.send(req, BodyHandlers.ofByteArray());
    int code = r.statusCode();
    if (code == 200) return r.body();
    if (code == 429 || code >= 500) {
        long backoff = (long) (Math.pow(2, attempts) * 1000)
                + ThreadLocalRandom.current().nextInt(500); // jitter
        Thread.sleep(backoff);
        attempts++;
    } else {
        break; // 403/404 — repetir não vai ajudar
    }
}

13. Armazenamento de URLs e filas

Um crawler é, em essência, o percurso de um grafo de links. São necessárias duas estruturas:

  • A fila (frontier) — as URLs que ainda faltam percorrer.
  • O conjunto de visitadas (visited) — para não percorrer a mesma URL duas vezes nem entrar em loop.

Em memória (para tarefas pequenas)

java
Queue<String> frontier = new ConcurrentLinkedQueue<>();
Set<String> visited = ConcurrentHashMap.newKeySet();

if (visited.add(url)) {       // add devolve false se já estava
    frontier.offer(url);
}

Parsing de URL

Antes de normalizar ou filtrar um link, é preciso decompô-lo em suas partes (esquema, host, porta, caminho, query, fragmento). Em Java existe para isso o java.net.URI padrão:

java
import java.net.URI;

URI uri = URI.create("https://Site.com:443/catalog/item?id=7&ref=a#section");

String scheme   = uri.getScheme();    // https
String host     = uri.getHost();      // Site.com
int    port     = uri.getPort();      // 443
String path     = uri.getPath();      // /catalog/item
String query    = uri.getQuery();     // id=7&ref=a
String fragment = uri.getFragment();  // section

// resolução de um link relativo para absoluto:
URI abs = uri.resolve("../other");    // https://Site.com:443/other

O jsoup faz o mesmo automaticamente com os links da página — link.absUrl("href") (veja a seção 3) já devolve a URL absoluta apoiando-se no baseUri. A decomposição em componentes com URI serve justamente para a normalização posterior.

Os mesmos princípios de análise de URL se aplicam em outras stacks — veja o artigo à parte «Parsing de URL».

O que é importante

  • Normalização de URL antes da deduplicação (apoiando-se no Parsing de URL): remover as âncoras #..., ordenar os parâmetros da query, unificar as maiúsculas do host e decidir sobre http/https e a barra final. Caso contrário, site.com/a e site.com/a/ contarão como distintas.
  • Deduplicação em grandes volumes. Manter centenas de milhões de URLs em um HashSet é inviável por memória — recorre-se a um filtro de Bloom (BloomFilter do Guava) ou o conjunto é externalizado para o Redis ou para um banco de dados.
  • Persistência. Em rastreamentos longos, a fila e o conjunto visited devem sobreviver a um reinício: Redis (List/Set), Kafka/RabbitMQ como fila de tarefas, um banco relacional ou um armazenamento embutido.
  • Prioridades. Às vezes é necessária uma PriorityBlockingQueue (primeiro as seções importantes) ou um limite de profundidade de rastreamento.
  • Distribuição. Ao escalar para muitas máquinas, a fila e a deduplicação vão para um broker externo ou para o Redis, para que os workers não dupliquem o trabalho.

Arquitetura típica: um produtor tira URLs do frontier → um pool de workers baixa e parseia → os links extraídos são filtrados, normalizados, conferidos contra o visited e devolvidos ao frontier; os dados são gravados no armazenamento.


14. Frameworks prontos para usar

Para não escrever um crawler do zero, existem soluções prontas:

  • crawler4j — um crawler multithread simples em Java, de partida rápida.
  • webmagic — um framework flexível (inspirado no Scrapy) com pipelines e agendador.
  • Apache Nutch — um crawler pesado, escalável e de nível industrial (muitas vezes junto com Hadoop/Solr).
  • StormCrawler — um crawler distribuído sobre o Apache Storm para o processamento em fluxo.

Para a maioria das tarefas práticas, a combinação «OkHttp/HttpClient + jsoup + um pool de threads próprio + fila no Redis» é mais que suficiente e resulta mais transparente que a «mágica» dos frameworks.


15. Vantagens e desvantagens

Vantagens da implementação em Java

  • Desempenho e multithreading. A JVM suporta muito bem uma concorrência alta; as threads virtuais (Java 21+) levam o scraping I/O-bound a outro nível.
  • Ecossistema maduro. jsoup, OkHttp, Apache HttpClient, Selenium/Playwright, Jackson — tudo estável e bem documentado.
  • Confiabilidade e segurança de tipos. A tipagem estrita detecta erros em tempo de compilação; conveniente para crawlers grandes e de vida longa.
  • Integração com a stack corporativa. Fácil de embutir em um serviço Spring e de conectar a Kafka, bancos de dados e monitoramento.
  • Multiplataforma. Um único JAR roda em qualquer lugar.

Desvantagens

  • Verbosidade. Há mais código do que em Python; um protótipo rápido com requests + BeautifulSoup se escreve em menos linhas.
  • Menos ferramentas específicas de scraping. Em Python, o ecossistema de scraping (Scrapy e afins) é mais rico e popular.
  • Sites com JavaScript são uma dor de cabeça. Sem navegador (Selenium/Playwright), o conteúdo dinâmico não é capturado, e os navegadores são pesados e vorazes.
  • Corrida armamentista anti-bot. A impressão digital TLS, a análise de comportamento e os captchas exigem manutenção constante; o cliente Java padrão é detectado com facilidade.
  • Codificações. Os acentos, a cedilha e o ISO-8859-1 exigem cuidado (seção 4) — uma fonte comum de erros.
  • Fragilidade. Qualquer scraper quebra quando o layout do site muda; é preciso manter os seletores.

Quando Java é uma boa escolha

Um crawler grande, de vida longa e com muita carga; uma equipe que já trabalha com uma stack Java; a necessidade de multithreading intensivo e de integração com a infraestrutura. Para o pontual «extrair uma tabelinha de uma única página», Python costuma ser mais rápido em tempo de desenvolvimento.


As versões das bibliotecas correspondem a 2026 (jsoup 1.22.2). Antes de usá-las, confira as versões atuais no Maven Central — a API pode ter mudado.