Raspadores
Responsáveis pela etapa de coleta de dados na Arquitetura do Querido Diário do Querido Diário, os raspadores são robôs programados para obter os arquivos de diários oficiais e seus metadados nos sites publicadores. O principal desafio é o de aumentar cada vez a cobertura, visando alcançar todos os 5570 municípios brasileiros.
Entendendo o código
Os raspadores são desenvolvidos em Python utilizando o framework Scrapy. Os principais componentes de código do repositório se relacionam conforme o diagrama enumerado abaixo. Cada um é mais aprofundado nos tópicos enumerados correspondentes a seguir e, ao fim, é apresentado a ordem em que esses componentes são mobilizados quando um comando de raspagem é acionado.
1. O framework Scrapy
O framework de raspagem que o Querido Diário utiliza é o Scrapy. Isso quer dizer que o Scrapy já oferece alguns recursos genéricos, restando ao Querido Diário a decisão de utilizá-los e/ou modificá-los para aplicação mais específica que o projeto demande.
Caso não conheça, a documentação oficial tem uma seção de primeiros passos, com páginas como uma primeira olhada no Scrapy (ENG) e tutorial Scrapy (ENG), que indicamos. Para o Scrapy, cada script de raspagem se chama spider, título que aparecerá diversas vezes também no Querido Diário.
2. A classe Gazette
Tendo em vista que coletar os arquivos de diários oficiais é o objetivo aqui, a classe Gazette (diário oficial em inglês) foi criada para vincular cada edição às suas informações complementares (metadados). Esta classe é criada a partir de scrapy.Item e é composta por atributos personalizados para os fins do Querido Diário.
- class Gazette(date, edition_number, is_extra_edition, power, file_urls)
- date
- Tipo:
- Definição:
Data de publicação informada no site publicador de um diário oficial.
- edition_number
- Tipo:
string
- Definição:
Número informado no site publicador de um diário oficial.
- is_extra_edition
- Tipo:
boolean
- Definição:
Informação do site publicador se a edição de um diário oficial é extra. Costuma ser identificado pela presença de termos como “Extra”, “Extraordinário”, “Suplemento”. Quando não informado, deve ser fixado em
False
por padrão (hard coded).
- power
- Tipo:
string
- Opções:
'executive'
ou'executive_legislative'
- Definição:
Informação se o conteúdo do diário oficial é do poder executivo exclusivamente ou se aparecem atos oficiais do poder legislativo também.
Atributo de preenchimento manual pela pessoa desenvolvedora (hard coded) a partir da consulta em alguns arquivos de diários oficias disponibilizados no site publicador.
- file_urls
- Tipo:
list[string]
- Definição:
URL para download do arquivo correspondente a edição do diário oficial. Costuma ser apenas uma URL.
3. A classe BaseGazetteSpider
BaseGazetteSpider é a classe-mãe para todos os raspadores do Querido Diário.
Ela é criada a partir de scrapy.Spider, em uma relação de herança. Juntas,
scrapy.Spider e BaseGazetteSpider criam uma lista de elementos obrigatórios
para os raspadores UFMunicipioSpider
implementarem. A procedência de cada
campo é indicada na definição da classe.
4. As classes UFMunicipioSpider
Para cada município integrado ao Querido Diário, uma classe UFMunicipioSpider
correspondente deve existir no diretório de spiders. Sua responsabilidade é a
de coletar as informações nos sites publicadores de diários oficiais do município
em questão.
Estruturalmente, a UFMunicipioSpider
faz uma ponte entre as duas classes
mencionadas acima:
a. Ela é criada a partir de BaseGazetteSpider. Isso significa que herda a exigência de implementar elementos provindos dela.
b. Ela cumpre sua missão uma vez que tenha construíndo um objeto Gazette
,
contendo todos os metadados listados no item 2.
- class UFMunicipioSpider(BaseGazetteSpider)
- name
- Procedência:
- Tipo:
string
- Definição:
Nome do raspador no formato
uf_nome_do_municipio
. Definido para ser usado no comando de execução da spider.
- TERRITORY_ID
- Procedência:
- Tipo:
string
- Definição:
Código do IBGE para o município conforme listado no arquivo de territórios.
- allowed_domains
- Procedência:
- Tipo:
list[string]
- Definição:
Domínios em que a spider está autorizada a navegar. Evita que a spider visite ou colete arquivos de outros sites.
- start_urls
- Procedência:
- Tipo:
list[string]
- Definição:
URL da página onde ficam os diários oficiais no site publicador. É apenas uma URL e não deve ser a homepage do site.
- Exigência:
Opcional. Saiba mais em Quando usar start_urls ou start_requests()
- start_date
- Procedência:
- Tipo:
- Definição:
Data da primeira edição de diário oficial disponibilizado no site publicador.
- end_date
- Procedência:
- Tipo:
- Definição:
Data da última edição de diário oficial disponibilizado no site publicador.
- Exigência:
Implícito. Por padrão, é preenchido automaticamente com a data da execução do raspador (
datetime.today().date()
). Só é explicitado em raspadores que coletam sites descontinuados, mas que seguem no ar por conservação de memória.
- start_requests()
- Procedência:
- Retorna:
- Definição:
Método para criar URLs para as páginas de diários oficiais do site publicador
- Exigência:
Opcional. Saiba mais em Quando usar start_urls ou start_requests()
Esqueleto de um UFMunicipioSpider
Com isso, já é possível visualizar um esqueleto de todos os scripts de raspadores do Querido Diário e entender suas partes básicas. Note como todos os elementos listados acima a seguir.
from gazette.items import Gazette
from gazette.spiders.base import BaseGazetteSpider
class UFMunicipioSpider(BaseGazetteSpider):
name
TERRITORY_ID
allowed_domains
start_urls
start_date
def start_requests():
def parse():
yield Gazette(
date
edition_number
is_extra_edition
file_urls
power
)
5. As classes SistemaGazetteSpider
Em alguns casos, o site publicador de diários oficiais tem o mesmo layout entre diversas cidades. Isso parece acontecer quando municípios contratam a mesma solução publicadora de diários ou de desenvolvimento de sites. Por exemplo, note como os sites de Acajutiba (BA), Cícero Dantas (BA) e Monte Santo (BA) tem a mesma aparência.
A implicação disso para o Querido Diário é que o código das classes BaAcajutibaSpider, BaCiceroDantasSpider e BaMonteSantoSpider seria enormemente parecido: seus raspadores “navegariam” nos sites da mesma maneira para obter metadados que ficam na mesma posição.
Para simplificar a situação, enxugando repetição de código e facilitando a adição de novos raspadores a partir do padrão conhecido - até porque estes são 3 casos, quantos outros existem? - temos as classes SistemaGazetteSpider.
Nota
Adotamos a terminologia sistema replicável para nos referir ao padrão e município replicado os municípios que o utilizam.
Importante
Vários padrões são conhecidos. Seus códigos estão no diretório de bases e seus layouts na Lista de Sistemas Replicáveis
Esqueleto de um SistemaGazetteSpider
Como parte da família de spiders do Querido Diário, a SistemaGazetteSpider
é criada a partir de BaseGazetteSpider e precisa, ao final, coletar Gazettes.
Porém, cabe aqui o exercício de separar o que é recurso comum a diversos sites -
para ficar no código de SistemaGazetteSpider - do que é específico de cada
município - para ficar no código de UFMunicipioSpider
.
De modo geral, o método parse()
responsável por navegar
o site fica em SistemaGazetteSpider e os atributos TERRITORY_ID
,
name
, start_date
e
power
, por serem dados particulares de cada município, tendem a
ficar em UFMunicipioSpider
.
SistemaGazetteSpider
from gazette.items import Gazette
from gazette.spiders.base import BaseGazetteSpider
class SistemaGazetteSpider(BaseGazetteSpider):
def parse():
yield Gazette(
date
edition_number
is_extra_edition
file_urls
)
UFMunicipioSpider para o padrão SistemaGazetteSpider
from gazette.spiders.base.<sistema> import SistemaGazetteSpider
class UFMunicipioSpider(SistemaGazetteSpider):
TERRITORY_ID
name
start_date
power
Atenção
Perceba que os atributos allowed_domains
e start_urls
e o método start_requests()
não aparecem nos rascunhos.
Eles são os elementos mais influenciados pela situação, podendo ficar no código
de SistemaGazetteSpider ou UFMunicipioSpider a depender do caso.
6. Fluxo de execução
Idealmente, ao executar o comando de raspagem para
um raspador qualquer, o Scrapy aciona seu método start_requests()
fazendo uma requisição inicial para a URL definida no atributo start_urls
.
A Response recebida é entregue ao método de callback parse()
,
onde metadados são coletados, um objeto Gazette
é construído e é enviado
ao motor do Scrapy para executar, de fato, a ação de baixar o arquivo do diário
oficial.
Há dois contextos em que, por exigência da situação, esse fluxo pode não acontecer do jeito ilustrado:
quando
UFMunicipioSpider
tem seu própriostart_requests()
, não sendo usado o que existe em scrapy.Spider.quando o
parse()
não é usado como callback padrão.
Quando usar start_urls
ou start_requests()
Primeiro, é importante destacar que um método start_requests()
sempre existe,
pois já é implementado pelo Scrapy. A questão aqui é quando a implementação padrão,
ilustrada acima, não é suficiente.
Por exemplo, um site que organiza diários por data ou intervalo, a URL da requisição inicial pode precisar preencher campos de data. Ou um que atenda vários municípios ou poderes, pode ser necessário código identificador. Ainda, se uma API for encontrada, a URL muda a depender dos endpoints e seus campos. Outros diversos casos acontecem.
Nessas situações, a opção nativa não serve por ser muito restrita, uma vez que o quê ela espera receber é uma URL já fixada.
Quando a situação demanda várias URLs e/ou parâmetros de contexto, a operação
padrão do método start_requests()
deve ser sobreescrita com um start_requests()
novo dentro do raspador que implemente uma lógica particular de construção dinâmica
de URLs. Com start_requests()
gerando URLs, o atributo
start_urls
não tem porque existir.
Exemplo com |
Exemplo com |
---|---|
O método parse()
Por padrão, o fluxo de execução desemboca em parse()
.
Para implementar um parse()
que cumpra bem seu papel de obtenção de metadados a partir do conteúdo textual da Response
do site, é importante que a pessoa desenvolvedora saiba inspecionar uma página
web a fim de identificar seletores e construir expressões regulares (RegEx)
convenientes para serem usados no método.
O Scrapy conta com seletores (ENG) nativos que podem ser testados usando o Scrapy shell. Já para expressões regulares, é comum o uso da biblioteca re de Python e, como sugestão, sites que testam strings regex, como Pyregex, podem ajudar.
Menos comuns, mas por vezes necessárias, outras bibliotecas já estão entre as dependências do repositório de raspadores e podem ser úteis: dateparser, para tratar datas em diferentes formatos, e chompJS, para transformar objetos JavaScript em estruturas Python.
Dica
Materiais complementares são indicados na seção Aprenda mais sobre raspagem
Quando o parse()
não é o método de callback
O parse()
só é o método padrão para o qual a Response é
enviada por ser assim que o método start_requests()
nativo do Scrapy define.
Porém, quando for o caso do raspador implementar um start_requests() próprio,
pode ser opção da pessoa desenvolvedora indicar outro método como callback. O
raspador para Macapá-AP é um exemplo disso.
Importante
Um método nomeado como parse()
pode não existir, mas
o papel que espera-se que ele cumpra segue necessário e deve ser realizado pelo
novo callback ou outros métodos auxiliares adicionados ao raspador.
Contribuindo com raspadores
Passos iniciais
Antes de colocar a mão na massa, é necessário configurar o ambiente de desenvolvimento com as dependências que o repositório de raspadores precisa para funcionar e escolher um município para o qual contribuir. Você achará informações sobre esses passos iniciais nas referências abaixo. Lembre-se de seguir o Guia de Contribuição durante as interações no repositório.
Repositório: https://github.com/okfn-brasil/querido-diario
Configuração do Ambiente de Desenvolvimento: CONTRIBUTING
Mapeamento de municípios para contribuição: Quadro de Expansão de Cidades
Atenção
No momento, os únicos casos de raspadores sendo integrados são os para diários completos ou diários agregados. Atente-se a isso antes de começar a desenvolver e entenda mais dessa situação na seção sobre tipos de diários oficiais
Diretrizes
O desenvolvimento de raspadores é norteado pelo interesse de coletar todos os diários oficiais disponíveis fazendo o menor número de requisições possível ao site publicador, evitando correr o risco de sobrecarregá-lo. Queremos, também, que seja possível fazer coletas com períodos personalizados (uma semana, um mês, …) dentro da série histórica completa de disponibilização.
Por isso, UFMunicipioSpider
tem os atributos start_date
e
end_date
e seus métodos parse()
e/ou start_requests()
devem ter, onde e como for conveniente,
filtros ou condicionais para controlar a data sendo raspada, mantendo-a dentro do
intervalo de interesse ao seguir executando.
Dica
Durante o desenvolvimento, para evitar fazer requisições repetidas nos sites é possível utilizar a configuração HTTPCACHE_ENABLED do Scrapy. Isso também faz com que as execuções sejam mais rápidas, já que todos os dados ficam armazenados localmente.
Modelo para um raspador
Abaixo, temos um protótipo inicial e genérico para raspadores do Querido Diário. Eles acabam não sendo estritamente assim, uma vez que a depender da situação outros atributos, métodos ou bibliotecas podem ser necessários ou, quando for o caso, pode precisar de uma classe SistemaGazetteSpider intermediária.
from datetime import date
import scrapy import Request
from gazette.items import Gazette
from gazette.spiders.base import BaseGazetteSpider
class UFMunicipioSpider(BaseGazetteSpider):
name = "uf_nome_do_municipio"
TERRITORY_ID = ""
allowed_domains = [""]
start_urls = [""]
start_date = date()
def start_requests(self): # (caso necessário)
# Lógica de geração de URLs
# ...
yield Request()
# ... métodos auxiliares opcionais ...
def parse(self, response):
# Lógica de extração de metadados
# partindo de response ...
#
# ... o que deve ser feito para coletar DATA DO DIÁRIO?
# ... o que deve ser feito para coletar NÚMERO DA EDIÇÃO?
# ... o que deve ser feito para coletar se a EDIÇÃO É EXTRA?
# ... o que deve ser feito para coletar a URL DE DOWNLOAD do arquivo?
yield Gazette(
date = date(),
edition_number = "",
is_extra_edition = False,
file_urls = [""],
power = "executive",
)
Estratégias comuns
Navegar pelo diretório de spiders lendo o código de raspadores existentes ajudará o desenvolvimento de novos raspadores, principalmente com ideias para desafios frequentes. Abaixo, estão listadas mecânicas comuns que aparecem em sites e referências de raspadores que implementaram uma solução.
Paginação
Quando as publicações de diários oficiais estão separadas em várias páginas referenciadas por botões como “página 1”, “página 2”, “próxima página”.
Caso com Paginação |
Raspador |
---|---|
Filtro por data
Quando o site publicador oferece um formulário com campos de data para filtrar as publicações.
Caso com Filtro |
Raspador |
---|---|
Presença de APIs
Quando é percebido que as requisições do site se dão por meio de APIs Públicas devolvendo um formato JSON.
Caso com API |
Raspador |
---|---|
Executando um raspador
Para executar um raspador, utilizamos o comando crawl, cuja sintaxe e opções mais relevantes são apresentadas a seguir. Este comando é um dos comandos padrão (ENG) oferecidos pelo framework Scrapy e conhecer os demais também pode ajudar a desenvolver raspadores com mais facilidade.
O comando de raspagem deve ser executado no diretório /data_collection
com o
ambiente de desenvolvimento ativo. Nele, surgirá um arquivo SQLite queridodiario.db
e um diretório /data
, organizado por TERRITORY_ID
e date
, onde ficam os arquivos de diários oficiais baixados.
scrapy crawl uf_nome_do_municipio [+ opções]
-a start_date=AAAA-MM-DD
define novo valor parastart_date
-a end_date=AAAA-MM-DD
define novo valor paraend_date
-s LOG_FILE=nome_arquivo.log
salva as mensagens de log em arquivo de texto-o nome_arquivo.csv
salva a lista de diários oficiais e metadados coletados em arquivo tabular
Execuções exigidas
Para garantir que o raspador implemente todos os recursos necessários, experimente algumas execuções-chave fazendo as validações indicadas na seção a seguir. Testes com essas execuções são feitos durante a revisão de uma contribuição.
1. Coleta última edição. Veja a data da edição mais recente no site publicador e execute a raspagem a partir dela.
scrapy crawl uf_nome_do_municipio -a start_date=AAAA-MM-DD
2. Coleta intervalo. Escolha um intervalo como uma semana, algumas semanas, um mês ou alguns meses e execute a coleta desse intervalo arbitrário.
scrapy crawl uf_nome_do_municipio -a start_date=AAAA-MM-DD -a end_date=AAAA-MM-DD
3. Coleta completa. Execute a coleta sem filtro por datas para obter toda a série histórica de edições no site publicador.
scrapy crawl uf_nome_do_municipio -s LOG_FILE=nome_arquivo.log -o nome_arquivo.csv
Com esses três casos, garante-se que o raspador funciona para as rotinas do projeto: ao integrar um novo município ao Querido Diário, o raspador é executado para obter todos os diários oficiais existentes (coleta completa). A partir disso, no dia-a-dia, eles devem obter apenas a edição daquele dia (coleta última edição). Por fim, para confirmar que nenhuma edição retardatária foi perdida, coletas de redundância são feitas por precaução (coleta intervalo).
Verificando a execução do raspador
Diários oficiais coletados
Em /data_collection/data
, deve-se verificar a aderência dos arquivos baixados,
como: se são .pdf
mesmo; se o conteúdo é de um diário oficial realmente; se
a data dentro do arquivo corresponde à data do diretório em que o arquivo está.
Arquivos auxiliares
nome_arquivo.csv
Veja se os campos de metadados (date
, edition_number
,
is_extra_edition
, power
e file_urls
)
estão todos coerentemente preenchidos para todos os itens. Como o arquivo é uma tabela,
é útil ordenar as colunas para fazer a conferência. Em particular, é importante
prestar atenção se a primeira e a última edição estão dentro do intervalo de coleta
definido em start_date
e end_date
e se faltam certas datas ou números de edição que possam indicar que edições não
foram coletadas.
nome_arquivo.log
Tem uma seção de dados e estatísticas do comportamento do raspador ao fim do arquivo.
Nela, verifique pela existência de erros (log_count/ERROR
) e analise outras
informações relevantes (como file_count
, retry/count
, downloader/request_count
).
Havendo indícios de problemas, procure no texto do log pelas mensagens que podem
ajudar a entender mais situação a fim de corrigí-las.
Aprenda mais sobre raspagem
> Vídeos
- Módulo 3 do Curso Python para Inovação Cívica da Escola de Dados:
Aula 1: Apresentando o Querido Diário
Aula 2: Por dentro do Querido Diário
Aula 3: Introdução a Orientação a Objeto
Aula 7: Selecionando elementos com XPath
Aula 8: Expressões Regulares
Aula 10: Indo além
> Sessões de desenvolvimento ou revisão de raspadores gravadas
- Ana Paula Gomes
- Giulio Carvalho
Coding Dojo: Raspador para Petrópolis-RJ do início ao fim
- Renne Rocha
> Textos
Giulio Carvalho, Entenda como analisar *sites* de diários oficiais para raspagem de dados
Juliana Trevine, Conheça os desafios de raspagem do Querido Diário
Ana Paula Gomes, Quero tornar Diários Oficiais acessíveis. Como começar?
Lucas Villela, Monitorando o governo de Araraquara/SP
José Vanz, Como funciona o robozinho do Serenata que baixa os diários oficiais?