Formati e fonti di dati 8 min di lettura

Parsing di HTML, link e testo: problemi, codifiche ed esempi in vari linguaggi

Come estrarre testo e link dall’HTML: codifiche, markup rotto, URL relativi ed esempi di soluzione in Python, JavaScript, PHP, Go e Java.

TW
Team Web-Scraping.it
Raccolta dati per le esigenze del business
Pubblicato il 14 marzo 2025

Estrarre dati dalle pagine web passa, in sostanza, per tre operazioni di parsing: analizzare il markup HTML, raccogliere i link e isolare il contenuto testuale. A prima vista il compito sembra semplice, ma in pratica emergono quasi sempre degli ostacoli: markup «sporco», contenuto dinamico, blocchi e — un classico particolarmente doloroso per i siti in italiano — i problemi con le codifiche e le lettere accentate.

In questo articolo esaminiamo i tre scenari di base (HTML, link, testo) e i problemi tipici, con esempi di implementazione in diversi linguaggi.


1. Parsing dell’HTML

Il parsing dell’HTML consiste nel costruire, a partire dalla stringa di markup, un albero di elementi (DOM) in cui ci si può muovere e da cui si possono selezionare i nodi necessari. Il più delle volte si lavora con:

  • selettori CSS.product .price, div#content > p;
  • XPath//div[@class="product"]//span[@class="price"];
  • espressioni regolari — ammissibili solo in casi molto semplici; per l’HTML vero e proprio è meglio non usarle (l’HTML non è un linguaggio regolare).

L’idea chiave: non analizzare l’HTML con le espressioni regolari. Usa un parser specializzato, capace di ricostruire il markup «rotto» proprio come fa il browser.

2. Parsing dei link

La raccolta dei link è la base di qualsiasi crawler. Di solito si tratta di:

  1. Selezionare tutti i tag <a> con l’attributo href.
  2. Convertire i link relativi in assoluti (/pagehttps://site.com/page).
  3. Filtrare: eliminare mailto:, tel:, javascript:, le ancore #, i duplicati.
  4. Normalizzare gli URL (maiuscole/minuscole dell’host, slash finale, ordine dei parametri della query).

L’errore principale dei principianti è trattare l’href come se fosse già un indirizzo assoluto pronto all’uso. Sui siti reali i link possono essere relativi, relativi al protocollo (//cdn.site.com/...) o dipendere dal tag <base href>.

3. Estrazione del testo

Estrarre il testo significa ottenere il contenuto «umano» senza tag. Qui è importante:

  • eliminare <script>, <style> e gli elementi nascosti;
  • gestire correttamente gli spazi, gli a capo e gli spazi indivisibili (&nbsp;);
  • decodificare le entità HTML (&amp;&, &egrave;è, &laquo;«);
  • se possibile, separare il contenuto principale da navigazione, pubblicità e footer (il compito noto come «content extraction»).

4. Quali problemi puoi incontrare

HTML «sporco» e non valido. Tag non chiusi, annidamento sbagliato, virgolette mancanti. Un buon parser perdona questi errori; le espressioni regolari no.

Contenuto dinamico (JavaScript). Se i dati vengono caricati via AJAX/JS, non sono nell’HTML originale. Serve un browser headless (Selenium, Playwright, Puppeteer) oppure bisogna puntare direttamente all’API o alle richieste XHR che la pagina esegue.

Layout che cambia. I selettori si rompono a ogni redesign. Meglio scegliere segnali stabili (id, attributi semantici, microdati) invece di catene di classi lunghe e fragili.

Protezione anti-bot. Captcha, controllo dello User-Agent, rate-limiting, blocco per IP, Cloudflare. Servono pause tra le richieste, rotazione di header e proxy, rispetto del robots.txt e buon senso.

Codifiche e lettere accentate. Il problema più frequente del web italiano; gli dedichiamo una sezione a parte più sotto.

Prestazioni. Su grandi volumi contano l’asincronia, i pool di connessioni e l’analisi in streaming (in stile SAX) invece del caricamento dell’intero DOM in memoria.

Aspetti legali ed etici. Le condizioni d’uso del sito, i dati personali, il diritto d’autore, il carico sul server altrui.


5. Codifiche e lettere accentate

La maggior parte dei problemi con i caratteri «corrotti» (città al posto di città, perch� al posto di perché) si riduce a una sola causa: i byte sono stati letti con una codifica diversa da quella con cui erano stati scritti.

Nel web italiano convivono tre codifiche principali:

  • UTF-8 — lo standard moderno, il default del web attuale;
  • Windows-1252 (CP1252) — codifica legacy, ancora viva sui siti più vecchi;
  • ISO-8859-1 (Latin-1) — il classico degli anni Novanta; in pratica i browser lo trattano come Windows-1252.

Come determinare la codifica corretta

L’ordine di priorità, così come lo applica il browser:

  1. L’header HTTP Content-Type: text/html; charset=windows-1252.
  2. Il meta tag nell’HTML: <meta charset="utf-8"> oppure <meta http-equiv="Content-Type" content="text/html; charset=windows-1252">.
  3. Il BOM (Byte Order Mark) all’inizio del file, nel caso di UTF.
  4. Il rilevamento automatico dalla statistica dei byte (librerie chardet, charset-normalizer).

Una sottigliezza importante: non si può determinare la codifica da una stringa già decodificata. La codifica si determina sui byte grezzi della risposta, e solo dopo si decodifica in stringa.

Errori tipici

  • Decodificare Windows-1252 come UTF-8 → errore o «spazzatura».
  • Il charset dichiarato nell’header non coincide con quello reale: il server «mente».
  • Codifiche miste all’interno della stessa pagina.
  • Doppia decodifica o codifica (mojibake).
  • Dimenticare di indicare la codifica quando si salva il risultato in un file o nel database.

La ricetta universale: leggiamo i byte → determiniamo la codifica → decodifichiamo in Unicode → dentro il programma lavoriamo solo in Unicode → in uscita codifichiamo in UTF-8.


6. Esempi di implementazione in vari linguaggi

In tutti gli esempi il compito è lo stesso: caricare la pagina, determinare correttamente la codifica, raccogliere tutti i link ed estrarre il testo del titolo.

Python — requests + BeautifulSoup

La coppia più popolare. requests prova a rilevare la codifica da solo e BeautifulSoup digerisce bene l’HTML «sporco».

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"})

# Rilevamento corretto della codifica dal contenuto (non solo dall'header)
resp.encoding = resp.apparent_encoding  # usa charset-normalizer/chardet

soup = BeautifulSoup(resp.text, "lxml")  # il parser lxml è veloce e robusto

# Estrazione del testo
title = soup.title.get_text(strip=True)
print("Titolo:", title)

# Parsing dei link: da relativi ad assoluti, con filtro
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"Link trovati: {len(links)}")

Se la codifica va determinata a mano a partire dai byte:

python
import charset_normalizer

raw = resp.content                      # byte grezzi
best = charset_normalizer.from_bytes(raw).best()
html = str(best)                        # stringa Unicode già corretta

JavaScript / Node.js — axios + cheerio

cheerio è una API in stile jQuery per il server. La codifica conviene determinarla esplicitamente con iconv-lite, perché Node di default si aspetta UTF-8.

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

(async () => {
  const url = "https://example.com";
  // Riceviamo esattamente i byte
  const { data } = await axios.get(url, {
    responseType: "arraybuffer",
    headers: { "User-Agent": "Mozilla/5.0" },
  });

  // Rileviamo la codifica e decodifichiamo
  const encoding = chardet.detect(data) || "utf-8";
  const html = iconv.decode(Buffer.from(data), encoding);

  const $ = cheerio.load(html);

  // Testo
  console.log("Titolo:", $("title").text().trim());

  // Link
  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); // da relativi ad assoluti
  });
  console.log("Link trovati:", links.size);
})();

PHP — DOMDocument

Il DOMDocument integrato sa fare il parsing dell’HTML «di fabbrica». Con le lettere accentate è fondamentale passargli la codifica giusta; il trucco più affidabile è mostrato qui sotto.

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

// Se la pagina è in windows-1252, la convertiamo in UTF-8 prima del parsing
$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); // silenziamo gli errori dell'HTML «sporco»
// Trucco perché DOMDocument non rovini l'UTF-8: dichiariamo la codifica esplicitamente
$dom->loadHTML('<?xml encoding="UTF-8">' . $html);
libxml_clear_errors();

$xpath = new DOMXPath($dom);

// Testo
$titleNode = $xpath->query("//title")->item(0);
echo "Titolo: " . trim($titleNode->textContent) . PHP_EOL;

// Link
$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 tramite chiave
}
echo "Link trovati: " . count($links) . PHP_EOL;

Go — net/http + goquery

goquery replica l’API di jQuery. Per le codifiche c’è il pacchetto golang.org/x/net/html/charset, che legge da solo il charset dagli header e dai meta tag.

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 automaticamente il flusso in UTF-8 secondo il charset dell'header o del 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)
    }

    // Testo
    fmt.Println("Titolo:", doc.Find("title").First().Text())

    // Link
    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("Link trovati:", len(links))
}

Java — Jsoup

Jsoup è una delle librerie più comode: download, parsing, selettori e rilevamento della codifica in un unico pacchetto.

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";
        // Jsoup rileva da solo la codifica da header e meta tag
        Document doc = Jsoup.connect(url)
                .userAgent("Mozilla/5.0")
                .get();

        // Testo
        System.out.println("Titolo: " + doc.title());

        // Link (absUrl li converte in assoluti)
        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("Link trovati: " + links.size());
    }
}

7. Raccomandazioni rapide

  • Non fare il parsing dell’HTML con le espressioni regolari: usa un parser completo (lxml, cheerio, goquery, Jsoup, DOMDocument).
  • Determina la codifica dai byte, non dalla stringa, e porta tutto in UTF-8 il prima possibile.
  • Converti sempre i link relativi in assoluti (urljoin, new URL(base), absUrl).
  • Con i siti dinamici preparati a usare un browser headless o a lavorare con l’API interna.
  • Rispetta il robots.txt, inserisci delle pause e non mettere in ginocchio il server altrui.