from pyquery import PyQuery
def parse_telegrama(url):
pq = PyQuery(url)
resultado_mesa = {}
for tr in pq('.tablon tbody tr, .TVOTOS tbody tr'):
resultado_mesa[pq('th', tr).text()] = int(pq('td', tr).text().replace('.', ''))
return resultado_mesa
escrutinio = parse_telegrama('http://www.resultados.gob.ar/telegramas/04/001/0013J/040010013J2669.htm')
escrutinio
{'COALICION CIVICA - AFIRMACION PARA UNA REPUBLICA IGUALITARIA (ARI)': 8, 'ENCUENTRO VECINAL CORDOBA': 4, 'FRENTE DE IZQUIERDA Y DE LOS TRABAJADORES': 0, 'FRENTE PARA LA VICTORIA': 35, 'FRENTE PROGRESISTA CIVICO Y SOCIAL': 20, 'UNION CIVICA RADICAL': 36, 'UNION POR CORDOBA': 56, 'UNION PRO': 40, 'VECINALISMO INDEPENDIENTE': 33, 'Votos en blanco': 4, 'Votos nulos': 8, 'Votos recurridos': 0}
sum(escrutinio.values())
244
Si nos fijamos el total de votos dice 277 (dato que no es digitalizado!, por qué?), es decir, no se han computado 33 votos del FIT.
En este programa voy a hacer un intento de buscar mesas sospechosas, para que las revisemos entre todos.
Autor: Martín Gaitán gaitan@gmail.com twitter: http://twitter/tin_nqn_
licencia: CC-by
un crawler que devuelve un arbol de diccionarios anidados.
def crawl(base_url):
"""
Devuelve un arbol con los resultados de cada mesa
Puede tardar mucho.
>>> crawl("http://www.resultados.gob.ar/telegramas/IMUN04.htm")
"""
arbol = {}
pq = PyQuery(base_url)
pq = pq.make_links_absolute(base_url='http://www.resultados.gob.ar/telegramas/')
for anchor in pq('a'):
url = pq(anchor).attr('href')
if '04/' in url:
# mesa
data = parse_telegrama(url)
else:
# abrir nueva rama
data = crawl(url)
arbol[pq(anchor).text()] = data
return arbol
Tener los datos en forma de árbol en memoria no es muy útil. Mejor es modelar una base de datos para poder hacer consultas o agregaciones. Por ejemplo, "obtener todas las mesas donde el FIT haya sacado 0 voto".
%install_ext https://gist.github.com/mgaitan/7207448/raw/ce87e68226ef4ad12cb1aabfa42ebacaf66b3bce/django_orm_magic.py
Installed django_orm_magic.py. To use it, type: %load_ext django_orm_magic
%load_ext django_orm_magic
%%django_orm
from django.db import models
from django.db.models import Sum
class Municipio(models.Model):
nombre = models.CharField(max_length=100)
def __unicode__(self):
return self.nombre
class Circuito(models.Model):
numero = models.CharField(max_length=100)
municipio = models.ForeignKey('Municipio', null=True)
def __unicode__(self):
return u"Circuito %s (%s)" % (self.numero, self.municipio)
class Mesa(models.Model):
circuito = models.ForeignKey('Circuito', null=True)
numero = models.CharField(max_length=100, unique=True)
url = models.URLField()
@property
def computados(self):
return self.votomesa_set.aggregate(Sum('votos'))['votos__sum']
def __unicode__(self):
return u"Mesa %s (%s)" % (self.numero, self.circuito)
class Opcion(models.Model):
# partido, blanco, etc.
nombre = models.CharField(max_length=100, unique=True)
def __unicode__(self):
return self.nombre
class VotoMesa(models.Model):
mesa = models.ForeignKey('Mesa')
opcion = models.ForeignKey('Opcion')
votos = models.IntegerField()
def __unicode__(self):
return u"%s: %d" % (self.opcion, self.votos)
class Meta:
unique_together = ('mesa', 'opcion')
/tmp/tmp7KULms/orm_magic
Eso deja nuestros modelos listos para usar.
Ahora refactorizo el motor que "chupa" los datos y los mete en la base de datos.
def crawl2(base_url, nivel='prov', parent=None):
pq = PyQuery(base_url)
pq = pq.make_links_absolute(base_url='http://www.resultados.gob.ar/telegramas/')
for anchor in pq('a'):
url = pq(anchor).attr('href')
text = pq(anchor).text()
if nivel == 'circuito':
# mesa
mesa = Mesa.objects.create(numero=text, circuito=parent, url=url)
for opcion, votos in parse_telegrama(url).iteritems():
opcion, _ = Opcion.objects.get_or_create(nombre=opcion)
VotoMesa.objects.create(mesa=mesa, opcion=opcion, votos=votos)
elif nivel == 'prov':
municipio, _ = Municipio.objects.get_or_create(nombre=text)
crawl2(url, nivel="muni", parent=municipio)
elif nivel == 'muni':
circuito, _ = Circuito.objects.get_or_create(numero=text, municipio=parent)
crawl2(url, nivel="circuito", parent=circuito)
Por ejemplo, para la municipalidad de Totoral, hago
# 024 - Totoral
crawl2('http://www.resultados.gob.ar/telegramas/ICIR04024.htm', 'muni')
Mesa.objects.all()
[<Mesa: Mesa 7631 (Circuito 0350 (None))>, <Mesa: Mesa 7632 (Circuito 0350 (None))>, <Mesa: Mesa 7633 (Circuito 0351 (None))>, <Mesa: Mesa 7634 (Circuito 0351 (None))>, <Mesa: Mesa 7635 (Circuito 0351 (None))>, <Mesa: Mesa 7636 (Circuito 0351 (None))>, <Mesa: Mesa 7637 (Circuito 0351A (None))>, <Mesa: Mesa 7638 (Circuito 0351A (None))>, <Mesa: Mesa 7639 (Circuito 0352 (None))>, <Mesa: Mesa 7640 (Circuito 0352 (None))>, <Mesa: Mesa 7641 (Circuito 0352 (None))>, <Mesa: Mesa 7642 (Circuito 0352 (None))>, <Mesa: Mesa 7643 (Circuito 0352 (None))>, <Mesa: Mesa 7644 (Circuito 0352A (None))>, <Mesa: Mesa 7645 (Circuito 0353 (None))>, <Mesa: Mesa 7646 (Circuito 0354 (None))>, <Mesa: Mesa 7647 (Circuito 0355 (None))>, <Mesa: Mesa 7648 (Circuito 0355 (None))>, <Mesa: Mesa 7649 (Circuito 0355 (None))>, <Mesa: Mesa 7650 (Circuito 0356 (None))>, '...(remaining elements truncated)...']
Opcion.objects.all()
[<Opcion: FRENTE PROGRESISTA CIVICO Y SOCIAL>, <Opcion: VECINALISMO INDEPENDIENTE>, <Opcion: UNION CIVICA RADICAL>, <Opcion: Votos recurridos>, <Opcion: Votos nulos>, <Opcion: FRENTE DE IZQUIERDA Y DE LOS TRABAJADORES>, <Opcion: UNION POR CORDOBA>, <Opcion: UNION PRO>, <Opcion: ENCUENTRO VECINAL CORDOBA>, <Opcion: Votos en blanco>, <Opcion: COALICION CIVICA - AFIRMACION PARA UNA REPUBLICA IGUALITARIA (ARI)>, <Opcion: FRENTE PARA LA VICTORIA>]
Fit = Opcion.objects.get(nombre='FRENTE DE IZQUIERDA Y DE LOS TRABAJADORES')
VotoMesa.objects.filter(opcion=Fit).aggregate(Sum('votos'))
{'votos__sum': 177}
from IPython.display import display_html, HTML, IFrame
for mesa in Mesa.objects.filter(votomesa__votos=0, votomesa__opcion=Fit):
display_html(HTML("<a href='%s'>%s</a> - Total computados: %d" % (mesa.url, mesa, mesa.computados)))
Por ejemplo, el primer resultado se computaron 4 votos en blanco de más.
IFrame(src=Mesa.objects.get(numero__contains='7634').url, width="100%", height=300)
Esto da la pauta de los muchos errores que pueden existir.
Hacer la recolección de datos para todo córdoba puede tardar un rato y al parecer, el servidor nos "banea" si hay muchas peticiones seguidas. Esta noche voy a dejar corriendo una version con pausas y comparto la base de datos para que podamos hacer consultas y demás
# datos de todo cordoba. Puede tardar
crawl2('http://www.resultados.gob.ar/telegramas/IMUN04.htm')
--------------------------------------------------------------------------- KeyboardInterrupt Traceback (most recent call last) <ipython-input-6-5504bb3d3624> in <module>() 1 # datos de todo cordoba ----> 2 crawl2('http://www.resultados.gob.ar/telegramas/IMUN04.htm') <ipython-input-5-f373b49a274f> in crawl2(base_url, nivel, parent) 18 elif nivel == 'prov': 19 municipio, _ = Municipio.objects.get_or_create(nombre=text) ---> 20 crawl2(url, nivel="muni", parent=municipio) 21 22 elif nivel == 'muni': <ipython-input-5-f373b49a274f> in crawl2(base_url, nivel, parent) 22 elif nivel == 'muni': 23 circuito, _ = Circuito.objects.get_or_create(numero=text, municipio=parent) ---> 24 crawl2(url, nivel="circuito", parent=circuito) <ipython-input-5-f373b49a274f> in crawl2(base_url, nivel, parent) 10 if nivel == 'circuito': 11 # mesa ---> 12 mesa = Mesa.objects.create(numero=text, circuito=parent, url=url) 13 14 for opcion, votos in parse_telegrama(url).iteritems(): /usr/local/lib/python2.7/dist-packages/django/db/models/manager.pyc in create(self, **kwargs) 135 136 def create(self, **kwargs): --> 137 return self.get_query_set().create(**kwargs) 138 139 def bulk_create(self, *args, **kwargs): /usr/local/lib/python2.7/dist-packages/django/db/models/query.pyc in create(self, **kwargs) 375 obj = self.model(**kwargs) 376 self._for_write = True --> 377 obj.save(force_insert=True, using=self.db) 378 return obj 379 /usr/local/lib/python2.7/dist-packages/django/db/models/base.pyc in save(self, force_insert, force_update, using) 461 if force_insert and force_update: 462 raise ValueError("Cannot force both insert and updating in model saving.") --> 463 self.save_base(using=using, force_insert=force_insert, force_update=force_update) 464 465 save.alters_data = True /usr/local/lib/python2.7/dist-packages/django/db/models/base.pyc in save_base(self, raw, cls, origin, force_insert, force_update, using) 553 if update_pk: 554 setattr(self, meta.pk.attname, result) --> 555 transaction.commit_unless_managed(using=using) 556 557 # Store the database on which the object was saved /usr/local/lib/python2.7/dist-packages/django/db/transaction.pyc in commit_unless_managed(using) 118 using = DEFAULT_DB_ALIAS 119 connection = connections[using] --> 120 connection.commit_unless_managed() 121 122 def rollback_unless_managed(using=None): /usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.pyc in commit_unless_managed(self) 201 self.validate_thread_sharing() 202 if not self.is_managed(): --> 203 self._commit() 204 self.clean_savepoints() 205 else: /usr/local/lib/python2.7/dist-packages/django/db/backends/__init__.pyc in _commit(self) 48 def _commit(self): 49 if self.connection is not None: ---> 50 return self.connection.commit() 51 52 def _rollback(self): KeyboardInterrupt:
Circuito.objects.all().count()
338
from django.db import connections, connection
connections.databases
{'default': {'ENGINE': 'django.db.backends.sqlite3', 'HOST': '', 'NAME': 'db.sqlite', 'OPTIONS': {}, 'PASSWORD': '', 'PORT': '', 'TEST_CHARSET': None, 'TEST_COLLATION': None, 'TEST_MIRROR': None, 'TEST_NAME': None, 'TIME_ZONE': 'America/Chicago', 'USER': ''}}
ls db.sqlite
db.sqlite
!nautilus .
Mesa.objects.count()
4740
for mesa in Mesa.objects.filter(votomesa__votos=0, votomesa__opcion=Fit):
display_html(HTML("<a href='%s'>%s</a> - Total computados: %d" % (mesa.url, mesa, mesa.computados)))