No mês passado escrevi um artigo com um programa para capturar todos os items da primeira página de cada categoria do MercadoLivre.
Lá, lidava com alguns problemas como:
- limite de concorrência no download das páginas
- processamento de html em thread, síncrono
- manter a maior parte do processo assíncrono, para ganhar tempo e CPU
Depois disso, precisei fazer umas alterações no código e acabei modificando um pouco programa, usando outras técnicas como:
- cooperação de tarefas para limitar a concorrência
- processamento de html inline, mais rápido (pois usa backend em C)
- encadear algumas funções, executando inline
Cada item desta lista corresponde aos itens da lista mais acima, respectivamente.
O esquema de cooperação do twisted é muito melhor que o Controller que havia criado anteriormente. Porém, muito mais complicado para jovens aprendizes. Recomendo este link para mais detalhes.
Sobre o processamento do html, vale a pena verificar o lxml. Antes, havia usado BeautifulSoup, que é muito bom, mas perde violentamente em desempenho e suporte a broken-html.
Por fim, o truque de usar generators para executar alguns callbacks inline é incrível, e absurdamente prático em casos como esse, do programa abaixo.
O resultado é final é o mesmo, mas a melhoria em desempenho é absurda. Fiz alguns testes na minha máquina e obtive o seguinte:
- esta versão consome, em média, 20% menos de CPU
- como não há necessidade de gravar os arquivos no disco, não consome disco
- o processo todo ficou 657% mais rápido, simplesmente
- ainda, o código é muito menor +_+
Veja ai:
#!/usr/bin/env python
# coding: utf-8
from lxml import html
from twisted.web import client
from twisted.python import log
from twisted.internet import task, defer, reactor
class MercadoLivre:
def __str__(self):
return 'http://www.mercadolivre.com.br/jm/ml.allcategs.AllCategsServlet'
def parse_categories(self, content):
category = subcategory = ''
doc = html.fromstring(content)
for link in doc.iterlinks():
el, attr, href, offset = link
try: category = el.find_class('categ')[0].text_content()
except: pass
else: continue
if category:
try: subcategory = el.find_class('seglnk')[0].text_content()
except: continue
else: yield (href, category, subcategory)
def parse_subcategory(self, content):
doc = html.fromstring(content)
for element in doc.find_class('col_titulo'):
yield element[0].text_content()
class Engine:
def finish(self, result):
reactor.stop()
@defer.inlineCallbacks
def fetch_categories(self, link, parser):
try:
doc = yield client.getPage(link)
defer.returnValue(parser(doc))
except Exception, e:
print e
def fetch_subcategory(self, links, parser, limit):
coop = task.Cooperator()
work = (client.getPage(link[0]).addCallback(parser).addCallback(self.page_items, *link) for link in links)
result = defer.DeferredList([coop.coiterate(work) for x in xrange(limit)])
result.addCallback(self.finish)
result.addErrback(log.err)
def page_items(self, items, href, category, subcategory):
print 'Categoria: %s / %s' % (category.encode('utf-8'), subcategory.encode('utf-8'))
for item in items: print ' -> %s' % item.encode('utf-8')
print ''
def main(limit, *parsers):
e = Engine()
for parser in parsers:
links = e.fetch_categories(str(parser), parser.parse_categories)
links.addCallback(e.fetch_subcategory, parser.parse_subcategory, limit)
if __name__ == '__main__':
reactor.callWhenRunning(main, 150, MercadoLivre())
reactor.run()

Comentários