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
- Introdução: o que é o web scraping e seu marco legal
- Como baixamos a página: clientes HTTP
- Bibliotecas para o parsing do conteúdo
- Solução de problemas com acentos e cedilha (codificações)
- Obtenção do status da resposta e dos cabeçalhos
- Trabalho com cookies
- Trabalho com HTTPS/SSL
- Uso de proxies
- Scraping através do TOR
- Multithreading
- Renderização de JavaScript: sites dinâmicos
- Proteção anti-bot: User-Agent, atrasos, novas tentativas
- Armazenamento de URLs e filas
- Frameworks de crawling prontos para usar
- 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-Aftere os códigos429/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.
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:
HttpResponse<byte[]> resp =
client.send(request, HttpResponse.BodyHandlers.ofByteArray());
byte[] raw = resp.body(); // decodificamos depois, conhecendo o charset2.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.
// 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.
// 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.
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».
// 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:
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 texto3.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.
// 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:
- no cabeçalho HTTP
Content-Type: text/html; charset=ISO-8859-1; - em
<meta charset="...">ou<meta http-equiv="Content-Type">dentro do HTML; - 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>:
// 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 charsetSe o charset for conhecido de antemão, indique-o de forma explícita:
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:
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).
// 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 umString. - 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-8e 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.
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().
6. Trabalho com cookies
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
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
// 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:
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:
// ⚠️ 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:
keytool -import -alias mysite -file mysite.crt -keystore custom.jksKeyStore 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
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:
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)
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:
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
- Instale o TOR (o pacote
torou o Tor Browser). - O TOR sobe um proxy SOCKS5 local, normalmente em
127.0.0.1:9050(no Tor Browser,9150). - Para trocar de circuito (novo IP), usa-se o ControlPort (
9051) com o comandoNEWNYM.
Um torrc mínimo:
SocksPort 9050
ControlPort 9051
CookieAuthentication 19.2. Requisições através do TOR (SOCKS5)
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.SOCKSleva isso em conta; se usar o JDK puro, fique atento aos vazamentos de DNS.
9.3. Troca de IP com o comando NEWNYM
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
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:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (String url : urls) {
executor.submit(() -> fetchAndParse(url));
}
} // close() espera todas as tarefas terminarem10.3. CompletableFuture + HttpClient assíncrono
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
Semaphorepor 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
Documentdo jsoup entre threads; para coleções compartilhadas, useConcurrentHashMapouConcurrentLinkedQueue; para contadores,AtomicInteger/LongAdder. - Backpressure. Não despeje um milhão de tarefas na fila de uma vez — use uma fila limitada (
ArrayBlockingQueue) eCallerRunsPolicypara que o produtor se freie. - Um único
OkHttpClient/HttpClientpara 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:
- Encontrar a API interna (veja 3.3) — quase sempre o melhor caminho: mais rápido, mais estável e sem navegador.
- Selenium WebDriver — controle de um navegador real (Chrome/Firefox via WebDriver). Pesado e lento, mas enxerga tudo o que o usuário vê.
- 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.
- HtmlUnit — um navegador leve embutido em Java; executa JS de forma limitada, mas sem binários externos.
// 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 oRetry-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:
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)
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:
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/otherO 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 sobrehttp/httpse a barra final. Caso contrário,site.com/aesite.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 (BloomFilterdo 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 + BeautifulSoupse 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-1exigem 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.