domingo, 21 de noviembre de 2010

XML PARSE: de XML a fichero plano con COBOL.

El XML parse es una utilidad del COBOL que podéis encontrar en el propio manual de IBM.
Pero, ¿alguna vez has tenido que usarlo? Y digo más, ¿alguna vez has tenido que usarlo para transformar un XML que subido a host se convirtiese en un fichero de más de 500.000 registros?

Se puede decir que para pequeños textos no tiene mucha dificultad, pero como a nosotros nos gusta el riesgo, vamos a explicar como transformar un fichero bien "gordo" con el XML PARSE.
Al final de este artículo podréis descargaros el código completo del programa de ejemplo.

Lo primero que tenemos que hacer es "subir" el fichero xml que tengamos en local a nuestro servidor. En nuestro ejemplo vamos a suponer que tendremos un fichero plano de longitud fija 129, y unos 500.000 registros.

Si el fichero XML tenía una estructura de este tipo:



<elemento1>
<elemento2 atributo1=atr1-uno/>
<elemento2 atributo1=atr1-uno/>
</elemento1>
<elemento3>
<elemento4>elem4-uno</elemento4>
<elemento5 atributo2=atr2-uno/>
<elemento5 atributo2=atr2-dos/>
<elemento6>elem6-uno</elemento6>
<elemento6>elem6-dos</elemento6>
</elemento3>


En nuestro fichero plano tendrá una estructura de este tipo:

Aquí vemos aparecer los primeros problemas:

1. Para el proceso de parseo necesitaremos conocer tanto el inicio como el final de un elemento/atributo, pero, cuando leamos el fichero, la información puede venir separada en diferentes registros.

2. Además, COBOL tiene un máximo de caracteres que puede procesar a la vez, es decir, no podemos declarar una variable que mida "longitud de registro * nº de registros" para guardar toda la información del fichero en ella.
La máxima longitud soportada es 134217727.
La solución: procesar el fichero por "trocitos".

Definiremos una variable WX-REG-XML con PIC X(250000), que será lo que mida cada uno de nuestros "trocitos" (podéis definirla con la longitud que consideréis conveniente).
Luego leeremos del fichero de entrada hasta rellenar esa variable.


Proceso:
PERFORM UNTIL WI-POS GREATER 249400(para dejar un margen de espacios en blanco al final de la variable WX-REG-XML) OR FIN-FICHERO

PERFORM LEER-FICHERO

END-PERFORM

MOVE WI-POS TO WI-POS-AUX(nos guardamos la posición donde tenemos que mover el siguiente registro que leamos)

IF FIN-TABLA
PERFORM BUSCA-INICIO-ELEMENTO(buscamos las etiquetas de inicio de los elementos)
END-IF

PERFORM BUSCA-FIN-ELEMENTO(buscamos las etiquetas de final de los elementos)

PERFORM PROCESO-XML(hacemos el XML PARSE)

MOVE WX-REG-XML-AUX TO WX-REG-XML


Leer-fichero:
READ FICHERO INTO WR-FICHERO

SI FS-FICHERO = '00'
MOVE WR-FICHERO TO WX-REG-XML(WI-POS:129)

ADD 129 TO WI-POS
END-IF


De esta forma tendremos guardado en nuestra variable WX-REG-XML el texto del ejemplo 2 en una sola línea.

Gracias a nuestro fichero xsd sabremos los nombres de todos los elementos/atributos de nuestro xml. Esto nos ayudará en la siguiente parte del proceso: detectar el inicio y el fin de cada elemento raíz. En nuestro caso, los elementos raíz serán "elemento1" y "elemento3", y escribiremos la información de cada uno de ellos en un fichero de salida diferente.

Buscar el inicio de un elemento raíz:

PERFORM VARYING WI-INI FROM 1 BY 1 UNTIL FIN-BUSCA-INI OR WI-INI GREATER 249800

IF WX-REG-XML(WI-INI:9) EQUAL 'elemento1'
SET SI-ELEMENTO1 TO TRUE
SET FIN-BUSCA-INI
END-IF

IF WX-REG-XML(WI-INI:9) EQUAL 'elemento3'
SET SI-ELEMENTO3 TO TRUE
SET FIN-BUSCA-INI TO TRUE
END-IF

END-PERFORM


Una vez que sabemos el elemento raíz que vamos a procesar, buscaremos el final de ese elemento.
Aquí nos podemos encontrar con otro problema, pues puede darse el caso de que un elemento raíz contenga tanta información que no quepa en nuestra variable WX-REG-XML y en este caso no encontraríamos la etiqueta de final de elemento "</elemento1>".
La solución en este caso pasa por controlar en todo momento el final de algún elemento hijo.

Vamos a ver todo esto con un ejemplo:
"elemento1"

PERFORM VARYING WI-FIN FROM 1 BY 1 UNTIL FIN-BUSCA-FIN

a) Controlamos la posición del final del atributo:

IF WX-REG-XML(WI-FIN:2) EQUAL '/>'
COMPUTE WX-FIN-ATR = WI-FIN + 2
END-IF


b) Buscamos el final del elemento raíz

IF WX-REG-XML(WI-FIN:12) EQUAL '</elemento1>'
COMPUTE WX-POS-INI = WI-FIN + 12


c) Guardamos la información que ya no pertenece a "elemento1" en una variable auxiliar

MOVE WX-REG-XML(WX-POS-INI:) TO WX-REG-XML-AUX
MOVE SPACES TO WX-REG-XML(WX-POS-INI:)


d)Calculamos la posición donde moveremos el siguiente registro que leamos

COMPUTE WI-POS = WI-POS-AUX - WX-POS-INI + 1

SET FIN-BUSCA-FIN TO TRUE
SET FIN-TABLA TO TRUE


e)Al mismo tiempo que buscamos el final del "elemento1", controlamos el final de la variable WX-REG-XML.

ELSE

f)Si hemos llegado al final del registro sin encontrar el final del "elemento1", crearemos un nombre de elemento raíz auxiliar e incrementable de este tipo:

05 WX-TABLA-AUX.
10 FILLER PIC X(7) VALUE '<TBLAUX'.
10 WX-NUMTBL PIC 9(8).
10 FILLER PIC X VALUE '>'.


para tratar, en una siguiente pasada, el resto de la información de "elemento1" hasta que encontremos el final "</elemento1>".

IF WX-REG-XML(WI-FIN:200) EQUAL SPACES

ADD 1 TO WA-NUMTBL

MOVE WA-NUMTBL TO WX-NUMTBL
MOVE WX-TABLA-AUX TO WX-REG-XML-AUX(1:16) <--Ponemos la etiqueta <TBLAUX00000001>

MOVE WX-REG-XML(WX-FIN-ATR:) TO WX-REG-XML-AUX(17:)
MOVE SPACES TO WX-REG-XML(WX-FIN-ATR:)

COMPUTE WI-POS = WI-POS-AUX - WX-FIN-ATR + 17

SET FIN-BUSCA-FIN TO TRUE
SET NO-FIN-TABLA TO TRUE
END-IF
ENF-IF
END-PERFORM


De esta forma, la siguiente parte de información que procesaríamos sería algo así:


El hecho de que busquemos 200 espacios se debe a que el texto de un elmento/atributo puede incluir un gran número de espacios en un texto grande. Como nuestros registros miden 129, incluso aunque haya un salto de línea dentro del texto, 200 espacios es un margen suficiente:



En este caso tendríamos 129 espacios, menor que 200. Obviamente este número puede ser el que más os convenga.

Una vez que tenemos guardada la información a procesar en nuestra variable WX-REG-XML, viene la parte fácil, el "parseo" propiamente dicho.

Proceso XML:


XML PARSE WX-REG-XML
PROCESSING PROCEDURE PARSEO-XML
ON EXCEPTION
DISPLAY 'ERROR EN PARSEO XML:'XML-CODE
NOT ON EXCEPTION
CONTINUE
END-XML


El procesado se haría en el párrafo PARSEO-XML. En nuestro caso, como queremos diferenciar entre "elemento1" y "elemento2" crearemos dos párrafos diferenciados dentro de PARSEO-XML:

Parseo-XML:


EVALUATE TRUE
WHEN SI-ELEMENTO1
PERFORM PROCESO-XML-ELEMENTO1
WHEN SI-ELEMENTO2
PERFORM PROCESO-XML-ELEMENTO2
END-EVALUATE


Proceso-XML-elemento1:

EVALUATE XML-EVENT
WHEN 'START-OF-ELEMENT'
DISPLAY 'ELEMENTO:'XML-TEXT

WHEN 'CONTENT-CHARACTERS'
DISPLAY 'TEXTO DEL ELEMENTO:'XML-TEXT

WHEN 'END-OF-ELEMENT'
DISPLAY 'FIN DE ELEMENTO:'XML-TEXT
IF XML-TEXT EQUAL 'elemento2'
PERFORM ESCRIBIR-FICHERO1
END-IF

WHEN 'ATTRIBUTE-NAME'
DISPLAY 'ATRIBUTO:'XML-TEXT

IF XML-TEXT EQUAL 'atributo1'
SET SI-ATR1 TO TRUE
END-IF

WHEN 'ATTRIBUTE-CHARACTERS'
DISPLAY 'TEXTO DEL ATRIBUTO:'XML-TEXT

IF SI-ATR1
Informamos el fichero de salida desde XML-TEXT
END-IF

WHEN 'COMMENT'
DISPLAY 'COMENTARIOS:'XML-TEXT

WHEN 'EXCEPTION'
DISPLAY 'EXCEPCION:'XML-CODE

WHEN OTHER
DISPLAY 'ERROR NO CONTROLADO:'XML-EVENT
END-EVALUATE


Proceso-XML-elemento3:

EVALUATE XML-EVENT
WHEN 'START-OF-ELEMENT'
DISPLAY 'ELEMENTO:'XML-TEXT

EVALUATE XML-TEXT
WHEN 'elemento4'
SET SI-ELEM4 TO TRUE
WHEN 'elemento6'
SET SI-ELEM6 TO TRUE
END-EVALUATE

WHEN 'CONTENT-CHARACTERS'
DISPLAY 'TEXTO DEL ELEMENTO:'XML-TEXT

EVALUATE TRUE
WHEN SI-ELEM4
Informamos el fichero de salida desde XML-TEXT
WHEN SI-ELEM6
Informamos el fichero de salida desde XML-TEXT
END-EVALUATE

WHEN 'END-OF-ELEMENT'
DISPLAY 'FIN DE ELEMENTO:'XML-TEXT
IF XML-TEXT EQUAL 'elemento3'
PERFORM ESCRIBIR-FICHERO2
END-IF

WHEN 'ATTRIBUTE-NAME'
DISPLAY 'ATRIBUTO:'XML-TEXT

IF XML-TEXT EQUAL 'atributo2'
SET SI-ATR2 TO TRUE
END-IF

WHEN 'ATTRIBUTE-CHARACTERS'
DISPLAY 'TEXTO DEL ATRIBUTO:'XML-TEXT

IF SI-ATR2
Informamos el fichero de salida desde XML-TEXT
END-IF

WHEN 'COMMENT'
DISPLAY 'COMENTARIOS:'XML-TEXT

WHEN 'EXCEPTION'
DISPLAY 'EXCEPCION:'XML-CODE

WHEN OTHER
DISPLAY 'ERROR NO CONTROLADO:'XML-EVENT
END-EVALUATE


Podéis encontrar un ejemplo muy bueno con todos los posibles eventos en la página de ibm: ejemplo parseo XML.

En este artículo hemos querido centrarnos más en la parte del procesado de los datos de entrada que en el parseo propiamente dicho, pues nos ha parecido la parte más compleja. Pero si alguien quiere que entremos más en detalle del XML PARSE, XML EVENT etc. no tiene más que pedirlo : )

Archivos:
Código del programa de prueba.
Fichero XML original.
Códigos de aviso XML.
Códigos de error XML.

10 comentarios:

Anónimo dijo...

Un crack eres un autentico crack, sin duda no lo hubiesemos logrado sin ti.

Anónimo dijo...

una pregunta acerca de xml parse, no encuentro por ningun lado la descripcion de los codigos de retorno que te devuelve la funcion XML-PARSE, me seria de gran ayuda...gracias
un ejemplo EXCEPTION: {01607698}

Anónimo dijo...

Sos muy grosoo

Tallian dijo...

Los XML-CODE de las excepciones del parseo las podéis encontrar en la web de IBM (hay un enlace en el artículo).
De todas formas voy a subir dos archivos pdf con los códigos. Pondré los links al final del artículo.
Espero que os sirva!

Unknown dijo...

Buenas!
Muchas gracias por la info. Estoy teniendo problemas al parsear cadenas con acentos ya que en el XML-TEXT quedan caracteres raros. Imagino que es problema de la codificación UTF. ¿Se les ocurre alguna manera de solucionarlo?
Un saludo y muchas gracias!

Jose Antonio dijo...

Este comentario ha sido eliminado por el autor.

Jose Antonio dijo...

Finalmente lo solucioné con la siguiente configuración en el XML:
xml version="1.0" encoding="iso-8859-1"

Lo dicho, un saludo y muchas gracias!

Tallian dijo...

A ti Jose Antonio!

SULFATO_ATÓMICO dijo...

Buenas, el archivo XML original no está.
:(
Pueden por favor resubirlo?
Muchas gracias por adelantado!!

Analista COBOL dijo...

Muy buena el soporte XML PARSE