sábado, 22 de octubre de 2016

XPath y su aplicación para extracción de datos


XPath es una sintaxis que se usa para recorrer los elementos de un documento XML
Mediante estos comandos podemos obtener cualquier nodo del árbol de elementos.
SINTAXIS
Para trastear con ejemplos, usaremos esta página
http://www.freeformatter.com/xpath-tester.html
e introduciremos el siguiente XML:
<libro>
    <titulo>Libro 1</titulo>
    <autor>Autor 1</autor>
    <capitulo num="1">
       Capitulo1
      <parrafo>
        Texto 1.1
      </parrafo>
      <parrafo destacar="si">
         Texto 1.2
         <enlace href="enlace">Enlace 1</enlace> 
         Texto 1.3
      </parrafo>
      </capitulo>
    <capitulo num="2" public="si" >
      Capitulo 2
        <parrafo>
         Texto 2.1
      </parrafo>
        <parrafo>
         Texto 2.2
         <enlace href="enlace">Enlace 2</enlace> 
         Texto 2.3
      </parrafo>
  
    </capitulo>
    <apendice num="a" public="si">
      Apéndice 1
        <parrafo>
         Texto 3.1
      </parrafo>
      <parrafo>
         Texto 3.2
         <enlace href="enlace">Enlace 3</enlace> 
         Texto 3.3
      </parrafo>
    </apendice>
  </libro>

SintaxisSignificadoResultado



/libro
Seleccionaremos el elemento raíz de la estructura de elementos o nodos, y todos los nodos que cuelgan de ella<libro>
  <titulo>Libro 1</titulo>
  <autor>Autor 1</autor>
  <capitulo num="1">
    Capitulo1
    <parrafo>Texto 1.1</parrafo>
    <parrafo destacar="si">
/libro/capituloDevuelve todos los capítulos que cuelgan de Libro<capitulo num="1">
  Capitulo1
  <parrafo>Texto 1.1</parrafo>
  <parrafo destacar="si">
   ...
<capitulo num="2" public="si">
  Capitulo 2
  <parrafo>Texto 2.1</parrafo>
 
/libro/capitulo[2]Devuelve el 2º capítulo de Libro<capitulo num="2" public="si">
  Capitulo 2
  <parrafo>Texto 2.1</parrafo>
  <parrafo>
    Texto 2.2
    <enlace href="enlace">Enlace 2</enlace>
    Texto 2.3
  </parrafo>
</capitulo>
//parrafoDevuelve todos los párrafos, estén donde estén en el documento.<parrafo>Texto 1.1</parrafo>
<parrafo destacar="si">
  Texto 1.2
  <enlace href="enlace">Enlace 1</enlace>
  Texto 1.3
</parrafo>
<parrafo>Texto 2.1</parrafo>
<parrafo>
  Texto 2.2
  <enlace href="enlace">Enlace 2</enlace>
  Texto 2.3
</parrafo>
<parrafo>Texto 3.1</parrafo>
<parrafo>
  Texto 3.2
  <enlace href="enlace">Enlace 3</enlace>
  Texto 3.3
</parrafo>
//parrafo/@destacar Para un elemento párrafo que contenga el atributo “destacar”, devuelve el valor de el atributro “Destacardestacar="si"
//parrafo[@destacar]Devuelve el párrafo que contenga un atributo “destacar”, da igual su valor<parrafo destacar="si">
  Texto 1.2
  <enlace href="enlace">Enlace 1</enlace>
  Texto 1.3
</parrafo>
//parrafo[@destacar=”si”]Devuelve el párrafo que contenga un atributo “destacar” con valor “si”<parrafo destacar="si">
  Texto 1.2
  <enlace href="enlace">Enlace 1</enlace>
  Texto 1.3
</parrafo>
//capitulo[@num>=2] Devuelve el capítulo cuyo numero sea mayor o igual que 2<capitulo num="2" public="si">
  Capitulo 2
  <parrafo>Texto 2.1</parrafo>
  <parrafo>
    Texto 2.2
    <enlace href="enlace">Enlace 2</enlace>
    Texto 2.3
  </parrafo>
</capitulo>
//capitulo//enlaceDevuelve todos los enlaces que cuelguen de capítulos<enlace href="enlace">Enlace 1</enlace>
<enlace href="enlace">Enlace 2</enlace>
//capitulo[last()]Devuelve el útlimo capítulo<capitulo num="2" public="si">
  Capitulo 2
  <parrafo>Texto 2.1</parrafo>
  <parrafo>
    Texto 2.2
    <enlace href="enlace">Enlace 2</enlace>
    Texto 2.3
  </parrafo>
</capitulo>
//capitulo[2]/text()   Devuelve el texto que hay en el capítulo 2Capitulo 2
//capitulo[position()>1]  Devuelve los capítulos a partir de la posición 2<capitulo num="2" public="si">
  Capitulo 2
  <parrafo>Texto 2.1</parrafo>
  <parrafo>
    Texto 2.2
    <enlace href="enlace">Enlace 2</enlace>
    Texto 2.3
  </parrafo>
</capitulo>
//enlace[contains(text(),'Enlace 2')]
/ancestor::capitulo
Devuelve el capítulo donde haya un enlace con valor "Enlace 3"<capitulo num="2" public="si">
  Capitulo 2
  <parrafo>Texto 2.1</parrafo>
  <parrafo>
    Texto 2.2
    <enlace href="enlace">Enlace 2</enlace>
    Texto 2.3
  </parrafo>
</capitulo>
//parrafo[contains(text(),"Texto 1.1")]
/ancestor::capitulo
/following-sibling::capitulo
//enlace/text()
(leyendo de derecha a izquierda)
Devuelveme el texto del enlace del capítulo siguiente al capítulo donde haya un parrafo que contenga "Texto 1.1"
(leyendo de izquierda a derecha)
Localízame a un parrafo cuyo texto contiene "Texto 1.1", navega cualquier antepasado (padre, abuelo, etc) que sea de tipo capítulo, vete al siguiente capítulo y me devuelves el texto de todos los enlaces
Enlace 2


ENLACES
Podéis ampliar más información en:
 
- http://www.w3.org/TR/xpath   Especificación del XPath
- http://www.w3schools.com/xml/xpath_intro.asp
- https://msdn.microsoft.com/en-us/library/ms256471(v=vs.110).aspx
- http://saxon.sourceforge.net/saxon6.5.3/expressions.html
 
 Tipos de descarga de html
El HTML, al estar basado en XML, también se le puede aplicar XPath.
Antes de meternos en materia, voy a explicar un poco cómo nos llegan los datos de las páginas web.

Por HTML:
El navegador envía una petición al servidor, y en la respuesta, se devuelve HTML con los datos que queremos sacar. Este método no representa problema alguno y lo podemos atacar con XPath.

 
Por JavaScript:
El navegador envía una petición al servidor, y el servidor devuelve una plantilla HTML con JavaScript, y el navegador al interpretarlo, llama a unas apis que devuelven los datos, y rellenan la plantilla con los datos traídos en la segunda parte. (p.e, morningstar, finance.yahoo.com).
Este método se está poniendo de moda ya que hace algo más complicado extraer los datos, y por otra parte usa tecnologías más recientes (Ajax, Angular) que hace que la página se cargue sin postbacks.

 
Si estamos intentando traernos HTML por métodos que no tengan navegador (tipo funciones GSheets ImportXML, ImportHTML, HttpWebRequest en .Net ) nos traerá la plantilla HTML y los scripts JavaScript, pero estas funciones no procesan el JavaScript, por lo que no podemos obtener datos mediante este método.
 
Para extraer datos de este tipo de página, hay que analizar cómo se descarga los datos (con Fiddler se puede ver las llamadas que hace la web), interpretando su API, etc.
En el caso de Morningstar.com, podríamos sacar la información mediante el botón de exportar CSV






También en esta dirección hay más información de algunas llamadas a la API de Morningstar

 
Ejemplo de construcción y uso de XPath
 
Vamos a extraer información financiera de las páginas de InvertirEnBolsa.info para rellenar un Google Spreadsheet.

Recordamos que GSheets posee funciones financieras propias para sacar cotizaciones, volúmenes, PER, BPA, etc)

Para crear la ruta XPath de un elemento, por ejemplo el EBITDA del 2015 de Coca-Cola:
Abrimos con chrome la página de CocaCola en IeB
http://www.invertirenbolsa.info/historicodividendos/empresa/coca-cola
Pulsamos F12 en chrome para que se nos abran las tools developer.

Con la pestaña "elements" seleccionada(1), pulsamos "seleccionar elemento"(2) y seleccionamos el elemento que queremos obtener(3), y así puedes ver el elemento HTML que queremos seleccionar(4).




En este caso, vemos que con buscar el siguiente elemento TD al TD que su texto sea "EBITDA". Fácil, no? 

El Xpath sería:
Localízame un elemento TD cuyo texto sea 'EBITDA', te vas al 1er siguiente hermano.
//td[text()='EBITDA']/following-sibling::td[1]

La fórmula en el GSheets a usar sería:
=IMPORTXML("http://www.invertirenbolsa.info/historicodividendos/empresa/coca-cola"; "//td[text()='EBITDA']/following-sibling::td[1]")

El resultado es

1,58%
10.698,00
 

 
Este resultado devuelve 2 datos ya que nos está cogiendo el % ebitda que hay en otra tabla. 

Para centrar bien el resultado, tenemos que afinar más la búsqueda, añadiendo la tabla a la que pertenece el elemento.

Para ello, subimos hacia arriba para ver la tabla de la que cuelga el TD, y vemos que es descendiente de un table cuya class contiene "cuarta tabla".


El XPath correcto sería:
Localízame una tabla cuyo atributo class contiene 'cuarta-tabla', y dentro de sus elemento, busca un elemento TD cuyo texto sea 'EBITDA', y me devuelves el 1er siguiente hermano.

//table[contains(@class,'cuarta-tabla')]//td[text()='EBITDA']/following-sibling::td[1]

La fórmula en el GSheets a usar sería:
=IMPORTXML("http://www.invertirenbolsa.info/historicodividendos/empresa/coca-cola"; "//table[contains(@class,'cuarta-tabla')]//td[text()='EBITDA']/following-sibling::td[1]")

El resultado es
 10.698,00

Esta es una forma de componer una expresión XPath mirando directamente el HTML. Tienes más control al comprender de dónde estás cogiendo el dato, incluso modificar el xpath dinámicamente de teniendo en cuenta el año, el ticker, etc.

Otra alternativa es, al seleccionar el elemento a rescatar, con botón derecho, copiar Xpath.


Esta forma no me acaba de convencer, ya que en el momento que cambien el orden o metan algún campo más, es mayor el riesgo de que no muestre el resultado correcto.

El resultado mediante este método es
//*[@id="gkMainbody"]/div/div[3]/div/div[2]/div[3]/table/tbody/tr[7]/td[2]

También hay plugins de chrome para generar automáticamente la ruta XPath, como XPath Helper

Espero que os sea de utilidad.

Saludos.