Web Scraping com Scrapy – primeiros passos

Imagine que você queira extrair conteúdo da Web que não esteja em apenas uma página só: você precisa de uma maneira de “navegar” no site para as páginas que realmente contém as informações úteis. Por exemplo, você pode estar interessado nas notícias destaques do dia no Portal Brasil.gov.br, mas somente aquelas das seções “Infraestrutura” e “Ciência e Tecnologia”.

webpage-brasil-links

Bem, há uns tempos atrás, já mostramos aqui no blog como usar a biblioteca requests para acessar páginas disponíveis na Web usando nossa linguagem predileta. Também mostramos como usar a biblioteca BeautifulSoup para facilitar a extração do conteúdo útil da página, o que chamamos de Web Scraping. Hoje, vamos mostrar como usar o framework Scrapy, que contém todas essas funcionalidades e muitas outras mais, de maneira que agiliza bastante resolver problemas como esse da introdução. Tudo em Python, lógico! =)

Vale notar então, que o Scrapy busca resolver não só a extração de conteúdo das páginas (scraping), mas também a navegação para as páginas relevantes para a extração (crawling). Para isso, uma ideia central no framework é o conceito de Spider — na prática, objetos Python com algumas características especiais que você escreve o código e o framework aciona.

Só para você ter uma ideia de como se parece, dê uma olhada no código de um programa que usa Scrapy para extrair informações (link, título e visualizações) de um canal do YouTube abaixo. Não se preocupe em entender esse código ainda, estamos mostrando aqui só para você ter um feeling do código com Scrapy. Ao terminar esse tutorial, você será capaz de entender e escrever programas como esse. =)

import scrapy
from scrapy.contrib.loader import ItemLoader

class YoutubeVideo(scrapy.Item):
    link = scrapy.Field()
    title = scrapy.Field()
    views = scrapy.Field()

class YoutubeChannelLister(scrapy.Spider):
    name = 'youtube-channel-lister'
    youtube_channel = 'LongboardUK'
    start_urls = ['https://www.youtube.com/user/%s/videos' % youtube_channel]

    def parse(self, response):
        for sel in response.css("ul#channels-browse-content-grid > li"):
            loader = ItemLoader(YoutubeVideo(), selector=sel)

            loader.add_xpath('link', './/h3/a/@href')
            loader.add_xpath('title', './/h3/a/text()')
            loader.add_xpath('views', ".//ul/li[1]/text()")

            yield loader.load_item()

Mas antes de começarmos a falar mais sobre o Scrapy, certifique-se de tê-lo instalado em sua última versão (dependendo do caso, você pode precisar usar o comando sudo ou a opção –user para o pip install):

pip install --upgrade scrapy

Nota: dependendo do seu ambiente Python, a instalação pode ser um pouco enrolada por causa da dependência do Twisted. Se você usa Windows, confira as instruções específicas no guia de instalação oficial. Se você usa uma distribuição Linux baseada em Debian, pode querer usar o repositório APT oficial do Scrapy. Se você está usando o pip no Ubuntu, pode precisar instalar os pacotes libffi-dev, libssl-dev, libxml2-dev e libxslt1-dev antes.

Para seguir este tutorial, você precisará do Scrapy com número de versão 0.24 para cima. Você pode verificar a versão do Scrapy instalada com o comando:

python -c 'import scrapy; print('%s.%s.%s' % scrapy.version_info)'

A saída desse comando no ambiente que usamos para esse tutorial está assim:

$ python -c 'import scrapy; print("%s.%s.%s" % scrapy.version_info)'
0.24.2

A anatomia de uma aranha

spider_anatomy

Um Scrapy spider é responsável por definir como seguir os links “navegando” por um site (o que chamamos de crawling) e como extrair as informações das páginas em estruturas de dados Python. 

Para definir um spider mínimo, crie uma classe estendendo scrapy.Spider e dê um nome ao spider usando o atributo name:

import scrapy

class MinimalSpider(scrapy.Spider):
    """A menor Scrapy-Aranha do mundo!"""
    name = 'minimal'

Coloque isso em um arquivo com o nome minimal.py e rode o seu spider para conferir se está tudo certo, usando o seguinte comando:

scrapy runspider minimal.py

Caso estiver tudo certo, você verá na tela algumas mensagens do log marcadas como INFO e DEBUG. Caso houver alguma mensagem marcada com ERROR, significa que deu algo errado e você precisa conferir se tem algum erro no código do spider.

A vida de um spider começa com a geração de requisições HTTP (objetos do tipo Request) para o motor do framework acionar. A parte do spider responsável por isso é o método start_requests(), que retorna um iterable contendo as primeiras requisições a serem feitas para o spider.

Adicionando esse elemento ao nosso spider mínimo, ficamos com:

import scrapy

class MinimalSpider(scrapy.Spider):
    """A menor Scrapy-Aranha do mundo!"""
    name = 'minimal'

    def start_requests(self):
        return [scrapy.Request(url)
                for url in ['http://www.google.com', 'http://www.yahoo.com']]

O método start_requests() deve retornar um iterable de objetos scrapy.Request, que representam uma requisição HTTP a ser acionada pelo framework (incluindo URL, parâmetros, cookies, etc) e definem uma função a ser chamada para quando a requisição completar — uma função callback. 

Nota: Caso esteja familiarizado com implementar AJAX com JavaScript, essa maneira de trabalhar disparando requisições e registrando callbacks pode soar familiar.

No nosso exemplo, retornamos uma lista de requisições simples para o site do Google e do Yahoo, mas o método start_requests() também poderia ser implementado como um Python generator.

Se você tentou executar o exemplo como está agora, pode ter notado que ainda está faltando coisa, o Scrapy irá cuspir duas mensagens marcadas como ERROR, reclamando que um método não foi implementado:

....
  File "/home/elias/.virtualenvs/scrapy/local/lib/python2.7/site-packages/scrapy/spider.py", line 56, in parse
    raise NotImplementedError
exceptions.NotImplementedError:

Isso ocorre porque, como não registramos nenhuma função callback para os objetos Request, o Scrapy tentou chamar o callback padrão, que é o método parse() do objeto Spider. Vamos adicionar esse método ao nosso spider mínimo, para podemos executar o spider:

import scrapy

class MinimalSpider(scrapy.Spider):
    """A menor Scrapy-Aranha do mundo!"""
    name = 'minimal'

    def start_requests(self):
        return (scrapy.Request(url)
                for url in ['http://www.google.com', 'http://www.yahoo.com'])

    def parse(self, response):
        self.log('ACESSANDO URL: %s' % response.url)

Se você executar novamente agora o spider com o comando: scrapy runspider minimal.py deverá observar na saída algo semelhante a:

2014-07-26 15:39:56-0300 [minimal] DEBUG: Crawled (200) <GET http://www.google.com.br/?gfe_rd=cr&amp;ei=_PXTU8f6N4mc8Aas1YDABA> (referer: None)
2014-07-26 15:39:56-0300 [minimal] DEBUG: ACESSANDO URL: http://www.google.com.br/?gfe_rd=cr&amp;ei=_PXTU8f6N4mc8Aas1YDABA
2014-07-26 15:39:57-0300 [minimal] DEBUG: Redirecting (302) to <GET https://br.yahoo.com/?p=us> from <GET https://www.yahoo.com/>
2014-07-26 15:39:58-0300 [minimal] DEBUG: Crawled (200) <GET https://br.yahoo.com/?p=us> (referer: None)
2014-07-26 15:39:58-0300 [minimal] DEBUG: ACESSANDO URL: https://br.yahoo.com/?p=us

Para deixar o código do nosso spider ainda mais enxuto, podemos nos aproveitar do funcionamento padrão do método start_requests(): caso você não o defina, o Scrapy irá criar requisições para uma lista de URLs num atributo com o nome start_urls — exatamente o que estamos fazendo. Portanto, podemos reduzir o código acima e manter o mesmo funcionamento, usando:

import scrapy

class MinimalSpider(scrapy.Spider):
    """A menor Scrapy-Aranha do mundo!"""
    name = 'minimal'
    start_urls = [
        'http://www.google.com',
        'http://www.yahoo.com',
    ]

    def parse(self, response):
        self.log('ACESSANDO URL: %s' % response.url)

Como no método parse() mostrado acima, todos os callbacks recebem o conteúdo da resposta da requisição HTTP como argumento (em um objeto Response). É dentro do callback, onde já temos o conteúdo das páginas que nos interessam, que fazemos a extração das informações, ou seja, o data scraping propriamente dito.

excited-scrapy

Callbacks, Requests & Items

As funções registradas como callback associadas às requisições podem retornar um iterable de objetos, em que cada objeto pode ser:

  • um objeto de uma classe scrapy.Item que você define para conter os dados coletados da página
  • um objeto da classe scrapy.Request representando ainda outra requisição a ser acionada (possivelmente registrando outro callback)

Com esse esquema de requisições e callbacks que podem gerar novas requisições (com novos callbacks), você pode programar a navegação por um site gerando requisições para os links a serem seguidos, até chegar nas páginas com os itens que nos interessam. Por exemplo, para um spider que precise extrair produtos de um site de compras navegando em páginas por categorias, você poderia usar uma estrutura como a seguinte:

import scrapy

class SkeletonSpider(scrapy.Spider):
    name = 'spider-mummy'
    start_urls = ['http://www.someonlinewebstore.com']

    def parse(self, response):
        for c in [...]:
            url_category = ...
            yield scrapy.Request(url_category, self.parse_category_page)

    def parse_category_page(self, response):
        for p in [...]:
            url_product = ...
            yield scrapy.Request(url_product, self.parse_product)

    def parse_product(self, response):
        ...

Na estrutura acima, o callback padrão — método parse() — trata a resposta da primeira requisição ao site da loja e gera novas requisições para as páginas das categorias, registrando outro callback para tratá-las — o método parse_category_page(). Este último faz algo parecido, gerando as requisições para as páginas dos produtos, desta vez registrando um callback que extrai os objetos itens com os dados do produto.

Por que eu preciso definir classes para os itens?

O Scrapy propõe que você crie algumas classes que representem os itens que você pretende extrair das páginas. Por exemplo, se você deseja extrair os preços e detalhes de produtos de uma loja virtual, poderia representar uma classe como a seguinte:

import scrapy

class Produto(scrapy.Item):
    descricao = scrapy.Field()
    preco = scrapy.Field()
    marca = scrapy.Field()
    categoria = scrapy.Field()

Como pode ver, são simples subclasses de scrapy.Item, em que você adiciona os campos desejados (objetos da classe scrapy.Field). Você pode usar uma instância dessa classe como se fosse um dicionário Python:

>>> p = Produto()
>>> p['preco'] = 13
>>> print p
{'preco': 13}

A maior diferença para um dicionário tradicional é que um Item, por padrão, não permite você atribuir um valor para uma chave que não foi declarada como campo:

>>> p['botemo'] = 54
...
KeyError: 'Produto does not support field: botemo'

A vantagem de definir classes para os itens é que isso permite você aproveitar outros recursos do framework que funcionam para essas classes. Por exemplo, o recurso de exportação de dados possibilita escolher entre exportar os itens coletados para JSON, CSV, XML, etc. Ou ainda, o esquema de pipeline de itens, que permite você plugar outros processamentos em cima dos itens coletados (coisas tipo, validar o conteúdo, remover itens duplicados, armazenar no banco de dados, etc).

Let’s do some scraping!

Para fazer o scraping propriamente dito, isto é, a extração dos dados da página, é legal você conhecer XPath, uma linguagem feita para fazer consultas em conteúdo XML — base do mecanismo de seletores do framework. Caso não conheça XPath você pode usar seletores CSS no Scrapy, mas encorajamos você a conhecer XPath mais de perto, pois ela permite expressões mais poderosas do que CSS (de fato, as funções de CSS no Scrapy funcionam convertendo os seletores CSS para expressões com XPath).

Você pode testar o resultado de expressões XPath ou CSS para uma página usando o Scrapy shell. Rode o comando:

scrapy shell http://pt.stackoverflow.com

Esse comando dispara uma requisição para a URL informada e abre um shell Python (ou IPython, caso o tenha instalado) disponibilizando alguns objetos para você explorar. O objeto mais importante é o response, que contém a resposta da requisição HTTP e equivale ao argumento response recebido pelas funções de callback.

dog-excited-tem_ate_um_shell

>>> response.url
'http://pt.stackoverflow.com'
>>> response.headers
{'Cache-Control': 'public, no-cache="Set-Cookie", max-age=60',
 'Content-Type': 'text/html; charset=utf-8',
 'Date': 'Fri, 01 Aug 2014 02:27:12 GMT',
 'Expires': 'Fri, 01 Aug 2014 02:28:12 GMT',
 'Last-Modified': 'Fri, 01 Aug 2014 02:27:12 GMT',
 'Set-Cookie': 'prov=cf983b7c-a352-4713-9aa8-6deb6e262b01; domain=.stackoverflow.com; expires=Fri, 01-Jan-2055 00:00:00 GMT; path=/; HttpOnly',
 'Vary': '*',
 'X-Frame-Options': 'SAMEORIGIN'}

Você pode usar os métodos xpath() e css() do objeto response para executar uma busca no conteúdo HTML da resposta:

>>> response.xpath(&amp;amp;amp;quot;//title&amp;amp;amp;quot;) # obtem o elemento &amp;amp;amp;lt;title&amp;amp;amp;gt; usando XPath
[<Selector xpath='//title' data=u'<title>Stack Overflow em Portugu\xeas</titl'>]
>>> response.css('title') # obtem o elemento <title> com seletor CSS
[<Selector xpath=u'descendant-or-self::title' data=u'<title>Stack Overflow em Portugu\xeas</titl'>]
>>> len(response.css('div')) # conta numero de elementos <div>
252

O resultado de chamar um desses métodos é um objeto lista que contém os objetos seletores resultantes da busca e possui um método extract() que extrai o conteúdo HTML desses seletores. Os objetos seletores contidos nessa lista, por sua vez, além de possuírem o método extract() para extrair o conteúdo dele, também possuem métodos xpath() e css() que você pode usar fazer uma nova busca no escopo de cada seletor.

Veja os exemplos abaixo ainda no mesmo Scrapy shell, que ajudam a esclarecer as coisas.

Extrai conteúdo HTML do elemento , acionando método extract() da lista de seletores (repare como o resultado é uma lista Python):

>>> response.xpath("//title").extract()
[u'<title>Stack Overflow em Portugu\xeas</title>']

Guarda o primeiro seletor do resultado numa variável, e aciona o método extract() do seletor (veja como agora o resultado é uma string):

>>> title_sel = response.xpath('//title')[0]
>>> title_sel.extract()
u'<title>Stack Overflow em Portugu\xeas</title>'

Aplica a expressão XPath text() para obter o conteúdo texto do seletor, e usa o método extract() da lista resultante:

>>> title_sel.xpath('text()').extract()
[u'Stack Overflow em Portugu\xeas']

Imprime a extração do primeiro seletor resultante da expressão XPath text() aplicada no seletor da variável title_sel:

>>> print title_sel.xpath('text()')[0].extract()
Stack Overflow em Português

Bem, dominando essa maneira de trabalhar com seletores, a maneira simples de extrair um item é simplesmente instanciar a classe Item desejada e preencher os valores obtidos usando essa API de seletores.

Veja abaixo o código de um spider usando essa técnica para obter as perguntas mais frequentes do StackOverflow brazuca:

import scrapy
import urlparse

class Question(scrapy.Item):
    link = scrapy.Field()
    title = scrapy.Field()
    excerpt = scrapy.Field()
    tags = scrapy.Field()

class StackoverflowTopQuestionsSpider(scrapy.Spider):
    name = 'so-top-questions'

    def __init__(self, tag=None):
        questions_url = 'http://pt.stackoverflow.com/questions'
        if tag:
            questions_url += '/tagged/%s' % tag

        self.start_urls = [questions_url + '?sort=frequent']

    def parse(self, response):
        build_full_url = lambda link: urlparse.urljoin(response.url, link)

        for qsel in response.css("#questions > div"):
            it = Question()

            it['link'] = build_full_url(
                qsel.css('.summary h3 > a').xpath('@href')[0].extract())
            it['title'] = qsel.css('.summary h3 &amp;amp;amp;gt; a::text')[0].extract()
            it['tags'] = qsel.css('a.post-tag::text').extract()
            it['excerpt'] = qsel.css('div.excerpt::text')[0].extract()

            yield it

Como você pode ver, o spider declara uma classe Item com o nome Question, e usa a API de seletores CSS e XPath para iterar sobre os elementos HTML das perguntas (obtidos com o seletor CSS #questions > div), gerando um objeto Question para cada com os campos preenchidos (link, título, tags e trecho da pergunta).

Duas coisas são interessantes que você note na extração feita no callback parse(): a primeira é que usamos um pseudo-seletor CSS ::text para obter o conteúdo texto dos elementos, evitando as tags HTML. A segunda é como usamos a função urlparse.urljoin() combinando a URL da requisição com conteúdo do atributo href para ter certeza que o resultado seja uma URL absoluta.

Coloque esse código em um arquivo com o nome top_asked_so_questions.py e execute-o usando o comando:

scrapy runspider top_asked_so_questions.py -t json -o perguntas.json

Se tudo deu certo, o Scrapy vai mostrar na tela os itens que extraiu e também escrever um arquivo perguntas.json contendo os mesmos itens. No fim da saída, devem aparecer algumas estatísticas da execução, incluindo a contagem dos itens extraídos:

2014-08-02 14:27:37-0300 [so-top-questions] INFO: Dumping Scrapy stats:
    {'downloader/request_bytes': 242,
     'downloader/request_count': 1,
     ...
     'item_scraped_count': 50,
     'log_count/DEBUG': 53,
     'log_count/INFO': 8,
     ...
     'start_time': datetime.datetime(2014, 8, 2, 17, 27, 36, 912002)}
2014-08-02 14:27:37-0300 [so-top-questions] INFO: Spider closed (finished)

question_block_little_dudes-are_belong_to_us

Argumentos aracnídeos

Talvez você notou que a classe do spider tem um construtor aceitando um argumento tag opcional. Podemos passar esse argumento para o spider para obter as perguntas frequentes com a tag python, usando a opção -a:

scrapy runspider top_asked_so_questions.py -t json -o perguntas.json -a tag=python

Usando esse truque você pode fazer spiders mais genéricos, que você passe alguns parâmetros e obtém um resultado diferente. Por exemplo, você poderia fazer um spider para sites que possuam a mesma estrutura HTML, parametrizando a URL do site. Ou ainda, um spider para um blog em que os parâmetros definam um período desejado para extrair posts e comentários.

Juntando tudo

Nas seções anteriores, você viu como fazer crawling com o Scrapy, navegando entre as páginas de um site usando o mecanismo de “navegação” criando requisições com funções callback. Viu também como usar a API de seletores para extrair o conteúdo da página em itens e executar o spider usando o comando scrapy runspider.

Agora, vamos juntar tudo isso em um spider que resolve o problema que apresentamos na introdução: vamos fazer scraping das notícias destaques do Portal Brasil, oferecendo uma opção para informar o assunto (Infraestrutura, Educação, Esporte, etc). Dessa forma, se apenas executar o spider, ele deve fazer scraping das notícias destaques na página inicial; caso informe um assunto, ele deve fazer scraping dos destaques da página daquele assunto.

Nota: Antes de começar a escrever um spider, é útil explorar um pouco as páginas do site usando o navegador e a ferramenta scrapy shell, assim você pode ver como o site é organizado e testar alguns seletores CSS ou XPath no shell. Existem também extensões para os browsers que permitem você testar expressões XPath em uma página: XPath Helper para o Chrome e XPath Checker para o Firefox. Descobrir a melhor maneira de extrair o conteúdo de um site com XPath ou CSS é mais uma arte do que uma ciência, e por isso não tentaremos explicar aqui, mas vale dizer que você aprende bastante com a experiência.

Veja como fica o código do spider:

import scrapy
import urlparse

class Noticia(scrapy.Item):
    titulo = scrapy.Field()
    conteudo = scrapy.Field()
    link = scrapy.Field()
    data_publicacao = scrapy.Field()

class PortalBrasilDestaques(scrapy.Spider):
    name = 'portal-brasil'

    def __init__(self, assunto=None):
        main_url = 'http://www.brasil.gov.br'
        if assunto:
            self.start_urls = ['%s/%s' % (main_url, assunto)]
        else:
            self.start_urls = [main_url]

    def parse(self, response):
        """Recebe a pagina com as noticias destaques, encontra os links
        das noticias e gera requisicoes para a pagina de cada uma
        """
        links_noticias = response.xpath(
            "//div/h1/a/@href"
            " | //div/h3/a/@href[not(contains(.,'conteudos-externos'))]"
        ).extract()

        for link in links_noticias:
            url_noticia = urlparse.urljoin(response.url, link)
            yield scrapy.Request(url_noticia, self.extrai_noticia)

    def extrai_noticia(self, response):
        """Recebe a resposta da pagina da noticia,
        e extrai um item com a noticia
        """
        noticia = Noticia()

        noticia['link'] = response.url
        noticia['titulo'] = response.xpath(&amp;amp;amp;quot;//article/h1/text()&amp;amp;amp;quot;)[0].extract()
        noticia['conteudo'] = response.xpath(
            "string(//div[@property='rnews:articleBody'])")[0].extract()
        noticia['data_publicacao'] = ''.join(
            response.css('span.documentPublished::text').extract()).strip()

        yield noticia

Da mesma forma como antes, você pode rodar o spider com:

scrapy runspider portal_brasil_destaques.py -t json -o destaques-capa.json

E para obter os destaques de cada seção, pode usar comandos como:

scrapy runspider portal_brasil_destaques.py -t json -o destaques-infraestrutura.json -a assunto=infraestrutura
scrapy runspider portal_brasil_destaques.py -t json -o destaques-ciencia-e-tecnologia.json -a assunto=ciencia-e-tecnologia

O código desse spider é bem semelhante ao anterior na sua estrutura, com o suporte a argumentos no construtor.

A principal diferença é que neste, o primeiro callback (método parse()) gera outras requisições para as páginas das notícias, que são tratadas pelo segundo callback: o método extrai_noticia(), que faz a extração do conteúdo da notícia propriamente dita.

A extração de conteúdo nesse último spider também está um pouco mais complexa, considerando a expressão XPath usada para obter os links das notícias, que filtra os links que contenham a string ‘conteudos-externos’ em seu endereço, pois não são links de notícias. Note como aproveitamos que Python concatena strings literais para quebrar a expressão XPath em duas linhas.

Conclusão

Se você chegou até aqui, parabéns! Aqui vai um troféu pra você:

trofeu-scrapy

Agora que você já aprendeu a escrever spiders Scrapy e está habilitado a baixar a Internet inteira no seu computador, tente não ser banido nos sites por aí! 😀

Visite a documentação oficial do Scrapy, tem bastante coisa legal lá, desde um tutorial ensinando a criar projetos Scrapy completos, perguntas frequentes, dicas para crawlings grandes, como depurar um spider, dicas para evitar ser banido e muito mais.

 

Links úteis:

Obrigado pela revisão, Valdir e Zé!
Anúncios

Manipulando strings como se fossem arquivos – StringIO

Há tempos atrás eu precisei extrair dados de pagamentos de arquivos pdf, mas a API que eu estava utilizando para extrair os dados do pdf trabalhava exclusivamente com arquivos. Isto é, a função que convertia o pdf em texto precisava receber um arquivo como argumento, para nele escrever o texto extraído do pdf. Entretanto, criar um arquivo (mesmo que temporário), escrever nele, e por fim, ler os dados que me interessavam a partir dele, não era algo muito conveniente, afinal, tudo que eu precisava eram os dados dos pagamentos para efetivar alguns registros no banco de dados. Foi aí que conheci o StringIO.

StringIO é uma classe Python que representa strings em estruturas que se comportam como se fossem arquivos (com a mesma interface dos objetos file), mas que ficam residentes em memória, como strings comuns. Isso pode ser útil quando lidamos com APIs cuja interface exige objetos file.

Para ter uma ideia melhor do que é a StringIO, veja o exemplo abaixo:

Importante: em Python 3.x, a classe StringIO foi movida para o módulo io (dica do alansteixeira). Para usá-la, faça: from io import StringIO

>>> from StringIO import StringIO
>>> fp = StringIO("uma string qualquer")
>>> fp.readline()
'uma string qualquer'
>>> fp.readline()
''

Também podemos criar um objeto StringIO, passar para alguma função escrever algo nele, e então obter os valores escritos chamando o método getvalue().

>>> fp = StringIO()
>>> def func(f):
>>>     f.write("hello")
>>> func(fp)
>>> fp.getvalue()
'hello'

Quando criamos uma API, às vezes é necessário fornecer uma mesma funcionalidade através de duas funções que diferem nos tipos dos parâmetros esperados: uma recebendo um arquivo e outra recebendo uma string. Isso acontece em algumas APIs conhecidas, como a de manipulação de JSON, que fornece as funções load()/dump() e loads()/dumps(), onde as primeiras recebem um arquivo como parâmetro e as últimas recebem uma string. O que uma classe como a StringIO nos permite fazer é implementar a lógica da função somente na função que recebe o arquivo. Dessa forma, a função que recebe uma string pode empacotá-la em um objeto StringIO e então chamar a função que recebe um arquivo, passando o objeto em questão para ela.

Uma outra coisa interessante que podemos fazer com a StringIO é interceptar a saída do programa que iria para a stdout (descobri isso aqui: http://effbot.org/librarybook/stringio.htm). Para fazer isso, temos que substituir a saída padrão (acessível via sys.stdout) por um objeto StringIO. Assim, tudo que for impresso via print será gravado em um objeto StringIO e não na saída-padrão. Veja:

# -*- encoding:utf-8 -*-
import sys
from StringIO import StringIO

backup_stdout = sys.stdout
sys.stdout = StringIO()

# será escrito em um StringIO
print "hello world"

# pega uma referência ao objeto StringIO antes de restaurar a stdout
fp = sys.stdout
sys.stdout = backup_stdout
print "stdout: " + fp.getvalue()

E, assim como fizemos com a stdout, poderíamos ter feito com a stderr (saída-padrão de erros), para interceptar as mensagens de erro e, quem sabe, analisá-las.

Enfim, sempre que se deparar com uma API que exige arquivos como parâmetros, lembre-se da StringIO caso não esteja a fim de acessar o disco e de lidar com arquivos.

each_cons — percorrendo sequências em N elementos por vez

Recentemente descobri o each_cons, um método interessante da API de Ruby no mixin Enumerable. Esse método permite você percorrer uma sequência de tantos em tantos elementos por vez.

Por exemplo, se você tem uma lista [1, 3, 5, 7, 9] você pode usar o each_cons para percorrer de 2 em 2 elementos:

(1, 3), (3, 5), (5, 7), (7, 9)

ou de 3 em 3 elementos de cada vez:

(1, 3, 5), (3, 5, 7), (5, 7, 9)

Ele ajuda a criar operações que envolvem uma espécie de “janela deslizante” (sliding window), um conjunto de elementos da sequência sobre o qual podemos inferir alguma informação.

Essa abstração se revela bem útil na hora de escrever certos algoritmos e por isso é interessante tê-la como carta na manga.

Implementando em Python

Python não tem um método ou função equivalente ao each_cons na API padrão, mas encontrei algumas sugestões de implementação nas respostas a uma pergunta no StackOverflow. Eis uma delas, que funciona para listas e tuplas:

import itertools
def each_cons(xs, n):
    return itertools.izip(*(xs[i:] for i in xrange(n)))

Para entender como esse código funciona, vamos por partes. Acompanhe:

>>> xs = [1, 2, 3, 4, 5]
>>> xs[0:] # primeiro slice
[1, 2, 3, 4, 5]
>>> xs[1:] # segundo slice
[2, 3, 4, 5]
>>> xs[2:] # terceiro slice
[3, 4, 5]
>>> zip(xs[0:], xs[1:], xs[2:]) # zip dos slices
[(1, 2, 3), (2, 3, 4), (3, 4, 5)]

Os passos acima delineiam o algoritmo dessa implementação do each_cons. Basicamente, obtemos slices da lista na quantidade de elementos que queremos na nossa janela deslizante, e aplicamos zip() neles.

O código mostrado usa uma generator expression para gerar os slices, o truque da estrela nos argumentos para passar os argumentos pro zip e a função izip do módulo itertools que é o equivalente da builtin zip() para generators.

Exemplos de utilidade

1) Encontrar os pares de números com distância entre si maior que 1:

>>> numeros = [1, 2, 3, 5, 9, 10]
>>> [(a, b) for a, b in each_cons(numeros, 2) if b - a > 1]
[(3, 5), (5, 9)]

2) Descobrir se os números de uma lista formam uma sequência:

>>> all([a + 1 == b for a, b in
                        each_cons([1, 2, 3, 4, 5], 2)])
True
>>> all([a + 1 == b for a, b in
                        each_cons([1, 3, 5, 7], 2)])
False

3) Calcular as médias das vendas de cada trimestre:

>>> totais_mensais = [123.45, 54.3, 428, 144.2, 245.45]
>>> [float(a+b+c)/3 for a, b, c in
                        each_cons(totais_mensais, 3)]
[201.91666666666666, 208.83333333333334, 272.55]

Esse tipo de média é conhecido por média móvel, é comum em aplicações financeiras e também é útil para amaciar as linhas de um gráfico.

4) Percorrer as linhas de um arquivo procurando por duplicatas:

>>> linhas = open('arquivo.txt').readlines()
>>> [(num, a) for num,(a,b) in
                  enumerate(each_cons(linhas, 2), 1)
                  if a == b]
[(3, 'tres\n'), (5, 'quatro\n')]

Para um arquivo.txt contendo o texto:

um
dois
tres
tres
quatro
quatro
cinco

Generalizando para generators

A função each_cons que apresentamos acima, apesar de funcionar bem para listas e tuplas, não funciona para generators nem para objetos xrange(). Veja o erro que acontece quando tentamos rodar com um generator:

>>> each_cons((a for a in xrange(10)), 2) # testando com um generator
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in each_cons
TypeError: type object argument after * must be a sequence, not generator

É interessante termos uma solução que funcione também para generators, para o caso de estarmos lidando com um grande volume de dados vindo de um banco de dados, por exemplo.

Para obter isso, basta usar mais algumas funções do módulo itertools na brincadeira:

import itertools
def each_cons(xs, n):
    return itertools.izip(*(itertools.islice(g, i, None)
                          for i,g in
                          enumerate(itertools.tee(xs, n))))

A implementação mostrada acima funciona com sequências (listas, tuplas, strings), generators e objetos xrange() também.

Serialização de Objetos em Python

Sumário

Vez por outra precisamos enviar dados via rede, seja através de uma já tradicional conexão HTTP, ou até mesmo através de um socket UDP cruzão, e é bem comum que esses dados estejam representados em nosso programa através de uma instância de uma classe por nós mesmos definida. No entanto, na hora de enviar esse objeto pela rede, é preciso que tenhamos esses dados representados de forma contínua (diferentemente de simples referências a posições de memória) e, muitas vezes, de uma forma que possa ser lida por um sistema diferente do sistema no qual o objeto foi criado. Para atender a esses requisitos, é necessária a serialização de dados, que trata da representação de objetos ou estruturas de dados em um formato que permita que estas sejam armazenado em um disco (ou enviadas pela rede) para posterior recriação do objeto em memória.

Veja mais sobre serialização no artigo da Wikipedia sobre o assunto.

Como serializar?

Em Python, existem diversos mecanismos disponíveis para serialização de dados. A escolha vai depender do tipo de aplicação exigida. Veremos a seguir alguns mecanismos para serialização de objetos:

Pickle

pickle é um módulo que provê a serialização de objetos Python, transformando objetos quaisquer em sequências de bytes. No exemplo a seguir, vamos serializar uma lista:

>>> import pickle
>>> lista = [1, 'hello!', [1, 2, 3]]
>>> s = pickle.dumps(lista)
>>> print s
"(lp0\nI1\naS'hello!'\np1\na(lp2\nI1\naI2\naI3\naa."
>>> type(s)
<type 'str'>

O método dumps() é o responsável por pegar os dados do objeto em questão e gerar uma sequência de bytes capaz de representar tais dados, de forma que estes possam ser transmitidos pela rede, armazenados em um arquivo, e depois, recuperados para o seu formato original (em nosso caso, um objeto list).

>>> print s
"(lp0\nI1\naS'hello!'\np1\na(lp2\nI1\naI2\naI3\naa."
>>> lista_recuperada = pickle.loads(s)
>>> print lista_recuperada
[1, 'hello!', [1, 2, 3]]
>>> type(lista_recuperada)
<type 'list'>

Já o método loads() é responsável por pegar uma sequência de bytes (representada por uma string) e convertê-la de volta para o objeto Python que originalmente representava (veja o exemplo acima).

O dumps() e o loads() serializam e de-serializam os objetos para strings e a partir de strings, respectivamente. Existem também as versões dos mesmos métodos que lidam com dados serializados que estão armazenados em arquivos. São eles os métodos dump() e load() (sem o s no final do nome).

Para serializar um objeto usando a função dump(), é preciso passar a ela o arquivo no qual queremos que o objeto serializado seja gravado:

>>> pickle.dump(lista, open('data.pkl', 'wb'))

(Repare que passamos uma referência ao arquivo já aberto, não somente o nome do arquivo)

Podemos agora verificar o conteúdo do arquivo data.pkl pelo shell do sistema operacional:

user@host$ cat data.pkl
(lp0
I1
aS'hello!'
p1
a(lp2
I1
aI2
aI3
aa.

Para reconstruir as informações contidas no arquivo em um objeto Python, vamos usar o método load():

>>> recuperada = pickle.load(open('data.pkl'))
>>> print recuperada
[1, 'hello!', [1, 2, 3]]

Barbada, não? Ainda existe também uma implementação do mesmo protocolo no módulo cPickle, que, por ser implementado em C, possui um desempenho muito superior ao do Pickle (de acordo com a documentação, pode ser até 1000 vezes mais rápido). Porém, por não se tratar de código Python, existem algumas restrições nele, como não podermos subclasseá-lo (estendê-lo).

Apesar de ser fácil de utilizar, o pickle serializa os dados em um formato próprio (não-popular com outras linguagens). Sendo assim, o pickle será uma boa opção para serializar objetos para envio/gravação somente para outros programas também escritos em Python.

Serializando objetos customizados

É comum criarmos classes novas em nossos projetos e muitas vezes é necessário serializar instâncias dessas classes. O Pickle pode ser usado para isso também. Veja o exemplo abaixo, onde criamos uma classe Objeto, e em seguida serializamos uma instância dela:

>>> class Objeto(object):
....:
....:    def __init__(self):
....:        self.x = 42
....:        self.s = 'hello, world'
....:        self.l = [1, 2, 3]
>>> o = Objeto()
>>> s = pickle.dumps(o)
>>> print s
ccopy_reg
_reconstructor
p0
(c__main__
Objeto
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
S'x'
p6
I42
sS's'
p7
S'hello, world'
p8
sS'l'
p9
(lp10
I1
aI2
aI3
asb.
>>> obj = pickle.loads(s)
>>> print obj
<__main__.Objeto object at 0x1c12790>

Marshal

O módulo marshal tem uma interface bem semelhante ao pickle, com seus métodos load, loads, dump e dumps. Porém, não é recomendado o seu uso para serialização de objetos em aplicações, por não ser garantida a compatibilidade entre versões do interpretador Python (de acordo com a documentação, esse módulo existe para uso interno no interpretador).

Exemplos de uso:

>>> import marshal
>>> print marshal.dumps(lista)
[ishello![iii
>>> print marshal.loads(s)
[1, 'hello!', [1, 2, 3]]

As informações são serializadas para um formato binário. Não vamos nos alongar muito nesse módulo, visto que ele não deve ser usado em aplicações do dia-a-dia.

Struct

struct é um módulo que faz o meio de campo entre objetos Python e estruturas em C. Agora, ao invés dos métodos dump e load, temos pack e unpack.

>>> import struct
>>> p = struct.pack('i5s', 42, 'hello')
>>> p
'*\x00\x00\x00hello'

O seu uso é mais complicado do que o pickle ou o marshall. Vamos rever a chamada à função pack na linha 2 do trecho de código acima:

pack('i5s', 42, 'hello')

O primeiro argumento passado para a função pack deve ser o formato que irá definir como será a estrutura C que irá armazenar os dados dos nossos objetos Python. No exemplo acima, informamos através da string 'i5s' que a estrutura possui 2 campos:

  • um int (representato por i);
  • uma string (char * em C) com 5 posições ('5s');

Para recriar os objetos Python através do dado serializado em forma de struct, vamos usar a função unpack:

>>> num, s = struct.unpack('i5s', p)
>>> print num
42
>>> print s
'hello'
>>> struct.unpack('i5s', p)
(42, 'hello')

Perceba que a função unpack retorna uma tupla contendo os dados que estavam empacotados dentro da estrutura.

Esse formato também não é o melhor para transporte de dados, pois é dependente da arquitetura do computador. Assim sendo, um pacote empacotado em uma máquina poderia ter problemas para ser desempacotado em uma máquina de arquitetura diferente. Além disso, ele só é capaz de empacotar dados dos tipos mais simples, como os tipos numéricos, strings e booleanos.

JSON

O JSON talvez seja hoje o formato para dados intercambiáveis mais utilizado. Esse formato de dados é muito usado em serviços web, e também para o transporte de dados usando outros protocolos. Como ele já foi visto em outros posts (aqui e aqui), não vamos nos aprofundar muito na sua utilização. Vamos ver somente um exemplo simples:

>>> import json
>>> lista = [1, 'hello!', [1, 2, 3]]
>>> s = json.dumps(lista)
>>> s
'[1, "hello!", [1, 2, 3]]'
>>> print type(s)
<type 'str'>
>>> l = json.loads(s)
>>> l
'[1, u'hello!', [1, 2, 3]]'
>>> print type(l)
<type 'list'>

Como já foi visto nos posts anteriormente referidos, JSON pode ser usado para trafegar estuturas de dados mais complexas, em um formato parecido com o de dicionários Python. Assim, esse post aqui mostra praticamente nada da capacidade desse formato (se quiser saber mais, veja os posts anteriores).

Caso você não conheça o JSON, sugiro fortemente que procure documentação sobre ele, pois é um formato muito bom para tráfego de dados entre ambientes heterogêneos.

Shelve

O shelve é um módulo que provê um tipo de dados com uma interface similar a de um dicionário (chamado de shelf), e que agrega a funcionalidade de persistir esse dicionário em um arquivo para uso posterior. Ou seja, o shelve nos provê dicionários persistentes.

Vamos ver um exemplo:

>>> import shelve
>>> user = shelve.open('data.txt')
>>> user['nickname'] = 'stummjr'
>>> user['city'] = 'Blumenau'
>>> user['twitter'] = 'stummjr'
>>> print user
{'city': 'Blumenau', 'twitter': 'stummjr', 'nickname': 'stummjr'}
>>> user.close()

Perceba que a chamada a shelve.open() abre um shelf (se ainda não existir, ele é criado). Depois, podemos manipular o objeto retornado por esta chamada como se fosse um dicionário. Para persistir os dados no arquivo data.txt, é necessário fechar o shelf em questão (user.close()).

Em outro momento, poderíamos recuperar os dados da seguinte forma:

>>> import shelve
>>> user = shelve.open('data.txt')
>>> print user
{'city': 'Blumenau', 'twitter': 'stummjr', 'nickname': 'stummjr'}
>>> user['blog'] = 'pythonhelp.wordpress.com'
>>> user.close()

Legal, né? O shelve nos dá uma forma bem prática de persistir dados. O exemplo acima mostra um caso bem simplificado, mas poderíamos usar um shelf para armazenar dados de várias pessoas, por exemplo:

>>> users = shelve.open('users.dat')
>>> users['stummjr'] = {'nickname': 'stummjr', 'blog': 'pythonhelp.wordpress.com', 'city': 'Blumenau'}
>>> users['eliasdorneles'] = {'nickname': 'eliasdorneles', 'blog': 'eljunior.wordpress.com', 'city': 'Floripa'}
>>> print users
{
    'eliasdorneles': {
        'blog': 'eljunior.wordpress.com',
        'city': 'Floripa',
        'nickname': 'eliasdorneles'
    },
    'stummjr': {
        'blog': 'pythonhelp.wordpress.com',
        'city': 'Blumenau',
        'nickname': 'stummjr'
    }
}

>>> users.close()
>>> users = shelve.open('users.dat')
>>> print users['stummjr']['blog']
pythonhelp.wordpress.com

Então, qual devemos usar?

Antes de mais nada, fique atento às restrições que cada abordagem possui. Por exemplo, dentro das opções apresentadas acima, a única que possui implementação em uma ampla variedade de linguagens é o JSON. Por outro lado, o shelve nos provê essa facilidade de manipular dados em dicionários e persistí-los no disco depois. Tudo irá depender do seu objetivo ao serializar os dados.

Interoperabilidade é importante? Então vá no JSON de olhos fechados. Quer uma forma de serializar dados para uma única plataforma e que seja econômica no tamanho dos dados? Talvez struct seja a sua escolha. Enfim, leia a documentação e descubra qual das alternativas acima melhor se encaixa em suas necessidades.

glob — listando arquivos de diretórios

De vez en quando, é necessário que obtenhamos uma lista com os arquivos presentes em um diretório, para que, por algum motivo possamos abri-los e fazer alguma operação sobre eles. A forma mais simples de fazer isso é usando a função listdir(), do módulo os.

>>> import os
>>> print os.listdir('/')
['home', 'media', 'lib64', 'tmp', 'mnt', 'opt', 'boot', 'sys', 'srv', 'dev', 'selinux', 'proc', 'root', 'lib32', 'etc', 'bin', 'usr', 'vmlinuz', 'lib', 'run', 'sbin', 'var', 'initrd.img']

Mas, poderíamos querer listar somente os arquivos .py, por exemplo, para abrí-los, em sequência. É aí que entra o módulo glob. Ele permite que listemos os arquivos de um diretório, usando expressões semelhantes as que usamos no shell, como por exemplo: *.py.

>>> import glob
>>> print glob.glob('*.py')
['a.py', 'b.py', 'novo.py']

Assim, se eu quisesse abrir e imprimir o conteúdo de todos os arquivos .py que estão em determinado diretório, excluindo as linhas comentadas, poderia fazer o seguinte:

>>> import glob
>>> for file in glob.glob('*.py'):
...     for line in open(file):
...         if not line.strip().startswith('#'):
...             print line

Mais informações sobre o glob em: http://docs.python.org/library/glob.html

Trabalhando com datas e horas em Python – datetime

Módulos utilizados nesse post:

  • date
  • datetime
  • timedelta

Esse post vai mostrar alguns exemplos simples de como utilizar o módulo datetime [1] para manipularmos, em Python, dados que representam datas. Antes de qualquer coisa, vou enumerar algumas operações que são corriqueiras quando precisamos lidar com datas:

  1. Obter a data atual;
  2. Obter a data antes ou após X dias da data atual;
  3. Calcular a diferença de dias entre duas datas;
  4. Descobrir o dia da semana em que determinada data cai;

Agora vamos ver como resolvê-las, usando o módulo datetime.

1. Obter a data atual

O módulo datetime possui dentro de si uma classe date [2] definida. Nessa classe, existem alguns métodos para manipulação de datas, como a função today, que retorna um objeto do tipo datetime.date.

>>> from datetime import date
>>> hj = date.today()
>>> print hj
2012-07-10
>>> print hj.day
10
>>> print hj.month
07
>>> print hj.year
2012

2. Obter a data há ou daqui a X dias

Para isso, iremos converter primeiramente a nossa data em um número ordinal, através do método toordinal(), que nos retorna a quantidade de dias passados desde o dia 1/1/1 até a data recebida como argumento. Depois disso, basta somar (ou subtrair) a esse número inteiro o número de dias da diferença que queremos calcular e então reconverter o inteiro para data, através do método fromordinal(). Abaixo, obtivemos a data a daqui exatos 45 dias.

>>> from datetime import date
>>> hj = date.today()
>>> print hj.toordinal()
734694
>>> futuro = date.fromordinal(hj.toordinal()+45) # hoje + 45 dias
>>> print futuro
2012-08-24

3. Calcular a diferença de dias entre datas

Para realizar essa, vamos obter as duas datas entre as quais queremos saber o intervalo de dias e depois usar o operador de subtração (-) para fazer a operação. O operador subtração, quando aplicado a datas, retorna um objeto do tipo timedelta, contendo a diferença entre as datas. Esse objeto possui um atributo chamado days, que obviamente nos dá o número de dias representados pelo delta.

>>> from datetime import date
>>> hj = date.today()
>>> print hj.toordinal()
734694
>>> futuro = date.fromordinal(hj.toordinal()+45) # hoje + 45 dias</pre>
>>> diferenca = futuro - hj
>>> print diferenca.days
45

4. Descobrir o dia da semana de uma data

Essa é fácil. Após construir uma data, podemos chamar o método weekday() do objeto date. Ele retornará um número inteiro entre 0 (represendo segunda-feira) e 6 (representando domingo).

>>> from datetime import date
>>> hj = date.today()
>>> print hj.weekday()
1
Para que apareça o dia da semana por extenso, em português, podemos usar uma tupla para armazenar os dias da semana, de acordo  com os valores retornados pelo método weekday().
>>> from datetime import date
>>> hj = date.today()
>>> dias = ('Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado', 'Domingo')
>>> print "Hoje é", dias[hj.weekday()]
Hoje é, Terça-feira

Os módulos datetime e date fazem muito mais do que o que foi mostrado aqui. Agora, acesse a documentação dos módulos e faça você mesmo alguns testes.

[1] http://docs.python.org/library/datetime.html

[2] http://docs.python.org/library/datetime.html#datetime.date

Um contador de elementos em sequências

Às vezes precisamos realizar a contagem do número de aparições de determinados elementos em uma sequência, como uma string ou uma lista. Por exemplo, quero saber quantas vezes cada letra apareceu em uma string. Como fazer isso em Python? Primeiro, vamos fazer “na mão” e depois vamos conhecer uma solução “pronta”.

def conta_ocorrencias(s):
    ocorrencias = {}
    for c in s:
        if c in ocorrencias:
            ocorrencias[c] = ocorrencias[c] + 1
        else:
            ocorrencias[c] = 1
    return ocorrencias

A solução acima apresentada utiliza um dicionário armazenando como chaves as letras encontradas e, como valor para cada letra, a quantidade de ocorrências desta. Percorremos a string s letra por letra e, se a letra atual já estiver sendo usada como chave no dicionário ocorrencias, o valor correspondente a tal letra é incrementado em um (ou seja, encontramos mais uma ocorrência da letra na string recebida). Se a letra ainda não estiver aparecendo como chave do dicionário, então é criada uma entrada neste com a letra sendo usada como chave e o valor associado 1 (ocorrencias[c] = 1). Vamos analisar o teste feito dentro do for:

...
        if c in ocorrencias:
            ocorrencias[c] = ocorrencias[c] + 1
        else:
            ocorrencias[c] = 1
...

Esse teste é necessário porque se tentarmos acessar uma chave inexistente de um dicionário, é retornado um KeyError. Assim, precisamos testar para verificar se a letra atual já foi anteriormente inserida no dicionário ou não. Se foi, daí sim podemos incrementar o valor associado. Se não foi, daí temos que incluir tal valor com o número 1 associado.

Para simplificar isso, podemos usar o módulo collections [1]. Esse módulo nos fornece um tipo especial de dicionário chamado de defaultdict [2]. Esse dicionário permite que especifiquemos, ao construir um dicionário, uma função que será chamada para retornar um valor padrão para quando a chave solicitada não existir no dicionário. Com ele, é possível fazer o seguinte:

    ocorrencias = collections.defaultdict(int)
    ...
        ocorrencias[c] = ocorrencias[c] + 1
    ...
A função int(), quando chamada sem argumentos, retorna 0. Como passamos ela ao construtor do dicionário ocorrências, é ela que será chamada quando houver um acesso a uma chave inexistente. Isso possibilita que usemos esse valor para fazer a soma no código acima. Na primeira vez que é executado para determinado caractere, o código acima será executado como se fosse:
    ocorrencias[c] = 0 + 1
Nas vezes seguintes, ao invés de 0 (valor obtido ao tentarmos acessar o valor de uma chave inexistente), teremos como valor o número atual de ocorrências para o caractere contido na variável c. O código completo segue abaixo:
import collections
def conta_ocorrencias(s):
    ocorrencias = collections.defaultdict(int)
    for c in s:
        ocorrencias[c] = ocorrencias[c] + 1
    return ocorrencias

E a solução “pronta”?

O jeito mais simples de fazer a contagem de ocorrências dos elementos de uma sequência é através da classe Counter [3], também presente no módulo collections. Vamos ver um exemplo de utilização:

>>> import collections
>>> s = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut"
>>> c = collections.Counter(s)
>>> print c.most_common(5)
[(' ', 13), ('i', 11), ('e', 8), ('t', 8), ('d', 7)]

Um dos métodos mais interessantes é o most_common() [4], que retorna uma lista contendo os elementos mais comuns na sequência. Se passarmos um número n como argumento, ele irá retornar os n valores mais comuns na sequência, como no exemplo acima.

Além do Counter e do defaultdict, o módulo collections fornece várias outras estruturas úteis, que veremos em posts futuros.

[1] http://docs.python.org/library/collections.html

[2] http://docs.python.org/library/collections.html#collections.defaultdict

[3] http://docs.python.org/library/collections.html#collections.Counter

[4] http://docs.python.org/library/collections.html#collections.Counter.most_common