El módulo xml.dom.pulldom ofrece un punto intermedio para procesar XML en Python. Combina un flujo de eventos controlado por el consumidor, parecido a SAX, con la posibilidad de construir subárboles DOM completos solo cuando son necesarios. Esta estrategia resulta útil cuando un documento es demasiado grande para cargarlo entero en memoria, pero ciertas secciones necesitan navegación, atributos, hijos y serialización DOM.
En lugar de construir todo el árbol al inicio, el programa recorre eventos como inicio de elemento, texto, comentarios y fin del documento. Cuando encuentra un elemento relevante, llama a expandNode() para materializar únicamente ese subárbol. Así se equilibra el bajo consumo de memoria del parsing por eventos con la comodidad de los nodos DOM.
Cuándo usar pulldom
pulldom encaja bien con catálogos, exportaciones financieras, logs, feeds, informes e integraciones heredadas con miles de registros repetidos. El sistema puede necesitar solo elementos con cierto estado, precio, identificador o fecha.
Cuando el documento completo cabe cómodamente en memoria y se necesita navegar libremente, consulta minidom en Python. Si solo necesitas eventos y valores extraídos sin fragmentos DOM, revisa xml.sax en Python. Para muchas transformaciones estructurales, ElementTree en Python suele ofrecer una API más sencilla.
Primer ejemplo
from xml.dom import pulldom
eventos = pulldom.parse("catalogo.xml")
for evento, nodo in eventos:
if evento == pulldom.START_ELEMENT and nodo.tagName == "producto":
eventos.expandNode(nodo)
print(nodo.toxml())
Antes de expandNode(), el nodo inicial no contiene todos los hijos materializados. Después de la expansión se comporta como un elemento de minidom y permite usar getAttribute(), getElementsByTagName() y toxml().
Filtrar antes de expandir
La principal ventaja aparece cuando los atributos disponibles en el evento inicial permiten decidir si vale la pena construir el subárbol.
from decimal import Decimal
from xml.dom import pulldom
eventos = pulldom.parse("catalogo.xml")
for evento, nodo in eventos:
if evento != pulldom.START_ELEMENT:
continue
if nodo.tagName != "producto":
continue
try:
precio = Decimal(nodo.getAttribute("precio"))
except Exception:
continue
if precio >= Decimal("100.00"):
eventos.expandNode(nodo)
print(nodo.toxml())
Esto evita crear objetos DOM para productos que no interesan. Los documentos externos grandes siguen necesitando límites de bytes, profundidad, cantidad de elementos, atributos y fragmentos expandidos.
Eventos disponibles
El flujo puede producir START_DOCUMENT, END_DOCUMENT, START_ELEMENT, END_ELEMENT, CHARACTERS, COMMENT, PROCESSING_INSTRUCTION e IGNORABLE_WHITESPACE. El tipo de nodo cambia según el evento y puede ser documento, elemento o texto.
No supongas que todo el texto llegará en un único evento. Los parsers orientados a eventos pueden dividir datos contiguos en varios fragmentos. Si recoges texto sin expandir el subárbol, acumula las partes en una lista y únelas al recibir el evento de cierre.
Mantener contexto con una pila
El flujo de eventos es plano. Para conocer la ruta del elemento actual, mantén una pila de nombres.
from xml.dom import pulldom
eventos = pulldom.parse("datos.xml")
pila = []
for evento, nodo in eventos:
if evento == pulldom.START_ELEMENT:
pila.append(nodo.tagName)
ruta = "/".join(pila)
if ruta == "catalogo/seccion/producto":
eventos.expandNode(nodo)
print(nodo.getAttribute("id"))
pila.pop()
elif evento == pulldom.END_ELEMENT and pila:
pila.pop()
Como expandNode() consume los eventos internos del elemento, ajusta la pila de forma coherente y valida el comportamiento con archivos pequeños.
Archivos, streams y strings
parse() acepta un nombre de archivo o un objeto similar a archivo. parseString() recibe XML ya cargado en memoria. Para datos obtenidos por red, descarga con límites explícitos y considera un archivo temporal antes del parsing. El artículo de urllib.request en Python explica descargas limitadas, y tempfile en Python muestra almacenamiento temporal seguro.
from io import BytesIO
from xml.dom import pulldom
xml_bytes = b"<raiz><item id='1'/></raiz>"
eventos = pulldom.parse(BytesIO(xml_bytes))
No aceptes un string ilimitado de un cliente para pasarlo directamente a parseString(). Aplica el límite antes de reservar el buffer completo.
Namespaces
pulldom.parse() activa el soporte de namespaces en el parser suministrado. En XML con espacios de nombres, usa namespaceURI, localName y tagName con cuidado. El prefijo puede cambiar sin alterar el significado.
from xml.dom import pulldom
eventos = pulldom.parse("feed.xml")
for evento, nodo in eventos:
if evento == pulldom.START_ELEMENT:
if nodo.namespaceURI == "urn:ejemplo:catalogo" and nodo.localName == "producto":
eventos.expandNode(nodo)
print(nodo.toxml())
Comparar solo un prefijo como cat:producto vuelve el código frágil. Prefiere la URI y el nombre local.
Seguridad de entidades externas
La documentación oficial advierte que el XML no autenticado puede ser peligroso. Desde Python 3.7.1, el parser SAX usado por defecto ya no procesa entidades externas generales. No reactives esa función para documentos enviados por usuarios. Las entidades externas pueden provocar lectura de archivos locales, solicitudes de red inesperadas o ataques de expansión.
Si necesitas un parser personalizado, instala un EntityResolver que rechace recursos externos y mantén desactivadas las features externas. El procesamiento incremental no vuelve seguro un XML hostil por sí solo.
Límites de profundidad y volumen
Define límites de bytes de entrada, número de eventos, profundidad, longitud de atributos, cantidad de nodos expandidos y tamaño de salida serializada. Un archivo pequeño puede contener anidamiento extremo o textos enormes.
MAX_EVENTOS = 1_000_000
MAX_PROFUNDIDAD = 100
contador = 0
pila = []
for evento, nodo in eventos:
contador += 1
if contador > MAX_EVENTOS:
raise ValueError("Se superó el límite de eventos")
if evento == pulldom.START_ELEMENT:
pila.append(nodo.tagName)
if len(pila) > MAX_PROFUNDIDAD:
raise ValueError("XML demasiado profundo")
elif evento == pulldom.END_ELEMENT and pila:
pila.pop()
Expandir, extraer y liberar
Un subárbol expandido mantiene objetos DOM mientras existan referencias. Procesa el nodo, extrae solo los valores necesarios y descártalo. No guardes todos los nodos expandidos de un archivo grande.
resultados = []
for evento, nodo in eventos:
if evento == pulldom.START_ELEMENT and nodo.tagName == "producto":
eventos.expandNode(nodo)
resultados.append({
"id": nodo.getAttribute("id"),
"xml": nodo.toxml(),
})
nodo.unlink()
unlink() puede liberar referencias internas antes, pero úsalo solo cuando el fragmento ya no sea necesario.
Serializar no significa sanitizar
toxml() genera sintaxis XML, pero no convierte los valores en confiables. El contenido extraído necesita validación antes de usarse en HTML, SQL, comandos, rutas o logs. El guía de html.entities en Python explica por qué decodificar y sanitizar son operaciones distintas.
Manejo de errores
Captura los fallos de parsing en una frontera clara, registra origen y posición sin revelar secretos y rechaza documentos incompletos. Las integraciones críticas no deberían corregir XML malformado en silencio.
from xml.dom import pulldom
from xml.sax import SAXParseException
try:
eventos = pulldom.parse("entrada.xml")
for evento, nodo in eventos:
pass
except SAXParseException as exc:
print(f"XML inválido en la línea {exc.getLineNumber()}")
Pruebas recomendadas
Prueba archivos vacíos, prefijos alternativos, atributos ausentes, texto fragmentado, profundidad excesiva, entidades externas desactivadas, documentos truncados, varios encodings y múltiples elementos objetivo. En fixtures pequeños, compara el resultado con ElementTree o minidom.
Cuándo elegir otra API
Usa ElementTree.iterparse() cuando basten un árbol ligero y eventos incrementales. Usa SAX cuando no necesites DOM. Usa minidom cuando el documento completo sea pequeño y la compatibilidad DOM importe. Elige pulldom cuando solo algunos fragmentos necesiten comportamiento DOM completo.
Conclusión
xml.dom.pulldom permite recorrer XML como eventos y expandir selectivamente los elementos relevantes. Esto reduce memoria sin renunciar a la comodidad DOM en los fragmentos importantes.
Filtra antes de expandir, mantén desactivadas las entidades externas, aplica límites y libera referencias cuanto antes. Consulta la documentación oficial de pulldom y las orientaciones de seguridad XML de Python.







