O módulo xml.etree.ElementInclude adiciona suporte limitado a XInclude para árvores criadas com xml.etree.ElementTree. XInclude permite que um documento XML indique que outro arquivo XML ou texto deve ser inserido em determinado ponto da árvore.
Esse recurso pode simplificar configurações, documentação, catálogos e arquivos grandes divididos em partes. Porém, inclusões também criam riscos: leitura de arquivos fora do diretório esperado, acesso a URLs, ciclos, explosão de conteúdo e dependências difíceis de rastrear. Por isso, loaders personalizados, caminhos permitidos e profundidade máxima são essenciais.
O que é XInclude
XInclude usa o namespace http://www.w3.org/2001/XInclude. O elemento xi:include possui um atributo href e pode usar parse="xml" ou parse="text".
<documento xmlns:xi="http://www.w3.org/2001/XInclude">
<titulo>Relatório</titulo>
<xi:include href="secoes/resumo.xml" parse="xml"/>
</documento>
Ao expandir a inclusão, o elemento xi:include é substituído pela raiz do XML indicado. Para texto, o conteúdo do arquivo é inserido como string.
Primeiro exemplo
from xml.etree import ElementTree, ElementInclude
arvore = ElementTree.parse("documento.xml")
raiz = arvore.getroot()
ElementInclude.include(raiz)
arvore.write(
"resultado.xml",
encoding="utf-8",
xml_declaration=True,
)
O loader padrão trata o href como nome de arquivo. Isso é conveniente para documentos locais confiáveis, mas não deve ser usado sem restrições quando o XML ou os caminhos podem ser controlados por usuários.
Integração com ElementTree
ElementInclude trabalha sobre elementos ou árvores de ElementTree. Antes de usar XInclude, domine parsing, namespaces, busca e serialização. O guia de ElementTree no Python cobre essas operações.
A expansão ocorre in-place: a árvore original é modificada. Se você precisar preservar o documento antes da inclusão, faça uma cópia segura ou carregue novamente a fonte.
parse=”xml” e parse=”text”
Com parse="xml", o loader deve retornar um Element. Com parse="text", deve retornar uma string. O encoding padrão do loader de texto é UTF-8, mas pode ser informado no elemento.
<documento xmlns:xi="http://www.w3.org/2001/XInclude">
<rodape>
<xi:include href="ano.txt" parse="text" encoding="utf-8"/>
</rodape>
</documento>
Texto incluído não vira automaticamente markup XML. Ele é inserido como conteúdo textual e será escapado durante a serialização.
base_url para referências relativas
O parâmetro base_url ajuda a resolver caminhos relativos com base na localização do documento principal.
from pathlib import Path
from xml.etree import ElementTree, ElementInclude
arquivo = Path("configs/principal.xml").resolve()
arvore = ElementTree.parse(arquivo)
ElementInclude.include(
arvore.getroot(),
base_url=arquivo.as_uri(),
max_depth=4,
)
Não confie apenas em base_url como proteção. Ele resolve referências, mas não impede ../, URLs externas ou caminhos absolutos quando o loader aceita esses valores.
Profundidade máxima
include() usa max_depth=6 por padrão. O limite reduz o risco de recursão excessiva e explosão de inclusões. Passar None remove o limite e raramente é uma boa ideia.
Escolha um valor menor quando a estrutura do projeto for conhecida. Para configurações com apenas um ou dois níveis, max_depth=2 pode ser suficiente.
Ciclos de inclusão
Considere a.xml incluindo b.xml e b.xml incluindo a.xml. A profundidade máxima interrompe o ciclo depois de algumas expansões, mas o sistema deveria detectar a repetição e produzir um erro claro.
Um loader personalizado pode manter um conjunto de caminhos já visitados. Como o loader recebe apenas href, parse e encoding, o estado pode ficar em um closure ou objeto chamável.
Loader seguro com diretório permitido
from pathlib import Path
from xml.etree import ElementTree
RAIZ = Path("conteudos").resolve()
class LoaderLocal:
def __init__(self):
self.visitados = set()
def __call__(self, href, parse, encoding=None):
caminho = (RAIZ / href).resolve()
if RAIZ not in caminho.parents and caminho != RAIZ:
raise ValueError("Inclusão fora do diretório permitido")
if caminho in self.visitados:
raise ValueError("Ciclo de XInclude detectado")
self.visitados.add(caminho)
if caminho.stat().st_size > 2 * 1024 * 1024:
raise ValueError("Arquivo incluído é grande demais")
if parse == "xml":
return ElementTree.parse(caminho).getroot()
if parse == "text":
return caminho.read_text(encoding=encoding or "utf-8")
raise ValueError(f"Modo de parsing inválido: {parse}")
Depois, passe a instância para include():
loader = LoaderLocal()
ElementInclude.include(
arvore.getroot(),
loader=loader,
max_depth=4,
)
O exemplo bloqueia path traversal, ciclos simples e arquivos grandes. Em sistemas reais, também limite a soma de bytes de todas as inclusões, quantidade de arquivos, profundidade e tempo total.
Links simbólicos
Path.resolve() ajuda a revelar links simbólicos e componentes ... Ainda assim, existe risco de condições de corrida entre validação e abertura. Para ambientes hostis, trabalhe com diretórios controlados, permissões restritas e APIs de abertura seguras do sistema operacional.
Evitar inclusões pela rede
Um loader pode buscar URLs, mas isso cria riscos de SSRF, redirects, DNS rebinding, respostas enormes e indisponibilidade. Para XML recebido de terceiros, prefira proibir HTTP e HTTPS.
Quando a rede for realmente necessária, mantenha uma allowlist de hosts, valide IPs públicos e privados, limite redirects, timeout e bytes. O artigo de urllib.request no Python mostra controles para downloads.
Arquivos temporários e cache
Se inclusões remotas forem baixadas antes do processamento, use arquivos temporários seguros e cache com integridade. Veja tempfile no Python e hashlib no Python.
Não use o valor de href diretamente como nome de arquivo. Gere nomes internos e armazene o mapeamento separadamente.
Fallback e limitações
O suporte do Python é limitado e não oferece XPointer completo. Não presuma compatibilidade com toda a especificação XInclude ou com ferramentas XML avançadas. Teste os documentos reais do seu ecossistema.
Quando a aplicação precisa de schemas, validação complexa, XPath completo ou políticas avançadas de resolução, uma biblioteca XML especializada pode ser mais adequada.
Validação depois da expansão
A inclusão pode inserir elementos inesperados, namespaces diferentes ou dados que violam regras de negócio. Valide a árvore resultante, não apenas cada arquivo isolado.
itens = arvore.getroot().findall(".//item")
if len(itens) > 10_000:
raise ValueError("Quantidade excessiva de itens")
for item in itens:
if not item.get("id"):
raise ValueError("Item sem ID")
Serialização e atomicidade
Ao salvar o resultado, escreva em um arquivo temporário no mesmo diretório e substitua o destino somente após sucesso. Isso evita deixar um XML parcial se ocorrer erro durante a expansão ou escrita.
Logs e diagnóstico
Registre o documento principal, o caminho normalizado de cada inclusão, tamanho, profundidade e resultado. Não registre conteúdo sensível. Mensagens de erro devem indicar qual inclusão falhou sem expor caminhos internos desnecessários.
Testes recomendados
Teste inclusão XML e texto, encoding inválido, arquivo ausente, caminho absoluto, ../, link simbólico, ciclo direto e indireto, profundidade excedida, arquivo muito grande, modo parse inválido, namespaces e árvore resultante.
Erros comuns
Os erros mais frequentes são usar o loader padrão com XML externo, remover max_depth, permitir URLs livres, não detectar ciclos, validar o caminho antes de resolve(), confiar no href como nome local e não validar a árvore expandida.
Conclusão
xml.etree.ElementInclude permite dividir XML em arquivos menores e expandir XInclude dentro de árvores ElementTree. O recurso é simples, mas a resolução de caminhos e recursos precisa ser tratada como operação sensível.
Use loader personalizado, diretório permitido, limites de profundidade, bytes e quantidade, e valide o resultado final. Consulte a documentação oficial de XInclude no ElementTree e a recomendação XInclude do W3C.







