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

Sugestão de livro: Two Scoops of Django

2scoops

O Django é uma baita ferramenta que auxilia muitos desenvolvedores a concretizar seus projetos web com agilidade e simplicidade impressionantes. A documentação do framework é bastante vasta. São blogs de desenvolvedores, listas de email, livros bem completos, a trilha no StackOverflow, além de muitos e muitos projetos abertos no GitHub e BitBucket, e é claro, a excelente e completíssima documentação oficial. Até aí, tudo perfeito. Material para iniciantes querendo aprender Django existe de monte, mas quando as dúvidas começam a ficar um pouco mais específicas, ou questões relacionadas à boas práticas em projetos Django, a coisa começa a ficar mais escassa. Felizmente para nós, Djangonautas, o Daniel Greenfeld e a Audrey Roy começaram a resolver um pouco desse problema escrevendo o excelente Two Scoops of Django: Best Practices for Django 1.5.

O livro não é um tutorial e tampouco uma documentação exaustiva do Django, mas sim uma valiosa coleção de dicas e conselhos sobre boas práticas em projetos Django, atualizada para a versão 1.5. Já nos primeiros capítulos, fiquei com aquela sensação de “putz, eu tô fazendo tudo do jeito mais difícil nos meus projetos!”. Os autores vão mostrando os problemas e apresentando as soluções de uma forma bem prática, passando dicas, alertas, e, o que achei mais legal de tudo, as Package Tips, que são dicas sobre pacotes de terceiros que os autores costumam usar em seus projetos e que são uma verdadeira mão-na-roda.

Talvez você esteja pensando consigo próprio: “ah, eu já vi várias coisas dessas espalhadas pela web…”. Aí é que está o ponto principal, pois os autores pegaram a vasta experiência que possuem e compilaram uma série de dicas em um só lugar. E quando falo de dicas, não pense que são trechinhos pequenos de texto com links para outros recursos. Pelo contrário, os autores se preocuparam em explicar bem o porquê das coisas, sem cansar o leitor.

Outra coisa que achei interessante é que, diferentemente de um monte de livros que a gente vê por aí, parece que os autores deixaram de lado a preocupação de que o livro deles possa ficar obsoleto por passar dicas pontuais de pacotes específicos para resolver determinados problemas. Me parece que muitos autores limitam a abrangência de seus livros por medo de abordar um assunto mais específico, que poderia sofrer mudanças em breve (talvez o sentimento de estar sendo eternizado pelo livro deixe alguns autores meio confusos). Os autores do Two Scoops of Django não se preocuparam muito com isso e até se comprometeram em publicar erratas caso alguns elementos sofram mudanças nos próximos tempos.

O livro em si é muito bem organizado, com um formato muito bom para a leitura. Os autores se preocuparam MUITO e conseguiram fazer um layout excelente para ser lido em e-readers. Eu comprei a versão para Kindle, e esse é o primeiro livro técnico que leio em que não é preciso ficar diminuindo o tamanho da fonte para conseguir ler decentemente os trechos de código. Parabéns aos autores pela preocupação com os leitores da versão digital do livro!

O conteúdo

Não vou fazer aqui uma análise completa do livro. Vou listar apenas algumas coisas importantes que aprendi com o livro:

  • Como estruturar meus projetos Django;
  • Que as class-based-views são muito fáceis de usar;
  • Que na versão 1.5 do Django ficou barbada estender o modelo User;
  • Que realizar processamento nos templates é roubada;
  • Que dá pra manter configurações (settings.py) específicas para diferentes ambientes;
  • Que import relativo existe; (isso mesmo, eu não conhecia esse recurso)
  • Que select_related() quebra um galhão pra consultas grandes;
  • E muitas outras coisas! (muitas mesmo!) 🙂

Enfim, o conteúdo do livro é fantástico! Recomendo a todo mundo que tem um pouquinho de experiência com o Django que compre e leia esse livro. Não é preciso ser especialista no framework para se aproveitar do conteúdo dele. Se você está na dúvida se o livro é adequado para você, dê uma conferida no conteúdo dele na página oficial.

Eu recomendo!

De 0 a 10, dou nota 10 para esse livro. Li ele apenas uma vez, mas já vou começar a reler para fixar bem as dicas, pois são muitas coisas novas.

Se quiser seguir minha dica, estão à venda as versões impressa e digital do livro. Comprando direto pela página do livro, é possível comprar o pacote digital (formatos PDF, mobi e ePub, tudo DRM-free) por 17 dólares (preço em 22/06/2013). Na Amazon americana, está à venda a versão impressa. E ainda, se quiser comprar pela Amazon Brasil, eles estão vendendo a versão para Kindle.

Se ainda estiver na dúvida se o livro vale mesmo a pena, leia os reviews dos leitores na Amazon.

pydoc – Documentação de módulos Python

Em mais um post sobre documentação, vamos conhecer hoje o pydoc [1]. O pydoc é um módulo que gera documentação (em formato amigável) sobre módulos Python, a partir dos docstrings que estão presentes nestes. A documentação pode ser apresentada em mais de uma forma. Uma delas, é o formato manpage [2]. Para ver a documentação de um módulo em formato manpage, podemos invocar o pydoc através de um shell do sistema operacional:

$ pydoc timeit

A documentação pode ser vista na tela abaixo:

Uma vantagem óbvia disso é não precisar abrir um shell python para apenas tirar uma dúvida sobre algum método. Além da visualização manpage-style, o pydoc também gera documentação em formato HTML.

$ pydoc -w timeit
wrote timeit.html

O comando acima gera um arquivo com a extensão html com o nome do módulo em questão, para ser visualizado em um browser.

Além de mostrar a documentação em formato manpage e de gerar arquivos html com a documentação, também é possível “levantar” um servidor web que irá servir as páginas html com a documentação dos módulos disponíveis no sistema. Para isso, basta chamar o pydoc com a opção -p, passando o número da porta onde o servidor irá escutar como argumento.
$ pydoc -p 8080
pydoc server ready at http://localhost:8080/

Acessando no browser o endereço localhost:8080, é possível navegar nas páginas de documentação dos módulos.

Por fim, existe outra opção que abre uma interface gráfica onde o usuário poderá procurar pelo módulo sobre o qual deseja ver a documentação e abrir o html correspondente no browser.

Na figura acima, ao dar duplo-clique sobre um dos itens retornados pela busca, é aberto no browser o documento html correspondente ao módulo escolhido.

É claro que, além dessas formas de visualizar a documentação dos módulos, também podemos usar o tradicional builtin help() para ver a documentação dentro do shell Python.

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

[2] https://en.wikipedia.org/wiki/Man_page

Acesso fácil à documentação com ipython

Para quem não sabe, quando desejamos informações de ajuda sobre algum módulo/método/builtin, podemos invocar o builtin help(). Por exemplo, quero saber detalhes sobre a função len():

>>> help(len)
Help on built-in function len in module __builtin__:
len(...)
 len(object) -> integer
 Return the number of items of a sequence or mapping.
(END)

Isso irá ler o atributo __doc__ do objeto em questão. Mas, em minha opinião de preguiçoso, é um pouco chato sempre chamar a função help(), passando como argumento o objeto do qual desejamos mais informações. Se você concorda comigo, saiba que o ipython [1] fornece um “atalho” bem simples para acessarmos a documentação de determinado objeto. Basta adicionar um ponto de interrogração ao final do nome do objeto, que o ipython mostra o texto de ajuda. Veja a figura abaixo:

iPython mostrando informações sobre método.

Além da interrogação simples (?), podemos utilizar também a interrogação dupla (??) para obter informações adicionais sobre o método/módulo, incluindo o seu código-fonte, quando disponível. Veja abaixo:

iPython mostrando informações extra

É claro que as vantagens do ipython vão muito além desse recurso. Mas, as outras ficam para outro post.

[1] http://ipython.org/

Testando código – doctest

Em um post antigo, vimos o que são docstrings e para que servem. No post de hoje, veremos como utilizá-las para especificar testes sobre nossas funções.

Considere que estamos escrevendo um programa, e precisamos escrever uma função que calcule o dobro de um determinado número. Então, escrevemos o código:

def dobro(x):
    result = x * 2
    return result

Agora, para saber se está funcionando corretamente, vamos testar chamando a função dobro, passando como argumento alguns valores para os quais nós conhecemos o valor de retorno que a função deveria retornar:

>>> dobro(1)
2
>>> dobro(2)
4
>>> dobro(3)
6
>>> dobro(4)
8
>>> dobro(5)
10

Verificando visualmente os resultados, nos parece que a função está funcionando corretamente (e está, de fato). Daí, vem um colega e diz que não é necessário criarmos uma variável result, podendo retornar diretamente o retorno da expressão x * 2. Daí vamos editar nosso código para que se pareça com o código abaixo:

def dobro(x):
    return x * 2

Agora, teremos que rodar novamente os 5 testes acima e verificar, linha por linha, se o resultado está correto. Isso é chato, e também, em casos mais complexos, muito propenso a erros. Para não precisar rodar manualmente os testes e verificar visualmente se o resultado está correto, vamos utilizar o módulo doctest [1]. Com esse módulo, basta que colemos os testes (realizados no terminal interativo) dentro da docstring da função correspondente. Por exemplo, para aplicar doctests na nossa função dobro, vamos modificar o código para que se pareça com o código abaixo:

def dobro(x):
	"""
	Funcao que retorna o dobro do valor passado como argumento.
	>>> dobro(1)
	2
	>>> dobro(2)
	4
	>>> dobro(3)
	6
	>>> dobro(4)
	8
    >>> dobro(5)
    10
	"""
	return x * 2

Agora, vou executar os testes, chamando o módulo doctest pela linha de comando de um shell Linux (poderia ser por um terminal Windows/Mac):

user@host:~/ $ python -m doctest -v t.py
Trying:
 dobro(1)
Expecting:
 2
ok
Trying:
 dobro(2)
Expecting:
 4
ok
Trying:
 dobro(3)
Expecting:
 6
ok
Trying:
 dobro(4)
Expecting:
 8
ok
Trying:
 dobro(5)
Expecting:
 10
ok
1 items had no tests:
 t
1 items passed all tests:
 5 tests in t.dobro
5 tests in 2 items.
5 passed and 0 failed.
Test passed.

Ao chamar o módulo doctest pela linha de comando, passando como entrada o arquivo que contém a função dobro() (t.py, no caso), esse módulo vai varrer o código-fonte atual em busca de funções que contenham docstrings cujo conteúdo seja similar ao que aparece em uma tela de terminal interativo do Python. Ao se deparar com tal conteúdo (como por exemplo: >>> dobro(1)), o doctest vai executar tal código e ver se o resultado é igual ao que aparece na linha seguinte na docstring. Assim, se tivermos feito alguma alteração que quebrou o funcionamento da função, ao rodar o doctest, esse erro será acusado. Vamos ver isso na prática. Vou alterar a expressão de retorno da função dobro para x * 3:

def dobro(x):
    """
    Funcao que retorna o dobro do valor passado como argumento.
    >>> dobro(1)
    2
    >>> dobro(2)
    4
    >>> dobro(3)
    6
    >>> dobro(4)
    8
    >>> dobro(5)
    10
    """
    return x * 3

Agora, vou rodar o código acima e vamos ver o resultado.


user@host:~/ $ python -m doctest -v t.py
 Trying:
 dobro(1)
 Expecting:
 2
 **********************************************************************
 File "t.py", line 4, in t.dobro
 Failed example:
 dobro(1)
 Expected:
 2
 Got:
 3
 Trying:
 dobro(2)
 Expecting:
 4
 **********************************************************************
 File "t.py", line 6, in t.dobro
 Failed example:
 dobro(2)
 Expected:
 4
 Got:
 6
 Trying:
 dobro(3)
 Expecting:
 6
 **********************************************************************
 File "t.py", line 8, in t.dobro
 Failed example:
 dobro(3)
 Expected:
 6
 Got:
 9
 Trying:
 dobro(4)
 Expecting:
 8
 **********************************************************************
 File "t.py", line 10, in t.dobro
 Failed example:
 dobro(4)
 Expected:
 8
 Got:
 12
 Trying:
 dobro(5)
 Expecting:
 10
 **********************************************************************
 File "t.py", line 12, in t.dobro
 Failed example:
 dobro(5)
 Expected:
 10
 Got:
 15
 1 items had no tests:
 t
 **********************************************************************
 1 items had failures:
 5 of 5 in t.dobro
 5 tests in 2 items.
 0 passed and 5 failed.
 ***Test Failed*** 5 failures.
 

Ao final, podemos ver um mini-relatório da execução.

Assim, sempre que for preciso alterar minha função dobro(), não precisarei rodar e conferir os resultados manual e visualmente. Basta manter a doctring dentro da função e executar o o módulo doctest passando como entrada o arquivo que contém minha função, que as verificações serão realizadas por este.

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

Acentuação em programas Python

Python, por padrão, interpreta seus programas usando a codificação de caracteres ASCII. Tal codificação não é capaz de representar os caracteres acentuados (á, á, â, ã, …), que são muito utilizados na nossa língua portuguesa. Então, como escrever um comentário no código utilizando a grafia correta, sem precisar escrever ‘é’ como ‘eh’? Como escrever mensagens a serem passadas ao usuário utilizando acentuação nos caracteres? Como evitar que a mensagem de erro abaixo apareça como saída do programa?

File "foo.py", line 2
SyntaxError: Non-ASCII character '\xc3' in file foo.py on line 2, but no
encoding declared;see http://www.python.org/peps/pep-0263.html for details

É preciso indicar ao Python qual é a codificação de caracteres que nosso arquivo de código está utilizando. Isso pode ser feito incluindo a seguinte linha de código no cabeçalho do arquivo.

# coding=<nome da codificação>

ou:

# -*- coding: <nome da codificação> -*-

As distribuições Linux, em sua maioria, utilizam a codificação UTF-8 para representação de caracteres. Assim, para utilizar caracteres com acentuação em um arquivo Python, usando Linux, é preciso adicionar ao começo* do arquivo:

# coding=UTF-8

ou:

# -*- coding: UTF-8 -*-

* A linha que indica a codificação pode ser a primeira linha do arquivo, ou a segunda, caso o arquivo contenha a indicação do binário do interpretador na primeira linha. Ex.:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

Mais informações em: http://www.python.org/dev/peps/pep-0263

docstrings

Docstrings são strings que inserimos dentro de nosso código Python com o intuito de fornecer uma explicação sobre o funcionamento deste. Essa string deve ser colocada como a primeira linha da definição de uma classe, método ou função.

O texto representado por tal string será apresentado quando for executado o comando help() utilizando como entrada a função onde a docstring está inserida. Considere o exemplo da função dobro:

def dobro(x):
    """Esta função retorna o dobro do número x"""
    return x*2

Quando executarmos o comando help sobre a função acima, receberemos o conteúdo da string colocada como a primeira linha:

>>> help(dobro)
Help on function dobro in module __main__:

dobro(x)
    Esta função retorna o dobro do número x

A string inserida como docstring também pode ser acessada através do atributo __doc__ daquela função:

>>> print dobro.__doc__
Esta função retorna o dobro do número x

Isso vale também para classes e métodos:

 class Data:
     """Classe utilizada para a representação de datas.
     As datas são representadas no formato dia, mês e ano.
     """
     ...
     def passaDia(self):
         """Acrescenta um dia na data."""
         ...

Ao invocarmos o comando help sobre a classe Data, obteremos o seguinte resultado:

>>> help(Data)
Help on class Data in module __main__:

class Data
 |  Classe utilizada para a representação de datas.
 |  As datas são representadas no formato dia, mês e ano.
 | 
 |  Methods defined here:
 | 
 |  passaDia(self)
 |      Acrescenta um dia na data.

Dúvidas sobre o que e como colocar em uma docstring? Consulte a PEP 257 (PEP é a sigla referente a: Python Enhancement Proposals).