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