Comprobador de XML bien formado: el primer paso de cualquier analizador
Escribe lo que comprueba cualquier analizador antes de leer un XML: etiquetas bien anidadas, un único elemento raíz, atributos entre comillas y sin repetir, referencias a entidades válidas, comentarios, CDATA y la declaración en su sitio, con el primer error y su línea y columna exactas.
- Documentos XML bien formados
- Análisis léxico a mano
- Pilas para comprobar el anidamiento
- Expresiones regulares
- Entidades y referencias de carácter
- Posición de un error (línea y columna)
Enunciado
Un documento XML bien formado cumple las reglas de sintaxis del propio XML, sin mirar ninguna DTD ni esquema: un solo elemento raíz, cada etiqueta cerrada en el orden inverso al que se abrió, los atributos entre comillas y sin repetir, & solo para empezar una referencia válida (&, <, é…) y < solo para empezar una etiqueta. Si un documento no es bien formado, ningún analizador puede leerlo: se detiene en el primer error y lo indica con su línea y columna.
La entrada trae varios documentos separados por una línea =====. Para cada uno, tu programa dice si es bien formado y, si lo es, cuántos elementos y atributos tiene, su profundidad y su raíz; si no, el primer error. El código de partida ya separa los documentos, calcula la línea y la columna de una posición y escribe los resultados.
Qué tiene que hacer el programa
- Recorre el documento de izquierda a derecha; en el primer error lanza
MalFormado(posición, mensaje)con la posición del<(o del&, del atributo o de lo que falle). Instrucciones de procesamiento<?…?>(la declaración<?xml …?>solo en la posición 0:la declaración XML solo puede ir al principio); comentarios<!--…-->(sin--dentro:un comentario no puede contener --, en la posición de ese--); secciones<![CDATA[…]]>(solo dentro del raíz); y<!DOCTYPE…>(solo antes del raíz). Si alguno no se cierra:falta cerrar …(la instrucción de procesamiento con ?>, el comentario con -->, la sección CDATA con ]]> o la declaración DOCTYPE). - Una etiqueta de apertura:
<seguido de un nombre válido (letra o_, después letras, cifras,_,-o., y un prefijo opcional con:; si no,nombre de etiqueta no válido) y sus atributos, separados por espacios:atributo mal escrito en <x>,atributo repetido «a»,al atributo «a» le falta el valor,el valor del atributo «a» debe ir entre comillas,falta cerrar las comillas del atributo «a»,el valor del atributo «a» no puede contener <(en la posición del<) yfalta cerrar la etiqueta <x>. Puede terminar en/>(vacía) o en>. - Una etiqueta de cierre
</x>(con espacios opcionales antes del>; si no,etiqueta de cierre mal escrita) debe cerrar la última abierta:se esperaba </a> y se encontró </b>, oetiqueta de cierre </b> sin abrir. Un segundo elemento cuando el raíz ya se cerró:solo puede haber un elemento raíz. - Fuera del raíz solo puede haber espacios (
texto fuera del elemento raíz, en la posición del primer carácter que no es espacio). En el texto y en los valores de los atributos, cada&debe empezar una referencia&,<,>,",',&#N;o&#xH;; si no:referencia a entidad no válida «…»con lo que sigue al&hasta un espacio,;,<o&(incluido el;si está). - Al final, si queda algo abierto:
falta cerrar <x>(el último abierto) en la posición del final; si no hubo ningún elemento:el documento no tiene elemento raíz. Si todo está bien, devuelve(elementos, profundidad máxima, atributos, raíz).
Entrada
Uno o varios documentos XML separados por una línea =====.
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.
Un documento correcto y los errores típicos
Entrada
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE biblioteca SYSTEM "biblioteca.dtd">
<!-- Préstamos de octubre -->
<biblioteca centro="IES Ejemplo" curso='2026-2027'>
<libro isbn="978-84-0000-001-1" disponible="no">
<titulo>Programación & algoritmos</titulo>
<autor>Ana Ruiz</autor>
<nota><![CDATA[Usa <b>negrita</b> & más]]></nota>
<portada/>
</libro>
<libro isbn="978-84-0000-002-8">
<titulo>Redes épicas</titulo>
</libro>
</biblioteca>
=====
<alumnos>
<alumno id="1">
<nombre>Eva</nombre>
</alumnno>
</alumnos>
=====
<alumnos>
<alumno id="1" id="2">Ana</alumno>
</alumnos>
=====
<config>
<ruta valor=C:\datos></ruta>
</config>
=====
<a>
<b>Pan & vino</b>
</a>
=====
<pedido>
<linea>1</linea>
</pedido>
<pedido>
<linea>2</linea>
</pedido>
=====
<notas>
<!-- revisar -- urgente -->
</notas>Salida por consola
Documento 1: bien formado Elementos: 8 · profundidad máxima: 3 · atributos: 5 · raíz: <biblioteca> Documento 2: mal formado Línea 4, columna 3: se esperaba </alumno> y se encontró </alumnno> Documento 3: mal formado Línea 2, columna 18: atributo repetido «id» Documento 4: mal formado Línea 2, columna 15: el valor del atributo «valor» debe ir entre comillas Documento 5: mal formado Línea 2, columna 10: referencia a entidad no válida «&» Documento 6: mal formado Línea 4, columna 1: solo puede haber un elemento raíz Documento 7: mal formado Línea 2, columna 16: un comentario no puede contener --
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. Un bucle que mira qué empieza aquí
En cada posición, decide qué hay por los primeros caracteres: <?, <!--, <![CDATA[, <!DOCTYPE, </, < o texto. Cada caso consume lo suyo y deja i justo detrás.
while i < n:
if doc[i] != "<":
... # texto hasta el siguiente <
elif doc.startswith("<!--", i):
...
elif doc.startswith("</", i):
...2. La pila de etiquetas abiertas
Al abrir un elemento lo apilas; al cerrarlo, el nombre tiene que coincidir con la cima. La profundidad máxima es el tamaño máximo que alcanza la pila (contando las etiquetas vacías <x/>).
3. Los atributos
Tras el nombre, repite: espacios, nombre del atributo, =, comilla de apertura y la misma comilla de cierre. Un conjunto con los nombres ya vistos detecta los repetidos.
4. Posiciones exactas
Lanza siempre el error con la posición donde empieza el problema; posicion(doc, i) la convierte en línea y columna contando los saltos de línea anteriores.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.
Ejemplo
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE biblioteca SYSTEM "biblioteca.dtd">
<!-- Préstamos de octubre -->
<biblioteca centro="IES Ejemplo" curso='2026-2027'>
<libro isbn="978-84-0000-001-1" disponible="no">
<titulo>Programación & algoritmos</titulo>
<autor>Ana Ruiz</autor>
<nota><![CDATA[Usa <b>negrita</b> & más]]></nota>
<portada/>
</libro>
<libro isbn="978-84-0000-002-8">
<titulo>Redes épicas</titulo>
</libro>
</biblioteca>
=====
<alumnos>
<alumno id="1">
<nombre>Eva</nombre>
</alumnno>
</alumnos>
=====
<alumnos>
<alumno id="1" id="2">Ana</alumno>
</alumnos>
=====
<config>
<ruta valor=C:\datos></ruta>
</config>
=====
<a>
<b>Pan & vino</b>
</a>
=====
<pedido>
<linea>1</linea>
</pedido>
<pedido>
<linea>2</linea>
</pedido>
=====
<notas>
<!-- revisar -- urgente -->
</notas>
Documento 1: bien formado Elementos: 8 · profundidad máxima: 3 · atributos: 5 · raíz: <biblioteca> Documento 2: mal formado Línea 4, columna 3: se esperaba </alumno> y se encontró </alumnno> Documento 3: mal formado Línea 2, columna 18: atributo repetido «id» Documento 4: mal formado Línea 2, columna 15: el valor del atributo «valor» debe ir entre comillas Documento 5: mal formado Línea 2, columna 10: referencia a entidad no válida «&» Documento 6: mal formado Línea 4, columna 1: solo puede haber un elemento raíz Documento 7: mal formado Línea 2, columna 16: un comentario no puede contener --
Solución explicada
Ver la solución completa
1import re
2import sys
3
4NOMBRE = re.compile(r"[A-Za-z_][\w.\-]*(?::[A-Za-z_][\w.\-]*)?")
5REFERENCIA = re.compile(r"&(?:amp|lt|gt|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);")
6
7
8class MalFormado(Exception):
9 def __init__(self, pos, mensaje):
10 self.pos = pos
11 self.mensaje = mensaje
12
13
14def posicion(texto, pos):
15 """Línea y columna (desde 1) de una posición del texto."""
16 linea = texto.count("\n", 0, pos) + 1
17 columna = pos - (texto.rfind("\n", 0, pos) + 1) + 1
18 return linea, columna
19
20def comprobar_texto(doc, inicio, fin):
21 """Dentro del texto, & solo puede empezar una referencia válida."""
22 i = doc.find("&", inicio, fin)
23 while i != -1:
24 if not REFERENCIA.match(doc, i):
25 trozo = re.match(r"&[^\s;<&]*;?", doc[i:fin]).group()
26 raise MalFormado(i, f"referencia a entidad no válida «{trozo}»")
27 i = doc.find("&", i + 1, fin)
28
29
30def analizar(doc):
31 """Recorre el documento; lanza MalFormado en el primer error y si no, devuelve las estadísticas."""
32 pila, i, n = [], 0, len(doc)
33 raiz, cerrada = None, False
34 elementos = profundidad = atributos = 0
35 while i < n:
36 if doc[i] != "<":
37 fin = doc.find("<", i)
38 fin = n if fin == -1 else fin
39 if not pila and doc[i:fin].strip():
40 k = i + len(doc[i:fin]) - len(doc[i:fin].lstrip())
41 raise MalFormado(k, "texto fuera del elemento raíz")
42 comprobar_texto(doc, i, fin)
43 i = fin
44 continue
45 if doc.startswith("<?", i):
46 fin = doc.find("?>", i)
47 if fin == -1:
48 raise MalFormado(i, "falta cerrar la instrucción de procesamiento con ?>")
49 objetivo = re.match(r"<\?([^\s?]*)", doc[i:]).group(1)
50 if objetivo.lower() == "xml" and i != 0:
51 raise MalFormado(i, "la declaración XML solo puede ir al principio")
52 i = fin + 2
53 elif doc.startswith("<!--", i):
54 fin = doc.find("-->", i + 4)
55 if fin == -1:
56 raise MalFormado(i, "falta cerrar el comentario con -->")
57 dentro = doc.find("--", i + 4, fin)
58 if dentro != -1:
59 raise MalFormado(dentro, "un comentario no puede contener --")
60 i = fin + 3
61 elif doc.startswith("<![CDATA[", i):
62 if not pila:
63 raise MalFormado(i, "una sección CDATA solo puede ir dentro del elemento raíz")
64 fin = doc.find("]]>", i)
65 if fin == -1:
66 raise MalFormado(i, "falta cerrar la sección CDATA con ]]>")
67 i = fin + 3
68 elif doc.startswith("<!DOCTYPE", i):
69 if raiz is not None:
70 raise MalFormado(i, "la declaración DOCTYPE debe ir antes del elemento raíz")
71 fin = doc.find(">", i)
72 if fin == -1:
73 raise MalFormado(i, "falta cerrar la declaración DOCTYPE")
74 i = fin + 1
75 elif doc.startswith("</", i):
76 m = re.match(r"</\s*(" + NOMBRE.pattern + r")\s*>", doc[i:])
77 if not m:
78 raise MalFormado(i, "etiqueta de cierre mal escrita")
79 nombre = m.group(1)
80 if not pila:
81 raise MalFormado(i, f"etiqueta de cierre </{nombre}> sin abrir")
82 if pila[-1] != nombre:
83 raise MalFormado(i, f"se esperaba </{pila[-1]}> y se encontró </{nombre}>")
84 pila.pop()
85 cerrada = not pila
86 i += m.end()
87 else:
88 m = NOMBRE.match(doc, i + 1)
89 if not m:
90 raise MalFormado(i, "nombre de etiqueta no válido")
91 nombre = m.group()
92 if cerrada or (raiz is not None and not pila):
93 raise MalFormado(i, "solo puede haber un elemento raíz")
94 j, vistos = m.end(), set()
95 while True:
96 espacios = re.match(r"\s*", doc[j:]).end()
97 k = j + espacios
98 if doc.startswith("/>", k) or doc.startswith(">", k):
99 break
100 if k >= n:
101 raise MalFormado(i, f"falta cerrar la etiqueta <{nombre}>")
102 a = NOMBRE.match(doc, k)
103 if not a or espacios == 0:
104 raise MalFormado(k, f"atributo mal escrito en <{nombre}>")
105 attr = a.group()
106 if attr in vistos:
107 raise MalFormado(k, f"atributo repetido «{attr}»")
108 vistos.add(attr)
109 igual = re.match(r"\s*=\s*", doc[a.end():])
110 if not igual:
111 raise MalFormado(k, f"al atributo «{attr}» le falta el valor")
112 v = a.end() + igual.end()
113 comilla = doc[v] if v < n else ""
114 if comilla not in ("'", '"'):
115 raise MalFormado(v, f"el valor del atributo «{attr}» debe ir entre comillas")
116 cierre = doc.find(comilla, v + 1)
117 if cierre == -1:
118 raise MalFormado(v, f"falta cerrar las comillas del atributo «{attr}»")
119 if "<" in doc[v + 1:cierre]:
120 raise MalFormado(doc.index("<", v + 1), f"el valor del atributo «{attr}» no puede contener <")
121 comprobar_texto(doc, v + 1, cierre)
122 atributos += 1
123 j = cierre + 1
124 elementos += 1
125 raiz = raiz or nombre
126 if doc.startswith("/>", k):
127 profundidad = max(profundidad, len(pila) + 1)
128 cerrada = cerrada or not pila
129 i = k + 2
130 else:
131 pila.append(nombre)
132 profundidad = max(profundidad, len(pila))
133 i = k + 1
134 if pila:
135 raise MalFormado(n, f"falta cerrar <{pila[-1]}>")
136 if raiz is None:
137 raise MalFormado(n, "el documento no tiene elemento raíz")
138 return elementos, profundidad, atributos, raiz
139
140
141def main():
142 documentos = re.split(r"^=====\s*$\n?", sys.stdin.read(), flags=re.M)
143 for k, doc in enumerate(documentos, 1):
144 doc = doc.rstrip("\n")
145 try:
146 elementos, profundidad, atributos, raiz = analizar(doc)
147 print(f"Documento {k}: bien formado")
148 print(f" Elementos: {elementos} · profundidad máxima: {profundidad} · atributos: {atributos} · raíz: <{raiz}>")
149 except MalFormado as e:
150 linea, columna = posicion(doc, e.pos)
151 print(f"Documento {k}: mal formado")
152 print(f" Línea {linea}, columna {columna}: {e.mensaje}")
153
154
155main()Ser bien formado es la condición mínima para que un documento sea XML. Ser válido es otra cosa: cumplir además una DTD o un XML Schema. Un documento puede estar bien formado y no ser válido, pero nunca al revés.
La pila es la estructura natural para cualquier lenguaje con elementos anidados: HTML, JSON, los paréntesis de una expresión o los bloques de un programa se comprueban igual.
XML es estricto a propósito: ante el primer error, el analizador para (es un error fatal). HTML hizo lo contrario y los navegadores corrigen lo que pueden, con reglas complicadísimas; XML prefirió que todos los programas lean igual cada documento.
Las referencias < y & existen porque < y & tienen significado especial; dentro de una sección CDATA no hace falta escaparlos, por eso se usan para meter código o HTML dentro de un XML.
Para ir más allá
- Comprueba también que los prefijos de los espacios de nombres estén declarados con
xmlns:prefijo. - En lugar de parar en el primer error, recupérate y lista todos (como hace un editor).
- Construye el árbol del documento y escríbelo con sangría.