Tabs

sábado, 15 de junio de 2013

Python for runners

Una de las cosas que más me sorprendió cuando empecé a visitar blogs de corredores, es la cantidad de informáticos con los que me encontraba... :) Tengo mis teorías al respecto, pero eso será para otro día. Hoy, voy a contar mi última experiencia de python for runners, que me sé de uno que seguramente le guste, y mientras se recupera de la Haría Extreme podrá echarle un ojo, y espero que a alguno más de los informáticos que por aquí andan le pique el gusanillo e incluso se anime a mejorar el código que os voy a mostrar, que está disponible en GitHub:

git://github.com/iZydro/python-for-runners.git

¡Si alguien se apunta, que me dé su user de GitHub y lo añado como Collaborator!

¿De qué va esto? Pues me picó la curiosidad y quería saber cuánta gente estaba tan loca como yo y había corrido la maratón de Barcelona y la de Madrid 6 semanas después, y ya que estábamos, cómo le había ido en ambas pruebas.

Primer problema: O no hay, o no he sabido encontrar un listado con la clasificación general de ambas pruebas... Pero sí que hay un formulario en la web de las dos carreras con el que podemos buscar los datos de un corredor por nombre o dorsal... Así que...

Primer reto: Conseguir el listado de participantes y sus tiempos.

La parte más complicada a priori, básicamente nunca había intentado hacer algo así, pero no es tan complejo. Una web con un formulario, sencillamente nos permite rellenar una serie de campos, y al pulsar el botón de Buscar, se envía una URL que informa al servidor qué datos queremos consultar y nos devuelve una web con los datos que hemos pedido. En el caso de Barcelona, además, el formulario está separado del resto de la página, lo que se conoce normalmente como iframe. Podéis comprobarlo vosotros mismos si vais a esta dirección:

http://www.zurichmaratobarcelona.es/cgi-bin/ZMB_result_res_maraton-es.py?value=2581

En la web de la maratón de Madrid no es un iframe, pero sí que es un formulario sencillo, como también se puede ver pinchando el enlace:

http://www.maratonmadrid.org/resultados/clasificacion.asp?carrera=10&parcial=10&clasificacion=1&dorsal=1985

Así que no iba a ser muy complicado. Básicamente, abro el navegador, voy escribiendo los enlaces, cambiando el número de dorsal, copio y pego el nombre y resultado en un Excel, y a por el siguiente, unos 14.769 en Barcelona y 10.162 en Madrid... ¿Sencillo? ¡No hijo no! ¡Mejor vamos a hacer unos scripts en python!

Para enviar formularios y descargar cosas de internet tenemos urllib2, que viene instalado de serie, y para extraer información de páginas web, opté por BeautifulSoup, que se instala muy fácilmente con easy_install o pip.

Y ahora, a picar. Lo más sencillo, enviar los formularios y recibir los datos.

 from urllib2 import urlopen  
 for number in range (1, 20000):  
      result = ""  
      feed = urlopen("http://www.zurichmaratobarcelona.es/cgi-bin/ZMB_result_res_maraton-es.py?value=" + str(number))  
      soup = BeautifulSoup(''.join(feed.read()))  

Con este sencillo programa podemos bajar 20.000 páginas web con los datos de los corredores... Pero cada web que bajamos se parece más o menos a esto...


<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"><title> ::: Zurich Marat? de Barcelona ::::::::::</title><script type="text/javascript">
    function parriba () {
        var arriba=top.window.document.getElementById("arriba");
        arriba.scrollIntoView();
    }
</script><style type="text/css"></style><link href="/css/csszurich.css" rel="stylesheet" type="text/css"></link><style type="text/css"></style><style type="text/css">
a:link {
        text-decoration: none;
        color:#FFCC00
}
a:visited {
        text-decoration: none;
        color:#FFCC00
}
a:hover {
        text-decoration: none;
        color:#FFCC00
}
a:active {
        text-decoration: none;
        color:#FFCC00
}
.style2 {color: #FF9900}
.caselles1 {    font-family: Tahoma;
        font-size: 10px;
        color: #000000;
        border: 1px solid #999999;
}

</style></head><body style="background-color:transparent;" onload="parriba();"><div style="margin:0px 0px 0px 18px" align="left"><table width="698" border="0" cellspacing="0" cellpadding="0"><tr xmlns=""><td colspan="11"><div align="left"><table border="0" cellpadding="0" cellspacing="0"><tr><td colspan="11" class="tahoma11negre"><div align="left"><span class="texte">Corredores encontrados: </span><span class="zurich1"><strong>1</strong></span>.
 <br><span class="tahoma11blanc"><img src="/inscripciones/img/spacer.gif" width="10" height="5"></span><br><span class="zurich1"><img src="/imgzurich/bola.gif" width="12" height="13" align="absmiddle">Distancia:</span>?<span class="zurich2">MARAT?N</span><br><span class="tahoma11blanc"><img src="/inscripciones/img/spacer.gif" width="10" height="5"></span><br><span class="zurich1"><img src="/imgzurich/bola.gif" width="12" height="13" align="absmiddle">Mostrando:</span><span class="zurich2"> Clasificaci?n General</span><br></div></td></tr><tr><td class="tahoma11negre"><img src="/inscripciones/img/spacer.gif" width="10" height="10"></td></tr></table></div></td></tr><tr xmlns=""><td><div align="left"><img src="/inscripciones/img/spacer.gif" height="2"></div></td></tr><tr xmlns=""><td colspan="7">?</td></tr><tr xmlns=""><td width="60" bgcolor="#5982B8" class="tahoma12blanc"><img src="/imgzurich/liloso1.jpg" width="11" height="27" align="absmiddle"><span class="tahoma12blanc">Posici?n</span></td><td bgcolor="#5982B8" class="tahoma12blanc">Dorsal</td><td bgcolor="#5982B8" class="tahoma12blanc">Nombre</td><td bgcolor="#5982B8" class="tahoma12blanc">Cat.</td><td bgcolor="#5982B8" class="tahoma12blanc">Club</td><td bgcolor="#5982B8" class="tahoma12blanc">Pa?s</td><td bgcolor="#5982B8" class="tahoma12blanc">Tiempo</td><td bgcolor="#5982B8" class="tahoma12blanc" style="background-image:url(/imgzurich/liloso2.jpg);background-position:top right;background-repeat:no-repeat;">V?deo</td></tr><tr xmlns=""><td bgcolor="#F6FAFD" class="texte" style="text-transform:uppercase;padding:0px 0px 0px 11px">00002</td><td width="50" bgcolor="#F6FAFD" class="texte">00002</td><td width="140" bgcolor="#F6FAFD" class="texte"><a target="_self" href="/cgi-bin/detalles_resultados.py?lloc=2;lang=es"><span style="color:#003366; text-decoration:underline">Abraham?Keter</span></a></td><td width="40" bgcolor="#F6FAFD" class="texte">SenM</td><td width="130" bgcolor="#F6FAFD" class="texte">
                 ?
               </td><td width="55" bgcolor="#F6FAFD" class="tahoma11negre"><span class="tahoma11groc Estilo1" style="color:#FF6600">KEN</span></td><td width="60" bgcolor="#F6FAFD" class="texte">02:10:48</td><td width="40" bgcolor="#F6FAFD" class="texte"><div align="center"><a style="border: 0px solid white" target="_top" href="http://mysports.tv/events/MB13/redirect.asp?r=2"><img style="border:0px solid white" src="/resultados/img/camera.gif"></a></div></td></tr><tr xmlns=""><td colspan="7"><div align="left"><img src="/inscripciones/img/spacer.gif" height="6"></div></td></tr><tr xmlns=""><form method="post" name="form" action="ZMB_result_res_maraton-es.py" target="result"><input type="hidden" name="order_index" value="0"><input type="hidden" name="skip" value="0"><input type="hidden" name="length" value="5"><input type="hidden" name="total" value="1"><input type="hidden" name="value" value="2"><td colspan="3" bgcolor="#5881B8" class="tahoma12blanc"><div align="left">??p?gina <span class="tahoma12blanc">1</span> de <span class="tahoma12blanc">1</span></div></td><td colspan="5" align="right" bgcolor="#5881B8" height="20"><span class="tahoma12blanc"><img src="/inscripciones/img/pointiti2cinvers.gif" width="12" height="13" align="absmiddle">?
            inicio
?<img src="/inscripciones/img/pointiti2cinvers.gif" width="12" height="13" align="absmiddle">?
            anterior
?-?
            siguiente
?<img src="/inscripciones/img/pointiti2c.gif" width="12" height="13" align="absmiddle">
?
            ?ltima
?<img src="/inscripciones/img/pointiti2c.gif" width="12" height="13" align="absmiddle">??
    </span></td></form></tr></table></div></body></html>

A ver, por aquí se ve algo...

<span style="color:#003366; text-decoration:underline">Abraham?Keter</span></a></td><td width="40" bgcolor="#F6FAFD" class="texte">SenM</td><td width="130" bgcolor="#F6FAFD" class="texte">                 ?               </td><td width="55" bgcolor="#F6FAFD" class="tahoma11negre"><span class="tahoma11groc Estilo1" style="color:#FF6600">KEN</span></td><td width="60" bgcolor="#F6FAFD" class="texte">02:10:48</td> 

Vale, tenemos el nombre y el tiempo en la web. Ahora sólo hay que sacarlo de ahí. Ahí es donde entra en juego BeautifulSoup, una librería que nos permite buscar elementos en una web, recorrerlos y extraer información de ellos. Gracias al inspector del Chrome, se puede ver fácilmente en qué tabla, fila y columna están los datos que necesitamos, y con BeautifulSoup acceder a ella. Para la web de Barcelona, tan sencillo como esto:

 feed = urlopen("http://www.zurichmaratobarcelona.es/cgi-bin/ZMB_result_res_maraton-es.py?value=" + str(number))  
 soup = BeautifulSoup(''.join(feed.read()))  
 tables = soup.findAll('table')  
 for table in tables:  
      rows = table.findAll('tr')  
      trc = 1  
      for tr in rows:  
           if trc == 7:  
                cols = tr.findAll('td')  
                tdc = 1  
                for td in cols:  
                     if tdc == 3:  
                          for tde in td:  
                               for tdea in tde:  
                                    for tdeas in tdea:  
                                         result = result + tdeas  
                     if tdc == 7:  
                          for tde in td:  
                               result = result + " | " + tde  
                     tdc = tdc + 1  
           trc = trc + 1  
 if result != "":  
      print result  
      sys.stdout.flush()  

¿Qué estoy haciendo? Le pido a BeautifulSoup que me busque cuántas tablas hay en la web, que las recorra una a una, en este caso, solo había una, en cada tabla, que recorra todas las filas, y si la fila es la número 7 (trc == 7), que recorra sus columnas, y en la columna 3, extraiga el elemento que está a dos niveles de profundidad (el nombre, que está dentro de un <a> y de un <span>), y en la 7, el elemento que está a un nivel de profundidad (el tiempo). Juntamos nombre y tiempo, separados por un signo "|", y lo sacamos por stdout, para poder verlo en pantalla o escribirlo a un archivo.

¿Cómo hago esto último? Desde línea de comando, llamo a mi programa y redirecciono la salida a un comando tee, que me muestra la salida por pantalla y además la escribe a un fichero. Así que hago:

python bnc.py | tee bcn.txt

¡Y varias horas después tengo un bcn.txt con los nombres de los 14.769 finishers y sus tiempos!

Vamos ahora a Madrid. Se me complicó un poco... Resulta que hay una tabla, pero dentro de esa tabla hay otras tablas dentro de unas celdas, y BeautifulSoup parece que se lía y lo de recorrer tablas, filas y columnas no funciona bien... Pero aprovecho otra feature de BeautifulSoup y lo hago de otra manera, le pido que me devuelva una tabla que incluya una característica en concreto, que en este caso, es que su ancho sea del 98%. Como sólo hay una tabla así, que es la que muestra los resultados, mezclo una técnica con otra y consigo sacar los nombres y resultados de esta manera:

 for number in range (1, 12000):  
      result = ""  
      feed = urlopen("http://www.maratonmadrid.org/resultados/clasificacion.asp?carrera=10&parcial=10&clasificacion=1&dorsal=" + str(number))  
      soup = BeautifulSoup(''.join(feed.read()))  
      t = soup.find("table", {"width":"98%"})  
      r = t.findAll("tr")  
      if len(r) > 1:  
           d = r[1].findAll("td")  
           for tde in d[2]:  
                for tdea in tde:  
                     for tdeas in tdea:  
                          result = result + str(tdeas)  
           for tde in d[3]:  
                for tdea in tde:  
                     for tdeas in tdea:  
                          result = result + " " + str(tdeas)  
           for tde in d[6]:  
                for tdea in tde:  
                     for tdeas in tdea:  
                          result = result + " | " + str(tdeas)  
           print result  
           sys.stdout.flush()  

Sencillamente, buscar la tabla en cuestión, ver si tiene más de una fila, y si es así, coger los datos de las columnas 2, 3 y 6 de la fila 2, que son nombre, apellido y tiempo, juntarlos, y sacarlos por stdout.

Igual que en Barcelona, ejecuto, tee, y mad.txt listo. Cambio un par de líneas en el programa y saco la clasificación femenina, que lleva otra numeración. Simplemente, el for es de 1 a 1200, y al urlopen le añado una "F" después de dorsal=

Ejecuto, y guardo mad-her.txt

¡Tengo tres archivos con nombres y tiempos de maratonianos, después de dejar dos noches el Mac trabajando, ahora, a divertirse!

Mi idea: leer los dos archivos, y construir dos list de objetos con nombre y tiempo. ¿Por qué no un diccionario indexado por el nombre? Lo descarté porque seguramente habría nombres repetidos...

Además, con los archivos ya descargados, me di cuenta de que los de Barcelona tenían caracteres extraños en el nombre (el espacio no era un espacio, era una cosa muy rara, un 0xc2a0), y que los nombres en Barcelona estaban en minúsculas y los de Madrid en mayúsculas. Así que mientras rellenaba la lista, aproveché para normalizarlo todo a ASCII puro y duro, con un sencillo lower(), y la función unidecode y la propiedad decode de los Strings. Ah, y quitar el primer 0 del tiempo para que los dos tengan el mismo formato.

 def read_results(file_name, vector_name):  
      for line in open(file_name, "r"):  
           line = line.strip()  
           runner_time = line.split(" | ")  
           runner_time[0] = runner_time[0].replace(chr(0xa0), " ")  
           runner_time[0] = runner_time[0].replace(chr(0xc2), "")  
           runner_time[0] = unidecode(runner_time[0].decode("unicode-escape").lower())  
           if runner_time[1][0] == "0":  
                runner_time[1] = runner_time[1][1:]  
           vector_name.append(runner_time)  

Con esta función, simplemente haciendo:

 mad = []  
 bcn = []   
 read_results("mad-her.txt", mad)  
 read_results("bcn.txt", bcn)  
 num_mad = len(mad)  
 num_bcn = len(bcn)  

Ya tengo dos list, cada una conteniendo un objeto de dos miembros, nombre y tiempo, y precalculo la cantidad de elementos de cada uno.

Lo siguiente, recorrer una lista, y después, para cada elemento de ésta, recorrer la otra buscando si el nombre en ambos elementos coincide. En python, es tan sencillo como esto:

 for madrid in mad:  
      runner_mad = str(madrid[0])  
      for barcelona in bcn:  
           runner_bcn = barcelona[0]  
           if runner_mad == runner_bcn:  
                # Lo tenemos!  

Ahora lo más divertido, qué hacemos cuando lo tenemos. Pues varias cosas... Convierto el tiempo a un objeto datetime y voy sumando los tiempos de cada prueba en las variables total_mad y total_bcn. Como sumar dos datetime (fechas) no tiene sentido en programación ni en la vida real, uso una función que añade segundos a una fecha, y paso el resultado de la carrera a segundos y lo sumo:

 time_mad = datetime.strptime(madrid[1], "%H:%M:%S")  
 time_bcn = datetime.strptime(barcelona[1], "%H:%M:%S")  
 total_mad = total_mad + timedelta(0, time_mad.hour * 3600 + time_mad.minute * 60 + time_mad.second)  
 total_bcn = total_bcn + timedelta(0, time_bcn.hour * 3600 + time_bcn.minute * 60 + time_bcn.second)  

Después, si el tiempo en BCN es menor que en MAD, voy incrementando el contador que indica cuanta gente corrió más rápido en BCN, si no, en MAD y sumo la diferencia de tiempos en total_diff, para sacar la media al final:

 if (time_mad > time_bcn):  
      diff = " BCN faster by " + str(time_mad - time_bcn)  
      bcn_faster = bcn_faster + 1  
      total_diff += (time_mad - time_bcn)  
 else:  
      diff = " MAD faster by " + str(time_bcn - time_mad)  
      mad_faster = mad_faster + 1  
      total_diff -= (time_bcn - time_mad)  

Unas líneas más para construir un string con el nombre, número de orden, tiempos, qué maratón corrió más rápido y por cuánto, y tenemos este tipo de salida:

25: ralph schneider           BCN: 3:34:43    MAD: 3:12:01   MAD faster by 0:22:42
26: isidro gilabert             BCN: 3:21:25    MAD: 3:22:14   BCN faster by 0:00:49
27: joao carmo                  BCN: 3:04:28    MAD: 3:07:21   BCN faster by 0:02:53

Y para los amantes de la estadística, todo eso que hemos ido sumando, ahora lo podemos presentar... Una pequeña función que calcula el tiempo total acumulado dividido entre los participantes, y nos devuelve horas, minutos y segundos para luego sacarlo por pantalla:

 def divide_time(my_time, total):  
      result = {}  
      seconds = my_time.second + my_time.minute * 60 + my_time.hour * 3600 + (my_time.day - 1) * 3600 * 24  
      div = seconds / total  
      hours = int(div/3600)  
      minutes = int((div - hours*3600) / 60)  
      seconds = div - hours*3600 - minutes*60  
      result["hours"] = str(hours)  
      if minutes < 10:  
           result["minutes"] = "0" + str(minutes)  
      else:  
           result["minutes"] = str(minutes)  
      if seconds < 10:  
           result["seconds"] = "0" + str(seconds)  
      else:  
           result["seconds"] = str(seconds)  
      return result  

Y así, sencillamente, hacemos esto:

 result = divide_time(total_mad, cnt_tot)  
 print "Average MAD time " + result["hours"] + ":" + result["minutes"] + ":" + result["seconds"]  
 result = divide_time(total_bcn, cnt_tot)  
 print "Average BCN time " + result["hours"] + ":" + result["minutes"] + ":" + result["seconds"]  

Et voilá! Al final me salen estos datos:


Masculino
Ran MAD faster: 45, ran BCN faster: 71
Average MAD time 3:51:20Average BCN time 3:45:19
Average difference: BCN faster by 6 minutes, 1 seconds

Femenino
Ran MAD faster: 5, ran BCN faster: 6
Average MAD time 3:56:09Average BCN time 3:56:02
Average difference: BCN faster by 0 minutes, 6 seconds

El código y los listados, se pueden husmear aquí...


Los entrenamientos siguen bien, recuperando la forma poco a poco... Pero eso será para otro post... :)

14 comentarios:

  1. Tremendo, así que eres un máquina no sólo corriendo sino también al teclado. Pa cagarse...

    ResponderEliminar
  2. Me levanto hoy domingo pensando en que mañana tengo que volver al hospital para la segunda sesión de los factores de crecimiento y me pregunto ¿cómo es posible que teniendo la fobia a las aguas que tengo, sea posible que me haya pinchado voluntariamente más de 20 veces en los últimos dos meses y que siga yendo al hospital tres lunes seguidos a ponerme vía, anestesias, extracción de sangre e inyección de plasma?.

    Tras leer tu entrada he encontrado la respuesta.

    Voy a cerrar mi despacho y montaré un psiquiátrico para maratonianos. Y a ti te dejaré que lleves el GPS todo el día.

    ResponderEliminar
  3. Sobresaliente. Usando el comando grep no hubieras podido sacar los nombres también?

    ResponderEliminar
  4. Y todo esto lo habrás hecho disfrazado de tu personaje de Star Trek favorito, ¿no?

    ResponderEliminar
  5. Muy pero que muy interesante. Tengo algo parecido pero para descargar torrents y uso expresiones regulares (usando las clases o los ids)... :P

    Lo único malo son los nombres repetidos, pero a falta de un DNI es la única opción viable que tiene para cruzar ambos listados.

    La verdad es que me acaba de sorprender gratamente el phyton.

    ResponderEliminar
  6. Oooooh sí!

    Esto me viene perfecto justo ahora que estoy aprendiendo Python... :-D

    ResponderEliminar
  7. Hala en mujeres el tiempo medio sólo se diferencia en 6".... es que es lo único que entiendo de toda la entrada.

    ResponderEliminar
  8. Me lo repita! J...... Isidro, demasiado para este usuario del monton.

    ResponderEliminar
  9. Babeando aún por esta entrada!

    Python es una maravilla, en buena hora me dio por adentrarme en ese lenguaje, aunque últimamente no he estado picando nada, I miss coding :'(

    Te voy a acribillar a preguntas cuando vengas, el año pasado me "corté" un poco por el tema de estar ante una eminencia del desarrollo de videojuegos, prepárate ;)

    ResponderEliminar
  10. ¡Ufff, perdido desde el principio!, jeje

    ResponderEliminar
  11. La lastima es no saber hacerlo, para probar con todos los maratones de de España

    ResponderEliminar
  12. Bueno de verdad no conocia beautifulsoup y andaba desesperado buscando algo como esto, al que pregunto si con grep no podia hacer eso, le dire que simplemente que no!!! si de verdad te fijas los datos no etan en la pagina si no en una tabla a la que apunta el dato que se muestra, segun lo que lei me viene como anillo al dedo ya que me gusta programar en python aunque no se tanto como quisiera, gracias voy a hacer una entrada en mi blog con mi trabajo y te hare los honores :D

    ResponderEliminar