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.

[TODO]

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:

datetime.date()

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:

scrapy.Spider.name

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:

BaseGazetteSpider

Tipo:

string

Definição:

Código do IBGE para o município conforme listado no arquivo de territórios.

allowed_domains
Procedência:

scrapy.Spider.allowed_domains

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:

scrapy.Spider.start_urls

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:

BaseGazetteSpider

Tipo:

datetime.date()

Definição:

Data da primeira edição de diário oficial disponibilizado no site publicador.

end_date
Procedência:

BaseGazetteSpider

Tipo:

datetime.date()

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:

scrapy.Spider.start_requests()

Retorna:

scrapy.Request()

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()

parse(response)
Procedência:

scrapy.Spider.parse()

Retorna:

Gazette

Definição:

Método que implementa a lógica de extração de metadados a partir do texto da Response obtida do site publicador. É o callback padrão.

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.

[TODO]

Há dois contextos em que, por exigência da situação, esse fluxo pode não acontecer do jeito ilustrado:

  1. quando UFMunicipioSpider tem seu próprio start_requests(), não sendo usado o que existe em scrapy.Spider.

  2. 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 start_urls

Exemplo com start_requests()

raspador para Paulínia-SP

raspador para Barreiras-BA

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.

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

Site de Manaus-AM

am_manaus.py

Site de Sobral-CE

ce_sobral.py

Site de João Pessoa-PB

pb_joao_pessoa.py

Filtro por data

Quando o site publicador oferece um formulário com campos de data para filtrar as publicações.

Caso com Filtro

Raspador

Site de Sobral-CE

ce_sobral.py

Site de Salvador-BA

ba_salvador.py

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

Exemplo de acesso à API em Natal-RN

rn_natal.py

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 para start_date

  • -a end_date=AAAA-MM-DD define novo valor para end_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

> Sessões de desenvolvimento ou revisão de raspadores gravadas

> Textos