Por qué XML sigue siendo relevante
Aunque JSON ha reemplazado a XML en la mayoría de APIs modernas, XML sigue siendo indispensable en sectores clave:
- Office Open XML:
.docx,.xlsx,.pptxson ZIPs de XML - SVG: gráficos vectoriales en XML
- RSS/Atom: feeds de noticias y podcasts
- SOAP: servicios web corporativos legacy (bancos, gobiernos, SAP)
- XHTML/HTML: el DOM del navegador es XML
- Android: layouts, manifiestos y recursos en XML
- Maven/Gradle:
pom.xmlpara proyectos Java - Configuración: Spring Boot, .NET config, Kubernetes manifests
ElementTree: la biblioteca estándar
import xml.etree.ElementTree as ET
from pathlib import Path
# ── Parsear XML desde string ───────────────────────────────────────────────
xml_str = '''<?xml version="1.0" encoding="UTF-8"?>
<biblioteca>
<libro isbn="978-0-13-110362-7">
<titulo>The C Programming Language</titulo>
<autores>
<autor>Brian W. Kernighan</autor>
<autor>Dennis M. Ritchie</autor>
</autores>
<año>1988</año>
<precio moneda="USD">45.99</precio>
<disponible>true</disponible>
</libro>
<libro isbn="978-0-201-63361-0">
<titulo>The Pragmatic Programmer</titulo>
<autores>
<autor>Andrew Hunt</autor>
<autor>David Thomas</autor>
</autores>
<año>1999</año>
<precio moneda="USD">39.99</precio>
<disponible>false</disponible>
</libro>
</biblioteca>'''
def parsear_libros(xml_contenido):
"""Extrae información de libros de un XML."""
raiz = ET.fromstring(xml_contenido)
libros = []
for libro in raiz.findall('libro'):
isbn = libro.get('isbn')
titulo = libro.findtext('titulo')
año = libro.findtext('año')
precio = libro.findtext('precio')
moneda = libro.find('precio').get('moneda')
autores = [a.text for a in libro.findall('autores/autor')]
disp = libro.findtext('disponible') == 'true'
libros.append({
'isbn': isbn,
'titulo': titulo,
'año': int(año),
'precio': float(precio),
'moneda': moneda,
'autores': autores,
'disponible': disp,
})
return libros
libros = parsear_libros(xml_str)
for libro in libros:
print(f"{libro['titulo']} ({libro['año']}) — ${libro['precio']}")
Crear XML con ElementTree
import xml.etree.ElementTree as ET
from xml.dom import minidom
def crear_xml_empleados(empleados, archivo_salida):
"""
Genera un XML estructurado desde una lista de diccionarios.
empleados = [{'nombre': ..., 'cargo': ..., 'salario': ..., 'habilidades': [...]}]
"""
empresa = ET.Element('empresa')
empresa.set('version', '1.0')
empresa.set('fecha', '2024-01-01')
departamentos_elem = ET.SubElement(empresa, 'empleados')
departamentos_elem.set('total', str(len(empleados)))
for emp in empleados:
emp_elem = ET.SubElement(departamentos_elem, 'empleado')
emp_elem.set('id', str(emp.get('id', '')))
for campo in ('nombre', 'cargo', 'email'):
if campo in emp:
hijo = ET.SubElement(emp_elem, campo)
hijo.text = str(emp[campo])
salario_elem = ET.SubElement(emp_elem, 'salario')
salario_elem.set('moneda', 'EUR')
salario_elem.text = str(emp.get('salario', 0))
if emp.get('habilidades'):
habs_elem = ET.SubElement(emp_elem, 'habilidades')
for hab in emp['habilidades']:
h = ET.SubElement(habs_elem, 'habilidad')
h.text = hab
# Formatear con indentación (Python 3.9+)
ET.indent(empresa, space=' ')
arbol = ET.ElementTree(empresa)
# Añadir declaración XML
arbol.write(archivo_salida, encoding='unicode', xml_declaration=True)
print(f"XML creado: {archivo_salida}")
return archivo_salida
empleados_data = [
{
'id': 1, 'nombre': 'Ana García', 'cargo': 'Ingeniería Senior',
'email': 'ana@empresa.com', 'salario': 65000,
'habilidades': ['Python', 'Docker', 'PostgreSQL']
},
{
'id': 2, 'nombre': 'Luis Martínez', 'cargo': 'Diseñador UX',
'email': 'luis@empresa.com', 'salario': 52000,
'habilidades': ['Figma', 'Sketch', 'CSS']
},
]
crear_xml_empleados(empleados_data, 'empleados.xml')
lxml: XML de alta performance con XPath
from lxml import etree
# ── XPath avanzado ─────────────────────────────────────────────────────────
def buscar_con_xpath(xml_ruta_o_str, expresion_xpath):
"""
Ejecuta una consulta XPath sobre un documento XML.
Mucho más potente que ElementTree.find().
"""
if isinstance(xml_ruta_o_str, str) and xml_ruta_o_str.startswith('<'):
raiz = etree.fromstring(xml_ruta_o_str.encode())
else:
arbol = etree.parse(xml_ruta_o_str)
raiz = arbol.getroot()
resultados = raiz.xpath(expresion_xpath)
return resultados
# Ejemplos de XPath
xml_bytes = xml_str.encode()
raiz = etree.fromstring(xml_bytes)
# Todos los títulos
titulos = raiz.xpath('//titulo/text()')
print("Títulos:", titulos)
# Libros disponibles con precio < 42
disponibles = raiz.xpath('//libro[disponible="true"]/titulo/text()')
print("Disponibles:", disponibles)
# Todos los autores únicos
autores = raiz.xpath('//autor/text()')
print("Autores:", autores)
# Libros ordenados por precio
libros_ordenados = raiz.xpath('//libro')
libros_ordenados.sort(key=lambda l: float(l.findtext('precio', '0')))
for libro in libros_ordenados:
print(f" {libro.findtext('titulo')}: ${libro.findtext('precio')}")
Namespaces XML
XML con namespaces es habitual en SOAP, XHTML y formatos Office:
from lxml import etree
xml_ns = '''<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<title>Mi Blog</title>
<entry>
<title>Primer artículo</title>
<id>urn:uuid:1234</id>
<updated>2024-01-15T10:30:00Z</updated>
<media:thumbnail url="https://ejemplo.com/img.jpg" width="320" height="240"/>
</entry>
</feed>'''
def parsear_atom_feed(xml_contenido):
"""Parsea un feed Atom con namespaces."""
raiz = etree.fromstring(xml_contenido.encode())
# Definir namespaces para XPath
ns = {
'atom': 'http://www.w3.org/2005/Atom',
'media': 'http://search.yahoo.com/mrss/',
}
titulo_feed = raiz.xpath('atom:title/text()', namespaces=ns)
entradas = raiz.xpath('atom:entry', namespaces=ns)
resultado = {
'titulo': titulo_feed[0] if titulo_feed else '',
'entradas': []
}
for entrada in entradas:
titulo = entrada.xpath('atom:title/text()', namespaces=ns)
updated = entrada.xpath('atom:updated/text()', namespaces=ns)
thumb = entrada.xpath('media:thumbnail', namespaces=ns)
item = {
'titulo': titulo[0] if titulo else '',
'updated': updated[0] if updated else '',
'thumbnail': thumb[0].get('url') if thumb else None,
}
resultado['entradas'].append(item)
return resultado
feed = parsear_atom_feed(xml_ns)
print(f"Feed: {feed['titulo']}")
for entrada in feed['entradas']:
print(f" - {entrada['titulo']} ({entrada['updated']})")
Convertir XML a JSON y viceversa
import json
import xml.etree.ElementTree as ET
def xml_elem_a_dict(elemento):
"""Convierte un elemento XML a diccionario Python recursivamente."""
resultado = {}
# Atributos
if elemento.attrib:
resultado['@atributos'] = elemento.attrib
# Texto directo
if elemento.text and elemento.text.strip():
if elemento.attrib or len(elemento):
resultado['#texto'] = elemento.text.strip()
else:
return elemento.text.strip()
# Elementos hijo
hijos = {}
for hijo in elemento:
valor_hijo = xml_elem_a_dict(hijo)
if hijo.tag in hijos:
# Convertir a lista si hay múltiples hijos con el mismo tag
if not isinstance(hijos[hijo.tag], list):
hijos[hijo.tag] = [hijos[hijo.tag]]
hijos[hijo.tag].append(valor_hijo)
else:
hijos[hijo.tag] = valor_hijo
resultado.update(hijos)
return resultado
def xml_a_json(xml_contenido_o_ruta, json_salida=None, indent=2):
"""Convierte XML a JSON."""
if isinstance(xml_contenido_o_ruta, str) and xml_contenido_o_ruta.startswith('<'):
raiz = ET.fromstring(xml_contenido_o_ruta)
else:
raiz = ET.parse(xml_contenido_o_ruta).getroot()
datos = {raiz.tag: xml_elem_a_dict(raiz)}
json_str = json.dumps(datos, ensure_ascii=False, indent=indent)
if json_salida:
Path(json_salida).write_text(json_str, encoding='utf-8')
print(f"XML→JSON: {json_salida}")
return json_str
def json_a_xml(datos_json, etiqueta_raiz='root', salida=None):
"""Convierte JSON/diccionario a XML básico."""
def dict_a_elem(tag, valor):
elem = ET.Element(tag)
if isinstance(valor, dict):
for clave, val in valor.items():
if clave.startswith('@'):
# Atributos XML
nombre_attr = clave[1:]
if isinstance(val, dict):
for k, v in val.items():
elem.set(k, str(v))
else:
elem.set(nombre_attr, str(val))
elif clave == '#texto':
elem.text = str(valor)
else:
if isinstance(val, list):
for item in val:
elem.append(dict_a_elem(clave, item))
else:
elem.append(dict_a_elem(clave, val))
elif isinstance(valor, list):
for item in valor:
elem.append(dict_a_elem(tag, item))
else:
elem.text = str(valor)
return elem
if isinstance(datos_json, str):
datos = json.loads(datos_json)
else:
datos = datos_json
raiz = dict_a_elem(etiqueta_raiz, datos)
ET.indent(raiz, space=' ')
arbol = ET.ElementTree(raiz)
if salida:
arbol.write(salida, encoding='unicode', xml_declaration=True)
print(f"JSON→XML: {salida}")
return ET.tostring(raiz, encoding='unicode')
# Ejemplo
json_str = xml_a_json(xml_str)
print("XML→JSON:", json_str[:200], "...")
datos_dict = {'nombre': 'Ana', 'edad': 28, 'habilidades': ['Python', 'SQL']}
xml_result = json_a_xml(datos_dict, 'persona')
print("JSON→XML:", xml_result)
Parsear RSS con lxml
def parsear_rss(url_o_ruta):
"""Parsea un feed RSS y extrae artículos."""
try:
import urllib.request
if url_o_ruta.startswith('http'):
with urllib.request.urlopen(url_o_ruta, timeout=10) as resp:
contenido = resp.read()
else:
contenido = Path(url_o_ruta).read_bytes()
except Exception as e:
print(f"Error cargando feed: {e}")
return []
raiz = etree.fromstring(contenido)
ns = {'dc': 'http://purl.org/dc/elements/1.1/'}
articulos = []
for item in raiz.xpath('//item'):
art = {
'titulo': item.findtext('title') or '',
'enlace': item.findtext('link') or '',
'descripcion': item.findtext('description') or '',
'fecha': item.findtext('pubDate') or '',
'autor': (item.findtext('author') or
item.xpath('dc:creator/text()', namespaces=ns) or [''])[0]
if not isinstance(item.findtext('author'), str) else
item.findtext('author') or '',
}
articulos.append(art)
print(f"Artículos encontrados: {len(articulos)}")
return articulos
Validar XML con Schema (XSD)
from lxml import etree
def validar_con_xsd(xml_contenido, xsd_contenido):
"""Valida un documento XML contra un Schema XSD."""
try:
xsd_doc = etree.fromstring(xsd_contenido.encode())
schema = etree.XMLSchema(xsd_doc)
xml_doc = etree.fromstring(xml_contenido.encode())
valido = schema.validate(xml_doc)
if valido:
print("✅ XML válido según el esquema XSD")
else:
print("❌ XML inválido:")
for error in schema.error_log:
print(f" Línea {error.line}: {error.message}")
return valido
except etree.XMLSyntaxError as e:
print(f"Error de sintaxis XML: {e}")
return False
# Ejemplo de esquema XSD
xsd = '''<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="biblioteca">
<xs:complexType>
<xs:sequence>
<xs:element name="libro" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="titulo" type="xs:string"/>
<xs:element name="año" type="xs:integer"/>
<xs:element name="disponible" type="xs:boolean"/>
</xs:sequence>
<xs:attribute name="isbn" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>'''
xml_valido = '''<biblioteca>
<libro isbn="978-0-13-110362-7">
<titulo>The C Programming Language</titulo>
<año>1988</año>
<disponible>true</disponible>
</libro>
</biblioteca>'''
validar_con_xsd(xml_valido, xsd)
ElementTree vs lxml: cuándo usar cada uno
| Criterio | ElementTree | lxml |
|---|---|---|
| Dependencias | ✅ Estándar (ninguna) | ⚠️ pip install lxml |
| Velocidad | ⚠️ Moderada | ✅ C puro, muy rápida |
| XPath | ⚠️ Limitado | ✅ XPath 1.0 completo |
| XSLT | ❌ No soportado | ✅ Completo |
| Validación XSD | ❌ No soportado | ✅ Completo |
| Namespaces | ⚠️ Incómodo | ✅ Nativo |
| HTML parsing | ❌ | ✅ html.fromstring() |
| Archivos grandes | ⚠️ Alto consumo RAM | ✅ iterparse eficiente |
Conclusión
Para tareas básicas de parsing y generación, xml.etree.ElementTree (biblioteca estándar) es suficiente. Para cualquier tarea que involucre XPath complejo, namespaces, validación XSD/DTD, XSLT o archivos XML grandes (> 25 MB), lxml es la elección correcta. La conversión XML↔JSON es trivial con Python y abre la puerta a integrar sistemas legacy SOAP/XML con APIs REST modernas.
Conversiones relacionadas
Conversiones de documento que siguen este tema: