🎯 Um navegador em nuvem personalizável e anti-detecção alimentado por Chromium desenvolvido internamente, projetado para rastreadores web e agentes de IA. 👉Experimente agora
De volta ao blog

Java Web Scraping com jsoup: Buscar, Analisar e Renderizar

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

04-Aug-2026

TL;DR:

  • jsoup é um parser HTML para Java com um mecanismo de seletor CSS, e vem com seu próprio cliente HTTP, então um scraper funcional é uma dependência e cerca de vinte linhas de código.
  • jsoup mantém apenas os primeiros 2 MiB de uma resposta por padrão. Em uma página de 2.235.648 bytes, manteve exatamente 2.097.152 bytes, descartou 68 de 255 entradas de referência, perdeu uma seção inteira e não descartou nada.
  • attr("href") retorna o href exatamente como escrito no HTML. attr("abs:href") resolve-o em relação ao URI base do documento.
  • jsoup não executa JavaScript. Em uma página renderizada pelo cliente, o mesmo código de seletor encontrou 0 itens diretamente e 10 itens quando o HTML chegou pré-renderizado.
  • Roteando a busca pelo Scrapeless Universal Scraping API muda apenas o transporte — o método de parsing permanece inalterado.
  • O plano gratuito do Scrapeless é suficiente para executar cada requisição neste guia.

Java é onde muitos dos dados extraídos acabam. Se o serviço que consome os dados for uma aplicação Spring ou Quarkus, manter a extração na mesma JVM remove uma barreira de linguagem, um passo de serialização e um segundo alvo de implantação.

A biblioteca que torna isso prático é o jsoup. Ele analisa HTML do mundo real da maneira que um navegador faz — fechando tags não fechadas, corrigindo aninhamentos e normalizando atributos, seguindo as regras de tratamento de erros na especificação de parsing HTML — e depois expõe o resultado através de uma API de seletor CSS.

Este guia constrói um scraper funcional contra dois sites ao vivo, e depois mede dois comportamentos que decidem se o scraper está correto: o que o jsoup descarta silenciosamente em uma página grande, e o que ele retorna em uma página que é renderizada no navegador.

O que o jsoup Oferece

jsoup é um parser primeiro e um cliente HTTP em segundo lugar. Jsoup.connect(url) retorna um construtor de requisição fluente; .get() realiza a requisição e devolve um Document que você consulta com seletores.

java Copy
String url = "https://books.toscrape.com/";
Document doc = Jsoup.connect(url).get();
String title = doc.selectFirst("h1").text();

Esse Document é uma árvore analisada em vez de uma string, então uma página malformada ainda produz uma estrutura consultável. Ele também carrega o URI base de onde foi buscado, o que torna possível a resolução de URLs absolutas mais tarde.

O que o jsoup não faz é executar scripts. Ele não possui um motor JavaScript e nem loop de eventos DOM. O que quer que o servidor envie é o que você obtém para analisar.

Configurando o Projeto

Uma dependência cobre o parsing. Gson está aqui apenas para ler o envelope JSON na última seção; se você pular essa seção, pode removê-lo.

xml Copy
<dependencies>
  <dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.21.1</version>
  </dependency>
  <dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.14.0</version>
  </dependency>
</dependencies>

Defina explicitamente o plugin do compilador. O Maven 3.8.7 ainda vincula maven-compiler-plugin 3.1 por padrão, que ignora maven.compiler.release e para com Source option 5 is no longer supported:

xml Copy
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.15.0</version>
    </plugin>
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>exec-maven-plugin</artifactId>
      <version>3.5.0</version>
      <configuration>
        <mainClass>com.example.Scraper</mainClass>
      </configuration>
    </plugin>
  </plugins>
</build>

Com maven.compiler.release definido como 17, o código abaixo compila em qualquer JDK atual. A execução de verificação foi feita com OpenJDK 21.0.11.

Buscar uma Página e Analisá-la

Defina um agente de usuário e um tempo limite em cada requisição. O agente padrão do jsoup se identifica como jsoup, e muitos sites variam sua resposta apenas com isso.

java Copy
static final String USER_AGENT =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    + "(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";

static Document fetch(String url) throws IOException {
    return Jsoup.connect(url)
        .userAgent(USER_AGENT)
        .timeout(30_000)
        .get();
}

timeout é em milissegundos e se aplica tanto à conexão quanto à leitura. Um get() que exceder isso gera SocketTimeoutException; um status não-2xx gera HttpStatusException, que carrega o código.

Selecionar os Dados

Os seletores são CSS padrão. select retorna cada correspondência como uma coleção Elements; selectFirst retorna um Element ou null.

java Copy
record Book(String title, String price, String url) {}

static List<Book> books(Document doc) {
    List<Book> found = new ArrayList<>();
    for (Element card : doc.select("article.product_pod")) {
        Element link = card.selectFirst("h3 > a");
        found.add(new Book(
            link.attr("title"),
            card.selectFirst("p.price_color").text(),
link.attr("abs:href")));
    }
    return found;
}

Contra o catálogo ao vivo, isso retorna 20 livros, sendo o primeiro A Light in the Attic a £51.77.

O prefixo abs: na última linha está fazendo um trabalho real. O link na fonte lê:

text Copy
catalogue/a-light-in-the-attic_1000/index.html

attr("href") dá a você essa string verbatim. Prefixar o nome do atributo com abs: resolve-o em relação ao URI base do documento usando o algoritmo em o Padrão de URL WHATWG, produzindo:

text Copy
https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html

Um scraper que armazena o valor bruto funciona bem até o dia em que algo mais tenta buscá-lo.

O Limite que Custa Dados a Você

jsoup limita o corpo da resposta que irá ler. O padrão é 2 MiB, documentado em a interface de requisição jsoup. Após esse ponto, ele para de ler e analisa o que tem. Não levanta exceção, não registra aviso, e não deixa sinal no Document para dizer que o corpo foi cortado.

Use execute() em vez de get() quando quiser ver a resposta antes de analisá-la:

java Copy
Connection.Response capped = Jsoup.connect(big)
    .userAgent(USER_AGENT).timeout(60_000).execute();
Connection.Response whole = Jsoup.connect(big)
    .userAgent(USER_AGENT).timeout(60_000).maxBodySize(0).execute();

Executando contra uma página de comparação da Wikipedia de 2.235.648 bytes, as duas respostas diferem exatamente pelo limite:

text Copy
status http, limite padrão: 200
bytes recebidos, limite padrão: 2097152
bytes recebidos, maxBodySize(0): 2235648
linhas da tabela, limite padrão: 544
linhas da tabela, maxBodySize(0): 544
entradas de referência, limite padrão: 187
entradas de referência, maxBodySize(0): 255
última seção, limite padrão: Referências
última seção, maxBodySize(0): Links externos

Ambos os runs retornaram HTTP 200. Ambos produziram um Document. As tabelas comparativas para as quais a página existe vieram idênticas, 544 linhas de qualquer maneira, porque estão perto do topo do documento.

Tudo abaixo do corte simplesmente está ausente. A lista de referências perdeu 68 de suas 255 entradas, e a seção final Links externos não existe na árvore truncada. Um scraper lendo as tabelas nunca notaria; um scraper coletando citações relataria silenciosamente uma subcontagem de um quarto e ainda pareceria saudável.

maxBodySize(0) remove o limite. Defina-o deliberadamente em vez de por reflexo — uma leitura não limitada em uma resposta inesperada é um problema por si só — mas defina-o conscientemente, e compare contra o Content-Length que o servidor relata, que a especificação de semântica HTTP define como a contagem de octetos do corpo.

https://quotes.toscrape.com/js/ marca a fronteira. Ele serve suas citações como um array JavaScript e constrói o DOM do lado do cliente, e o jsoup o busca com sucesso:

text Copy
bytes html buscados diretamente: 5479
citações encontradas pelo jsoup:       0

5.479 bytes, HTTP 200, zero resultados. A marcação que o jsoup recebeu realmente não contém elementos div.quote — eles são criados após a execução do script, e o jsoup não possui um mecanismo de script.

A maioria dos guias responde a isso mudando para uma ferramenta de automação de navegador, o que significa reescrever o código de extração também. Essa troca é a mesma que Cheerio e Puppeteer estão de cada lado no Node, e custa a mesma coisa em Java. A correção mais estreita é mudar apenas como o HTML chega. Essa tarefa pertence à Scrapeless Universal Scraping API. Ele renderiza a página e retorna o HTML resultante como uma string, que você entrega ao mesmo parser.

O HttpClient embutido do JDK é suficiente — nenhuma dependência de HTTP necessária:

java Copy
static String render(String url) throws IOException, InterruptedException {
    JsonObject input = new JsonObject();
    input.addProperty("url", url);
    input.addProperty("proxy_country", "US");
    input.addProperty("js_render", true);

    JsonObject payload = new JsonObject();
    payload.addProperty("actor", "unlocker.webunlocker");
    payload.add("input", input);

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.scrapeless.com/api/v2/unlocker/request"))
        .header("Content-Type", "application/json")
        .header("x-api-token", System.getenv("SCRAPELESS_API_KEY"))
        .timeout(Duration.ofSeconds(180))
```java
.POST(HttpRequest.BodyPublishers.ofString(payload.toString(), StandardCharsets.UTF_8))
        .build();

    HttpResponse<String> response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) {
        throw new IOException("unlocker retornou HTTP " + response.statusCode());
    }
    return JsonParser.parseString(response.body())
        .getAsJsonObject().get("data").getAsString();
}

O envelope da resposta é {"code": ..., "data": "<html renderizado>"}. Analise essa string com Jsoup.parse(html, url) — passando a URL define a URI base, então abs:href continua funcionando — e execute o mesmo método seletor idêntico:

java Copy
static List<String> quotes(Document doc) {
    List<String> found = new ArrayList<>();
    for (Element quote : doc.select("div.quote")) {
        found.add(quote.selectFirst("span.text").text()
            + " -- " + quote.selectFirst("small.author").text());
    }
    return found;
}
text Copy
bytes html renderizado:            8940
citações encontradas pelo mesmo parser: 10

Mesmo método, mesmos seletores, mesma API Document. A única coisa que mudou é de onde vieram os 8.940 bytes. Mantenha sua chave de API no ambiente, como SCRAPELESS_API_KEY acima, em vez de no código-fonte. O guia de introdução à API Universal Scraping cobre os parâmetros de solicitação restantes.

Começar leva um minuto — crie uma conta gratuita no Scrapeless e o plano gratuito cobre tudo neste guia.

Execute

Com a classe montada, um comando compila e executa:

bash Copy
export SCRAPELESS_API_KEY="sua-chave-de-api"
mvn -q -B compile exec:java

A saída completa da execução de verificação:

text Copy
jsoup 1.21.1 | java 21.0.11
--- página estática, analisada diretamente ---
livros na página 1: 20
primeiro título: A Light in the Attic
primeiro preço: £51.77
primeiro url:   https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
href como escrito no HTML: catalogue/a-light-in-the-attic_1000/index.html
--- limite de tamanho do corpo padrão ---
status http, limite padrão: 200
bytes recebidos, limite padrão: 2097152
bytes recebidos, maxBodySize(0): 2235648
linhas da tabela, limite padrão: 544
linhas da tabela, maxBodySize(0): 544
entradas de referência, limite padrão: 187
entradas de referência, maxBodySize(0): 255
última seção, limite padrão: Referências
última seção, maxBodySize(0): Links externos
--- página renderizada por javascript ---
bytes html buscados diretamente: 5479
citações encontradas por jsoup:       0
--- mesma página através da API Universal Scraping ---
bytes html renderizados:            8940
citações encontradas pelo mesmo parser: 10

Solução de Problemas

A opção de origem 5 não é mais suportada. O Maven vinculou seu maven-compiler-plugin padrão em vez do seu. Defina a versão do plugin em <build><plugins> conforme mostrado acima.

selectFirst retornou nulo e a linha seguinte gerou NullPointerException. O seletor não combinou nada. Verifique o elemento em relação ao HTML que o jsoup realmente recebeu — doc.html() — e não em relação ao que o inspetor do navegador mostra, que é o DOM pós-script.

Contagens são mais baixas do que a página mostra. Compare response.bodyAsBytes().length com o Content-Length do servidor. Se eles corresponderem a 2.097.152, o limite de tamanho do corpo é a causa.

HttpStatusException: 403. O servidor rejeitou a solicitação e não a análise. Defina um agente de usuário realista primeiro; se a página também exigir renderização, o pivô acima se aplica.

Mojibake no texto extraído. O jsoup lê o charset do cabeçalho Content-Type, depois da meta tag. Quando um servidor declara nenhum dos dois, passe a codificação explicitamente para Jsoup.parse(InputStream, String, String).

Conclusão

Para HTML estático, jsoup e o JDK são toda a cadeia de ferramentas: uma dependência, uma solicitação fluente, uma API de seletor CSS e uma árvore analisada que sobrevive a uma marcação ruim. Os dois comportamentos que determinam se os resultados são confiáveis são invisíveis por padrão — o limite de 2 MiB no corpo que retorna um documento parcial com aparência saudável e o conjunto de resultados vazio em uma página renderizada pelo cliente.

Ambos são fáceis de verificar. Compare os bytes recebidos com Content-Length e compare a contagem de um seletor com o que a página exibe. Quando a segunda verificação falha porque a marcação chega vazia, a solução é mudar o transporte e deixar o parser intacto.
Pronto para experimentar? Comece com o plano gratuito do Scrapeless e veja os preços atuais para volumes maiores.

FAQ

P: O jsoup é suficiente por si só ou eu preciso do Selenium?

Para HTML renderizado no servidor, o jsoup sozinho é suficiente e consideravelmente mais rápido, pois nunca inicia um navegador. Você precisa de uma etapa de renderização apenas quando os dados são criados pelo JavaScript — e como a medição acima mostra, essa etapa pode ser uma chamada HTTP que retorna HTML renderizado em vez de um navegador completo no seu processo.

P: Como posso saber se uma página precisa de JavaScript antes de escrever seletores?

Imprima doc.html() de uma busca simples com jsoup e procure um valor que você pode ver na página. Se o valor estiver ausente dessa string, mas visível no navegador, ele está sendo renderizado do lado do cliente. Comparar a contagem de seletores com a contagem visível capta a mesma coisa.

P: Qual é a razão prática para mudar maxBodySize?

O padrão mantém 2 MiB, que é menor do que muitas páginas de lista, arquivo e comparação. Se o seu alvo exceder esse limite, tudo que passar do corte estará ausente da árvore analisada, sem erro gerado. Defina maxBodySize(0) quando você precisar do documento inteiro e compare os bytes recebidos com Content-Length para saber quando isso importa.

P: O jsoup lida com HTML malformado?

Sim. Ele aplica as mesmas regras de recuperação de erros que um navegador, portanto, tags não fechadas e elementos mal aninhados ainda produzem uma árvore consultável. Essa é a principal razão para preferi-lo em relação a um parser XML estrito para páginas do mundo real.

P: É legal fazer scraping com Java?

A linguagem é irrelevante para a questão legal. O que importa são os dados que você coleta, os termos do site, as diretrizes de robôs que você respeita e a jurisdição em que opera. Restringa a coleta a páginas públicas, mantenha o volume de solicitações modesto e busque aconselhamento jurídico para qualquer coisa envolvendo dados pessoais.

Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.

Artigos mais populares

Catálogo