Templates de páginas com Python e Google App Engine

Sumário

def get(self):
    self.response.out.write("<html><head><title>Lista de Comentários</title></head><body>")
    for c in Comentario.objects.all():
        self.response.out.write("""<p>Nome: %s</p><p>Email: %s</p>
            <p>URL: %s</p><p>Comentário: %s</p>""" % 
                (c.nome_usuario, c.email, c.url, c.comentario))
    self.response.out.write("</body></html>")

Diga aí, o que você achou da implementação do método get() no código acima? Esse monte de HTML embutido em strings no código Python fica horrível, não?

Pois então, este post vai mostrar uma forma mais adequada para usar conteúdo em HTML no seu projeto web: os templates (ou, em bom português, modelos).

Templates são HTMLs açucarados

Templates, em um projeto Web com Python + Google App Engine, nada mais são do que arquivos HTML acrescidos de algumas marcações específicas para representar dados dinâmicos.

<html>
    <head><title></title></head>
    <body>
        <p>Nome: {{ nome }}</p>
        <p>Email: {{ email }}</p>
        <p>URL: {{ url }}</p>
        <p>Comentário: {{ comentario }}</p>
    </body>
</html>

Observe que em meio ao HTML, temos 4 campos diferentes: {{ nome }}, {{ url }}, {{ email }} e {{ mensagem }}. Em um template, sempre que houver uma string do tipo {{ nome }}, o mecanismo de templates tentará substituir tal string pelo valor da variável nome, de forma que esse valor apareça no HTML final (depois veremos como passar valores aos templates).

Além de campos para simples substituição pelo valor de variáveis, o mecanismo de template padrão do GAE fornece construções para fazer repetição, seleção, dentre outras coisas. Veja:

<html>
    <head><title></title></head>
    <body>
        {% for m in mensagens %}
            {% if m.publico %}
                <p>Nome: {{ m.nome_usuario }}</p>
                <p>Email: {{ m.email }}</p>
                <p>URL: {{ m.url }}</p>
                <p>Comentário: {{ m.comentario }}</p>
                <hr>
            {% endif %}
        {% endfor %}
    </body>
</html>

Tudo que o template acima precisaria receber é uma variável chamada comentarios, que fosse uma sequência de objetos do tipo Comentario.

Desse modo, separamos a parte de apresentação da parte lógica do nosso projeto. O código fica mais organizado, limpo e reusável. Vamos ver agora como acoplar esse mecanismo de templates ao nosso projetinho web do post anterior.

Juntando tudo

Precisamos, antes de mais nada, salvar o nosso arquivo que contém o template em um arquivo .html. Vamos chamá-lo de modelo.html:

<html>
    <head><title></title></head>
    <body>
        <h1>Comentários</h1>
        {% for m in mensagens %}
            <hr>
            <h2>Comentário de <em>{{ m.nome_usuario }}</em></h2>
            <p><strong>Email:</strong> {{ m.email }}</p>
            <p><strong>URL:</strong> {{ m.url }}</p>
            <p><strong>Comentário:</strong> {{ m.comentario }}</p>
        {% endfor %}
        <hr>
        <form method="POST" action="/">
            Nome: <p><input type="text" name="nome_usuario"></p>
            Email: <p><input type="text" name="email"></p>
            URL: <p><input type="text" name="url"></p>
            Comentario:<p><textarea name="comentario" cols="40" rows="10"></textarea></p>
            <button type="submit">Enviar</button>
        </form>
    </body>
</html>

Veja que nosso template primeiro renderiza os comentários e, no final deles, apresenta o formulário para envio de uma nova mensagem. Vamos agora reescrever o nosso método get() do post anterior. O que antes era:

def get(self):
    self.response.out.write(html)
    self.response.out.write('<ul>')
    for msg in Mensagem.all():
        self.response.out.write('<li>' + unicode(msg.comentario) + '</li>')
    self.response.out.write('</ul>')

Passará a ser:

def get(self):
    path = os.path.join(os.path.dirname(__file__), 'modelo.html')
    valores = {'mensagens': Mensagem.all()}
    self.response.out.write(template.render(path, valores))

Chamamos a função render() para gerar uma string com conteúdo HTML, com base no arquivo de template modelo.html (passado através da variável path) e do conjunto de valores a ser inserido de forma dinâmica no nosso HTML (dicionário valores). Lembra que, dentro do template, nós referenciávamos uma variável mensagens?

...    
{% for m in mensagens %}
    <hr>
    <h2>Comentário de <em>{{ m.nome_usuario }}</em></h2>
    <p><strong>Email:</strong> {{ m.email }}</p>
    <p><strong>URL:</strong> {{ m.url }}</p>
    <p><strong>Comentário:</strong> {{ m.comentario }}</p>
{% endfor %}
...

No exemplo acima, mensagens terá o conteúdo de Mensagem.all(), que é uma sequência contendo todas as mensagens existentes no datastore. Assim, esse trecho do template irá percorrer as mensagens e imprimir os valores dos campos nome_usuario, email, url e comentario nos locais correspondentes do HTML. Tudo isso é feito no lado servidor, ou seja, o navegador irá receber o HTML já pronto com os valores nos locais adequados. Um exemplo de trecho de HTML gerado no servidor poderia ser:

...
    <hr>
    <h2>Comentário de <em>Pedro Pedreira</em></h2>
    <p><strong>Email:</strong> [email protected]</p>
    <p><strong>URL:</strong> https://pythonhelp.wordpress.com/</p>
    <p><strong>Comentário:</strong> Um comentário qualquer. Valeu!</p>
...

Onde Pedro Pedreira é o valor de {{ m.nome_usuario }}, e assim por diante, para um registro qualquer.

Caso não tenha visto o post anterior, veja o código do projeto para então aplicar as mudanças para inclusão do template, ou então siga a leitura neste post para ver o projeto completo.

O projeto completo

Nosso projeto está localizado no diretório gaetest/ e possui a seguinte estrutura:

gaetest/
    app.yaml
    handlers.py
    modelo.html

Agora vamos ver o conteúdo dos arquivos:

app.yaml:

application: gaetest
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: handlers.py

handlers.py:

import os
import webapp2
from google.appengine.ext import db
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp.util import run_wsgi_app


class PostHandler(webapp2.RequestHandler):

    def get(self):
        path = os.path.join(os.path.dirname(__file__), 'modelo.html')
        valores = {'mensagens': Mensagem.all()}
        self.response.out.write(template.render(path, valores))

    def post(self):
        msg = Mensagem(
            nome_usuario=self.request.get('nome_usuario'),
            url=self.request.get('url'),
            email=self.request.get('email'),
            comentario=self.request.get('comentario')
        )
        msg.put()
        self.redirect('/')


class Mensagem(db.Model):
    nome_usuario = db.StringProperty(required=True)
    url = db.LinkProperty()
    email = db.EmailProperty()
    comentario = db.TextProperty(required=True)


mapeamento = [
    ('/', PostHandler),
]
app = webapp2.WSGIApplication(mapeamento)
run_wsgi_app(app)

modelo.html:

<html>
    <head><title></title></head>
    <body>
        <h1>Comentários</h1>
        {% for m in mensagens %}
            <hr>
            <h2>Comentário de <em>{{ m.nome_usuario }}</em></h2>
            <p><strong>Email:</strong> {{ m.email }}</p>
            <p><strong>URL:</strong> {{ m.url }}</p>
            <p><strong>Comentário:</strong> {{ m.comentario }}</p>
        {% endfor %}
        <hr>
        <form method="POST" action="/">
            Nome: <p><input type="text" name="nome_usuario"></p>
            URL: <p><input type="text" name="url"></p>
            Email: <p><input type="text" name="email"></p>
            Comentario:<p><textarea name="comentario" cols="40" rows="10"></textarea></p>
            <button type="submit">Enviar</button>
        </form>
    </body>
</html>

Executando o projeto

Salve o conteúdo acima nos arquivos correspondentes dentro da pasta gaetest e então rode o servidor local de testes. Para isso, vá até a pasta onde você descompactou o pacote na instalação do GAE e rode o seguinte comando:

./dev_appserver.py /caminho/para/o/projeto/gaetest/

Onde /caminho/para/o/projeto/gaetest/ é o caminho para o diretório onde está o projeto que queremos executar. Após isso, acesse o projeto no seu navegador através da URL localhost:8080.

Boas práticas

Boas práticas são muito importantes na hora de desenvolver um projeto de software. À medida que o projeto vai crescendo, a urgência por uma boa organização dos arquivos também cresce. É comum termos projetos com uma estrutura parecida com essa:

project/
    app.yaml
    handlers.py
    models.py
    templates/
        base.html
        form.html
        list.html

Veja que temos agora dois arquivos .py:

  • handlers.py: as classes que irão manipular nossas requisições;
  • models.py: as classes que representam os modelos de dados em nosso projeto (como a classe Mensagem no nosso exemplo);

Além disso, temos uma pasta específica para conter os templates e dentro dela ficam todos os templates que precisarmos.

Você é livre para definir a estrutura que quiser, mas é bom seguir um padrão que seja minimamente organizado.

Outros mecanismos de template

O mecanismo de template que vimos neste post é o que já vem incluso com o pacote do GAE, que por sua vez é o mecanismo do Django. Além desse, existem vários outros mecanismos que podem ser usados, como o jinja2, por exemplo.

Leia mais

Webscraping em Python

No post anterior, vimos como fazer requisições HTTP utilizando a excelente requests. A maioria dos documentos disponíveis na web estão em formato HTML, o que não é um grande problema quando acessamos via browser, pois ele é quem faz o trabalho sujo de renderizar o documento pra gente. Mas, se quisermos extrair apenas determinados trechos de informações que podem nos ser úteis, precisaremos percorrer o documento HTML em busca dessas informações, o que não é nada fácil, visto que além de pouca gente seguir os padrões para composição de documentos HTML, muitas vezes os criadores dos sites de onde queremos obter as informações não estão nem um pouco preocupados em classificar as informações que ali disponibilizam, deixando os dados soltos em uma barafunda de table, div, p, etc.

Nesse post vamos ver como fazer Web Scraping, que é a extração de dados de páginas web.

Scraping no reddit

Como já mencionei no post anterior, o reddit é um agregador de conteúdo submetido pelos usuários, e tem vários subreddits voltados a assuntos específicos. Por exemplo, o subreddit programming é um dos meus preferidos, pois além de apresentar as notícias mais relevantes, também apresenta artigos técnicos e discussões de altíssima qualidade. Veja a imagem abaixo, que contém a página inicial desse subreddit:

http2.1

O objetivo desse post é mostrar como fazer um extrator dos principais links submetidos pelos usuários ao reddit programming, usando uma técnica diferente daquela mostrada no final do post anterior.

O resultado esperado é um programinha em linha de comando que gere a seguinte saída:

Descrição do link - URL do link
Descrição do link - URL do link
Descrição do link - URL do link
Descrição do link - URL do link
Descrição do link - URL do link

A primeira coisa que temos que fazer quando queremos desenvolver um extrator de informações da web é entender a estrutura dos documentos HTML de onde vamos retirar a informação. Vamos dar uma olhada no código-fonte do reddit:

http2.2

Na imagem acima, estou usando o developer tools do Chrome para facilitar a visualização das informações do que precisamos no HTML (ctrl+alt+c abre essa ferramenta). Veja que ele já faz o highlighting das informações tanto no código-fonte quanto no documento renderizado. Assim é moleza descobrir o que precisamos extrair do HTML, não? Podemos ver na imagem acima que cada link submetido pelos usuários fica dentro de um elemento p com um atributo class com valor title, como vemos no trecho abaixo (retirado do reddit):

...
    <a class="title " href="http://www.ocamlpro.com/blog/2013/03/14/opam-1.0.0.html">
        OPAM, a new package manager for OCaml is officially released!
    </a>
    <span class="domain">
        (<a href="http://www.reddit.com/domain/ocamlpro.com/">
            ocamlpro.com
        </a>)
    </span>
...

Dentro do elemento p, temos outro elemento, a, que contém um atributo com a URL em si (href) e que tem como conteúdo o título do link, que também é de nosso interesse. A imagem abaixo mostra em mais detalhes uma parte do conteúdo da página que nos interessa.

http2.3

Perceba que precisamos extrair da página várias instâncias de informação como essa mostrada acima, pois para cada link submetido teremos uma estrutura como a apresentada acima. Assim, teremos que procurar por todas as ocorrências de links no documento HTML e extrair essas informações de lá.

Agora que já sabemos onde está a informação que precisamos em meio à bagunça do HTML, chegou o desafio principal, que é extrair essa informação usando um programinha. Mas, antes de meter a mão na massa, vamos conhecer a ferramenta que vai nos possibilitar fazer a extração dos dados de uma forma mais prática: a biblioteca BeautifulSoup.

BeautifulSoup

No post anterior, no último trecho de código, mostrei uma forma meio tosca de buscar a informação no HTML. Vou repetir aquele código aqui para você lembrar sobre o que estou falando:

import requests

def cria_lista_links(content):
    links = []
    for line in content.split('</p>'):
        index = line.find('class="title ')
        if index != -1:
            href_start = line.find('href="', index) + 6
            href_end = line.find('"', href_start)
            links.append(line[href_start:href_end])
    return links

r = requests.get("http://www.reddit.com/r/programming")
print '\n'.join(cria_lista_links(r.content))

É tosca porque não é muito precisa, visto que nossa busca se baseia somente em uma informação textual dentro de um conjunto de linhas com o HTML. Veja que fazemos a busca usando o caractere de " (aspas duplas) ou a string href= como parâmetro. Se o autor da página mudar de aspas duplas para aspas simples, ou de href= para href =, nossa busca já irá parar de funcionar.

A forma mais confiável de fazermos isso é obtida através do parsing (análise sintática) do conteúdo, de forma que ele nos devolva elementos HTML com seus atributos em uma estrutura bem fácil de manipular. Mas, fazer um bom parser para HTML daria bastante trabalho, ainda mais um parser que funcione bem com todas as variações que são usadas em documentos HTML pela web. A boa notícia é que esse parser já existe e está disponível pra gente através da biblioteca BeautifulSoup, que vamos utilizar daqui pra frente.

Analisando código HTML com BeautifulSoup

Considere o HTML apresentado abaixo. Observe com atenção a sua estrutura.

    <html>
        <head>
            <title>Uma página qualquer</title>
        </head>
        <body>
            <div id="content">
                <div class="content-title">
                    Aqui vai um conteúdo 
                    <a class="link" href="http://www.google.com/">qualquer</a>, 
                    um texto sem fundamento algum, escrito sem a mínima 
                    intenção de despertar qualquer interesse por parte
                    do <a class="link-old"> leitor</a>.
                </div>
                <div class="content-author">
                    Escrito por um autor qualquer, que não tinha algo 
                    melhor para escrever.
                </div>
                <div class="content-data">
                    16/03/2013
                </div>
            </div>
            <div class="footer">
                Nenhum direito reservado.
            </div>
        </body>
    </html>

Para entender melhor o HTML acima, vamos ver a árvore de representação dele: http2.4 Agora vamos iniciar um exemplo de análise do HTML acima usando o BeautifulSoup.

import BeautifulSoup

html_data = '''
<html>
    <head><title>Uma página qualquer</title></head>
    <body>
        <div id="content">
            <div class="content-title">
                Aqui vai um conteúdo 
                <a class="link" href="http://www.google.com/">qualquer</a>, 
                um texto sem fundamento algum, escrito sem a mínima 
                intenção de despertar qualquer interesse por parte
                do <a class="link-old"> leitor</a>.
            </div>
            <div class="content-author">
                Escrito por um autor qualquer, que não tinha 
                algo melhor para escrever.
            </div>
            <div class="content-data">
                16/03/2013
            </div>
        </div>
        <div class="footer">
            Nenhum direito reservado.
        </div>
    </body>
</html>'''

soup = BeautifulSoup.BeautifulSoup(html_data)

*Por praticidade, o conteúdo HTML foi inserido em uma string enorme chamada html_data.

Já podemos extrair informações da página, que foi “parseada” na última linha do trecho acima. Perceba que os atributos existentes em cada elemento possuem os mesmos nomes dos elementos no código HTML (.title por exemplo, se refere ao elemento title da página).

print soup.title
#<title>Uma página qualquer</title>

print soup.title.text
# Uma página qualquer

O atributo text de qualquer elemento sempre retorna o texto que está contido em tal elemento. Podemos também encadear o acesso a elementos. Abaixo, estamos acessando o primeiro div do body do HTML analisado.

print soup.body.div
# <div id="content"><div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div><div class="content-author">Escrito por um autor qualquer, que não tinha algo melhor para escrever.</div><div class="content-data">16/03/2013</div></div>

Perceba que no trecho de código acima foi impresso somente um dos dois divs que compõem o body (somente o de id content). Olhando para a árvore e para o valor impresso, vemos que foi impressa toda a subárvore do primeiro div do body. Como temos mais de um div dentro do body, quando tentamos acessar o atributo div, nos é retornado somente o primeiro dos divs. Para obter todos os divs do body, podemos fazer assim:

print soup.body.contents
# [u'\n', <div id="content"><div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div><div class="content-author">Escrito por um autor qualquer, que não tinha algo melhor para escrever.</div><div class="content-data">16/03/2013</div></div>, <div class="footer">Nenhum direito reservado.</div>, u'\n']

O atributo contents de um elemento qualquer, retorna os elementos que são seus filhos na árvore do HTML. Ou então, podemos percorrer os “filhos” de um elemento, usando o generator childGenerator():

for x in soup.body.childGenerator():
    print x, '\n'

Como resultado, teremos:

<div id="content"><div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div><div class="content-author">Escrito por um autor qualquer, que não tinha algo melhor para escrever.</div><div class="content-data">16/03/2013</div></div 

<div class="footer">Nenhum direito reservado.</div>

Além de acessar os elementos filhos de uma tag, podemos também acessar o elemento pai através do atributo parent. Aí vai um exemplo:

a = soup.body.div.a
print a.parent
# <div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div>

Como você pode ver, o elemento pai de a é o próprio div que o antecede.

Buscando por elementos específicos

Se quisermos buscar todos os elementos a de uma página HTML, podemos usar o método findAll():

print soup.findAll('a')
# [<a class="link" href="http://www.google.com/">qualquer</a>, <a class="link-old"> leitor</a>]

O método retorna uma lista contendo todas as ocorrências de elementos do tipo procurado. Também podemos obter elementos através de seus atributos id. Em nosso HTML, temos um div com id="content". Podemos obtê-lo com:

print soup.find(id="content")
# <div id="content"><div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div><div class="content-author">Escrito por um autor qualquer, que não tinha algo melhor para escrever.</div><div class="content-data">16/03/2013</div></div>

Ou então, buscar por elementos que estejam classificados em uma classe específica:

print soup.find(attrs={'class':'footer'})
# <div class="footer">Nenhum direito reservado.</div>

Enfim, dá pra fazer muita coisa usando a BeautifulSoup. Se quiser conhecê-la melhor, veja a documentação: http://www.crummy.com/software/BeautifulSoup/bs4/doc/.

Implementando o scraper do reddit

Como vimos anteriormente, um link no reddit possui a seguinte estrutura:

...

    <a class="title " href="http://www.ocamlpro.com/blog/2013/03/14/opam-1.0.0.html">
        OPAM, a new package manager for OCaml is officially released!
    </a>
    <span class="domain">
        (<a href="http://www.reddit.com/domain/ocamlpro.com/">
            ocamlpro.com
        </a>)
    </span>

...

Primeiro, precisamos obter o HTML direto do site do reddit:

import requests
import BeautifulSoup

r = requests.get('http://www.reddit.com/r/programming/')
soup = BeautifulSoup.BeautifulSoup(r.content)

Depois, vamos obter cada elemento a que possua o atributo class com valor igual a "title ". Podemos fazer isso usando o método findAll(), percorrendo o resultado e extraindo os atributos que nos interessam (href e text):

for a in soup.findAll('a', attrs={'class': 'title '}):
    print "%s - %s" % (a.text, a.get('href'))

Podemos melhorar o código acima, criando uma função que obtenha a lista de links de um subreddit qualquer:

import requests
import BeautifulSoup

def get_links_from(subreddit):
    links = []
    r = requests.get('http://www.reddit.com/r/%s/' % (subreddit))
    if r.status_code != 200:
        return None

    soup = BeautifulSoup.BeautifulSoup(r.content)

    for a in soup.findAll('a', attrs={'class': 'title '}):
        links.append((a.text, a.get('href')))
    return links

A função acima retorna uma lista contendo tuplas de pares (título do link, URL do link). Assim, podemos percorrer essa lista, imprimindo os valores:

for link_name, link_url in get_links_from('programming'):
    print "%s - %s" % (link_name, link_url)

for link_name, link_url in get_links_from('python'):
    print "%s - %s" % (link_name, link_url)

Está pronto! Barbada, hein? Que tal agora você começar a estudar aquele site de onde você quer extrair informações e implementar um webscraper?

Boa sorte! 🙂

Leitura adicional