Resumen de pedidos en XML con ElementTree
Lee el XML de pedidos que exporta una tienda, valida cada pedido (atributos obligatorios, estados, cantidades y precios), acumula por cliente y por estado y genera un resumen en XML con sangría. xml.etree.ElementTree para leer y escribir, Decimal y validación con excepciones.
- xml.etree.ElementTree
- findall y get
- Crear XML con SubElement
- ET.indent y tostring
- Decimal
- Excepciones con mensaje
Enunciado
La tienda online de una empresa exporta cada noche sus pedidos en XML para el sistema de gestión. El departamento comercial quiere un resumen del día, también en XML, para cargarlo en su cuadro de mando: cuántos pedidos hay en cada estado, cuánto ha comprado cada cliente y cuál es el producto que más se ha vendido.
El fichero lo genera un sistema antiguo y a veces trae pedidos incompletos: sin cliente, con un estado que no existe o con cantidades y precios mal escritos. Esos pedidos se descartan con un aviso que explique el motivo, y el resumen se hace con el resto.
Para leer y escribir XML no se trata como texto con split: se usa un analizador. Python trae xml.etree.ElementTree, que convierte el documento en un árbol de elementos con sus atributos, y que también sabe construir un árbol nuevo y escribirlo con la sangría correcta y los caracteres especiales escapados.
Qué tiene que hacer el programa
- La entrada es un documento XML con la raíz
<pedidos fecha="…">y elementos<pedido id cliente estado>que contienen elementos<linea ref cantidad precio/>. Si el XML no está bien formado, escribeXML mal formado (línea N)con la línea del error; si la raíz no espedidos,El elemento raíz debe ser <pedidos>. En los dos casos no se escribe nada más. - Un pedido se ignora, con el aviso
Aviso: pedido ID ignorado: motivo(o?si no tiene id), cuando: le falta id, cliente o estado (falta el atributo X, comprobados en ese orden); su estado no es pendiente, enviado, entregado ni cancelado (estado desconocido X); no tiene líneas (no tiene líneas); una línea no tiene ref o su cantidad no es un entero mayor que 0 (línea REF con una cantidad no válida, con?si falta la ref); o un precio no es un número (línea REF con un precio no válido). - Los pedidos válidos cuentan en su estado. Los cancelados no suman importe, ni unidades, ni cuentan como pedidos del cliente. El importe de un pedido es la suma de cantidad × precio de sus líneas, con decimales exactos.
- La salida es un XML
<resumen fecha pedidos importe>(la fecha de la raíz, el número de pedidos válidos y el importe total con dos decimales y punto), con dos espacios de sangría por nivel, que contiene:<estados>con un<estado nombre pedidos/>por estado en el orden pendiente, enviado, entregado, cancelado;<clientes>con un<cliente nombre pedidos importe/>por cliente, de mayor a menor importe (a igualdad, por nombre); y<masVendido ref unidades/>con la referencia de más unidades (a igualdad, la menor alfabéticamente), solo si se ha vendido algo. - Los avisos se escriben antes del XML.
Entrada
Un documento XML de pedidos.
Ejemplos de ejecución
Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.
Los pedidos de un día
Entrada
<?xml version="1.0" encoding="UTF-8"?>
<pedidos fecha="2026-09-30">
<pedido id="P-101" cliente="Ana Gil" estado="enviado">
<linea ref="CAB-HDMI" cantidad="2" precio="7.95"/>
<linea ref="RATON-01" cantidad="1" precio="24.99"/>
</pedido>
<pedido id="P-102" cliente="Muebles Ruiz & Hijos" estado="entregado">
<linea ref="SILLA-07" cantidad="4" precio="89.90"/>
</pedido>
<pedido id="P-103" cliente="Ana Gil" estado="cancelado">
<linea ref="SILLA-07" cantidad="10" precio="89.90"/>
</pedido>
<pedido id="P-104" cliente="Luis Mora" estado="enviado">
<linea ref="CAB-HDMI" cantidad="3" precio="7.95"/>
<linea ref="TECLADO" cantidad="1" precio="abc"/>
</pedido>
<pedido id="P-105" cliente="Ana Gil" estado="pendiente">
<linea ref="CAB-HDMI" cantidad="3" precio="7.95"/>
</pedido>
</pedidos>Salida por consola
Aviso: pedido P-104 ignorado: línea TECLADO con un precio no válido
<resumen fecha="2026-09-30" pedidos="4" importe="424.34">
<estados>
<estado nombre="pendiente" pedidos="1" />
<estado nombre="enviado" pedidos="1" />
<estado nombre="entregado" pedidos="1" />
<estado nombre="cancelado" pedidos="1" />
</estados>
<clientes>
<cliente nombre="Muebles Ruiz & Hijos" pedidos="1" importe="359.60" />
<cliente nombre="Ana Gil" pedidos="2" importe="64.74" />
</clientes>
<masVendido ref="CAB-HDMI" unidades="5" />
</resumen>Pedidos con errores
Entrada
<pedidos fecha="2026-10-01"> <pedido cliente="Sin id" estado="enviado"><linea ref="A" cantidad="1" precio="1"/></pedido> <pedido id="P-2" estado="enviado"><linea ref="A" cantidad="1" precio="1"/></pedido> <pedido id="P-3" cliente="Eva" estado="perdido"><linea ref="A" cantidad="1" precio="1"/></pedido> <pedido id="P-4" cliente="Eva" estado="enviado"/> <pedido id="P-5" cliente="Eva" estado="enviado"><linea cantidad="1" precio="1"/></pedido> <pedido id="P-6" cliente="Eva" estado="cancelado"><linea ref="B" cantidad="2" precio="5"/></pedido> </pedidos>
Salida por consola
Aviso: pedido ? ignorado: falta el atributo id
Aviso: pedido P-2 ignorado: falta el atributo cliente
Aviso: pedido P-3 ignorado: estado desconocido perdido
Aviso: pedido P-4 ignorado: no tiene líneas
Aviso: pedido P-5 ignorado: línea ? con una cantidad no válida
<resumen fecha="2026-10-01" pedidos="1" importe="0.00">
<estados>
<estado nombre="pendiente" pedidos="0" />
<estado nombre="enviado" pedidos="0" />
<estado nombre="entregado" pedidos="0" />
<estado nombre="cancelado" pedidos="1" />
</estados>
<clientes />
</resumen>Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. Analizar el documento
ET.fromstring(texto) devuelve el elemento raíz. Si el XML está mal formado lanza ET.ParseError, cuyo atributo position es una tupla (línea, columna).
2. Recorrer y leer atributos
raiz.findall("pedido") da los pedidos hijos de la raíz y nodo.get("cliente") un atributo (o None si no está; get("x", "") para un valor por defecto).
3. Validar con una función que lanza
Escribe leer_pedido(nodo) que devuelva los datos ya convertidos o lance ValueError("motivo"). En el bucle, except ValueError as e escribe el aviso con e: cada regla es un if y un raise.
4. Construir el XML de salida
Crea el elemento raíz con ET.Element("resumen", atributo=valor, …) y sus hijos con ET.SubElement(padre, "nombre", …). Los atributos tienen que ser textos: convierte los números con str o un f-string.
resumen = ET.Element("resumen", fecha=fecha, pedidos=str(validos), importe=f"{total:.2f}")
estados = ET.SubElement(resumen, "estados")5. Sangría y escritura
ET.indent(resumen, space=" ") añade la sangría (Python 3.9 o posterior) y ET.tostring(resumen, encoding="unicode") devuelve el texto. Los caracteres especiales de los atributos, como & o las comillas, se escapan solos.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 3 casos ocultos que buscan los errores típicos.
Ejemplo
<?xml version="1.0" encoding="UTF-8"?>
<pedidos fecha="2026-09-30">
<pedido id="P-101" cliente="Ana Gil" estado="enviado">
<linea ref="CAB-HDMI" cantidad="2" precio="7.95"/>
<linea ref="RATON-01" cantidad="1" precio="24.99"/>
</pedido>
<pedido id="P-102" cliente="Muebles Ruiz & Hijos" estado="entregado">
<linea ref="SILLA-07" cantidad="4" precio="89.90"/>
</pedido>
<pedido id="P-103" cliente="Ana Gil" estado="cancelado">
<linea ref="SILLA-07" cantidad="10" precio="89.90"/>
</pedido>
<pedido id="P-104" cliente="Luis Mora" estado="enviado">
<linea ref="CAB-HDMI" cantidad="3" precio="7.95"/>
<linea ref="TECLADO" cantidad="1" precio="abc"/>
</pedido>
<pedido id="P-105" cliente="Ana Gil" estado="pendiente">
<linea ref="CAB-HDMI" cantidad="3" precio="7.95"/>
</pedido>
</pedidos>
Aviso: pedido P-104 ignorado: línea TECLADO con un precio no válido
<resumen fecha="2026-09-30" pedidos="4" importe="424.34">
<estados>
<estado nombre="pendiente" pedidos="1" />
<estado nombre="enviado" pedidos="1" />
<estado nombre="entregado" pedidos="1" />
<estado nombre="cancelado" pedidos="1" />
</estados>
<clientes>
<cliente nombre="Muebles Ruiz & Hijos" pedidos="1" importe="359.60" />
<cliente nombre="Ana Gil" pedidos="2" importe="64.74" />
</clientes>
<masVendido ref="CAB-HDMI" unidades="5" />
</resumen>Solución explicada
Ver la solución completa
1import sys
2import xml.etree.ElementTree as ET
3from decimal import Decimal, InvalidOperation
4
5ESTADOS = ("pendiente", "enviado", "entregado", "cancelado")
6
7
8def leer_pedido(nodo):
9 """Comprueba un <pedido> y devuelve su id, cliente, estado e importe; lanza ValueError con el motivo."""
10 for atributo in ("id", "cliente", "estado"):
11 if not nodo.get(atributo):
12 raise ValueError(f"falta el atributo {atributo}")
13 if nodo.get("estado") not in ESTADOS:
14 raise ValueError(f"estado desconocido {nodo.get('estado')}")
15 lineas = nodo.findall("linea")
16 if not lineas:
17 raise ValueError("no tiene líneas")
18 importe = Decimal(0)
19 unidades = {}
20 for linea in lineas:
21 ref, cantidad, precio = linea.get("ref"), linea.get("cantidad", ""), linea.get("precio", "")
22 if not ref or not cantidad.isdigit() or int(cantidad) == 0:
23 raise ValueError(f"línea {ref or '?'} con una cantidad no válida")
24 try:
25 precio = Decimal(precio)
26 except InvalidOperation:
27 raise ValueError(f"línea {ref} con un precio no válido")
28 importe += int(cantidad) * precio
29 unidades[ref] = unidades.get(ref, 0) + int(cantidad)
30 return nodo.get("id"), nodo.get("cliente"), nodo.get("estado"), importe, unidades
31
32
33def main():
34 try:
35 raiz = ET.fromstring(sys.stdin.read())
36 except ET.ParseError as e:
37 print(f"XML mal formado (línea {e.position[0]})")
38 return
39 if raiz.tag != "pedidos":
40 print("El elemento raíz debe ser <pedidos>")
41 return
42
43 por_cliente = {} # cliente → [pedidos, importe]
44 por_estado = {e: 0 for e in ESTADOS}
45 vendidas = {}
46 total = Decimal(0)
47 validos = 0
48 for nodo in raiz.findall("pedido"):
49 try:
50 id_, cliente, estado, importe, unidades = leer_pedido(nodo)
51 except ValueError as e:
52 print(f"Aviso: pedido {nodo.get('id', '?')} ignorado: {e}")
53 continue
54 validos += 1
55 por_estado[estado] += 1
56 if estado == "cancelado":
57 continue # se cuenta, pero no suma importe ni unidades
58 datos = por_cliente.setdefault(cliente, [0, Decimal(0)])
59 datos[0] += 1
60 datos[1] += importe
61 total += importe
62 for ref, n in unidades.items():
63 vendidas[ref] = vendidas.get(ref, 0) + n
64
65 # El resumen se construye como un árbol y se serializa con sangría
66 resumen = ET.Element("resumen", fecha=raiz.get("fecha", ""), pedidos=str(validos), importe=f"{total:.2f}")
67 estados = ET.SubElement(resumen, "estados")
68 for estado in ESTADOS:
69 ET.SubElement(estados, "estado", nombre=estado, pedidos=str(por_estado[estado]))
70 clientes = ET.SubElement(resumen, "clientes")
71 for cliente, (n, importe) in sorted(por_cliente.items(), key=lambda c: (-c[1][1], c[0])):
72 ET.SubElement(clientes, "cliente", nombre=cliente, pedidos=str(n), importe=f"{importe:.2f}")
73 if vendidas:
74 ref, n = min(vendidas.items(), key=lambda v: (-v[1], v[0]))
75 ET.SubElement(resumen, "masVendido", ref=ref, unidades=str(n))
76 ET.indent(resumen, space=" ")
77 print(ET.tostring(resumen, encoding="unicode"))
78
79
80main()Un analizador de XML comprueba que el documento está bien formado antes de que el programa vea nada, y da acceso a los datos por su estructura (elementos y atributos), no por su posición en el texto. Por eso no le afectan los saltos de línea, el orden de los atributos ni las comillas simples o dobles.
La validación vive en leer_pedido, que traduce cada problema en una excepción con un mensaje claro. El bucle principal decide qué hacer con un pedido malo (avisar y seguir) sin mezclarse con las reglas.
Generar el XML con Element y SubElement en lugar de con print garantiza que la salida está bien formada: los nombres con & o comillas se escapan automáticamente, algo que es muy fácil olvidar al escribir etiquetas a mano.
Los importes se acumulan con Decimal y se formatean con dos decimales y punto, el formato que espera cualquier programa que lea el XML (la coma decimal es para las personas, no para los datos).
Para ir más allá
- Valida el XML de entrada con un XSD (con la librería lxml) en lugar de hacer las comprobaciones a mano.
- Transforma el resumen en una página HTML con una hoja XSLT.
- Lee el XML de un fichero enorme con
ET.iterparsepara no cargarlo entero en memoria.