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

    Desarrollador configurando un servidor HTTPS y certificado TLS con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: crea un servidor HTTPS local en Python

    Aprende HTTPSServer en Python para crear servicios HTTPS locales, configurar certificados, usar hilos y comprender sus límites.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Archivos protegidos que representan extracción segura de TAR con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: extrae TAR con seguridad

    Aprende tarfile extraction_filter en Python para extraer archivos TAR con validación, seguridad y control de rutas.

    Ler mais

    Tempo de leitura: 6 minutos
    08/10/2026
    Código Python mostrando avisos controlados con catch_warnings
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: captura warnings en pruebas Python

    Aprende catch_warnings en Python para capturar, probar y controlar avisos con filtros específicos y alcance seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código Python que representa referencias persistentes de pickle
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: serializa referencias externas

    Aprende pickle persistent_id en Python para referencias externas estables, validación, seguridad, rendimiento y compatibilidad.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código binario que representa buffers y vistas de memoria en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: cuenta valores sin copiar buffers

    Aprende memoryview.count en Python para contar bytes y valores en buffers sin copias, con formatos, límites y buenas prácticas.

    Ler mais

    Tempo de leitura: 6 minutos
    06/10/2026
    Código Python y anotaciones de tipos
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: evita imports circulares en anotaciones

    Aprende annotationlib en Python para inspeccionar anotaciones diferidas, evitar imports circulares y crear herramientas seguras.

    Ler mais

    Tempo de leitura: 6 minutos
    06/10/2026