Cómo se hace en C++: manipulando ficheros XML con PugiXML

Vamos a empezar una nueva serie de artículos llamada Cómo se hace en C++. Será una serie de artículos de carácter práctico en el que explicaremos como realizar ciertas tareas cotidianas. C++ a diferencia de otros lenguajes no tiene una gran biblioteca de clases en la que apoyarse para hacer las tareas más habituales (aunque cada vez la STL va creciendo más en funcionalidades). Es por eso que muchas veces nos debemos apoyar en bibliotecas de terceros para tareas comunes que implementan otros lenguajes. En esta serie de artículos vamos a ver como hacer ciertas cosas en C++ con bibliotecas populares de terceros.

En este primer artículo de la serie vamos a ver como manipular ficheros XML desde C++ con la biblioteca PugiXML. Se trata de una biblioteca muy rápida y liviana que cumple perfectamente su cometido.

Añadiendo la biblioteca

Podemos descargar la biblioteca de su página oficial. Tenemos dos versiones una con fin de líneas estilo Windows y otra con estilo Unix. Descargamos según nuestro sistema operativo. Una vez descargada comprobamos que tiene varias carpetas para poder compilar la biblioteca en diferentes entornos. Yo, personalmente, al ser una biblioteca tan pequeña prefiero no tenerla como parte independiente así que lo que hago es añadir a mi proyecto el contenido de la carpeta src.

La carpeta contiene los archivos: pugiconfig.hpp, pugixml.hpp y pugixml.cpp. El primero es un archivo de configuración en el que segun comentamos o descomentamos algunas opciones activamos y desactivamos algunas características de la biblioteca, como por ejemplo el uso de la STL, si queremos usar solo el fichero header (pugixml.hpp), etc. Eso ya dependerá de las características de nuestro proyecto. En un principio viene con todo comentado, es decir, todas las características activadas y para este artículo así lo dejaremos.

Ahora simplemente debemos añadir a nuestro código los includes correspondiente a nuestro programa.

#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
int main()
{
    return 0;
}

Necesitamos un XML

Para hacer nuestras pruebas necesitamos un documento XML que leer, en este caso vamos a usar un mapa generado por el programa Tiled Map Editor que es un fichero XML correctamente formado. El fichero sería el siguiente mapa.tmx.




    
        
    
    
        
            eJzt0jEOgCAQRNHNdmglVsL97+kcgAqTdQm/eP3PZNzMHACwtCKHnAlaRi6pcido2aUv8hMzfZGbP9KkJ+2L2py+b58AAAD/egG7Eguh
        
    
    
        
            eJxjYBgFowA7sKSx+lEw+IAeHfWSY5cZGXrI1UuJXfgAJWFMD0Arfw8WUAnEVXSyawoQTyVRTyMQN5Fp32jaoj8YiWFui4YHGxgJ7itF46OXa+ZkmgsCpLiP2DIOvVzD5T58+YmYvEat/IjLffjyEzF5jRg1xPiBkvilFBDjB3q4T4kOdoyCoQv0Bjk2G+R4sAMAfP0fsw==
        
    
    
        
            eJztzjENAAAIwDAUcWIQ48iAhDbZvwgA+KG3BwAAeCuPV8cbnEUQJA==
        
    

    <object x="1059" y="149" height="289"/>
</objectgroup>

Lo guardamos en el directorio donde se genera nuestro ejecutable para este ejemplo. Bien como comprobamos se trata de un documento XML bien formado con sus nodos y sus atributos. Lo que vamos a hacer es un programa que extraiga esa información y la muestre en una ventana de consola. Empecemos.

Cargando el documento

Lo primero que tenemos que saber es que todos los elementos que componen la biblioteca pugiXML se encuentran dentro del namespace pugi. Así pues todas las instrucciones que han referencia a la biblioteca irán precedidas de pugi::.

Empezamos creando un objeto pugi::xml_document que será el objeto que englobe nuestro árbol XML una vez lo hemos cargado. Acto seguido cargamos el documento con su método load_file el cual englobaremos dentro de un if para prevenir errores de carga.

#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
int main()
{
    pugi::xml_document doc;
     if (!doc.load_file("mapa.tmx"))
    {
        std::cerr << "Error al cargar el documento XML." << std::endl;
        return -1;
    }
    std::cin.get();
    return 0;
}

Una vez cargado el archivo en el objeto doc, lo que hacemos es obtener el nodo principal de el archivo que en este caso es map.

#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
int main()
{
    pugi::xml_document doc;
     if (!doc.load_file("mapa.tmx"))
    {
        std::cerr << "Error al cargar el documento XML." << std::endl;
        return -1;
    }
    // Creamos un objeto nodo
    pugi::xml_node root_node;
    // Le asignamos el nodo principal comprobando que sea correcto
    if (!(root_node = doc.child("map")))
    {
        std::cerr << "El documento no es un mapa valido." << std::endl;
        return -2;
    }
    std::cin.get();
    return 0;
}

Como vemos solo tenemos que crear un objeto pugi::xml_node y con el método child extraemos nodos hijos del documento pasando el nombre del nodo como parámetro. Lo hacemos dentro de un if que comprueba que el nodo exista.

Ahora vamos a trabajar con el objeto root_node que es el que contiene toda la información ya que el resto de nodos son hijos de este. El objetivo de este ejemplo es sacar toda la información del mapa y mostrarla por pantalla. Así que comenzaremos por los atributos del nodo mapa.

Extrayendo atributos

Extrar los atributos de un nodo es tan sencillo como usar el metodo attribute que nos devolverá un objeto pugi::xml_attribute que tiene varios métodos interesantes uno de ellos es value que nos da el valor de ese atributo como una cadena de caracteres, pero tiene otros como as_int, as_double, as_float o as_uint que nos devuelve el valor en el tipo de dato especificado.


#include 
#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
int main()
{
    pugi::xml_document doc;
     if (!doc.load_file("mapa.tmx"))
    {
        std::cerr << "Error al cargar el documento XML." << std::endl;
        return -1;
    }
    // Creamos un objeto nodo
    pugi::xml_node root_node;
    // Le asignamos el nodo principal comprobando que sea correcto
    if (!(root_node = doc.child("map")))
    {
        std::cerr << "El documento no es un mapa valido." << std::endl;
        return -2;
    }
    // Declaramos las variables necesarias
    std::string orientation;
    float version;
    unsigned int width, height, tile_width, tile_height;
    // Obtenemos los datos del mapa
    orientation = root_node.attribute("orientation").value();
    version = root_node.attribute("version").as_float();
    width = root_node.attribute("width").as_uint();
    height = root_node.attribute("height").as_uint();
    tile_width = root_node.attribute("tilewidth").as_uint();
    tile_height = root_node.attribute("tileheight").as_uint();
    // Mostramos los datos obtenidos en pantalla
    std::cout << "Mapa\n------\n";
    std::cout << "Version: " << version << std::endl;
    std::cout << "Orientacion: " << orientation << std::endl;
    std::cout << "Dimensiones: " << width << "x" << height << std::endl;
    std::cout << "Dimensiones de Tiles: " << tile_width << "x" << tile_height << std::endl;
    // Evitamos el cierre de la aplicacion en entornos
    std::cin.get();
    return 0;
}

Con este código nos debería salir en pantalla algo como lo siguiente.

Mapa
------
Version: 1
Orientacion: orthogonal
Dimensiones: 40x30
Dimensiones de Tiles: 32x32

Cabe destacar de los atributos que si lo ponemos dentro de un if igual que sucede con los nodos nos devolverá falso si el nodo no contiene el atributo. Ideal para saber si un determinado nodo tiene o no cierto atributo y actuar en consecuencia. Hasta aquí hemos visto como extrar información de los atributos de un nodo conocido, que es el principal. Ahora vamos a ver como extraer información de los nodos hijos.

Recorriendo nodos hijos

En nuestro mapa el nodo map contiene tres tipos de nodos hijos:

  • tileset

  • layer

  • objectgroup

Así pues sabemos que los nodos que nos vamos a encontrar son de uno de estos tres tipos. Lo que vamos a hacer a continuación es recorrer todos los nodos hijos de root_node y "preguntar" como se llaman.

#include 
#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
int main()
{
    pugi::xml_document doc;
     if (!doc.load_file("mapa.tmx"))
    {
        std::cerr << "Error al cargar el documento XML." << std::endl;
        return -1;
    }
    // Creamos un objeto nodo
    pugi::xml_node root_node;
    // Le asignamos el nodo principal comprobando que sea correcto
    if (!(root_node = doc.child("map")))
    {
        std::cerr << "El documento no es un mapa valido." << std::endl;
        return -2;
    }
    // Declaramos las variables necesarias
    std::string orientation;
    float version;
    unsigned int width, height, tile_width, tile_height;
    // Obtenemos los datos del mapa
    orientation = root_node.attribute("orientation").value();
    version = root_node.attribute("version").as_float();
    width = root_node.attribute("width").as_uint();
    height = root_node.attribute("height").as_uint();
    tile_width = root_node.attribute("tilewidth").as_uint();
    tile_height = root_node.attribute("tileheight").as_uint();
    // Mostramos los datos obtenidos en pantalla
    std::cout << "Mapa\n------\n";
    std::cout << "Version: " << version << std::endl;
    std::cout << "Orientacion: " << orientation << std::endl;
    std::cout << "Dimensiones: " << width << "x" << height << std::endl;
    std::cout << "Dimensiones de Tiles: " << tile_width << "x" << tile_height << std::endl;
    // Recorremos todos los nodos hijos de root_node
    for (pugi::xml_node node = root_node.first_child(); node; node = node.next_sibling())
    {
        std::string node_name = node.name();
    }
    // Evitamos el cierre de la aplicacion en entornos
    std::cin.get();
    return 0;
}

Lo hacemos con un bucle for que tiene como variable un pugi::xml_node al que en primer lugar le asignamos el primer nodo de root_node con el método first_child() que poseen los objetos nodos, que como es obvio nos devuelve el primer nodo hijo. A su vez next_sibling() lo que hace es cambiar al siguiente nodo hermano.

Ya dentro del for con el método name() obtenemos el nombre del nodo actual y lo almacenamos en la variable string node_name. AHora como sabemos que para este caso concreto el nombre del nodo solo puede ser de uno de estos tipos decidimos con unas sentencias if-else.

#include 
#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
int main()
{
    pugi::xml_document doc;
     if (!doc.load_file("mapa.tmx"))
    {
        std::cerr << "Error al cargar el documento XML." << std::endl;
        return -1;
    }
    // Creamos un objeto nodo
    pugi::xml_node root_node;
    // Le asignamos el nodo principal comprobando que sea correcto
    if (!(root_node = doc.child("map")))
    {
        std::cerr << "El documento no es un mapa valido." << std::endl;
        return -2;
    }
    // Declaramos las variables necesarias
    std::string orientation;
    float version;
    unsigned int width, height, tile_width, tile_height;
    // Obtenemos los datos del mapa
    orientation = root_node.attribute("orientation").value();
    version = root_node.attribute("version").as_float();
    width = root_node.attribute("width").as_uint();
    height = root_node.attribute("height").as_uint();
    tile_width = root_node.attribute("tilewidth").as_uint();
    tile_height = root_node.attribute("tileheight").as_uint();
    // Mostramos los datos obtenidos en pantalla
    std::cout << "Mapa\n------\n";
    std::cout << "Version: " << version << std::endl;
    std::cout << "Orientacion: " << orientation << std::endl;
    std::cout << "Dimensiones: " << width << "x" << height << std::endl;
    std::cout << "Dimensiones de Tiles: " << tile_width << "x" << tile_height << std::endl;
    // Recorremos todos los nodos hijos de root_node
    for (pugi::xml_node node = root_node.first_child(); node; node = node.next_sibling())
    {
        std::string node_name = node.name();
        // Actuamos en consecuencia segun el tipo de nodo
        if (node_name == "tileset")
        {
        }
        else if (node_name == "layer")
        {
        }
        else if (node_name == "objectgroup")
        {
        }
    }
    // Evitamos el cierre de la aplicacion en entornos
    std::cin.get();
    return 0;
}

Vamos a empezar extrayendo los datos de los nodos tileset. En este caso solo tenemos un nodo tileset con una serie de atributos y que contiene un nodo hijo llamado image. Vamos a extraer la información de sus atributos y de su nodo hijo.

#include 
#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
int main()
{
    pugi::xml_document doc;
     if (!doc.load_file("mapa.tmx"))
    {
        std::cerr << "Error al cargar el documento XML." << std::endl;
        return -1;
    }
    // Creamos un objeto nodo
    pugi::xml_node root_node;
    // Le asignamos el nodo principal comprobando que sea correcto
    if (!(root_node = doc.child("map")))
    {
        std::cerr << "El documento no es un mapa valido." << std::endl;
        return -2;
    }
    // Declaramos las variables necesarias
    std::string orientation;
    float version;
    unsigned int width, height, tile_width, tile_height;
    // Obtenemos los datos del mapa
    orientation = root_node.attribute("orientation").value();
    version = root_node.attribute("version").as_float();
    width = root_node.attribute("width").as_uint();
    height = root_node.attribute("height").as_uint();
    tile_width = root_node.attribute("tilewidth").as_uint();
    tile_height = root_node.attribute("tileheight").as_uint();
    // Mostramos los datos obtenidos en pantalla
    std::cout << "Mapa\n------\n";
    std::cout << "Version: " << version << std::endl;
    std::cout << "Orientacion: " << orientation << std::endl;
    std::cout << "Dimensiones: " << width << "x" << height << std::endl;
    std::cout << "Dimensiones de Tiles: " << tile_width << "x" << tile_height << std::endl;
    // Recorremos todos los nodos hijos de root_node
    for (pugi::xml_node node = root_node.first_child(); node; node = node.next_sibling())
    {
        std::string node_name = node.name();
        // Actuamos en consecuencia segun el tipo de nodo
        if (node_name == "tileset")
        {
            unsigned int firstgid, width, height;
            std::string name, source;
            firstgid = node.attribute("firstgid").as_uint();
            name = node.attribute("name").value();
            // Obtenemos el nodo hijo image si existe
            if(pugi::xml_node node_image = node.child("image"))
            {
                source = node_image.attribute("source").value();
                width = node_image.attribute("width").as_uint();
                height = node_image.attribute("height").as_uint();
            }
            // Una vez extraida la información la mostramos en pantalla
            std::cout << "--------\nTileset\n--------\n";
            std::cout << "Nombre: " << name << std::endl;
            std::cout << "Ruta: " << source << std::endl;
            std::cout << "Primer Tile: " << firstgid << std::endl;
            std::cout << "Dimensiones: " << width << "x" << height << std::endl;
        }
        else if (node_name == "layer")
        {
        }
        else if (node_name == "objectgroup")
        {
        }
    }
    // Evitamos el cierre de la aplicacion en entornos
    std::cin.get();
    return 0;
}

Como vemos comprobamos solamente que tenga un nodo hijo image para extraerlo, en realidad habría que comprobar con un if cada atributo si no estamos seguro de que el XML lo fuera a contener.

Una vez hecho esto pasamos a extraer los datos de las capas layer.

#include 
#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
int main()
{
    pugi::xml_document doc;
     if (!doc.load_file("mapa.tmx"))
    {
        std::cerr << "Error al cargar el documento XML." << std::endl;
        return -1;
    }
    // Creamos un objeto nodo
    pugi::xml_node root_node;
    // Le asignamos el nodo principal comprobando que sea correcto
    if (!(root_node = doc.child("map")))
    {
        std::cerr << "El documento no es un mapa valido." << std::endl;
        return -2;
    }
    // Declaramos las variables necesarias
    std::string orientation;
    float version;
    unsigned int width, height, tile_width, tile_height;
    // Obtenemos los datos del mapa
    orientation = root_node.attribute("orientation").value();
    version = root_node.attribute("version").as_float();
    width = root_node.attribute("width").as_uint();
    height = root_node.attribute("height").as_uint();
    tile_width = root_node.attribute("tilewidth").as_uint();
    tile_height = root_node.attribute("tileheight").as_uint();
    // Mostramos los datos obtenidos en pantalla
    std::cout << "Mapa\n------\n";
    std::cout << "Version: " << version << std::endl;
    std::cout << "Orientacion: " << orientation << std::endl;
    std::cout << "Dimensiones: " << width << "x" << height << std::endl;
    std::cout << "Dimensiones de Tiles: " << tile_width << "x" << tile_height << std::endl;
    // Recorremos todos los nodos hijos de root_node
    for (pugi::xml_node node = root_node.first_child(); node; node = node.next_sibling())
    {
        std::string node_name = node.name();
        // Actuamos en consecuencia segun el tipo de nodo
        if (node_name == "tileset")
        {
            unsigned int firstgid, width, height;
            std::string name, source;
            firstgid = node.attribute("firstgid").as_uint();
            name = node.attribute("name").value();
            // Obtenemos el nodo hijo image si existe
            if(pugi::xml_node node_image = node.child("image"))
            {
                source = node_image.attribute("source").value();
                width = node_image.attribute("width").as_uint();
                height = node_image.attribute("height").as_uint();
            }
            // Una vez extraida la información la mostramos en pantalla
            std::cout << "--------\nTileset\n--------\n";
            std::cout << "Nombre: " << name << std::endl;
            std::cout << "Ruta: " << source << std::endl;
            std::cout << "Primer Tile: " << firstgid << std::endl;
            std::cout << "Dimensiones: " << width << "x" << height << std::endl;
        }
        else if (node_name == "layer")
        {
            std::string name, data, encoding, compression;

            name = node.attribute("name").value();
            if(pugi::xml_node node_data = node.child("data"))
            {
                encoding = node_data.attribute("encoding").value();
                compression = node_data.attribute("compression").value();
                data = node_data.child_value();
            }
            std::cout << "--------\nLayer\n--------\n";
            std::cout << "Nombre: " << name << std::endl;
            std::cout << "Codificacion: " << encoding << std::endl;
            std::cout << "Compresion: " << compression << std::endl;
            std::cout << "Cadena de datos: " << data << std::endl;
        }
        else if (node_name == "objectgroup")
        {
        }
    }
    // Evitamos el cierre de la aplicacion en entornos
    std::cin.get();
    return 0;
}

Lo mas destacable del nuevo código es el uso de el método child_value() que nos devuelve el contenido del nodo, es decir lo que está entre su etiqueta de apertura y cierre en forma de cadena de caracteres.

A estas alturas queda poco misterio de como extraer atributos de nodos y de sus nodos hijos, así que queda como ejercicio implementar el objectgroup. Si queréis ver una versión completa y con sus comprobaciones de como se haría correctamente todo este sistema y almacenando los datos en objetos podéis revisar este archivo de un proyecto personal.

Qué pasa si no sabemos que datos va a tener el XML

Puede suceder que no sepamos que datos tiene el XML pasado ni donde está lo que buscamos. Podemos hacer uso de los iteradores para recorrer todo el xml y extraer todos sus datos sin conocerlos.

#include 
#include 
#include "pugiconfig.hpp"
#include "pugixml.hpp"
void getNodeInfo(pugi::xml_node);
int main()
{
    pugi::xml_document doc;
     if (!doc.load_file("mapa.tmx"))
    {
        std::cerr << "Error al cargar el documento XML." << std::endl;
        return -1;
    }   
    getNodeInfo(doc);
    // Evitamos el cierre de la aplicacion en entornos
    std::cin.get();
    return 0;
}
void getNodeInfo(pugi::xml_node node)
{
    for (pugi::xml_node_iterator it = node.begin(); it != node.end(); ++it)
    {
        std::cout << it->name() << "\n--------\n";
        for (pugi::xml_attribute_iterator ait = it->attributes_begin(); ait != it->attributes_end(); ++ait)
        {
            std::cout << " " << ait->name() << ": " << ait->value() << std::endl;
        }
        std::cout << std::endl;
        for (pugi::xml_node_iterator sit = node.begin(); sit != node.end(); ++sit)
        {
            getNodeInfo(*sit);
        }
    }
}

Esto solo es una muestra de lo que se puede lograr con pugiXML, puedes aprender mucho más leyendo la documentación oficial. Se recomienda leer acerca de xPath que se ha quedado fuera de este artículo.

Sitio oficial | pugiXML

Ver todos los comentarios en https://www.genbeta.com

VER 0 Comentario

Portada de Genbeta