¿Por qué este xpath falla al usar lxml en python?

Aquí hay un ejemplo de una página web de la que estoy tratando de obtener datos. http://www.makospearguns.com/product-p/mcffgb.htm

La xpath se tomó de las herramientas de desarrollo de Chrome, y Firepath en Firefox también puede encontrarla, pero al usar lxml solo devuelve una lista vacía para “texto”.

from lxml import html import requests site_url = 'http://www.makospearguns.com/product-p/mcffgb.htm' xpath = '//*[@id="v65-product-parent"]/tbody/tr[2]/td[2]/table[1]/tbody/tr/td/table/tbody/tr[2]/td[2]/table/tbody/tr[1]/td[1]/div/table/tbody/tr/td/font/div/b/span/text()' page = requests.get(site_url) tree = html.fromstring(page.text) text = tree.xpath(xpath) 

Imprimiendo el texto del árbol con

 print(tree.text_content().encode('utf-8')) 

muestra que los datos están allí, pero parece que xpath no está funcionando para encontrarlos. ¿Hay algo que este olvidando? La mayoría de los otros sitios que he probado funcionan bien usando lxml y xpath tomado de las herramientas de desarrollo de Chrome, pero algunos que he encontrado dan listas vacías.

1. Los navegadores cambian frecuentemente el HTML

Los navegadores cambian con bastante frecuencia el HTML que se le entrega para que sea “válido”. Por ejemplo, si sirve un navegador este HTML no válido:

 

bad paragraph

Note that cells and rows can be unclosed (and valid) in HTML

Para representarlo, el navegador es útil e intenta que sea un HTML válido y puede convertirlo en:

 

bad paragraph

Note that cells and rows can be unclosed (and valid) in HTML

Lo anterior se modifica porque los párrafos

no pueden estar dentro de

y se recomiendan

s. Los cambios que se aplican a la fuente pueden variar enormemente según el navegador. Algunos colocarán elementos no válidos antes de las tablas, otros después, algunas dentro de las celdas, etc.

2. Los Xpaths no son fijos, son flexibles al apuntar a los elementos.

Usando este HTML ‘arreglado’:

 

bad paragraph

Note that cells and rows can be unclosed (and valid) in HTML

Si intentamos apuntar al texto de la celda

, todo lo siguiente le dará aproximadamente la información correcta:

 //td //tr/td //tbody/tr/td /table/tbody/tr/td /table//*/text() 

Y la lista continúa…

sin embargo, en general, el navegador le dará la XPath más precisa (y menos flexible) que enumera todos los elementos del DOM. En este caso:

 /table[0]/tbody[0]/tr[0]/td[0]/text() 

3. Conclusión: los Xpaths dados por el navegador usualmente son inútiles

Esta es la razón por la que los XPaths producidos por las herramientas de desarrollo con frecuencia le darán el Xpath incorrecto cuando intente utilizar el HTML sin formato.

La solución, siempre se refiere al HTML en bruto y utiliza un XPath flexible pero preciso.

Examine el HTML real que tiene el precio:

 
Price: $149.95

¡Si quieres el precio, en realidad solo hay un lugar para mirar!

 //span[@itemprop="price"]/text() 

Y esto volverá:

 $149.95 

El xpath es simplemente incorrecto

Aquí está un fragmento de la página:

 

  Home >

Se puede ver que el elemento con id es "v65-product-parent" is of type tabla and has subelement tr`.

Solo puede haber un elemento con dicha id (de lo contrario, se rompería el xml).

El xpath está esperando tbody como hijo de un elemento dado (tabla) y no hay ninguno en toda la página.

Esto puede ser probado por

 >>> "tbody" in page.text False 

¿Cómo llegó Chrome a ese XPath?

Si simplemente descarga esta página por

 $ wget http://www.makospearguns.com/product-p/mcffgb.htm 

y revisar el contenido del mismo, no contiene un solo elemento llamado tbody

Pero si usa Chrome Developer Tools, encontrará algunas.

¿Cómo viene aquí?

Esto sucede a menudo, si JavaScript entra en juego y genera algo de contenido de la página cuando está en el navegador. Pero como señaló LegoStormtroopr, este no es nuestro caso y esta vez es el navegador, que modifica el documento para corregirlo.

¿Cómo obtener contenido de la página modificada dinámicamente dentro del navegador?

Tienes que darle una oportunidad a algún tipo de navegador. Por ejemplo, si usas selenium , lo obtendrías.

byselenium.py

 from selenium import webdriver from lxml import html url = "http://www.makospearguns.com/product-p/mcffgb.htm" xpath = '//*[@id="v65-product-parent"]/tbody/tr[2]/td[2]/table[1]/tbody/tr/td/table/tbody/tr[2]/td[2]/table/tbody/tr[1]/td[1]/div/table/tbody/tr/td/font/div/b/span/text()' browser = webdriver.Firefox() browser.get(url) html_source = browser.page_source print "test tbody", "tbody" in html_source tree = html.fromstring(html_source) text = tree.xpath(xpath) print text 

que imprime

 $ python byselenimum.py test tbody True ['$149.95'] 

Conclusiones

El selenium es excelente cuando se trata de cambios dentro del navegador. Sin embargo, es una herramienta un poco pesada y si puedes hacerlo de una manera más simple, hazlo de esa manera. Lego Stormrtoopr ha propuesto una solución más simple que funciona en una página web simplemente recuperada.

Tuve un problema similar (Chrome insertando elementos del cuerpo cuando haces Copiar como XPath). Como respondieron otros, debe mirar la fuente de la página, aunque el XPath proporcionado por el navegador es un buen lugar para comenzar. He encontrado que a menudo, eliminando las tags tbody lo arregla, y para probar esto escribí un pequeño script de Python para probar XPaths:

 #!/usr/bin/env python import sys, requests from lxml import html if (len(sys.argv) < 3): print 'Usage: ' + sys.argv[0] + ' url xpath' sys.exit(1) else: url = sys.argv[1] xp = sys.argv[2] page = requests.get(url) tree = html.fromstring(page.text) nodes = tree.xpath(xp) if (len(nodes) == 0): print 'XPath did not match any nodes' else: # tree.xpath(xp) produces a list, so always just take first item print (nodes[0]).text_content().encode('ascii', 'ignore') 

(Eso es Python 2.7, en caso de que la función "imprimir" no funcionara).