Buscando cep diretamente pelo site dos correios em python

Todos que trabalham com desenvolvimento de sistemas já se depararam com o seguinte problema: Como faço para buscar um endereço pelo cep ou vice-versa?

Existe um banco de ceps que pode ser encontrado facilmente usando algum sistema de buscas pela web, o problema é que esse banco é de 2009 e provavelmente desatualizado. Para vergonha dos brasileiros, o site dos correios não provê um webservice oficial para busca de ceps, o máximo que oferece é um form em que os resultados são devolvidos em html mesmo, nada de retorno em json/xml, inclusive os correios te vendem uma base de cep com 900 mil ceps pela pechincha de R$ 1.100,00.

Uma solução viável (até que os correios não botem um captcha na busca) é fazer um screen-scraping em python do resultado html devolvido, para isso já existe uma ótima biblioteca chamada cep que resolve esse problema.

Então eu tive a seguinte ideia, porque não usar o tornado para servir como um webservice do resultado que a biblioteca cep devolve? Eu posso usar o async http cliente do tornado e assim conseguir retornar esses resultados sem bloquear outras requisições que chegam ao meu servidor.

Fazendo um projetinho de 15 minutos eu cheguei no seguinte código (claro, ainda dá pra refatorar muita coisa ainda):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tornado.ioloop
import tornado.web
import tornado.httpclient
import tornado.escape

from urllib import urlencode

# http://pypi.python.org/pypi/cep
from cep import Correios

class MainHandler(tornado.web.RequestHandler):
    
    def get(self):
        self.write(
            '<html><body><form action="/search" method="get">'
            '<input type="text" name="query">'
            '<input type="submit" value="Pesquisar">'
            '</form></body></html>'
        )

class SearchHandler(tornado.web.RequestHandler):
    
    @tornado.web.asynchronous
    def get(self):
        # get search argument
        query = self.get_argument('query', None)
        # write error for empty argument
        if not query or query == '':
            self.write("Por favor preencha o formulário de busca")
            self.finish()
        # create data for request body
        data = dict(
            EndRow='10', 
            Metodo='listaLogradouro', 
            StartRow='1', 
            TipoConsulta='relaxation',
            relaxation=query.encode('cp1252'),
        )
        # create http request
        request = tornado.httpclient.HTTPRequest(
            'http://www.buscacep.correios.com.br/servicos/dnec/consultaLogradouroAction.do',
            method='POST',
            body=urlencode(data, True)
        )
        # create async http client
        http = tornado.httpclient.AsyncHTTPClient()
        # load page
        http.fetch(request, callback=self.process_response)
        
    def process_response(self, response):
        # if error raise status code 500
        if response.error: 
            raise tornado.web.HTTPError(500)
        # cep parsing
        c = Correios()
        resultados = c._parse_tabela(response.body)
        # create json
        json = tornado.escape.json_encode(resultados)
        # output json
        self.set_header('Content-Type', 'application/json')
        self.write(json)
        self.finish()

application = tornado.web.Application([
    tornado.web.url(r'/', MainHandler, name='main'),
    tornado.web.url(r'/search', SearchHandler, name='search'),
])

if __name__ == '__main__':
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Basicamente temos apenas dois handlers, o primeiro é um form com o campo query e o segundo é um handler assíncrono que recebe a query e processa uma request para o site dos correios, ao receber o html eu passo o processamento para a biblioteca cep e finalmente devolvo o resultado em json.

Pronto, agora você pode rodar um webservice local para buscar os ceps diretamente dos correios, lógico que esse projeto pode melhorar bastante, podemos guardar os resultados em um banco de dados evitando que sempre seja feita uma requisição ao site dos correios.