Parsing XML with Swift using XMLParser

Aunque pueda parecer cosa del pasado los archivos XML aún forman parte de la vida diaria de muchos desarrolladores, bien como archivos de configuración, formato de intercambio de datos o como respuesta a algunas API.

En este artículo verás cómo podemos leer archivos XML usando las herramientas de desarrollo propias de Apple.

Conociendo a XMLParser

El nucleo de todo esto, en lo que se basa la lectura de un archivo XML es el la clase XMLParser del framework Foundation.

An XMLParser notifies its delegate about the items (elements, attributes, CDATA blocks, comments, and so on) that it encounters as it processes an XML document.

XMLParser Apple documentation

La forma en la que XMLParser lee el contenido del archivo XML es siguiendo el estándar SAX, esto es, de una forma secuencia. No podemos movernos por el árbol de estructura del archivo.

La forma en la que XMLParser nos avisa de los elementos, secciones o atributos que va encontrando en mediante un delegado, concretamente XMLParserDelegate. Debemos tener una clase que implemente este protocolo y es en esa clase donde tendremos el código para leer y tratar los valores del archivo XML..

It does not itself do anything with those parsed items except report them

XMLParser Apple documentation

Enséñame un ejemplo

Para ilustrar el artículo vamos a leer un archivo XML con el detalle de libros. El archivo contendrá los elementos más comunes que nos encontraremos en la lectura de archivos XML así como algún comportamiento extraño y como solventarlo.

Con este ejemplo podemos cubrir como tratar la inmensa mayoría de casos que se nos pueden presentar a la hora de pasear nuestros documentos XML.

Diseñando la estrategia

Lo que vamos a hacer es tener usar tres XMLParser, uno para entidad del documento, esto es, Book, Author y Link.

El punto de entrada será BookParser, el cual cederá el control del parseo a AuthorParser y LinkParser cuando se alcance alguna de sus etiquetas.

De esta manera evitamos tener que estar almacenando el path del nodo en el que nos encontramos en un momento determinado para saber a qué entidad de nuestro modelo de datos debemos asignar el valor de un nodo del documento XML.

Además, necesitaremos otras variables más para llevar a cabo la lectura del archivo

Los preparativos

Como hemos dicho antes, la recuperación de información del archivo XML nos llegará mediante las llamadas que XMLParser haga al delegado que tenga establecido. Es decir, que necesitamos que nuestras clases implementen el protocolo XMLParserDelegate

Como vemos en el código anterior lo primero que hacemos es crear el XMLParser pasándolo un objeto Data con el contenido del XML, o bien una URL que apunte al archivo o recurso de red que contenga el documento.

Tras esto le indicamos al parser XML que nuestra clase es el delegado al que informar de los eventos ocurridos durante la lectura del documento. Y por último sólo queda comenzar a leer el documento, que lo hacemos invocando la función parse() de XMLParser.

Esta función devuelve un boleano que nos dirá si la lectura e interpretación del archivo ha terminado correctamente o no.

Extrayendo la información

Antes de sacar la información conviene que sepamos en que punto del documento XML nos encontramos de cara a asignar el contenido de las etiquetas a una propiedad u otra de nuestro modelo.

El posicionamiento lo conseguiremos usando estas cuatro funciones

Para saber cuando empezamos a leer el XML usamos…

func parserDidStartDocument(XMLParser)

…mientras que pasa saber cuando se ha llegado al final del XML usamos…

func parserDidEndDocument(XMLParser)

…para saber que hemos entrado en una etiqueta del XML empleamos…

func parser(XMLParser, didStartElement: String, namespaceURI: String?, qualifiedName: String?, attributes: [String : String])

…que como podemos ver también donde leeremos los atributos de una etiqueta. ¿Y para saber cuando hemos llegado al final de una etiqueta tenemos…

func parser(XMLParser, didEndElement: String, namespaceURI: String?, qualifiedName:String?)

Ahora que ya sabemos configurar el parser, nuestra posición en el documento y como vamos a recibir los eventos de lectura sólo nos queda sacar la información. En un archivo XML la encontraremos de tres maneras.

  1. Como contenido de una etiqueta
  2. Como atributo de una etiqueta
  3. Contenida en un CDATA

Si es como contenido de una etiqueta debemos usar la función…

func parser(XMLParser, foundCharacters: String)

…si es como atributo os remito de nuevo a…

func parser(XMLParser, didStartElement: String, namespaceURI: String?, qualifiedName: String?, attributes: [String : String])

Los atributos están contenido en el diccionario [String : String] que se pasa como parámetro. Y por último nos queda la información contenida en los bloques CDATA, y para ello tenemos…

func parser(XMLParser, foundCDATA: Data)

Dividir el parseo en parte

Usar un único parser puedo estar bien para documentos pequeños con un nivel de anudamiento casi nulo. Pero como ya sabemos que en nuestro día a día como desarrolladores esto es imposible vamos a dividir entre varias clases la responsabilidad de leer el documento XML.

Lo que hacemos es que BookParser comprueba en la función didStartElement... que etiqueta empezamos a leer. Si se corresponde con una que tiene su propio parser responsable le pasamos el control a dicha clase. Esto se haría de esta manera:

Lo primero que hacemos es crear la clase encargada del parseo y que debe implementar el protocolo XMLParserDelegate (1). Después le indicamos al XMLParser que el delegado al que debe informar de todos los eventos es la nueva clase creada (2) y por último guardamos una referencia a la clase (4)

El paso de información entre clases no es algo a tener en cuenta en este artículo, puedes hacerlo como más te guste. En este caso se hace mediante el Pattern Delegate (3).

Vida Extra

Si descargáis el Playground que acompaña a este artículo y que podéis descargar desde el enlace al final del artículo, veréis que cuando leo las cadenas de texto en lugar de asignar directamente el valor a la propiedad del modelo voy añadiendo lo leído en ese momento a la propiedad.

¿Y por qué así? Porque el parser detiene la lectura cuando encuentra determinados caracteres y la vuelve a reanudar a partir de ese punto. Os lo explico con una ejemplo.

Suponed que el valor de la etiqueta Publisher fuera Plaza & Janés. De entrada uno pensaría que el parser devolvería Plaza & Janés como valor leído de esa etiqueta, pero nada más los de la realidad. En realidad el parser hace cuatro lecturas

  1. Plaza
  2. &
  3. Jan
  4. és

Así que la solución para recuperar todo el contenido es usar la función append de String para ir añadiendo contenido según lo va enviando el parser.

Código fuente

En este repositorio de GitHub podéis descargar el Playground con el ejemplo usado como base para el artículo.