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.







