El módulo html.parser ofrece un parser de HTML y XHTML orientado a eventos. Se crea una subclase de HTMLParser y se sobrescriben métodos invocados al encontrar tags de apertura, cierre, texto, comentarios, referencias y declaraciones.
Es ligero, forma parte de la biblioteca estándar y tolera muchos documentos malformados de la web. Sirve para extraer enlaces, títulos, texto y metadatos, crear validadores específicos y procesar HTML en streaming. No construye un DOM completo, no ejecuta JavaScript y no sanitiza contenido.
Cómo funciona HTMLParser
El parser recibe texto con feed(). Los elementos completos generan eventos; los fragmentos incompletos se guardan hasta la siguiente llamada o hasta close().
from html.parser import HTMLParser
class DebugParser(HTMLParser):
def handle_starttag(self, tag, attrs):
print("inicio", tag, attrs)
def handle_endtag(self, tag):
print("fin", tag)
def handle_data(self, data):
print("texto", repr(data))
parser = DebugParser()
parser.feed("<h1>Hola & Python</h1>")
parser.close()
Con convert_charrefs=True, las referencias se convierten en caracteres Unicode, salvo en contextos especiales como script y style.
Extraer enlaces
from html.parser import HTMLParser
from urllib.parse import urljoin
class LinkParser(HTMLParser):
def __init__(self, base_url: str):
super().__init__()
self.base_url = base_url
self.links: list[str] = []
def handle_starttag(self, tag, attrs):
if tag != "a":
return
attributes = dict(attrs)
href = attributes.get("href")
if href:
self.links.append(urljoin(self.base_url, href))
parser = LinkParser("https://example.com/docs/")
parser.feed('<a href="python.html">Python</a>')
parser.close()
print(parser.links)
Los nombres de tags y atributos se convierten a minúsculas, las comillas se eliminan y los atributos sin valor reciben None. Consulta la guía de urllib.parse en Python.
Valida las URLs extraídas
Un enlace puede usar javascript:, data:, file: o una origen inesperada. El parser solo devuelve texto; no decide si es seguro.
from urllib.parse import urlsplit
parts = urlsplit(candidate)
if parts.scheme not in {"http", "https"}:
return
if parts.hostname not in {"example.com", "www.example.com"}:
return
Al descargar destinos, aplica protección SSRF, redirects, límites y timeout según la guía de urllib.request en Python.
Extraer texto visible
handle_data() recibe texto normal y también contenido de script y style. Mantén contexto para crear texto legible.
class TextParser(HTMLParser):
ignored = {"script", "style", "noscript"}
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.skip_depth = 0
self.parts: list[str] = []
def handle_starttag(self, tag, attrs):
if tag in self.ignored:
self.skip_depth += 1
def handle_endtag(self, tag):
if tag in self.ignored and self.skip_depth:
self.skip_depth -= 1
def handle_data(self, data):
if self.skip_depth == 0:
cleaned = " ".join(data.split())
if cleaned:
self.parts.append(cleaned)
def text(self) -> str:
return " ".join(self.parts)
HTML malformado puede cerrar tags en orden extraño. Un contador sirve en entradas controladas, pero no reemplaza las reglas completas de construcción de árbol HTML5.
Extraer título y description
class MetadataParser(HTMLParser):
def __init__(self):
super().__init__()
self.in_title = False
self.title_parts = []
self.description = None
def handle_starttag(self, tag, attrs):
attributes = dict(attrs)
if tag == "title":
self.in_title = True
elif tag == "meta" and attributes.get("name", "").lower() == "description":
self.description = attributes.get("content")
def handle_endtag(self, tag):
if tag == "title":
self.in_title = False
def handle_data(self, data):
if self.in_title:
self.title_parts.append(data)
@property
def title(self):
return " ".join("".join(self.title_parts).split())
Las páginas pueden contener tags duplicados, valores vacíos o metadatos generados por JavaScript. Define una política y limita el contenido acumulado.
Parsing incremental
feed() acepta fragmentos y guarda tags incompletas.
parser = LinkParser("https://example.com/")
for chunk in ["<a hr", 'ef="/docs">Doc', "umentación</a>"]:
parser.feed(chunk)
parser.close()
Esto combina con descargas por bloques. Sin embargo, feed() requiere str. Usa un decoder incremental para no cortar secuencias UTF-8. La guía de codecs en Python explica este proceso.
Decodificación incremental con límite
import codecs
parser = TextParser()
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
total = 0
while chunk := response.read(64 * 1024):
total += len(chunk)
if total > 5 * 1024 * 1024:
raise ValueError("HTML demasiado grande")
parser.feed(decoder.decode(chunk))
parser.feed(decoder.decode(b"", final=True))
parser.close()
Determina el encoding mediante una política fiable. Headers, BOM y declaraciones HTML pueden contradecirse. Scraping complejo puede requerir una biblioteca con reglas completas de encoding HTML.
Comentarios y declaraciones
handle_comment() recibe el contenido del comentario. handle_decl() procesa declaraciones como DOCTYPE.
class AuditParser(HTMLParser):
def handle_comment(self, data):
if "TODO" in data:
print("comentario de revisión")
def handle_decl(self, decl):
print("declaración", decl)
Nunca pongas secretos en comentarios: se envían a todos los clientes.
Referencias de caracteres
Con convert_charrefs=True, las entidades se convierten automáticamente. Para observar la forma original usa convert_charrefs=False e implementa handle_entityref() y handle_charref().
Convertir entidades no sanitiza HTML. Una cadena con ángulos decodificados puede ser peligrosa si después se inserta como markup sin escape contextual.
Parámetro scripting
Desde Python 3.14.1, HTMLParser acepta scripting. Si es verdadero, el contenido de noscript se entrega como texto bruto en vez de parsearse. No ejecuta JavaScript.
parser = TextParser(scripting=True)
Si la subclase define __init__, acepta y reenvía los argumentos a super().__init__().
HTML inválido y límites
El parser tolera markup inválido, pero no comprueba que los cierres correspondan ni genera todos los cierres implícitos de un navegador. La secuencia de eventos puede diferir del DOM real.
Para selectores CSS, edición estructural y fidelidad HTML5 usa Beautiful Soup, lxml o html5lib. Consulta la guía de web scraping con Beautiful Soup.
Parsing no es sanitización
HTMLParser no elimina scripts, atributos de evento, URLs peligrosas ni CSS malicioso. No lo uses solo para permitir HTML de usuarios.
La sanitización necesita una biblioteca mantenida, allowlists de tags, atributos y protocolos, y escape contextual al renderizar.
No ejecutes contenido extraído
No pases texto de script a eval(), shell u otro intérprete. No abras automáticamente URLs ni uses atributos como rutas locales. Trata todo HTML externo como no confiable.
Protección de recursos
Limita bytes, cantidad de links, longitud de atributos y texto acumulado.
if len(self.links) > 10_000:
raise ValueError("Demasiados enlaces")
if href and len(href) > 4_096:
return
El parser no ofrece política global; la subclase debe imponerla.
Reutilización y reset
Crea una instancia por documento o llama a reset() y limpia también todo estado propio. Olvidar listas y flags mezcla resultados.
Pruebas
Incluye atributos sin comillas, entidades, comentarios, tags divididas entre chunks, anidamiento incorrecto, scripts, URLs relativas, valores vacíos y límites. Asegura que close() se llame siempre.
Errores comunes
Los fallos típicos son pasar bytes, olvidar close(), asumir estructura igual al navegador, recoger scripts, aceptar cualquier URL, omitir límites, reutilizar estado, confundir entidades con sanitización y renderizar texto extraído como HTML.
Buenas prácticas
Usa una instancia por documento, decoder incremental, límites de bytes y eventos, validación de URLs y tratamiento explícito de scripts. Elige un parser de árbol para estructura completa. Para contenido de usuarios, usa sanitizador y escape contextual.
Conclusión
html.parser es una herramienta ligera para extracción orientada a eventos. Procesa HTML en bloques, tolera markup imperfecto y recopila texto, enlaces y metadatos sin dependencias. Sus límites son claros: no es navegador, parser HTML5 completo ni sanitizador de seguridad.
Consulta la documentación oficial de html.parser y la HTML Living Standard.







