El módulo xml.parsers.expat expone directamente el parser XML Expat en Python. Es rápido, orientado a eventos y no valida contra schemas. En lugar de construir un árbol, la aplicación registra callbacks para inicios de elementos, texto, comentarios, namespaces y declaraciones.
Este nivel de control resulta útil en protocolos, conversores, analizadores e integraciones que necesitan rendimiento o callbacks especializados. Para la mayoría de aplicaciones, ElementTree en Python o xml.sax en Python ofrece una interfaz más sencilla.
Crear un parser
Usa ParserCreate(). Un encoding opcional sobrescribe la declaración del documento. Expat admite de forma nativa un conjunto limitado que incluye UTF-8, UTF-16, ISO-8859-1 y ASCII.
from xml.parsers import expat
parser = expat.ParserCreate()
Una instancia solo puede procesar un documento. Crea un parser nuevo para cada archivo.
Registrar handlers
Los handlers se asignan directamente a atributos del parser.
from xml.parsers import expat
def inicio(nombre, atributos):
print("inicio", nombre, atributos)
def fin(nombre):
print("fin", nombre)
def texto(datos):
if datos.strip():
print("texto", repr(datos))
parser = expat.ParserCreate()
parser.StartElementHandler = inicio
parser.EndElementHandler = fin
parser.CharacterDataHandler = texto
parser.Parse("<raiz><item id='1'>Hola</item></raiz>", True)
El último argumento de Parse() debe ser verdadero en la llamada final. Indica que no llegarán más datos y permite detectar tokens incompletos.
Parsing incremental
El documento puede enviarse en bloques. Esto ayuda con archivos grandes y streams, pero el volumen total sigue necesitando un límite explícito.
MAX_BYTES = 20 * 1024 * 1024
recibidos = 0
with open("datos.xml", "rb") as archivo:
while bloque := archivo.read(64 * 1024):
recibidos += len(bloque)
if recibidos > MAX_BYTES:
raise ValueError("XML demasiado grande")
parser.Parse(bloque, False)
parser.Parse(b"", True)
Limita también eventos, profundidad, texto acumulado y tiempo. Leer por bloques evita una sola asignación enorme, pero no impide ataques de expansión.
El texto puede fragmentarse
Expat puede llamar CharacterDataHandler varias veces para un único texto lógico, especialmente en saltos de línea o límites de bloques. Acumula fragmentos hasta cerrar el elemento.
partes = []
parser.buffer_text = True
def texto(datos):
partes.append(datos)
buffer_text=True reduce callbacks, pero no garantiza uno por elemento. buffer_size controla el buffer.
Namespaces
Pasa un separador de un carácter a ParserCreate() para activar namespaces.
parser = expat.ParserCreate(namespace_separator=" ")
def inicio(nombre, atributos):
namespace, _, local = nombre.partition(" ")
print(namespace, local)
Los nombres se expanden como URI, separador y nombre local. No dependas del prefijo original.
Posiciones y errores
Durante callbacks, CurrentLineNumber, CurrentColumnNumber y CurrentByteIndex informan la posición. Después de un error, usa los atributos de error y ErrorCode.
from xml.parsers import expat
try:
parser.Parse("<raiz><item></raiz>", True)
except expat.ExpatError as error:
mensaje = expat.ErrorString(error.code)
print(mensaje, error.lineno, error.offset)
No registres el documento completo en producción. Guarda origen, posición y un contexto pequeño solo cuando sea seguro.
Atributos ordenados y especificados
Por defecto, los atributos llegan como diccionario. Con ordered_attributes=True, llegan como lista alternando nombre y valor según el orden de la fuente. La orden de atributos no debe tener significado semántico.
specified_attributes=True informa solo atributos presentes en el documento y omite defaults derivados de declaraciones. Requiere conocimiento cuidadoso de DTD.
Entidades externas
ExternalEntityRefHandler puede cargar recursos externos, pero expone archivos locales y red. Para XML controlado por usuarios, no implementes un handler que abra el systemId.
El artículo de xmlreader en Python muestra cómo bloquear fuentes externas en SAX. La regla es la misma: el documento no debe elegir recursos arbitrarios.
Entidades de parámetro y DTD
SetParamEntityParsing() controla entidades de parámetro y UseForeignDTD() permite solicitar una DTD alternativa. Estas funciones amplían la superficie de ataque y deberían permanecer desactivadas para documentos no confiables.
Reparse deferral
Expat 2.6 introdujo reparse deferral para evitar trabajo cuadrático cuando tokens enormes llegan en fragmentos. SetReparseDeferralEnabled(False) desactiva esa protección y puede reintroducir denegación de servicio.
if hasattr(parser, "GetReparseDeferralEnabled"):
assert parser.GetReparseDeferralEnabled()
Mantén la protección activa. Un handler puede no ejecutarse inmediatamente después de cada bloque; es parte del mecanismo.
Protección contra billion laughs
Python 3.14.6 puede exponer métodos para ajustar la amplificación de entidades.
if hasattr(parser, "SetBillionLaughsAttackProtectionActivationThreshold"):
parser.SetBillionLaughsAttackProtectionActivationThreshold(8 * 1024 * 1024)
if hasattr(parser, "SetBillionLaughsAttackProtectionMaximumAmplification"):
parser.SetBillionLaughsAttackProtectionMaximumAmplification(100.0)
Los defaults dependen de la biblioteca Expat vinculada. Valores demasiado bajos pueden rechazar documentos legítimos. Prueba payloads reales y no aumentes límites sin entender la amenaza.
Protección de amplificación de memoria
Versiones recientes también exponen controles de asignación.
if hasattr(parser, "SetAllocTrackerActivationThreshold"):
parser.SetAllocTrackerActivationThreshold(64 * 1024 * 1024)
if hasattr(parser, "SetAllocTrackerMaximumAmplification"):
parser.SetAllocTrackerMaximumAmplification(100.0)
Estos controles complementan, pero no reemplazan, límites de bytes, tiempo, profundidad y eventos.
Comentarios, CDATA e instrucciones
Existen handlers para comentarios, límites CDATA, instrucciones, declaración XML, DTD y namespaces. CharacterDataHandler recibe texto normal y contenido CDATA; los handlers de CDATA permiten distinguir los límites sintácticos.
GetInputContext()
Durante un callback, GetInputContext() puede devolver datos cercanos al evento. Úsalo solo para diagnóstico controlado porque puede revelar información sensible.
ParseFile()
ParseFile() acepta un objeto con read(nbytes). Es cómodo, pero ofrece menos control sobre conteo de bytes y cancelación que un bucle explícito con Parse().
Errores de encoding
Encodings no soportados y bytes inválidos generan ExpatError. No sobrescribas el encoding solo para ocultar el error. Corrige la fuente o realiza una conversión deliberada.
Pruebas recomendadas
Prueba entrada completa y fragmentada, texto dividido, namespaces, encodings, documentos truncados, tags incompatibles, atributos duplicados, tokens enormes, entidades internas, DTD, entidades externas bloqueadas, profundidad y controles de amplificación.
Errores comunes
Los fallos frecuentes son reutilizar un parser, olvidar la llamada final, asumir un único callback de texto, desactivar reparse deferral, cargar entidades externas sin política, confiar solo en protecciones internas y omitir límites de aplicación.
Elegir otra API
Usa Expat directamente para callbacks de bajo nivel, rendimiento y control fino. Usa ElementTree para árboles, SAX para una interfaz estandarizada y pulldom en Python para fragmentos DOM selectivos.
Conclusión
xml.parsers.expat es rápido y detallado, pero la aplicación debe gestionar estado, texto fragmentado, límites y seguridad. Mantén reparse deferral y las protecciones de amplificación activas, evita entidades externas y crea un parser por documento.
Consulta la documentación oficial de Expat en Python y el proyecto oficial Expat.







