
Tratar XML con JavaScript Core
23 03 2009Recientemente preguntaron en el maillist de javascript en español “ JavaEScript” sobre los ejemplos de W3Schools. El problema es que se quería insertar en una array el title, author, year y price de cada elemento book para posteriormente ordenarlos en una tabla. Al parecer estaban teniendo problema con que al leer el fichero no sabían discriminar los nodos que tiene. Los ejemplos de W3Schools utilizan la implementación DOM que viene en javascript por defecto en (casi) cualquier navegador moderno, el cuál no es precisamente fácil de utilizar y/o entender.
El XML en cuestión es el siguiente:
<bookstore><book category="cooking"> <title lang="en">Everyday Italian</title> <author>Giada De Laurentiis</author> <year>2005</year> <price>30.00</price> </book> <book category="children"> <title lang="en">Harry Potter</title> <author>J K. Rowling</author> <year>2005</year> <price>29.99</price> </book> <book category="web"> <title lang="en">XQuery Kick Start</title> <author>James McGovern</author> <author>Per Bothner</author> <author>Kurt Cagle</author> <author>James Linn</author> <author>Vaidyanathan Nagarajan</author> <year>2003</year> <price>49.99</price> </book> <book category="web" cover="paperback"> <title lang="en">Learning XML</title> <author>Erik T. Ray</author> <year>2003</year> <price>39.95</price> </book> </bookstore>
Modifique el ejemplo de http://www.w3schools.com/dom/tryit.asp?filename=try_dom_list_loop, para utilizar el DOM core sin ninguna librería que facilitara el trabajo y a la vez usar código que entendiera la persona que hizo la pregunta, para hacer lo que se pedía:
<html>
<head>
<script type="text/javascript" src="loadxmldoc.js"></script>
</head>
<body>
<script type="text/javascript">
var arrayExterior = new Array();
xmlDoc=loadXMLDoc("books.xml");
x=xmlDoc.getElementsByTagName("book");
for (i=0;i<x.length;i++)
{
var arrayInterior = new Array();
for (j=0;j<x[i].childNodes.length;j++)
{
if (x[i].childNodes[j].nodeType==1)
{
var textValue = x[i].childNodes[j].childNodes[0].nodeValue;
arrayInterior.push(textValue);
}
}
arrayExterior.push(arrayInterior);
}
document.write("Total elementos: "+arrayExterior.length+"<br/>")
for (i=0; i<arrayExterior.length; i++) {
document.write("book "+i+"<br/>");
for (j=0; j<arrayInterior.length; j++) {
document.write(arrayExterior[i][j]+"<br/>");
}
document.write("<hr/>");
}
</script>
</body>
</html>
El punto a destacar es que los nodos como book tienen un nodo implícito tipo text “invisible”, es decir, entre book y title existe un nodo text que no tienen ningún valor, algo similar a esto :
<book category="children"> <text-node/> <title lang="en">Harry Potter</title>
por eso lo mas fácil es un bucle que recorra los childNodes y preguntar el nodeType (1=Element, 3=Text, la lista completa aqui: http://www.w3schools.com/Dom/dom_nodetype.asp).
Comentarios : Deja un Comentario »
Etiquetas: xslt dom
Categorías : JavaScript, XML, XSLT
Benchmarks de lxml, urllib2 y pycurl
21 03 2009Llevo como mes y medio jugando con Python y la verdad me gusta. Para los que no estan famirializados con Python pues basta mencionar que urllib2 es una libreria para recuperar el contenido de una pagina Web. BeautifulSoup es un parser HTML/XML para lo que se conoce como screen-scraping, es decir analizar un contenido (en este caso texto) palabra por palabra para extraer la informacion que nos interesa. lxml viene siendo como urllib2 y BeautifulSoup en una sola libreria excepto que lxml nos permite utilizar XSLT y Selectores CSS para realizar el scraping. PyCurl en cambio, es una interfaz (o binding) de Python a la libreria de C libcurl. Ya que hablamos de bindings vale la pena mencionar,para entender el porque de este benchmark, que lxml es, a su vez, binding de libxml2 y libxslt. Los bindings son una interfaz, es decir, el modo de acceder a los servicios a librerias de C, generalmente, se trata de librerias del sistema operativo por lo que en teoria se supone que son mucho más rapidas quelas librerias escritas en python puro (o en java o ruby o cualquier otro lenguaje distinto a C/C++).
En resumen, urllib2 es libreria de python puro para realizar peticiones (requests) HTTP, lxml es binding de C para tratar XML/HTML (pero no para tratar requests HTTP), PyCurl es un binding de C para hacer peticiones HTTP (pero no para parsear el resultado). La pregunta entonces es, ¿cual combinacion ofrece el mejor rendimiento (performance)?. La pregunta me ha surgido mientras realizaba un ejercicio del libro Programming Collective Intelligence de Toby Seagaran el cual recomiendo (enlace enGoogle Books), por lo que el codigo original que utilizo es codigo del libro que adapte a los diferebtes casos de prueba.
Los benchmarks son: urllib2 con BeautifulSoup, solucion pura de lxml y PyCurl con lxml para parsear la pagina. Hay quemencionar que una de las grandes ventajas de PyCurl es utilizar su derivado CurlMulti ya que este permite realizar varias conexiones concurrentes lo cual es mucho más eficiente para este benchmark, sin embargo, el benchmark utiliza su modo normal el cual es secuencial ya que el propósito del benchmark es ver cual libreria es más rápida recuperando una pagina Web, extrayendo sus enlaces (<a>) y luego recuperando cada enlace para repetir el proceso, en escencia: un crawler/spider.
Primero, estos son los imports que necesitaremos:
import urllib2
from BeautifulSoup import *
from urlparse import urljoin
import pycurl
from lxml import etree, html
from StringIO import StringIO
import sys
urllib2, urlparse, StringIO y, por supuesto, sys son parte del core de Python (yo utilizo la version 2.5 en Ubuntu). las demas librerias se pueden obtener todas con apt-get (tambien cuentan con un instalador para Windows en sus respectivas paginas Web). Ahora definimos la primera función, la cual utiliza urllib2 + BeautifulSoup:
def crawl(pages, depth):
'''A partir de una lista de páginas, hacemos un crawling completo
hasta la profundidad dada'''
for i in range(depth):
newpages=set()
for page in pages:
try:
c=urllib2.urlopen(page)
except:
print 'Could not open %s' % page
continue
soup=BeautifulSoup(c.read())
links=soup('a')
print 'found %d links on %s' % (len(links), page)
for link in links:
if 'href' in dict(link.attrs):
url=urljoin(page, link['href'])
if url.find("'")!=-1:
print "' found on %s" % url
continue
url=url.split('#')[0] # remove location part
if url[0:4]=='http':
newpages.add(url)
pages=newpages
Esta función es, para efectos prácticos, la misma que encontramos es en libro. En resumen, la primera parte obtiene la pagina con urllib2, luego se parsea con BeautifulSoup y se extraen sus links. Luego se itera cada link obteniendo el ‘href‘ de cada uno, se utiliza urljoin para convertir los links relativos en absolutos, si el link apunta hacia un anchor (contiene #), se extrae la parte del anchor. Por ultimo se añade la URL obtenida al set newpages y asignamos el valor de newpages a pages de modo que la siguiente iteracion utilice las URLS que acabamos de encontrar. Ahora la versión lxml pura:
def lxmlCrawl(pages, depth):
for i in range(depth):
newpages=set()
for page in pages:
try:
doc = html.parse(page).getroot()
doc.make_links_absolute()
except:
print 'Could not open %s' % page
continue
links=doc.cssselect('a')
print 'found %d links on %s' % (len(links), page)
for link in links:
if 'href' in dict(link.attrib):
url = link.attrib['href']
if url.find("'")!=-1:
print "' found on %s" % url
continue
url=url.split('#')[0] # remove location part
if url[0:4]=='http':
newpages.add(url)
pages=newpages
esta versión es muy similar a la anterior excepto que (1) la peticion y el parseo se hace en un solo paso:
doc = html.parse(page).getroot()
(2) lxml nos da un metodo para convertir los links relativos a absolutos:
doc.make_links_absolute()
y (3) utilizamos un selector de CSS (‘a’) para obtener los links. Ahora la versión PyCurl (modo sencillo) + lxml para parsear el resultado:
def pycurlCrawl(pages, depth):
print depth
if depth>0:
queue=[]
newpages=set()
for page in pages:
try:
c = pycurl.Curl()
parser = PyCurlParser(page);
queue.append(parser)
c.setopt(c.URL, page)
c.setopt(c.WRITEFUNCTION, parser.curl_body_callback)
c.perform()
except:
print 'Could not open %s' % page
continue
c.close
for p in queue:
newpages.update(p.parseHTML())
print newpages
pycurlCrawl(newpages,depth-1)
class PyCurlParser:
def __init__(self, url):
self.url = url
self.contents = ''
self.newpages = set()
def curl_body_callback(self, buf):
self.contents = self.contents + buf
def parseHTML(self):
parser = etree.HTMLParser()
tree = etree.parse(StringIO(self.contents), parser)
links = tree.getroot().xpath('//a')
print 'found %d links on %s' % (len(links), self.url)
for link in links:
if 'href' in dict(link.attrib):
url=urljoin(self.url, link.attrib['href'])
print url
url=url.split('#')[0] # remove location part
if url[0:4]=='http':
self.newpages.add(url)
print 'added url: %s' % url
return self.newpages
esta versión es la mas larga y compleja y no es en vano, libcurl es una libreria muy potente, es asíncrona y no es precisamente sencilla, por lo que su binding hereda esa complejidad. Se compone de 2 partes principales: (1) la funcion pycurlCrawl y (2) la clase PyCurlParser.
pycurlCrawl se encarga de realizar las peticiones a cada página asignandole una instancia de PyCurlParser a cada peticion de curl, por último cada instancia es encolada (se agrega a queue). La cola y la clase PyCurlParser son necesarias porque PyCurl al ser asíncrona utiliza callbacks, pero cada peticion tarda distinto tiempo, es decir, algunas peticiones reciben archivos HTML de mayor tamaño que otras y algunas puede incluso que fallen (pero no fallan de inmediato, primero tiene que vencerse el TIMEOUT del request). Esto crea un problema de sincronización ya que la sentencia
c.setopt(c.WRITEFUNCTION, curl_body_callback)
dice a Curl que escriba el resultado de la peticion (es decir, el contenido de la página) a la funcion curl_body_callback la cual simplemente asigna el contenido de la página a la variable contents. Es importante distinguir que contents se llena con un buffer buf que es lo que envia Curl con su funcion c.WRITEFUNCTION. Esto se dá porque si la página es muy grande o la conexión muy lenta, necesitaremos varios chunks (buffers) para llenar todo el contenido de la página. El resultado de todo esto es que necesito asegurarme que cada peticion tenga su propia variable contents para que el bucle de pycurlCrawl siga enviando peticiones mientras cada instancia de PyCurlParser espera que su callback finalice. Como no sé cuanto pueda tardar, la forma más fácil (a mi modo de ver) es encolar todas las instancias de PyCurlParser para que, una vez finalizadas todas las peticiones, proceder a parsear la página y extraer sus enlaces (links), esto se hace con la función PyCurlParser.parseHTML. A diferencia de la version de lxml pura, aqui etree.HTMLParser() para parsear la página a partir de la variable tipo string contents para lo que hace falta StringIO que básicamente es convertir un string en un stream. Tambien, a diferencia de la versión de lxml puro, en esta versión utilizo una expresión XPath (‘//a’) en lugar de un selector CSS para obtener todos los enlaces de cada página. La última diferencia importante es que pycurlCrawl, a diferencia de sus versions para urllib2 y lxml puro, no usa un bucle for para controlar la profundidad del crawler sino que lo hace a traves de recursión
pycurlCrawl(newpages,depth-1)
esto tambien es una consecuencia del funcionamiento asíncrono de PyCurl.
Por último necesitamos una función que se encargará de medir el rendimiento de cada función. Para ello utilizamos la libreria de Python, timeit:
if __name__=='__main__':
from timeit import Timer
iter = int(sys.argv[1]) if len(sys.argv)==2 else 1
ul = Timer("crawl(['http://kiwitobes.com/wiki/Pirates_of_Silicon_Valley.html'],2)",\
"from __main__ import crawl")
u = ul.timeit(iter)
lx = Timer("lxmlCrawl(['http://kiwitobes.com/wiki/Pirates_of_Silicon_Valley.html'],2)",\
"from __main__ import lxmlCrawl")
x = lx.timeit(iter)
pc = Timer("pycurlCrawl(['http://kiwitobes.com/wiki/Pirates_of_Silicon_Valley.html'],2)",\
"from __main__ import pycurlCrawl")
p = lx.timeit(iter)
print "Completed in: urlib: %f lxml: %f pycurl: %f" % (u,x,p)
Para finalizar solo queda mencionar que la URL http://kiwitobes.com/wiki/ pertenece al blog de Toby Seagaran y es basicamente una copia estática (o sea que nadie la edita) de Wikipedia. Muchos enlaces en la url que utilizo de prueba fallaran, pero esto me da una aproximación más realista a lo que encontraria un crawler en condiciones reales. Para realizar el benchmark, se pone todo junto en un mismo archivo (compare.py) y desde linea de comando ejecutas:
python compare.py [iteraciones]
El argumento iteraciones es opcional, debe ser un numero entero positvo (si se omite se utiliza iteraciones=1) y es el numero de veces que se ejecutará cada función antes imprimir los resultados. La razón para hacer esto es que al ejecutarlo únicamente 1 vez el resultado de cada ejecución variaba demasiado, es decir, una vez crawl era la más rápida, a la siguiente era lxmlCrawl la más veloz, etc. por lo que decidí usar varias iteraciones y luego obtener un promedio sencillo de forma que los resultados sean estadisticamente más confiables.
Al ejecutarlo con 10 iteraciones obtuve el siguiente resultado:
Completed in: urlib: 991.923360 lxml: 742.943314 pycurl: 751.931897
es decir:
urllib: 99.192sec. lxml: 74.294sec.y pycurl: 75.193sec.
en promedio por cada ejecución. Se aceptan sugerencia de mejora de código y si alguien obtiene resultados distintos agradeceria que los ponga en los comentarios.
Comentarios : Deja un Comentario »
Etiquetas: callbacks, crawler, lxml, pycurl, Python, scree-scraping, spider, urllib2
Categorías : Programacion
Más sobre Musictracker
18 10 2008Descubri un bug muy curioso. Resulta que el player que uso actualmente es Amarok que en principio va enfocado a KDE. Como yo uso Xubuntu (basado en XFCE) queria evitar usarlo y probe varios otros (Exaille, gMusicBrowser, Quod Libet, Rythmbox, BPMx, etc.) pero ninguno tenia el grado de estabilidad que deseaba. Banshee se acercó, pero un dia simplemente dejo de funcionar asi que de vuelta a Amarok que la verdad sigue siendo el más completo y estable.
Volviendo al bug, resulta que una vez configurado Musictracker necesitas que antes de iniciar Pidgin hayas abierto Amarok (o el player que se este usando) de lo contrario Pidgin pedirá su status a Amarok y al no encontralo se queda colgado….
La solución, y creo que sobra decirlo, es abrir el player antes que Pidgin.
Comentarios : Deja un Comentario »
Etiquetas: apps, Linux, pidgin. musictracker
Categorías : Linux
Pidgin – Musictracker
16 10 2008El otro día vi en un foro gente buscando un player de linux que tuviera plugin para actualizar el estado en Pidgin según las canciones que se escucha en el player. Amarok, entre otros tiene un plugin que hace esto, pero en estos casos es mejor aplicar el Principio Hollywood:
No nos llames que ya te llamaremos nosotros
. Es decir, mejor utilizar Musictracker, un plugin de Pidgin que hace exactamente lo mismo y funciona con varios players incluyendo: Amarok, Rhythmbox, Audacious, XMMS, MPC/MPD, Exaile, Banshee y Quod Libet.
apt-get install pidgin-musictracker
ó sino usas Debian (o alguna variacion de Ubuntu) bajalo de su web: Musictracker.
Comentarios : Deja un Comentario »
Etiquetas: pidgin apps musictracker
Categorías : Linux
Uno de los mejores Monitores de Sistema
16 10 2008Conky es un simple monitor de sistema para X… bueno, ’simple’ lo que se dice simple no tanto si tomamos encuenta lo altamente configurable que es.
Personalmente no me gusta la configuracion que trae por defecto asi que aqui dejo un par de retoques que pintan el monitor directamente en el desktop. Funciona bien contra fondos oscuros pero sino, es fácil de configurar:
alignment top_right
background no
border_width 1
cpu_avg_samples 2
default_color white
default_outline_color white
default_shade_color white
draw_borders no
draw_graph_borders yes
draw_outline no
draw_shades no
font 10x20
gap_x 5
gap_y 60
minimum_size 5 5
net_avg_samples 2
no_buffers yes
out_to_console no
own_window no
own_window_class Conky
own_window_type desktop
own_window_transparent true
stippled_borders 0
update_interval 3.0
uppercase no
use_spacer none
double_buffer yesy
TEXT
$nodename - $sysname $kernel on $machine
$hr
${color grey}Uptime:$color $uptime
${color grey}Frequency (in MHz):$color $freq
${color grey}Frequency (in GHz):$color $freq_g
${color grey}RAM Usage:$color $mem/$memmax - $memperc% ${membar 4}
${color grey}Swap Usage:$color $swap/$swapmax - $swapperc% ${swapbar 4}
${color grey}CPU Usage:$color $cpu% ${cpubar 4}
${color grey}Processes:$color $processes ${color grey}Running:$color $running_processes
$hr
${color grey}File systems:
/ $color${fs_free /}/${fs_size /} ${fs_bar 6 /}
${color grey}Networking:
Up:$color ${upspeed eth0} k/s${color grey} - Down:$color ${downspeed eth0} k/s
$hr
${color grey}Name PID CPU% MEM%
${color lightgrey} ${top name 1} ${top pid 1} ${top cpu 1} ${top mem 1}
${color lightgrey} ${top name 2} ${top pid 2} ${top cpu 2} ${top mem 2}
${color lightgrey} ${top name 3} ${top pid 3} ${top cpu 3} ${top mem 3}
${color lightgrey} ${top name 4} ${top pid 4} ${top cpu 4} ${top mem 4}
se guarda como~/.conkyrc y luego puedes ejecutar conky -d. La configuracion es una pequeña modificación de la que viene por defecto. Tambien se puede hacer tail de algun log o ejecutar comandos con exec. Aqui se puede ver la lista completa de variables y de settings de configuración.
Comentarios : Deja un Comentario »
Etiquetas: apps, gestion, Linux, sistemas
Categorías : Linux
Drivel
16 10 2008Probando Drivel Journal Editor desde Xubuntu 8.0.4. Recordatorio: la url de wordpress debe terminar con /xmlrpc.php
Otra cosa importante es que el type es “Movable Type”.
Comentarios : Deja un Comentario »
Categorías : Blogroll
Problemas con eclipse
13 06 2008Hoy inicio eclipse y sorpresa: JVM terminated. Exit code=-1
Investigando un poco encontre una descripción y solución del problema. La solución es simplemente limpiar el archivo eclipse.ini, iniciar eclipse y luego modificar el eclipse.ini a como se tenia antes. Esto me funcionó, lo que no cuadra es que el caso en el citado blog y en otros describiendo similares implican que antes se instaló algun programa que tambien use Java o basado en eclipse (ya conocia yo bien los problemas de intalar WebSphere o algunas aplicaciones de Oracle que agregan su JRE propio al path lo que causa que eclipse intente iniciar con un JRE distinto al que deberia), pero el caso es que desde hace varias semanas no habia instalado nada nuevo ni en eclipse ni en el ordenador asi que, sí problema resuelto, ¿pero porque fallo?
La unica pista que tengo al respecto es que el dia anterior habia estado usando Aptana y Flex3 Builder, aún así, los llevo utilizando desde hace ya varias semanas y nunca me han dado problemas. En todo caso parece que haber usado primero Aptana luego Flex Builder y por ultimo hoy Eclipse desconfiguró el eclipse.ini de eclipse.
Comentarios : 2 Comentarios »
Etiquetas: eclipse ide java
Categorías : Java
Jamendo
23 05 2008Se trata de musica bajo licencia Creative Commons, puedes usarla como quieras siempre que reconozcas la autoria del grupo. Tambien es una buena forma de encontrar nueva música y una herramienta útil para promocionar tu propia música.
Me encontre esto bajo jazz experimental y me gustó mucho:
Revolution Void
GeoSuPhat
Comentarios : 1 comentario
Etiquetas: creative commons, jamendo, open source
Categorías : Musica