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")

Anuncios




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.





Problemas con eclipse

13 06 2008

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





Web Shell

16 04 2007

Firebug integrates with Firefox to put a wealth of web development
tools at your fingertips while you browse. You can edit, debug, and
monitor CSS, HTML, and JavaScript live in any web page.

Se trata basicamente de un IDE para desarrollo web dentro de Firefox. No es ninguna novedad ya que tengo casi un año de usarlo.

DWR en cambio:

DWR allows Javascript in a browser to interact with Java on a server and helps you manipulate web pages with the results.

Toma codigo Java de un servidor (servlet, action, etc.) y crea automaticamente codigo JavaScript que, mediante AJAX, permite invocar las clases de Java desde JavaScript.

Lo interesante de todo esto es que tomando la consola de JavaScript de Firebug, que permite ejecutar codigo interactivamente y muy en especial invocar el codigo q ya esta en la pagina HTML, y lo combinamos con la magia de DWR, que no es más que describir en XML que clases quieres invocar desde JS, lo que obtenemos es una shell web interactiva que permite llamar cualquier Servicio Web desde Firebug. Es decir, no hace falta compilar y deployar para hacer cualquier cambio, por pequeño que sea. Esto resulta ideal para debugging y pruebas. Y solo es el principio.





Aumentando SCMS

31 07 2006

SCMS (Semantic CMS) se basa en una premisa fundamental: Cada Esquema de Contenido (Tipo de Contenido) se refiere a un área de conocimiento especifico. Es decir, nuestra base de conocimiento sera la suma de cada dominio específico del CMS. Las búsquedas inteligentes son, en principio, únicamente para los dominios del SCMS. Dicho eso, podemos ampliar dicho conocimiento, es decir expander cada dominio de forma semi-automática mediante el uso de agentes inteligentes. La idea es tener agentes que busquen en sitios conocidos para ampliar la información que se ha agregado al CMS y que automaticamente agregue anotaciones semánticas sobre dicha información.

Ah si, adjunto un esquemita:

Gestor de Contenidos Semántico





CMS Semantico

14 07 2006

Ayer comence un nuevo proyecto: Construir un Gestor de Contenidos Semántico la idea me venia dando vueltas desde hace ya un tiempo asi que comence ayer. Hoy me he decidido que hay varias propuestas de lo mismo pero nada claro. Esto es lo que estoy utilizando:

Ingredientes:

  1. Un framework en Java para crear CMSs: Lenya
  2. Un framework en Java para la Web Semántica (crear ontologias y un motor de inferencia): Jena
  3.  Uno o varios Agentes Inteligentes: No me he decidido si me lo creo personalizado o utilizo un API como el de Agglets.

Concepto:
Lenya provee los modulos necesarios para la gestión de contenidos (autenticación, publicación, edición, gestion de permisos, etc.). Para búsqueda Lenya (y todos los demás) utiliza un agente de búsqueda en seste caso Lucene. Sin embargo, dichos agentes de búsqueda no hacen sino una categorización binaria, para la Web Semántica (Web 2.0) necesitamos crear Ontologias, al tratarse de un CMS, los dominios de conocimiento para tales ontologias serán las páginas del sitio Web creadas y editadas. Por tanto no es necesario, tener un agente inteligente que navegue por toda la Web, pero no estaria mal que el agente aumente el conocimiento del Sitio a traves de la Web. Si es necesario en cambio, un agente construya una Base de Conocimiento a partir de dichas ontologías. Por último, se necesita de un Motor de Inferencia que nos permita realizar consultas en lenguaje natural y que el sistema interprete la consulta y nos devuelva respuestas válidas. (Para más información sobre este tema referirse a Wikipedia 3.0: El fin de Google).

Para armar dicha base de Conocimiento (y por tanto las ontologías) utilizaré OWL que es, básicamente, un lenguaje XML para Ontologías. Dado que la base de conocimiento esta en XML, resulta muy conveniente que el conocimiento (contenido del sitio Web) esté tambien en XML, de aqui que Lenya sea mi elección ya que guarda todo el contenido en XML y no en esquemas propietarios de bases de datos. Jena provee un API de OWL para Java así como tambien un motor para SPARQL. Lenya debe ser capaz de comunicarse bastante bien con Jena ya que tambien es un API Java y al basarse en Cocoon, permite hacer todo tipo de transformaciones XSLT, de forma que todas las piezas encajan perfectamente.

La integración de todo el sistema es básicamente remplazar las búsquedas de Lucene por consultas de SAPRQL a la Base de Conocimiento creada por un agente a partir de las ontologias formadas sobre cada página del sitio.

Ire informando del avance de este proyecto.





Decompilando con Eclipse

13 01 2006

JAD+Eclipse=Jadclipse!!! Una herramienta muy útil para los programadores (especialmente si utilizan Eclipse. JAD es un decompilador de Java (convierte los archivos *.class en sus archivos fuentes originales), muy útil cuando se intenta trabajar (o depurar) una aplicación Java para la cual no tenemos los archivos fuente originales.

Varios sitios ofrecen instrucciones detalladas sobre come integrar JAD con Eclipse. En resumen: se descomprime JAD en el directorio que querramos y lo agregamos al PATH del sistema. Luego copiamos jadclipse en la carpeta de plugins de Eclipse. Por último, abrimos eclipse y configuramos el plugin diciéndole donde instalamos JAD en Window->Preferences->Java->JadClipse (JAD_HOME).

Dependiendo de la versión de Eclipse puede ser necesario tener que asociar el editor de jadclipse a los archivos *.class.

Con todo esto lo que logramos es que en lugar de obtener un “Source Not Found” al intentar ver el código de alguna clase para la que no tenemos la fuente, JAD nos la decompilara y veremos el código de dicha clase.

technorati tags: , ,