Leer y escribir archivos XML en Python

XML, o Extensible Markup Language, es un lenguaje de marcado que se usa comúnmente para estructurar, almacenar y transferir datos entre sistemas. Aunque no es tan común como...

XML, o Extensible Markup Language, es un lenguaje de marcado que se usa comúnmente para estructurar, almacenar y transferir datos entre sistemas. Si bien no es tan común como solía ser, todavía se usa en servicios como RSS y SOAP, así como para estructurar archivos como documentos de Microsoft Office.

Dado que Python es un lenguaje popular para la web y el análisis de datos, es probable que necesite leer o escribir datos XML en algún momento, en cuyo caso está de suerte.

A lo largo de este artículo, veremos principalmente el módulo ElementoÁrbol para leer, escribir y modificar datos XML. También lo compararemos con el módulo minidominio más antiguo en las primeras secciones para que pueda obtener una buena comparación de los dos.

Los módulos XML

El minidom, o implementación mínima de DOM, es una implementación simplificada del modelo de objeto de documento (DOM). El DOM es una interfaz de programación de aplicaciones que trata XML como una estructura de árbol, donde cada nodo del árbol es un objeto. Por lo tanto, el uso de este módulo requiere que estemos familiarizados con su funcionalidad.

El módulo ElementTree proporciona una interfaz más "Pythonic" para manejar XMl y es una buena opción para aquellos que no están familiarizados con el DOM. También es probable que sea un mejor candidato para ser utilizado por programadores novatos debido a su interfaz simple, que verá a lo largo de este artículo.

En este artículo, el módulo ElementTree se usará en todos los ejemplos, mientras que minidom también se demostrará, pero solo para contar y leer documentos XML.

Ejemplo de archivo XML

En los ejemplos a continuación, usaremos el siguiente archivo XML, que guardaremos como "items.xml":

1
2
3
4
5
6
<data>
    <items>
        <item name="item1">item1abc</item>
        <item name="item2">item2abc</item>
    </items>
</data>

Como puede ver, es un ejemplo XML bastante simple, que solo contiene algunos objetos anidados y un atributo. Sin embargo, debería ser suficiente para demostrar todas las operaciones XML de este artículo.

Leer documentos XML

Usando minidom

Para analizar un documento XML utilizando minidom, primero debemos importarlo desde el módulo xml.dom. Este módulo utiliza la función parse para crear un objeto DOM a partir de nuestro archivo XML. La función parse tiene la siguiente sintaxis:

1
xml.dom.minidom.parse(filename_or_file[, parser[, bufsize]])

Aquí, el nombre del archivo puede ser una cadena que contenga la ruta del archivo o un objeto de tipo de archivo. La función devuelve un documento, que se puede manejar como un tipo XML. Por lo tanto, podemos usar la función getElementByTagName() para encontrar una etiqueta específica.

Dado que cada nodo se puede tratar como un objeto, podemos acceder a los atributos y al texto de un elemento usando las propiedades del objeto. En el siguiente ejemplo, hemos accedido a los atributos y texto de un nodo específico y de todos los nodos juntos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from xml.dom import minidom

# parse an xml file by name
mydoc = minidom.parse('items.xml')

items = mydoc.getElementsByTagName('item')

# one specific item attribute
print('Item #2 attribute:')
print(items[1].attributes['name'].value)

# all item attributes
print('\nAll attributes:')
for elem in items:
    print(elem.attributes['name'].value)

# one specific item's data
print('\nItem #2 data:')
print(items[1].firstChild.data)
print(items[1].childNodes[0].data)

# all items data
print('\nAll item data:')
for elem in items:
    print(elem.firstChild.data)

El resultado es el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ python minidomparser.py 
Item #2 attribute:
item2

All attributes:
item1
item2

Item #2 data:
item2abc
item2abc

All item data:
item1abc
item2abc

Figura 1

Si quisiéramos usar un archivo ya abierto, podemos simplemente pasar nuestro objeto de archivo a parse así:

1
2
3
4
datasource = open('items.xml')

# parse an open file
mydoc = parse(datasource)

Además, si los datos XML ya se cargaron como una cadena, podríamos haber usado la función parseString() en su lugar.

Usando ElementTree

ElementTree nos presenta una forma muy sencilla de procesar archivos XML. Como siempre, para poder utilizarlo primero debemos importar el módulo. En nuestro código usamos el comando importar con la palabra clave as, que nos permite usar un nombre simplificado (ET en este caso) para el módulo en el código.

Después de la importación, creamos una estructura de árbol con la función parse y obtenemos su elemento raíz. Una vez que tenemos acceso al nodo raíz, podemos recorrer fácilmente el árbol, porque un árbol es un gráfico conexo.

Usando ElementTree, y al igual que el ejemplo de código anterior, obtenemos los atributos y el texto del nodo usando los objetos relacionados con cada nodo.

El código es el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import xml.etree.ElementTree as ET
tree = ET.parse('items.xml')
root = tree.getroot()

# one specific item attribute
print('Item #2 attribute:')
print(root[0][1].attrib)

# all item attributes
print('\nAll attributes:')
for elem in root:
    for subelem in elem:
        print(subelem.attrib)

# one specific item's data
print('\nItem #2 data:')
print(root[0][1].text)

# all items data
print('\nAll item data:')
for elem in root:
    for subelem in elem:
        print(subelem.text)

El resultado será el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ python treeparser.py 
Item #2 attribute:
item2

All attributes:
item1
item2

Item #2 data:
item2abc

All item data:
item1abc
item2abc

Figura 2

Como puede ver, esto es muy similar al ejemplo de minidom. Una de las principales diferencias es que el objeto attrib es simplemente un objeto de diccionario, lo que lo hace un poco más compatible con otro código de Python. Tampoco necesitamos usar value para acceder al valor del atributo del elemento como lo hicimos antes.

Es posible que haya notado cómo acceder a objetos y atributos con ElementTree es un poco más Pythonic, como mencionamos antes. Esto se debe a que los datos XML se analizan como listas y diccionarios simples, a diferencia de minidom, donde los elementos se analizan como xml.dom.minidom.Attr y "DOM Text nodes" personalizados.

Contar los elementos de un documento XML

Usando minidom

Como en el caso anterior, el minidom debe ser importado desde el módulo dom. Este módulo proporciona la función getElementsByTagName, que usaremos para encontrar el elemento de la etiqueta. Una vez obtenido, usamos el método integrado len() para obtener el número de subelementos conectados a un nodo. El resultado obtenido del siguiente código se muestra en la Figura 3.

1
2
3
4
5
6
7
8
9
from xml.dom import minidom

# parse an xml file by name
mydoc = minidom.parse('items.xml')

items = mydoc.getElementsByTagName('item')

# total amount of items
print(len(items))
1
2
$ python counterxmldom.py
2

Figura 3

Tenga en cuenta que esto solo contará el número de elementos secundarios bajo la nota en la que ejecuta len(), que en este caso es el nodo raíz. Si desea encontrar todos los subelementos en un árbol mucho más grande, deberá atravesar todos los elementos y contar cada uno de sus elementos secundarios.

Usando ElementTree

De manera similar, el módulo ElementTree nos permite calcular la cantidad de nodos conectados a un nodo.

Código de ejemplo:

1
2
3
4
5
6
import xml.etree.ElementTree as ET
tree = ET.parse('items.xml')
root = tree.getroot()

# total amount of items
print(len(root[0]))

El resultado es el siguiente:

1
2
$ python counterxml.py
2

Figura 4

Escritura de documentos XML {#escritura de documentos xml}

Usando ElementTree

ElementTree también es excelente para escribir datos en archivos XML. El siguiente código muestra cómo crear un archivo XML con la misma estructura que el archivo que usamos en los ejemplos anteriores.

Los pasos son:

  1. Cree un elemento, que actuará como nuestro elemento raíz. En nuestro caso, la etiqueta de este elemento es "datos".
  2. Una vez que tengamos nuestro elemento raíz, podemos crear subelementos usando la función SubElement. Esta función tiene la sintaxis:

Subelemento(padre, etiqueta, atributo={}, **extra)

Aquí parent es el nodo principal al que conectarse, attrib es un diccionario que contiene los atributos del elemento y extra son argumentos de palabras clave adicionales. Esta función nos devuelve un elemento, que se puede usar para adjuntar otros subelementos, como lo hacemos en las siguientes líneas al pasar elementos al constructor SubElement.
3. Aunque podemos agregar nuestros atributos con la función SubElement, también podemos usar la función set(), como lo hacemos en el siguiente código. El texto del elemento se crea con la propiedad texto del objeto Elemento.
4. En las últimas 3 líneas del código a continuación, creamos una cadena a partir del árbol XML y escribimos esos datos en un archivo que abrimos.

Código de ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import xml.etree.ElementTree as ET

# create the file structure
data = ET.Element('data')
items = ET.SubElement(data, 'items')
item1 = ET.SubElement(items, 'item')
item2 = ET.SubElement(items, 'item')
item1.set('name','item1')
item2.set('name','item2')
item1.text = 'item1abc'
item2.text = 'item2abc'

# create a new XML file with the results
mydata = ET.tostring(data)
myfile = open("items2.xml", "w")
myfile.write(mydata)

Ejecutar este código dará como resultado un nuevo archivo, "items2.xml", que debería ser equivalente al archivo original "items.xml", al menos en términos de la estructura de datos XML. Sin embargo, probablemente notará que la cadena resultante es solo una línea y no contiene sangría.

Búsqueda de elementos XML

Usando ElementTree

El módulo ElementTree ofrece la función findall(), que nos ayuda a encontrar elementos específicos en el árbol. Devuelve todos los artículos con la condición especificada. Además, el módulo tiene la función find(), que devuelve solo el primer subelemento que coincide con los criterios especificados. La sintaxis de ambas funciones es la siguiente:

1
findall(match, namespaces=None)
1
find(match, namespaces=None)

Para ambas funciones, el parámetro match puede ser un nombre de etiqueta XML o una ruta. La función findall() devuelve una lista de elementos, y find devuelve un solo objeto de tipo Element.

Además, hay otra función auxiliar que devuelve el texto del primer nodo que coincide con el criterio dado:

1
findtext(match, default=None, namespaces=None)

Aquí hay un código de ejemplo para mostrarle exactamente cómo operan estas funciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import xml.etree.ElementTree as ET
tree = ET.parse('items.xml')
root = tree.getroot()

# find the first 'item' object
for elem in root:
    print(elem.find('item').get('name'))

# find all "item" objects and print their "name" attribute
for elem in root:
    for subelem in elem.findall('item'):
    
        # if we don't need to know the name of the attribute(s), get the dict
        print(subelem.attrib)      
    
        # if we know the name of the attribute, access it directly
        print(subelem.get('name'))

Y aquí está el resultado de ejecutar este código:

1
2
3
4
5
6
$ python findtree.py 
item1
{'name': 'item1'}
item1
{'name': 'item2'}
item2

Figura 5

Modificación de elementos XML {#modificación de elementos XML}

Usando ElementTree

El módulo ElementTree presenta varias herramientas para modificar documentos XML existentes. El siguiente ejemplo muestra cómo cambiar el nombre de un nodo, cambiar el nombre de un atributo y modificar su valor, y cómo agregar un atributo adicional a un elemento.

El texto de un nodo se puede cambiar especificando el nuevo valor en el campo de texto del objeto de nodo. El nombre del atributo se puede redefinir usando la función set(name, value). La función set no tiene que funcionar solo en un atributo existente, también se puede usar para definir un nuevo atributo.

El siguiente código muestra cómo realizar estas operaciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import xml.etree.ElementTree as ET

tree = ET.parse('items.xml')
root = tree.getroot()

# changing a field text
for elem in root.iter('item'):
    elem.text = 'new text'

# modifying an attribute
for elem in root.iter('item'):
    elem.set('name', 'newitem')

# adding an attribute
for elem in root.iter('item'):
    elem.set('name2', 'newitem2')

tree.write('newitems.xml')

Después de ejecutar el código, el archivo XML resultante "newitems.xml" tendrá un árbol XML con los siguientes datos:

1
2
3
4
5
6
<data>
    <items>
        <item name="newitem" name2="newitem2">new text</item>
        <item name="newitem" name2="newitem2">new text</item>
    </items>
</data>

Como podemos ver al comparar con el archivo XML original, los nombres de los elementos del elemento han cambiado a "elemento nuevo", el texto a "texto nuevo", y el atributo "nombre2" se ha agregado a ambos. nodos.

También puede notar que escribir datos XML de esta manera (llamar a tree.write con un nombre de archivo) agrega más formato al árbol XML para que contenga líneas nuevas y sangría.

Creación de subelementos XML

Usando ElementTree

El módulo ElementTree tiene más de una forma de agregar un nuevo elemento. La primera forma que veremos es usando la función makeelement(), que tiene el nombre del nodo y un diccionario con sus atributos como parámetros.

La segunda forma es a través de la clase SubElement(), que toma el elemento principal y un diccionario de atributos como entradas.

En nuestro ejemplo a continuación, mostramos ambos métodos. En el primer caso, el nodo no tiene atributos, por lo que creamos un diccionario vacío (attrib = {}). En el segundo caso, usamos un diccionario completo para crear los atributos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import xml.etree.ElementTree as ET

tree = ET.parse('items.xml')
root = tree.getroot()

# adding an element to the root node
attrib = {}
element = root.makeelement('seconditems', attrib)
root.append(element)

# adding an element to the seconditem node
attrib = {'name2': 'secondname2'}
subelement = root[0][1].makeelement('seconditem', attrib)
ET.SubElement(root[1], 'seconditem', attrib)
root[1][0].text = 'seconditemabc'

# create a new XML file with the new element
tree.write('newitems2.xml')

Después de ejecutar este código, el archivo XML resultante se verá así:

1
2
3
4
5
6
7
8
9
<data>
    <items>
        <item name="item1">item1abc</item>
        <item name="item2">item2abc</item>
    </items>
    <seconditems>
         <seconditem name2="secondname2">seconditemabc</seconditem>
    </seconditems>
</data>

Como podemos ver al comparar con el archivo original, se han agregado el elemento "seconditems" y su subelemento "seconditem". Además, el nodo "segundo elemento" tiene "nombre2" como atributo, y su texto es "segundoelementobc", como se esperaba.

Eliminación de elementos XML

Usando ElementTree

Como probablemente esperaría, el módulo ElementTree tiene la funcionalidad necesaria para eliminar los atributos y subelementos del nodo.

Eliminar un atributo

El siguiente código muestra cómo eliminar el atributo de un nodo usando la función pop(). La función se aplica al parámetro de objeto attrib. Especifica el nombre del atributo y lo establece en Ninguno.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import xml.etree.ElementTree as ET

tree = ET.parse('items.xml')
root = tree.getroot()

# removing an attribute
root[0][0].attrib.pop('name', None)

# create a new XML file with the results
tree.write('newitems3.xml')

El resultado será el siguiente archivo XML:

1
2
3
4
5
6
<data>
    <items>
        <item>item1abc</item>
        <item name="item2">item2abc</item>
    </items>
</data>

Como podemos ver en el código XML anterior, el primer elemento no tiene el atributo "nombre".

Eliminando un sub-elemento

Se puede eliminar un subelemento específico mediante la función eliminar. Esta función debe especificar el nodo que queremos eliminar.

El siguiente ejemplo nos muestra cómo usarlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import xml.etree.ElementTree as ET

tree = ET.parse('items.xml')
root = tree.getroot()

# removing one sub-element
root[0].remove(root[0][0])

# create a new XML file with the results
tree.write('newitems4.xml')

El resultado será el siguiente archivo XML:

1
2
3
4
5
<data>
    <items>
        <item name="item2">item2abc</item>
    </items>
</data>

Como podemos ver en el código XML anterior, ahora solo hay un nodo "elemento". El segundo ha sido eliminado del árbol original.

Eliminación de todos los subelementos

El módulo ElementTree nos presenta la función clear(), que se puede usar para eliminar todos los subelementos de un elemento dado.

El siguiente ejemplo nos muestra cómo usar clear():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import xml.etree.ElementTree as ET

tree = ET.parse('items.xml')
root = tree.getroot()

# removing all sub-elements of an element
root[0].clear()

# create a new XML file with the results
tree.write('newitems5.xml')

El resultado será el siguiente archivo XML:

1
2
3
<data>
    <items />
</data>

Como podemos ver en el código XML anterior, todos los subelementos del elemento "items" se han eliminado del árbol.

Finalizando

Python ofrece varias opciones para manejar archivos XML. En este artículo, revisamos el módulo ElementTree y lo usamos para analizar, crear, modificar y eliminar archivos XML. También hemos usado el modelo minidom para analizar archivos XML. Personalmente, recomendaría usar el módulo ElementTree ya que es mucho más fácil trabajar con él y es el módulo más moderno de los dos.