Concurrencia (principalmente) en Python

2 01 2010

Este post y los que le sigan sobre concurrencia tendran un enfoque práctico, así que para entender varios temas sobre concurrencia en general aunque más específicos a Python, recomiendo leerse el articulo Python Threads and the Global Interpreter Lock. Para una introducción a threads (hilos) en Python también recomiendo Basic Threading in Python .

Este primer post tratará un ejemplo básico de Consumer/Producer utilizando Threads, en otros posts utilizaremos stackless, concurrenceconcurrence y finalmente compararemos con Erlang.

Queue

El modulo Queue implementa una cola multi-productora y multi-consumidora y es una forma sencilla y eficiente de implementar comunicación entre threads. Proporciona 3 tipos de colas Queue (FIFO), LifoQueue (LIFO) y PriorityQueue (por prioridad). Lo que haremos es implementar una cola FIFO (First In First Out), un consumidor que reciba mensajes de la cola, y una clase que produzca los mensajes. Los mensajes en este caso serán objetos tipo threads que imprimen un mensaje por pantalla. Como lo que queremos mostrar es que estos se ejecutan en paralelo, en lugar de imprimirlos en el orden que se introducen a la cola, cada thread tendra un timeout diferente y semi-aleatorio. El codigo es este:

## printer.py

import threading
import time
import Queue
import random

# Cola tipo FIFO
pool = Queue.Queue()

# Objeto que imprime un mensaje despues de 'secs' segundos
# corre en su propio thread.
class Printer(threading.Thread):
    def __init__(self, id, secs):
        self.id = id
        self.secs = secs
        threading.Thread.__init__ ( self )
        
    def run(self):
        time.sleep(self.secs)
        print self.id

# Escucha mensajes que entran en la cola y los ejecuta
class Consumer(threading.Thread):    
    def run(self):
        while True:
            # Obtenemos un cliente de la cola
            client = pool.get()

            # Nos aseguramos que haya un verdadero cliente en la
	    # variable 'client'.
            if client == "exit":
                    sys.exit(1) # Si el mensaje en la cola es el string "exit" terminamos la ejecución
                else:
                    client.start() # Ejecutamos el thread

# Producer que mete mensajes en la cola.
# Los mensajes son threads tipo "Printer"
def thread_factory():
    duration = [1,12,5,8,15]
    i = 0

    # Creamos y ejecutamos el thread consumidor
    consumer = Consumer()
    consumer.start()

    # Producimos 10 mensajes y los metemos en la cola
    while < 10:
        pool.put(Printer("Printer %d" % i, random.choice(duration)))
        i += 1

    pool.put("exit")

if __name__=='__main__':
    thread_factory()

Lo ejecutamos con python printer.py

La función thread_factory elige un numero de segundos al azar a partir de la lista duration y crea 10 threads tipo Printer, sin embargo, los threads no inician su ejecución hasta que consumer los ejecuta mediante client.start(). Lo interesante de esto es que consumer ejecutara cualquier objeto tipo thread que reciba en la cola. Por ejemplo, podemos crear una clase AutoPrinter que determine ella misma el tiempo a dormir antes de imprimir su mensaje:

class AutoPrinter(threading.Thread):
    
    def __init__(self, id):
        self.id = id
        duration = [1,12,5,8,15]
        self.secs = random.choice(duration)
        threading.Thread.__init__ ( self )
        
    def run(self):
        time.sleep(self.secs)
        print self.id

y lo metemos en la cola desde thread_factory:

def thread_factory():
    duration = [1,12,5,8,15]
    i = 0
    consumer = Consumer()
    consumer.start()
    while i<5:
        pool.put(Printer("Printer %d" % i, random.choice(duration)))
        i += 1
    
    while i<10:
        pool.put(AutoPrinter("Auto %d" % i))
        i += 1
    
    pool.put("exit")





Tratar XML con JavaScript Core

23 03 2009

Recientemente 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).





Benchmarks de lxml, urllib2 y pycurl

21 03 2009

Llevo 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.





Más sobre Musictracker

18 10 2008

Descubri 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.





Pidgin – Musictracker

16 10 2008

El 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.





Uno de los mejores Monitores de Sistema

16 10 2008

Conky 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.





Drivel

16 10 2008

Probando 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”.








Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.