Skip to content

Instantly share code, notes, and snippets.

@felipek
Created January 23, 2011 00:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save felipek/791700 to your computer and use it in GitHub Desktop.
Save felipek/791700 to your computer and use it in GitHub Desktop.
Arquitetura: Consumo 2.0

Projeto "Consumo"

O projeto Consumo tem por objetivo "obter" dados (legalmente, de forma segura) sobre utilização ou consumo de operadoras (ou demais serviços, abstraindo) tais como quantidade de dados trafegados. A ideia consiste em basicamente fazer uso de web scraping para obter as informações desejadas. É algo bastante comum. O objetivo final é disponibilizar os dados em aparelhos móveis, como iPhone e outros. Atualmente as operadoras brasileiras simplesmente não disponibilizam esses dados para acesso móvel. Quando é possível (não é em Flash ou HTML fora dos padrões), o usuário pode levar um bom tempo (p.ex., 5~10 minutos) carregando sites para obter uma informação tão simples quanto o seu consumo de dados. Além de levar esse tempo todo, consome mais uma grande quantidade de dados só para acessar essas informações.

Desde o início (Dezembro) foi amplamente divulgado que o projeto era apenas para resolver um problema pessoal, e não foi projetado como um "produto" para dar retorno ou algo similar. A versão iPhone seria inicialmente gratuita, mas por incentivo de amigos e bloggers, acabou sendo lançada por $0,99 principalmente para "cobrir" os custos de "backend" e suporte aos usuários, este último sendo a parte mais "custosa" de liberar um utilitário de utilização massiva. Justamente por não ser nenhum projeto elaborado, que foi implementado em pouco tempo, e que tem um potencial de colaboração grande, tem sua base de código totalmente aberta.

Arquitetura Original (1.0)

A arquitetura original, para simplificar a implementação (e facilitar alterações, novos suportes, entre outros) continha 3 principais elementos: o cliente, um servidor intermediário de "scraping", e o destino -- a origem dos dados. O lado do "servidor" era feito na linguagem Python, facilitando muito todo o processo de escrita de "scraping". Uma vantagem importante deste modelo é que a troca de dados maior ocorre entre (2 & 3), servidor e operadora. Entre (1 & 2), cliente/telefone e servidor, uns poucos dados são trocados, alguns poucos bytes. Uma desvantagem é que existe um ponto a mais de falha (o servidor intermediário precisa estar disponível e funcionando) e segurança, visto que os dados dos usuários trafegam por um canal a mais.

Em "ASCII Art", a arquitetura original funcionava da seguinte forma:

1. Cliente (telefone) <-> 2. Servidor (scraper) <-> 3. Fonte (operadora)

Bloqueio pela Operadora Vivo

Assim que foi lançada a versão iOS (1.0) do Consumo, algumas coisas aconteceram:

  • Terça (11): O aplicativo imediatamente chegou ao topo da App Store;
  • Terça (11): O hosting original não "segurou" a demanda, era muito simples;
  • Quarta (12): Foi trocado para um outro hosting, que não funcionou bem;
  • Quarta (12): Tentativas de normalização e migração para outro hosting;
  • Quinta (13): Foi trocado para um terceiro hosting, excelente e tudo OK;
  • Quinta (13): Quando tudo começou a funcionar, a Vivo ficou "instável";
  • Sexta (14): Quando tudo estava funcionando legal, a Vivo "bloqueou";
  • Sexta (14): Projeto foi removido (lá do topo) da App Store.

O bloqueio foi feito entre servidor e operadora (HTTP 403), 14/01/2011.

Nenhuma atitude foi tomada para ir "contra" o bloqueio (entre as diversas atitudes possíveis). Nunca se soube exatamente o motivo da operadora ter bloqueado. Algumas respostas iniciais do suporte da operadora eram simplesmente "o aplicativo depende do correto funcionamento dos nossos sistemas", e de fato os sistemas em questão não são muito estáveis. Depois, alguns usuários reportaram que as respostas do suporte já eram que "o aplicativo foi bloqueado por segurança". Abaixo um pedaço de uma resposta do suporte a um usuário:

(...)
Em atenção ao seu e-mail, esclareço que a Vivo bloqueou o acesso ao
Aplicativo "Consumo" / iPhone - Por segurança dos dados do cliente,
não sendo ativado novamente.
(...)

Fiquei sabendo sobre este feedback (indireto, digamos) só na Terça, 18/01/2011. Pessoalmente, havia desistido de continuar mantendo o projeto. Ele poderia ficar funcionando para alguns usuários, e a operadora aparentemente não permitira o seu funcionamento geral (aos demais usuários). Melhor não ir contra uma decisão da operadora. Mas tendo visto a resposta do suporte aos usuários, comecei a concluir que o bloqueio foi feito "por segurança", pois a operadora não estava de acordo com um servidor intermediário processando dados de usuários. É apenas uma suposição, que faz sentido.

Embora infelizmente nunca tenha tido um parecer oficial da operadora (mesmo tentando algum contato, ou mesmo esperando alguma notificação oficial), fiquei animado em mudar a arquitetura para resolver este "problema de segurança". A solução é simples: eliminar o servidor intermediário, o que é perfeitamente possível e na realidade o meu primeiro aplicativo iPhone já fazia isso. Exige um pouco mais de código. E requer algum tempo dedicado, que não estava exatamente nos planos (principalmente depois do esforço para deixar a 1.0 funcionando nos primeiros dias).

Minha decisão, na Terça/Quarta (18 - 19 de Janeiro) foi de tocar adiante. A solução "simples" tomaria algumas poucas horas (talvez uma noite). Resolvi que iria fazer algo um pouco mais demorado, e que estaria pronto até o final-de-semana. O objetivo geral era simplesmente mudar a arquitetura para ter o suposto "problema de segurança" resolvido, e com isso estabilizado talvez adicionar mais coisas ao projeto, como recursos e outras operadoras.

Nova Arquitetura

A nova arquitetura simplesmente elimina o servidor intermediário, de scraping:

1. Cliente (telefone) <-> 2. Fonte (operadora)

Prós e contras:

  1. Pró: resolve o "problema de segurança";
  2. Pró: menos um ponto de falha (disponibilidade);
  3. Contra: código nativo, difícil de ser alterado;
  4. Contra: mais dados entre cliente e destino.

Embora os prós (1 & 2) sejam muito interessantes, o contra 3 é muito complicado. O maior problema decorre da dificuldade de se "atualizar código" (update) para os milhares de usuários através da App Store. Além do processo de atualização (demorado), os usuários atualizam seus aplicativos aos poucos. Como o software é escrito em Objective-C, também existe a parte "chata" de migrar de algo mais simples (Python) para algo um pouco mais demorado (Objective-C). Mas a pior parte, mesmo, é atualizar o software na base de vários milhares de usuários, rapidamente.

Mas, por que seria necessário conseguir "atualizar rápido"?

Suponha o seguinte cenário:

  1. Os usuários estã utilizando normal o utilitário;
  2. A operadora (fonte) resolve mudar o "layout" dos dados;
  3. Todo o código de "captura de dados" fica perdido;
  4. Os usuários não conseguem mais ver os dados corretos.

Essas mudanças podem ocorrer tanto por manutenção dos sites e sistemas, bem como simplesmente para tentar enganar quem está "consumindo" esses dados, para evitar que softwares "derivados" peguem os dados. O tempo médio de aprovação, pelo menos na App Store, pode fazer com que uma atualização demore em torno de uma semana para ser disponibilizada. Tirando isso, ainda tem o tempo de atualização dos usuários.

O contra 4, "quantidade de dados trafegada entre cliente e fonte", não é expressivo. De qualquer modo, o software está consumindo bem menos do que acessar todo o sistema de operadora pelo navegador, fazendo requisições a diversas páginas, imagens, entre outros (mesmo com caching). Continua sendo vantagem. Já o contra 3 seria melhor evitar. No entanto, contornar esse "contra" requer um esforço grande. A solução que poderia levar poucas horas poderia levar algumas noites.

Em resumo, a ideia consiste em adicionar um "scripting", atualizável, entre cliente (telefone) e operadora, fazendo com que quaisquer mudanças possam ser tratadas dinamicamente, sem atualizar todo o software. O software cuida de temas gerais como gerenciamento de conta, listagem e exibição de dados, entre outros. Precisaria existir um mecanismo "dinâmico" de captura e interpretação de dados. Este mecanismo foi criado exclusivamente para o projeto. É importante lembrar que nos termos da App Store, a Apple não permite que código dinâmico seja executado tendo uma origem externa (p.ex., pegar um novo código online e executar). O código precisa estar já "bundled" (embutido) no projeto, e não pode ser capturado ou atualizado externamente. Essa cláusula (3.3.2) me fez criar algo novo, já que embutir Lua ou algo similar para fazer scraping dinâmico não seria suportado.

3.3.2. An Application may not download or install executable code.
Interpreted code may only be used in an Application if all scripts, code and
interpreters are packaged in the Application and not downloaded. The only
exception to the foregoing is scripts and code downloaded and run by Apple's
built-in WebKit framework. 

Agora, o processo seria assim:

1. Cliente (telefone) <- [scripting] -> 2. Fonte (operadora)

Caso seja necessário, a parte de "scripting" seria automagicamente atualizada. Este é justamente o objetivo de toda a arquitetura. Há diversas outras vantagens neste esquema. Por exemplo, deve ser realmente simples escrever suporte para outra operadora. Na realidade, uma outra operadora poderia ser executada sem sequer escrever código (Objective-C ou similar), apenas definições de busca e interpretação de dados, os monkeys :-)

Técnico: Scraper Monkey Definition (SMD)

Uma decisão que poderia ter sido tomada, por exemplo, é o "embutir" uma linguagem como Lua. Por acaso eu já havia feito isso em outro projeto "protótipo". No entanto, teria que portar o código para Lua ou outra linguagem de todo modo. E, em cima de Lua ou outro similar, precisaria ter um conjunto grande de suporte para bibliotecas e similares. Ficaria algo realmente grande, e talvez não funcionaria bem. Sem contar que, como foi mencionado acima, a cláusula 3.3.2 do contrato de desenvolvimento com a Apple não permite execução de código dinâmico de fonte externa.

Portanto, no lugar de embutir uma linguagem de scripting, resolvi criar um esquema de "definição de scraping". Eu não sei se já existe algo parecido, mas basicamente eu queria combinar: 1) Requisições e gerenciamento HTTP, 2) Navegação DOM/XPath, 3) Expressões regulares. Eu tentei procurar algo parecido pela Internet, mas não obtive sucesso. Na Quarta (19) comecei a escrever uma versão inicial. Para perceber bem as necessidades, primeiro "portei" o código Python (com BeautifulSoup) para Objective-C, facilitando o mapeamento de requisitos.

Para os mais "antigos", seria para Web/scraping o que é AWK para UNIX ;-) Embora tenha sido criada na década de 70 e para propósitos didáticos, foi um esquema de scripting que ganhou muita popularidade em UNIX. Trabalha com arquivos, estruturas de dados básicas, expressões regulares, fields. Embora eu seja muito mais novo que o AWK, que tem 34 anos, eu sempre fui fã e usuário (adminitração UNIX, scraping básico com curl/wget, entre outros).

Sabendo que precisaria de HTTP, DOM/XPath e Regex, juntei: ASI-HTTP-Request, TouchXML e RegexKitLite, viabilizando uma implementação em Objective-C com a combinação disso tudo. A entrada seria um JSON (ou correspondente nativo), seria feito o processamento, e a "saída" seria também um JSON representando um conjunto de variáveis/dados capturados e interpretados.

Em uma visão geral, essa é a ideia:

1. Definição JSON/plist/similar -> 2. Processamento (abstraído) -> 3. Resultado.

O objetivo era ser simples mas suficientemente "robusto" para suportar o que já estava sendo feito no projeto. O conceito é de uma árvore de requisições e lista de tratadores (nodes + handlers). Cada requisição pode ter requisições filhas, e diversos tratadores podem existir por requisição, consumindo os dados obtidos na requisição. Um tratador é basicamente uma XPath, RE, ou ambos combinados. Os resultados, processados, acabam em variáveis. Durante todo o processo, variáveis são "expandidas", de modo que o processamento pode iniciar com uma tabela de variáveis, novas podem ir sendo criadas ou combinadas durante o processo, e no final existe um resultado no formato de conjunto de variáveos.

Em resumo (o processamento da árvore é recursivo):

 [Começo, uma tabela de variáveis inicial]
 Vars (p.ex., searchString = "Rinzler", limit = "10")

 * Nodo (requisição)
   - [Definições HTTP]
   - <URL>
   - <Handlers>
     = XPath (//a[@class="result"...[0])
       -> Vars (first_link)
       -> Vars (first_name)
     = RE (Total ([^ ]+) entries)
       -> Vars (total_entries)
    * Nodo (requisição)
      ...
    * Nodo (requisição)
      ...

  [Depois do processamento, abstraído, resultado final]
  Vars (first_link = "http://...", first_name = "TRON Legacy", searchString...)

Como primeiro exemplo, pra exercitar a "teoria", fiz um "monkey" que fazia uma busca no Google, buscava um conjunto de resultados, e obtia o nome/link do primeiro e do último resultado, bem como o total de ocorrências encontradas. Tudo isso é obtido através de variáveis na tabela resultante. Esse exercício fez com que tudo que fosse posteriormente necessário já fosse implementado. A atividade seguinte era de finalmente "portar" o scraper Python+BeautifulSoup para "monkey" SMD ;-)

A tarefa de "migrar" foi surpreendentemente simples e divertida.

A versão 1.0 do "Vivo scraper monkey" pode ser vista aqui.

Scraper e aparelho

Técnico: Scraper Monkey

Eu gostei de ter feito o "scraper monkey". No decorrer desta semana (24/01) colocarei o código (que é relativamente simples) no Github junto com a atualização do Consumo, 2.0. A necessidade de "scraping" nativo é recorrente em projetos nativos iPhone, ou em sistemas embarcados de qualquer natureza. Além de facilitar a "escrita" dos scrapers, que acabam nem sendo mais em código, facilita também o processo de atualização, que pode ser feito rapidamente, dinamicamente, através da Web.

Eventualmente, talvez poderia fazer uma página e descrição do "modelo". Embora tenha usado plist (como visto no link acima de exemplo), naturalmente a ideia seria usar JSON. Como ambos são equivalentes dentro do contexto do Consumo, usei plist mesmo. Poderiam ser escritos "engines" para o SMD em outras linguagens como Java, C ou C++, ou mesmo Python e similares. A vantagem de ter um "scraper" atualizável facilmente sem atualização de código é interessante, mesmo para linguagens que poderiam ter fácil atualização de código em tempo de execução. O no caso da Apple, existe a 3.3.2.

Futuramente, diversas coisas poderiam ser criadas: definição de blocos/funções, reutilização de monkeys entre si (componentização), listas e tabelas (p.ex., coletar todos os 10 primeiros links de uma busca no Google ou algo similar), entre outros. Organizando o projeto, separado do Consumo, novas necessidades poderiam ser analisadas conforme necessário.

Perguntas e Respostas

  • P: O código será aberto?

    • Claro, vou atualizar no decorrer do tempo (da semana).
  • P: O código Python/BeautifulSoup será descontinuado?

    • Sim, infelizmente. Embora tenha utilidade ainda (script, automações, etc).
  • P: E se as "fontes" (operadoras) forem hostis e exigirem CAPTCHA?

    • Todos os milhões de clientes da operadora serão afetados negativamente.
    • Mesmo assim, o aplicativo pode "repassar" o CAPTCHA ao usuário.
  • P: E como funciona a atualização do "monkey"?

    • Ao inicializar, o aplicativo tenta atualizar suas "definições".
    • Uma atualização de definições só é feita se for necessário.
  • P: Onde fica esse script, exatamente?

  • P: Por que usar Dropbox?

    • Não quero que nem mesmo um byte por qualquer servidor próprio.
    • Agora eu sequer sei quantas vezes o aplicativo está sendo usado.
  • P: E se a operadora quiser bloquear mesmo assim?

    • Qual seria a alegação, agora? E mesmo assim, é difícil bloquear.
    • Agora, quem acessa são os próprios clientes, acesso direto.
  • P: Mas web scraping não é "ilegal"?

    • Nunca foi e nunca vai ser. Produtos como Buscapé nasceram disso.
    • Termos de uso/acesso podem influenciar, mas os dados são dos usuários!
  • P: Mas e se alegarem que são "muitos usuários" acessando?

    • Nesse caso, significa que precisam investir em infra!
    • O "peso" de acessar pelo Consumo é MUITO MENOR do que acessar pelo site.
    • Pelo Consumo, naturalmente, as pessoas acessam (e controlam) mais.
  • P: O projeto (Consumo) receberá novos recursos?

    • Evidentemente. Um que eu quero, pessoalmente, é histórico de consultas.
    • Veja uma listagem de outros recursos aqui.
  • P: E o suporte a novas operadoras?

    • Outras operadoras podem ser facilmente suportadas, ainda mais agora - monkeys!
    • Basta um novo "monkey" ser criado, sem envolver código, para suportar.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment