El módulo xml.etree.ElementInclude añade soporte limitado para XInclude en árboles creados con xml.etree.ElementTree. XInclude permite que un documento XML declare que otro archivo XML o recurso de texto debe insertarse en un punto específico.
Puede simplificar configuraciones, documentación, catálogos y documentos grandes divididos en secciones reutilizables. También crea riesgos: lectura de archivos fuera de un directorio permitido, solicitudes remotas, ciclos, expansión de contenido y dependencias ocultas. Los loaders personalizados, rutas autorizadas y límites estrictos son esenciales.
Qué hace XInclude
XInclude usa el namespace http://www.w3.org/2001/XInclude. Un elemento xi:include incluye href y puede usar parse="xml" o parse="text".
<documento xmlns:xi="http://www.w3.org/2001/XInclude">
<titulo>Informe</titulo>
<xi:include href="secciones/resumen.xml" parse="xml"/>
</documento>
Durante la expansión, el elemento se sustituye por la raíz del XML referenciado. En modo texto, se inserta contenido de caracteres.
Primer ejemplo
from xml.etree import ElementTree, ElementInclude
arbol = ElementTree.parse("documento.xml")
raiz = arbol.getroot()
ElementInclude.include(raiz)
arbol.write(
"resultado.xml",
encoding="utf-8",
xml_declaration=True,
)
El loader por defecto trata href como nombre de archivo. Es cómodo para proyectos locales confiables, pero no es seguro cuando el XML o las rutas pueden ser controlados por usuarios.
Integración con ElementTree
ElementInclude trabaja con objetos Element o ElementTree. Antes de usar XInclude, conviene dominar parsing, namespaces, búsquedas y serialización. Consulta ElementTree en Python.
La expansión modifica el árbol original. Si necesitas conservar la versión previa, vuelve a parsear la fuente o crea una copia controlada.
Modo XML y modo texto
Con parse="xml", el loader debe devolver un Element. Con parse="text", devuelve un string. El encoding de texto por defecto es UTF-8, salvo indicación contraria.
<documento xmlns:xi="http://www.w3.org/2001/XInclude">
<pie>
<xi:include href="anio.txt" parse="text" encoding="utf-8"/>
</pie>
</documento>
El texto incluido no se convierte automáticamente en markup XML. Permanece como contenido y será escapado al serializar.
Referencias relativas con base_url
El argumento base_url ayuda a resolver referencias relativas respecto al documento principal.
from pathlib import Path
from xml.etree import ElementTree, ElementInclude
archivo = Path("configs/principal.xml").resolve()
arbol = ElementTree.parse(archivo)
ElementInclude.include(
arbol.getroot(),
base_url=archivo.as_uri(),
max_depth=4,
)
base_url no es una barrera de seguridad. No bloquea ../, rutas absolutas ni URLs remotas si el loader las acepta.
Profundidad máxima
include() usa max_depth=6 por defecto. El límite reduce recursión y explosión de contenido. Pasar None desactiva el límite y rara vez es apropiado.
Elige un valor menor cuando la estructura sea conocida. Una configuración con uno o dos niveles debería usar una política equivalente.
Ciclos de inclusión
Existe un ciclo cuando a.xml incluye b.xml y b.xml vuelve a incluir a.xml. La profundidad máxima termina deteniendo el proceso, pero un loader debería detectar rutas repetidas y producir un error claro.
Loader local con directorio permitido
from pathlib import Path
from xml.etree import ElementTree
RAIZ = Path("contenidos").resolve()
class LoaderLocal:
def __init__(self):
self.visitados = set()
def __call__(self, href, parse, encoding=None):
ruta = (RAIZ / href).resolve()
if RAIZ not in ruta.parents and ruta != RAIZ:
raise ValueError("Inclusión fuera del directorio permitido")
if ruta in self.visitados:
raise ValueError("Ciclo XInclude detectado")
self.visitados.add(ruta)
if ruta.stat().st_size > 2 * 1024 * 1024:
raise ValueError("Archivo incluido demasiado grande")
if parse == "xml":
return ElementTree.parse(ruta).getroot()
if parse == "text":
return ruta.read_text(encoding=encoding or "utf-8")
raise ValueError(f"Modo no soportado: {parse}")
Pasa el loader de forma explícita:
loader = LoaderLocal()
ElementInclude.include(
arbol.getroot(),
loader=loader,
max_depth=4,
)
El ejemplo bloquea path traversal, ciclos simples y archivos grandes. En producción también limita bytes totales, número de archivos, profundidad y tiempo.
Enlaces simbólicos
Path.resolve() revela enlaces simbólicos y componentes .. antes de validar el directorio. Todavía puede existir una carrera entre comprobación y apertura. En entornos hostiles usa directorios controlados, permisos restrictivos y APIs seguras del sistema.
Evitar inclusiones remotas
Un loader puede buscar URLs, pero eso introduce SSRF, redirects, DNS rebinding, respuestas enormes y problemas de disponibilidad. Para XML de terceros, es preferible prohibir HTTP y HTTPS.
Si la red es imprescindible, usa allowlist de hosts y protocolos, valida IPs resueltas, limita redirects, timeout y bytes. El guía de urllib.request en Python explica descargas limitadas.
Archivos temporales y caché
Si los recursos remotos se descargan antes de la expansión, usa almacenamiento temporal seguro y caché con integridad. Consulta tempfile en Python y hashlib en Python.
No uses el href no confiable directamente como nombre local. Genera un identificador interno.
Limitaciones
Python ofrece soporte limitado y no implementa XPointer completo. No supongas compatibilidad con todas las herramientas XML avanzadas. Prueba los documentos reales de la integración.
Para schemas, XPath completo, validación avanzada o resolución compleja, puede ser mejor una biblioteca especializada.
Validar después de expandir
Un fragmento puede introducir elementos, namespaces o valores inesperados. Valida el árbol final, no solo cada archivo aislado.
items = arbol.getroot().findall(".//item")
if len(items) > 10_000:
raise ValueError("Demasiados items")
for item in items:
if not item.get("id"):
raise ValueError("Item sin ID")
Salida atómica
Al guardar el documento expandido, escribe en un archivo temporal del mismo directorio y reemplaza el destino solo después de una serialización exitosa. Así evitas XML parcial.
Logs y diagnóstico
Registra documento principal, ruta normalizada, tamaño, profundidad y resultado. No registres contenido sensible. Los errores deben identificar la inclusión fallida sin revelar rutas internas innecesarias.
Pruebas recomendadas
Prueba inclusiones XML y texto, encoding inválido, archivo ausente, rutas absolutas, ../, enlaces simbólicos, ciclos directos e indirectos, profundidad excedida, archivos grandes, modo inválido, namespaces y validación del árbol final.
Errores comunes
Los fallos frecuentes son usar el loader por defecto con XML externo, desactivar max_depth, permitir URLs arbitrarias, no detectar ciclos, validar antes de resolver la ruta, confiar en href como nombre local y no validar el resultado.
Conclusión
xml.etree.ElementInclude permite dividir XML en archivos reutilizables y expandir XInclude dentro de árboles ElementTree. La función es sencilla, pero la resolución de recursos debe tratarse como operación sensible.
Usa un loader personalizado, un directorio permitido, límites de profundidad y tamaño y validación final. Consulta la documentación oficial de XInclude en ElementTree y la recomendación XInclude del W3C.







