Formatos e fontes de dados 8 min de leitura

Parsing de HTML, links e texto: problemas, codificações e exemplos em várias linguagens

Como extrair texto e links do HTML: codificações, marcação quebrada, URLs relativas e exemplos de solução em Python, JavaScript, PHP, Go e Java.

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

Extrair dados de páginas web passa, em essência, por três operações de parsing: analisar a marcação HTML, coletar os links e isolar o conteúdo textual. À primeira vista a tarefa parece simples, mas na prática quase sempre afloram obstáculos: marcação «suja», conteúdo dinâmico, bloqueios e — um clássico especialmente doloroso nos sites em português — os problemas com as codificações, os acentos e as cedilhas.

Neste artigo repassamos os três cenários básicos (HTML, links, texto), os problemas típicos, e mostramos exemplos de implementação em várias linguagens.


1. Parsing de HTML

O parsing de HTML consiste em construir, a partir da string de marcação, uma árvore de elementos (DOM) pela qual se pode navegar e da qual se podem selecionar os nós necessários. O mais habitual é trabalhar com:

  • seletores CSS.product .price, div#content > p;
  • XPath//div[@class="product"]//span[@class="price"];
  • expressões regulares — admissíveis apenas em casos muito simples; para HTML de verdade não convém usá-las (HTML não é uma linguagem regular).

A ideia-chave: não parseie HTML com expressões regulares. Use um parser especializado, capaz de reconstruir a marcação «quebrada» do mesmo jeito que o navegador faz.

2. Parsing de links

A coleta de links é a base de qualquer rastreador (crawler). Normalmente consiste em:

  1. Selecionar todas as tags <a> com atributo href.
  2. Converter os links relativos em absolutos (/pagehttps://site.com/page).
  3. Filtrar: eliminar mailto:, tel:, javascript:, âncoras #, duplicados.
  4. Normalizar as URLs (maiúsculas e minúsculas do host, barra final, ordem dos parâmetros da consulta).

O principal erro dos novatos é tratar o href como se já fosse um endereço absoluto pronto para usar. Nos sites reais os links podem ser relativos, relativos ao protocolo (//cdn.site.com/...) ou depender da tag <base href>.

3. Extração de texto

Extrair o texto significa obter o conteúdo «humano» sem tags. Aqui o importante é:

  • remover <script>, <style> e os elementos ocultos;
  • tratar corretamente os espaços, as quebras de linha e os espaços inseparáveis (&nbsp;);
  • decodificar as entidades HTML (&amp;&, &ccedil;ç, &laquo;«);
  • na medida do possível, separar o conteúdo principal da navegação, da publicidade e do rodapé (a tarefa conhecida como «content extraction»).

4. Com que problemas você pode se deparar

HTML «sujo» e inválido. Tags sem fechar, aninhamento incorreto, aspas ausentes. Um bom parser perdoa esses erros; as expressões regulares, não.

Conteúdo dinâmico (JavaScript). Se os dados são carregados via AJAX/JS, eles não estão no HTML original. É preciso ou um navegador headless (Selenium, Playwright, Puppeteer), ou atacar diretamente a API ou as requisições XHR que a página realiza.

Layout que muda. Os seletores quebram a cada redesign. Convém escolher sinais estáveis (id, atributos semânticos, microdados) em vez de cadeias de classes longas e frágeis.

Proteção anti-bots. Captchas, checagem do User-Agent, rate-limiting, bloqueio por IP, Cloudflare. São necessárias pausas entre requisições, rotação de cabeçalhos e proxies, respeito ao robots.txt e bom senso.

Codificações e caracteres do português. A dor mais frequente da web lusófona; dedicamos a ela uma seção própria mais abaixo.

Desempenho. Com grandes volumes importam a assincronia, os pools de conexões e a análise em fluxo (no estilo SAX) em vez de carregar o DOM inteiro na memória.

Aspectos legais e éticos. Os termos de uso do site, os dados pessoais, os direitos autorais, a carga que o servidor alheio suporta.


5. Codificações, acentos e cedilhas

A maioria dos problemas com caracteres «corrompidos» (não em vez de não, informa��o em vez de informação) se reduz a uma única causa: os bytes foram lidos com uma codificação diferente daquela com que foram escritos.

Na web lusófona convivem três codificações principais:

  • UTF-8 — o padrão moderno, o valor padrão da web atual;
  • Windows-1252 (CP1252) — codificação legada, ainda viva em sites antigos;
  • ISO-8859-1 (Latin-1) — o padrão clássico dos anos noventa; na prática, os navegadores o tratam como Windows-1252.

Como determinar a codificação correta

A ordem de prioridades, tal como o navegador a aplica:

  1. O cabeçalho HTTP Content-Type: text/html; charset=windows-1252.
  2. A tag meta no HTML: <meta charset="utf-8"> ou <meta http-equiv="Content-Type" content="text/html; charset=windows-1252">.
  3. O BOM (Byte Order Mark) no início do arquivo, no caso de UTF.
  4. A autodetecção por estatística de bytes (bibliotecas chardet, charset-normalizer).

Uma nuance importante: não dá para determinar a codificação a partir de uma string já decodificada. A codificação se determina sobre os bytes brutos da resposta, e só depois se decodifica para string.

Erros típicos

  • Decodificar Windows-1252 como UTF-8 → erro ou «lixo».
  • O charset declarado no cabeçalho não coincide com o real: o servidor «mente».
  • Codificações misturadas dentro de uma mesma página.
  • Dupla decodificação ou codificação (mojibake).
  • Esquecer de indicar a codificação ao gravar o resultado em um arquivo ou no banco de dados.

A receita universal: lemos os bytes → determinamos a codificação → decodificamos para Unicode → dentro do programa trabalhamos só em Unicode → na saída codificamos em UTF-8.


6. Exemplos de implementação em várias linguagens

Em todos os exemplos a tarefa é a mesma: carregar a página, determinar corretamente a codificação, coletar todos os links e extrair o texto do título.

Python — requests + BeautifulSoup

A dupla mais popular. O requests tenta detectar a codificação por conta própria, e o BeautifulSoup digere bem o HTML «sujo».

python
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin

url = "https://example.com"
resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})

# Detecção correta da codificação pelo conteúdo (não apenas pelo cabeçalho)
resp.encoding = resp.apparent_encoding  # usa charset-normalizer/chardet

soup = BeautifulSoup(resp.text, "lxml")  # o parser lxml é rápido e robusto

# Extração do texto
title = soup.title.get_text(strip=True)
print("Título:", title)

# Parsing de links: de relativos a absolutos, com filtragem
links = set()
for a in soup.select("a[href]"):
    href = a["href"].strip()
    if href.startswith(("mailto:", "tel:", "javascript:", "#")):
        continue
    links.add(urljoin(url, href))

print(f"Links encontrados: {len(links)}")

Se for preciso determinar a codificação manualmente a partir dos bytes:

python
import charset_normalizer

raw = resp.content                      # bytes brutos
best = charset_normalizer.from_bytes(raw).best()
html = str(best)                        # string Unicode já correta

JavaScript / Node.js — axios + cheerio

O cheerio é uma API no estilo jQuery para o servidor. A codificação convém determinar de forma explícita com o iconv-lite, já que o Node espera UTF-8 por padrão.

javascript
const axios = require("axios");
const cheerio = require("cheerio");
const iconv = require("iconv-lite");
const chardet = require("chardet");

(async () => {
  const url = "https://example.com";
  // Recebemos exatamente os bytes
  const { data } = await axios.get(url, {
    responseType: "arraybuffer",
    headers: { "User-Agent": "Mozilla/5.0" },
  });

  // Detectamos a codificação e decodificamos
  const encoding = chardet.detect(data) || "utf-8";
  const html = iconv.decode(Buffer.from(data), encoding);

  const $ = cheerio.load(html);

  // Texto
  console.log("Título:", $("title").text().trim());

  // Links
  const links = new Set();
  $("a[href]").each((_, el) => {
    const href = ($(el).attr("href") || "").trim();
    if (/^(mailto:|tel:|javascript:|#)/.test(href)) return;
    links.add(new URL(href, url).href); // de relativos a absolutos
  });
  console.log("Links encontrados:", links.size);
})();

PHP — DOMDocument

O DOMDocument integrado sabe parsear HTML «de fábrica». Com os caracteres acentuados é crucial passar a ele a codificação correta; o truque mais confiável é mostrado a seguir.

php
<?php
$url = "https://example.com";
$html = file_get_contents($url);

// Se a página estiver em windows-1252, convertemos para UTF-8 antes de parsear
$encoding = mb_detect_encoding($html, ["UTF-8", "Windows-1252", "ISO-8859-1"], true);
if ($encoding && $encoding !== "UTF-8") {
    $html = mb_convert_encoding($html, "UTF-8", $encoding);
}

$dom = new DOMDocument();
libxml_use_internal_errors(true); // silenciamos os erros do HTML «sujo»
// Truque para o DOMDocument não quebrar o UTF-8: declaramos a codificação explicitamente
$dom->loadHTML('<?xml encoding="UTF-8">' . $html);
libxml_clear_errors();

$xpath = new DOMXPath($dom);

// Texto
$titleNode = $xpath->query("//title")->item(0);
echo "Título: " . trim($titleNode->textContent) . PHP_EOL;

// Links
$links = [];
foreach ($xpath->query("//a[@href]") as $a) {
    $href = trim($a->getAttribute("href"));
    if (preg_match('/^(mailto:|tel:|javascript:|#)/', $href)) continue;
    $links[$href] = true; // deduplicação por chave
}
echo "Links encontrados: " . count($links) . PHP_EOL;

Go — net/http + goquery

O goquery replica a API do jQuery. Para as codificações existe o pacote golang.org/x/net/html/charset, que lê sozinho o charset dos cabeçalhos e das tags meta.

go
package main

import (
    "fmt"
    "net/http"
    "net/url"

    "github.com/PuerkitoBio/goquery"
    "golang.org/x/net/html/charset"
)

func main() {
    target := "https://example.com"
    resp, err := http.Get(target)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    // Converte o fluxo para UTF-8 automaticamente conforme o charset do cabeçalho ou da meta
    utf8Reader, err := charset.NewReader(resp.Body, resp.Header.Get("Content-Type"))
    if err != nil {
        panic(err)
    }

    doc, err := goquery.NewDocumentFromReader(utf8Reader)
    if err != nil {
        panic(err)
    }

    // Texto
    fmt.Println("Título:", doc.Find("title").First().Text())

    // Links
    base, _ := url.Parse(target)
    links := map[string]bool{}
    doc.Find("a[href]").Each(func(_ int, s *goquery.Selection) {
        href, _ := s.Attr("href")
        if u, err := base.Parse(href); err == nil {
            links[u.String()] = true
        }
    })
    fmt.Println("Links encontrados:", len(links))
}

Java — Jsoup

O Jsoup é uma das bibliotecas mais cômodas: download, parsing, seletores e detecção da codificação em um único pacote.

java
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.util.HashSet;
import java.util.Set;

public class Parser {
    public static void main(String[] args) throws Exception {
        String url = "https://example.com";
        // O Jsoup detecta a codificação sozinho a partir de cabeçalhos e tags meta
        Document doc = Jsoup.connect(url)
                .userAgent("Mozilla/5.0")
                .get();

        // Texto
        System.out.println("Título: " + doc.title());

        // Links (absUrl os converte em absolutos)
        Set<String> links = new HashSet<>();
        for (Element a : doc.select("a[href]")) {
            String href = a.absUrl("href");
            if (href.isBlank()) continue;
            links.add(href);
        }
        System.out.println("Links encontrados: " + links.size());
    }
}

7. Recomendações breves

  • Não parseie HTML com expressões regulares: use um parser completo (lxml, cheerio, goquery, Jsoup, DOMDocument).
  • Determine a codificação a partir dos bytes, não da string, e passe tudo para UTF-8 o quanto antes.
  • Converta sempre os links relativos em absolutos (urljoin, new URL(base), absUrl).
  • Com os sites dinâmicos, prepare-se para usar um navegador headless ou para trabalhar com a API interna.
  • Respeite o robots.txt, faça pausas e não derrube o servidor alheio.