xml.sax en Python: procesa XML por eventos

Publicado el: 22/08/2026
Tempo de leitura: 5 minutos
Close-up of a laptop screen with code and a coffee mug, perfect for tech abstract themes.

El paquete xml.sax implementa la Simple API for XML, un modelo de parsing orientado a eventos. En lugar de construir un árbol completo, el parser lee el documento y llama métodos cuando encuentra inicio y fin de elementos, texto, namespaces, instrucciones de procesamiento y errores.

SAX resulta útil para archivos grandes, pipelines de transformación, importaciones y validaciones que no necesitan conservar todo el XML en memoria. La contrapartida es que la aplicación debe mantener estado, acumular texto fragmentado y decidir qué hacer en cada evento.

Primer handler SAX

import xml.sax
from xml.sax.handler import ContentHandler

class CatalogoHandler(ContentHandler):
    def startElement(self, name, attrs):
        print("inicio", name, dict(attrs))

    def characters(self, content):
        if content.strip():
            print("texto", repr(content))

    def endElement(self, name):
        print("fin", name)

xml.sax.parse("catalogo.xml", CatalogoHandler())

parse() crea un parser, conecta el handler y procesa un archivo o stream. Todo el trabajo de la aplicación ocurre dentro de callbacks; no se devuelve un árbol.

characters puede llamarse varias veces

El parser no garantiza que el texto contiguo llegue en una sola llamada. El buffering interno puede dividir un campo en varios fragmentos.

class ProductosHandler(ContentHandler):
    def __init__(self):
        super().__init__()
        self.tag_actual = None
        self.buffer = []

    def startElement(self, name, attrs):
        self.tag_actual = name
        self.buffer.clear()

    def characters(self, content):
        if self.tag_actual in {"nombre", "precio"}:
            self.buffer.append(content)

    def endElement(self, name):
        if name in {"nombre", "precio"}:
            valor = "".join(self.buffer).strip()
            print(name, valor)
        self.tag_actual = None
        self.buffer.clear()

Acumula contenido hasta el evento de cierre. Nunca trates cada callback characters() como un campo completo.

Estado y pila de elementos

Para estructuras anidadas, mantiene una pila que represente la ruta actual.

class RutaHandler(ContentHandler):
    def __init__(self):
        self.pila = []

    def startElement(self, name, attrs):
        self.pila.append(name)
        print("/".join(self.pila))

    def endElement(self, name):
        if not self.pila or self.pila[-1] != name:
            raise ValueError("estado inconsistente")
        self.pila.pop()

Define una profundidad máxima para que XML hostil extremadamente anidado no consuma recursos ilimitados.

Parser configurable

import xml.sax
from xml.sax.handler import feature_namespaces

parser = xml.sax.make_parser()
parser.setFeature(feature_namespaces, True)
parser.setContentHandler(MiHandler())
parser.parse("datos.xml")

make_parser() devuelve un XMLReader. Configura features antes del parsing. Cambiarlas durante la lectura puede lanzar SAXNotSupportedException.

Namespaces

Con namespaces habilitados, implementa startElementNS() y endElementNS(). El nombre llega como una tupla (URI, nombre_local).

class AtomHandler(ContentHandler):
    ATOM = "http://www.w3.org/2005/Atom"

    def startElementNS(self, name, qname, attrs):
        uri, local = name
        if uri == self.ATOM and local == "entry":
            print("nueva entrada")

qname puede ser None si no está activado el reporte de prefijos. Usa URI y nombre local para la lógica de negocio.

Eventos de mapeo de prefijos

startPrefixMapping() y endPrefixMapping() informan scopes de prefijos. Su orden no está garantizado como una pila simple entre sí. Mantén un mapa por scope si necesitas interpretar QNames dentro de texto o atributos.

Atributos

El parser puede reutilizar el objeto attrs. Si los datos deben sobrevivir al callback, haz una copia.

def startElement(self, name, attrs):
    atributos = dict(attrs.items())
    self.cola.append((name, atributos))

En modo namespace, usa AttributesNS y claves basadas en URI y nombre local.

Locator para línea y columna

class HandlerLocalizado(ContentHandler):
    def setDocumentLocator(self, locator):
        self.locator = locator

    def startElement(self, name, attrs):
        if name == "producto" and "id" not in attrs:
            linea = self.locator.getLineNumber()
            columna = self.locator.getColumnNumber()
            raise ValueError(
                f"producto sin id en {linea}:{columna}"
            )

El locator es correcto durante los callbacks. Copia línea y columna inmediatamente si necesitas conservar la información.

Tratamiento de errores

Un ErrorHandler recibe warnings, errores recuperables y errores fatales.

from xml.sax.handler import ErrorHandler

class Errores(ErrorHandler):
    def warning(self, exception):
        print("aviso", exception)

    def error(self, exception):
        raise exception

    def fatalError(self, exception):
        raise exception

Sin handler personalizado, los errores suelen lanzar SAXParseException. No continúes usando resultados parciales después de un error recuperable salvo que exista una política muy específica.

Bloquear entidades externas

Las entidades externas generales están desactivadas por defecto en versiones actuales de Python. No reactives feature_external_ges para XML suministrado por usuarios.

from xml.sax.handler import EntityResolver, feature_external_ges
from xml.sax.xmlreader import InputSource
import io

class ResolverBloqueado(EntityResolver):
    def resolveEntity(self, publicId, systemId):
        fuente = InputSource()
        fuente.setCharacterStream(io.StringIO(""))
        return fuente

parser.setFeature(feature_external_ges, False)
parser.setEntityResolver(ResolverBloqueado())

Las entidades externas pueden permitir lectura de archivos locales o conexiones salientes. Evalúa el modelo de amenazas antes de cambiar el valor seguro.

InputSource

InputSource puede especificar byte stream, character stream, encoding y system identifier.

from xml.sax.xmlreader import InputSource

fuente = InputSource()
fuente.setByteStream(archivo_binario)
fuente.setSystemId("entrada-controlada.xml")
parser.parse(fuente)

No uses un system ID remoto no confiable. Puede influir en resolución de recursos y diagnósticos.

Límites de bytes, elementos y profundidad

SAX reduce memoria, pero no elimina ataques de CPU, profundidad o texto gigante. Cuenta elementos, atributos, profundidad, longitud de campos y bytes totales.

class HandlerLimitado(ContentHandler):
    def __init__(self, max_elementos=100_000):
        self.max_elementos = max_elementos
        self.elementos = 0
        self.profundidad = 0

    def startElement(self, name, attrs):
        self.elementos += 1
        self.profundidad += 1
        if self.elementos > self.max_elementos:
            raise ValueError("demasiados elementos")
        if self.profundidad > 100:
            raise ValueError("XML demasiado profundo")

    def endElement(self, name):
        self.profundidad -= 1

Procesar registros en streaming

Un patrón común conserva únicamente el registro actual.

class RegistrosHandler(ContentHandler):
    def __init__(self, destino):
        self.destino = destino
        self.registro = None
        self.campo = None
        self.buffer = []

    def startElement(self, name, attrs):
        if name == "registro":
            self.registro = {"id": attrs.get("id")}
        elif self.registro is not None:
            self.campo = name
            self.buffer = []

    def characters(self, content):
        if self.campo is not None:
            self.buffer.append(content)

    def endElement(self, name):
        if self.registro is None:
            return
        if name == "registro":
            self.destino(self.registro)
            self.registro = None
        elif name == self.campo:
            self.registro[name] = "".join(self.buffer).strip()
            self.campo = None
            self.buffer = []

Valida y persiste cada registro. Usa transacciones e idempotencia cuando una entrada pueda repetirse.

Interrumpir antes del final

Lanza una excepción propia al encontrar el dato deseado o alcanzar un límite. Asegúrate de cerrar el stream externo en un bloque finally.

LexicalHandler

Un LexicalHandler opcional recibe comentarios, límites de DTD y límites de CDATA. Configúralo con property_lexical_handler. No todos los parsers soportan todas las propiedades; trata SAXNotRecognizedException y SAXNotSupportedException.

DTDHandler

Un DTDHandler recibe notations y entidades no parseadas. La mayoría de aplicaciones no lo necesita. No actives DTD externo o validación solo para recibir más eventos en contenido no confiable.

Generar XML con saxutils

xml.sax.saxutils incluye escape(), quoteattr() y XMLGenerator.

from xml.sax.saxutils import XMLGenerator

with open("salida.xml", "w", encoding="utf-8") as archivo:
    generador = XMLGenerator(archivo, encoding="utf-8")
    generador.startDocument()
    generador.startElement("estado", {})
    generador.characters("ok & validado")
    generador.endElement("estado")
    generador.endDocument()

El generador escapa correctamente los datos de texto. Aun así, valida nombres dinámicos de tags y atributos.

SAX, ElementTree o minidom

SAX es ideal para streaming y memoria controlada. ElementTree es más sencillo cuando necesitas consultar y modificar un árbol. Minidom ofrece un modelo DOM completo, pero conserva muchos objetos en memoria.

Consulta ElementTree en Python y minidom en Python.

Encoding

Prefiere proporcionar bytes para que la declaración XML determine el encoding. Si decodificas antes, garantiza que el codec corresponde al documento. Para problemas de codecs, consulta codecs en Python.

Logs

Registra nombre lógico del documento, cantidades, duración y ubicación del error. No registres XML completo, credenciales, tokens ni datos personales. Limita mensajes originados por el parser.

Pruebas recomendadas

Prueba fragmentación de characters(), espacios, namespaces, copia de atributos, profundidad excesiva, campos gigantes, XML malformado, entidades externas bloqueadas, features no soportadas, interrupción temprana, encoding y rollback de persistencia.

Errores comunes

Los fallos frecuentes son asumir un único callback de texto, no mantener pila, guardar attrs sin copiar, habilitar entidades externas, olvidar límites, usar prefijos en vez de URI, continuar tras errores, mezclar estado entre registros y elegir SAX cuando se necesita navegar hacia atrás.

Conclusión

xml.sax procesa XML como una secuencia de eventos y permite archivos grandes con memoria controlada. Implementa handlers pequeños, acumula texto hasta el cierre, modela namespaces por URI, bloquea entidades externas, impone límites y valida cada registro antes de persistir.

Consulta la documentación oficial de xml.sax y la referencia del proyecto SAX. Para XML externo, conserva los valores seguros y combina streaming con límites estrictos de recursos.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    minidom en Python: manipula XML con DOM

    Aprende xml.dom.minidom en Python para leer, navegar, crear y serializar XML con nodos DOM, namespaces, memoria y seguridad.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Vibrant green snake coiled on a tree branch amidst lush jungle foliage.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ElementTree: lee y modifica XML en Python

    Aprende ElementTree en Python para leer, buscar, modificar y generar XML con namespaces, parsing incremental, límites y seguridad.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    A retro blue mailbox attached to a vibrant yellow wall, perfect for vintage themes.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    poplib en Python: lee correos con POP3

    Aprende poplib en Python para acceder a POP3 con TLS, listar y descargar mensajes, usar UIDL, aplicar límites y evitar

    Ler mais

    Tempo de leitura: 6 minutos
    22/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    imaplib en Python: lee correos con IMAP

    Aprende imaplib en Python para acceder a buzones IMAP con TLS, buscar por UID, leer sin marcar mensajes, usar flags

    Ler mais

    Tempo de leitura: 6 minutos
    22/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ftplib en Python: FTP y FTPS seguros

    Aprende ftplib en Python para listar, descargar y subir archivos por FTP o FTPS con TLS, timeouts, límites, reanudación y

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    Rack de servidores que representa un endpoint creado con xmlrpc.server en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    xmlrpc.server: crea servidores XML-RPC

    Aprende xmlrpc.server en Python para crear servidores XML-RPC, registrar funciones, limitar métodos y rutas y evitar exposición insegura.

    Ler mais

    Tempo de leitura: 6 minutos
    21/08/2026