Como funciona o robozinho do Serenata que baixa os diários oficiais?

Recentemente comecei a fazer parte do programa de embaixadores da Open Knowledge Brasil (OKBR). Como minha primeira contribuição, comecei adicionar novos spiders no diario-oficial. Esse repositório possui diversos spiders para a raspagem de dados dos diários oficiais de cada município brasileiro. Como sou de Santa Catarina, decidi inicial com as cidades do meu Estado. Felizmente, no tempo de escrita deste artigo, minha primeira pull request está aguardando aprovação! :)

Durante esse meio tempo, outros embaixadores demonstraram interesse em adicionar ou modificar os spiders para suas cidades. Ai que entra esse artigo... eu pretendo explicar na forma mais didática que eu conseguir, tudo o que é necessário para começar a entender como adicionar ou alterar um spider. Para isso, irei explicar como funciona cada parte necessária para isso. Incluindo como baixar o código, como rodar um spider, um pouco de python, Scrapy, HTML, entre outros. Não pretendo ser muito técnico para deixar o documento mais acessivel possível. Mas também não muito superficial que não diga nada. Ou seja, existe a possiblidade de não agradar ninguém. xD.

Por favor, entre em contato se sentir falta de algo! Então vamos lá...

diario-oficial

O projeto diario-oficial da OKBR, tem como objetivo realizar a raspagem de dados dos diário oficiais do maior número de municípios brasileiros possíveis. Como fazemos isso? Bom, os diários oficiais são distribuídos em arquivos doc, pdf e etc. Para conseguir ter todos esses arquivos utilizamos um robozinho que literalmente abre todas as páginas dos diários oficiais de cada município e baixa todos esses arquivos. Uma vez com os arquivos baixados, convertemos eles em texto (arquivos txt) e pronto! Depois disso podem ser analisados mais facilmente com as mágicas da ciência de dados! :-)

Antes de mais nada vamos rodar um spider para ver como funciona. Depois vamos ver como ele é escrito. Para baixar o projeto você ira precisar do git. A instalação do git vai variar de acordo com seu ambiente. Veja a documentação sobre a instalação aqui. Se precisar de ajuda, não deixe de falar nos canais de comunicação dos Embaixadores ou do Serenata. Podemos ajudar por lá! Aliás, esse artigo será todo baseado no meu ambiente, que é um Linux. Não creio que vão existir grandes diferenças entre os sistemas que possa impossibilitar de seguir esse material. A principal diferença, acredito, que seja como irá baixar o repositório do projeto. Mas como disse anteriormente, não hesite em perguntar!

Blz, uma vez com o gitinstalado precisamos baixar o repositório, ou seja o código que o robozinho ira executar. Para isso, vá até a página do projeto no github e copie a URL:

Uma vez com a URL podemos clonar o repositório. Clonar é um termo do git que basicamente é um sinônimo para baixar um repositório:

jvanz@earth:~> git clone https://github.com/okfn-brasil/diario-oficial.git
Cloning into 'diario-oficial'...
remote: Enumerating objects: 30, done.
remote: Counting objects: 100% (30/30), done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 2253 (delta 13), reused 19 (delta 9), pack-reused 2223
Receiving objects: 100% (2253/2253), 11.68 MiB | 7.04 MiB/s, done.
Resolving deltas: 100% (1320/1320), done.

Agora que você já tem o código fonte na sua máquina, podemos executar algum spider que já existe. Os spider já existentes podem ser encontrados no diretório processing/data_collection/gazette/spiders/:

jvanz@earth:~/diario-oficial> ls processing/data_collection/gazette/spiders/
al_maceio.py            base.py                      go_goiania.py       pr_cascavel.py       pr_maringa.py         rr_boa_vista.py      sp_campinas.py   sp_jundiai.py              to_palmas.py
am_manaus.py            ce_fortaleza.py              __init__.py         pr_curitiba.py       pr_ponta_grossa.py    rs_caxias_do_sul.py  sp_franca.py     sp_santos.py
ba_feira_de_santana.py  es_associacao_municipios.py  mg_uberaba.py       pr_foz_do_iguacu.py  rj_rio_de_janeiro.py  rs_porto_alegre.py   sp_guaruja.py    sp_sao_jose_dos_campos.py
ba_salvador.py          go_aparecida_de_goiania.py   ms_campo_grande.py  pr_londrina.py       ro_porto_velho.py     sc_florianopolis.py  sp_guarulhos.py  to_araguaina.py
jvanz@earth:~/diario-oficial> 

Agora que já sabemos quais sãos as cidades já mapeadas, vamos rodar nosso robozinho para baixar os arquivos? Para rodar o robozinho e baixar os arquivos de Florianópolis - SC, podemos executar o seguinte comando:

$ docker-compose run --rm processing bash -c "cd data_collection && scrapy crawl sc_florianopolis"

Note que para executar o nosso robô, precisamos instalar o docker e o docker-comose. Veja como instala-los aqui e aqui

Você provavelmente irá ver algo parecido com isso:

2019-11-26 01:40:56 [scrapy.utils.log] INFO: Scrapy 1.6.0 started (bot: gazette)
2019-11-26 01:40:56 [scrapy.utils.log] INFO: Versions: lxml 4.4.1.0, libxml2 2.9.9, cssselect 1.1.0, parsel 1.5.2, w3lib 1.21.0, Twisted 19.10.0, Python 3.6.8 (default, Jun 11 2019, 01:16:11) - [GCC 6.3.0 20170516], pyOpenSSL 19.0.0 (OpenSSL 1.1.1d  10 Sep 2019), cryptography 2.8, Platform Linux-4.12.14-lp151.28.20-default-x86_64-with-debian-9.9
2019-11-26 01:40:56 [scrapy.crawler] INFO: Overridden settings: {'BOT_NAME': 'gazette', 'LOG_FILE': 'sc_florianopolis', 'NEWSPIDER_MODULE': 'gazette.spiders', 'SPIDER_MODULES': ['gazette.spiders']}
2019-11-26 01:40:56 [scrapy.extensions.telnet] INFO: Telnet Password: bc2ce55e70ec1566
2019-11-26 01:40:56 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.memusage.MemoryUsage',
 'scrapy.extensions.logstats.LogStats']
2019-11-26 01:40:56 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
 'scrapy.downloadermiddlewares.retry.RetryMiddleware',
 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2019-11-26 01:40:56 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
 'scrapy.spidermiddlewares.referer.RefererMiddleware',
 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2019-11-26 01:40:56 [scrapy.middleware] INFO: Enabled item pipelines:
['gazette.pipelines.GazetteDateFilteringPipeline',
 'gazette.parser.GazetteFilesPipeline',
 'scrapy.pipelines.files.FilesPipeline',
 'gazette.pipelines.ExtractTextPipeline',
 'gazette.pipelines.PostgreSQLPipeline']
2019-11-26 01:40:56 [scrapy.core.engine] INFO: Spider opened
2019-11-26 01:40:56 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2019-11-26 01:40:56 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:57 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)
2019-11-26 01:40:58 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_20.19.06.6b3ba3f7d8914621f5065d4d0f6c9d5e.pdf> (referer: None)
2019-11-26 01:40:58 [scrapy.pipelines.files] DEBUG: File (downloaded): Downloaded file from <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_20.19.06.6b3ba3f7d8914621f5065d4d0f6c9d5e.pdf> referred in <None>
2019-11-26 01:40:58 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_23.01.36.df3583c76e3e2ce083a2275cf3e9adbe.pdf> (referer: None)
2019-11-26 01:40:58 [scrapy.pipelines.files] DEBUG: File (downloaded): Downloaded file from <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_23.01.36.df3583c76e3e2ce083a2275cf3e9adbe.pdf> referred in <None>
2019-11-26 01:40:58 [scrapy.pipelines.files] DEBUG: File (uptodate): Downloaded file from <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_20.19.06.6b3ba3f7d8914621f5065d4d0f6c9d5e.pdf> referred in <None>
2019-11-26 01:40:58 [scrapy.pipelines.files] DEBUG: File (uptodate): Downloaded file from <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_23.01.36.df3583c76e3e2ce083a2275cf3e9adbe.pdf> referred in <None>
2019-11-26 01:40:58 [scrapy.core.scraper] DEBUG: Scraped from <200 http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial>
{'date': datetime.date(2019, 11, 18),
 'file_checksum': 'f24ca0401f64b6de3dc8a647dcbbea52',
 'file_path': 'full/c7955799d47d4fe59b6f582dad4b5c172508dac9.pdf',
 'file_url': 'http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_20.19.06.6b3ba3f7d8914621f5065d4d0f6c9d5e.pdf',
 'is_extra_edition': False,
 'power': 'executive_legislature',
 'scraped_at': datetime.datetime(2019, 11, 26, 1, 40, 57, 88559),
 'source_text': '                            DIÁRIO OFICIAL ELETRÔNICO\n'
                '                                              DO MUNICÍPIO DE '
                'FLORIANÓPOLIS\n'
                'Edição Nº 2568                          Florianópolis/SC, '
                'segunda-feira, 18 de novembro de '
                '2019                                                                  '
                'pg. 1\n'
                '                              '
                'Sumário:                                         '
                'Administrativo e Financeiro, lotado na Secretaria\n'
                'Orgãos '
                'Municipais                                                     '
                'Pg.      Municipal da Casa Civil, matrícula 49327-9,\n'
                'SECRETARIA MUNICIPAL DA CASA '
                'CIVIL                                     1\n'
                '                                                                               '
                'devidamente habilitado pela CNH sob nº\n'
                '                                                                               '
                '05084675850, categoria AB. Art. 2º A\n'
                'SECRETARIA MUNICIPAL DE '
                'ADMINISTRAÇÃO                                  1\n'
                '                                                                               '
                'responsabilidade administrativa, civil e penal, em\n'
                'SECRETARIA MUNICIPAL DA FAZENDA                   '
                '2                            caso de colisões, lesões '
                'corporais ou mesmo óbitos\n'
                'SECRETARIA MUNICIPAL DE TRANSPARÊNCIA,AUDITORIA '
                'E                              decorrentes do objetivo desta '

                [...]
                Texto omitido porque é MUITA coisa
                [...]

                'Portaria, qual seja,\n'
                'CONTROLE                                          '
                '4                            autorizar a condução do '
                'automóvel da Secretaria\n'
                'SECRETARIA MUNICIPAL DE '
                'EDUCAÇÃO                                       5       '
                'Municipal da Casa Civil, conforme termo de\n'
                'SECRETARIA MUNICIPAL DE '
                'INFRAESTRUTURA                                 6       '
                              '\n'
                '\n'
                '\n'
                'S.M.C.C.\n'
                'SECRETÁRIO: EVERSON '
                'MENDES                                        CONTROLE: '
                'THAMARA MALTA                                            '
                'TELEFONE: (48) 3251-6062\n'
                '\x0c',
 'territory_id': '4205407'}
2019-11-26 01:40:58 [scrapy.core.scraper] DEBUG: Scraped from <200 http://www.pm

Vamos dar uma analisada no que isso tudo significa.

Essa linha mostra qual é a página de web que o nosso robô está acessando e procurando os arquivos:

2019-11-26 01:40:56 [scrapy.core.engine] DEBUG: Crawled (200) <POST http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial> (referer: None)

Esse são arquivos que ele encontrou e baixou:

2019-11-26 01:40:58 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_20.19.06.6b3ba3f7d8914621f5065d4d0f6c9d5e.pdf> (referer: None)
2019-11-26 01:40:58 [scrapy.pipelines.files] DEBUG: File (downloaded): Downloaded file from <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_20.19.06.6b3ba3f7d8914621f5065d4d0f6c9d5e.pdf> referred in <None>
2019-11-26 01:40:58 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_23.01.36.df3583c76e3e2ce083a2275cf3e9adbe.pdf> (referer: None)
2019-11-26 01:40:58 [scrapy.pipelines.files] DEBUG: File (downloaded): Downloaded file from <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_23.01.36.df3583c76e3e2ce083a2275cf3e9adbe.pdf> referred in <None>
2019-11-26 01:40:58 [scrapy.pipelines.files] DEBUG: File (uptodate): Downloaded file from <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_20.19.06.6b3ba3f7d8914621f5065d4d0f6c9d5e.pdf> referred in <None>
2019-11-26 01:40:58 [scrapy.pipelines.files] DEBUG: File (uptodate): Downloaded file from <GET http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_23.01.36.df3583c76e3e2ce083a2275cf3e9adbe.pdf> referred in <None>

Da uma olhada nisso, aqui é texto extraido do arquivo baixado no site do diário oficial:

2019-11-26 01:40:58 [scrapy.core.scraper] DEBUG: Scraped from <200 http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial>
{'date': datetime.date(2019, 11, 18),
 'file_checksum': 'f24ca0401f64b6de3dc8a647dcbbea52',
 'file_path': 'full/c7955799d47d4fe59b6f582dad4b5c172508dac9.pdf',
 'file_url': 'http://www.pmf.sc.gov.br/arquivos/diario/pdf/18_11_2019_20.19.06.6b3ba3f7d8914621f5065d4d0f6c9d5e.pdf',
 'is_extra_edition': False,
 'power': 'executive_legislature',
 'scraped_at': datetime.datetime(2019, 11, 26, 1, 40, 57, 88559),
 'source_text': ' DIÁRIO OFICIAL ELETRÔNICO\n'
 ' DO MUNICÍPIO DE '
 'FLORIANÓPOLIS\n'
 'Edição Nº 2568 Florianópolis/SC, '
 'segunda-feira, 18 de novembro de '
 '2019 '
 'pg. 1\n'
 ' '
 'Sumário: '
 'Administrativo e Financeiro, lotado na Secretaria\n'
 'Orgãos '
 'Municipais '
 'Pg. Municipal da Casa Civil, matrícula 49327-9,\n'
 'SECRETARIA MUNICIPAL DA CASA '
 'CIVIL 1\n'
 ' '
 'devidamente habilitado pela CNH sob nº\n'
 ' '
 '05084675850, categoria AB. Art. 2º A\n'
 'SECRETARIA MUNICIPAL DE '
 'ADMINISTRAÇÃO 1\n'
 ' '
 'responsabilidade administrativa, civil e penal, em\n'
 'SECRETARIA MUNICIPAL DA FAZENDA '
 '2 caso de colisões, lesões '
 'corporais ou mesmo óbitos\n'
 'SECRETARIA MUNICIPAL DE TRANSPARÊNCIA,AUDITORIA '
 'E decorrentes do objetivo desta '
                  [...]
                Texto omitido porque é MUITA coisa
                [...]
 'Portaria, qual seja,\n'
 'CONTROLE '
 '4 autorizar a condução do '
 'automóvel da Secretaria\n'
 'SECRETARIA MUNICIPAL DE '
 'EDUCAÇÃO 5 '
 'Municipal da Casa Civil, conforme termo de\n'
 'SECRETARIA MUNICIPAL DE '
 'INFRAESTRUTURA 6 '
 '\n'
 '\n'
 '\n'
 'S.M.C.C.\n'
 'SECRETÁRIO: EVERSON '
 'MENDES CONTROLE: '
 'THAMARA MALTA '
 'TELEFONE: (48) 3251-6062\n'
 '\x0c',
 'territory_id': '4205407'}

Você pode deixar rodando até o robozinho baixar tudo, mas isso vai levar um tempinho. Para parar o robô, aperte Ctrl + c. Agora dê uma olhada nos arquivos baixados em data/full:

jvanz@earth:~/serenata/diario-oficial> ls data/full/                                                                                                                                                              
000206dc56c5753213168d1aad67e6d925862c0b.doc       3f67f0c7ee0e054bd477dd585bcaf895c712a175.doc.txt   7f3e870070e45083e503cc9858068d0dd87af122.pdf.txt   bf7f18a1e85067bee44f26cb5cd62274df08aa89.doc             
000206dc56c5753213168d1aad67e6d925862c0b.doc.txt   3f6a9abc3115beaa26b20cd5e43f403a570c9a4e.doc       7f3fb482b9472f8fcc03c84090ee9825b19b9483.doc       bf7f18a1e85067bee44f26cb5cd62274df08aa89.doc.txt         
00067ca470afa2d2ac2f9fffc0bc4f6c9e9cc0ef.pdf       3f6a9abc3115beaa26b20cd5e43f403a570c9a4e.doc.txt   7f3fb482b9472f8fcc03c84090ee9825b19b9483.doc.txt   bf7f52e8382df0d6ec569dae967018a07168c672.doc             
00067ca470afa2d2ac2f9fffc0bc4f6c9e9cc0ef.pdf.txt   3f6b8276365051fce95e154c128d2cca300b2ea0.doc       7f45558c7ade30fecb2f3c068be633190953fb71.doc       bf7f52e8382df0d6ec569dae967018a07168c672.doc.txt         
000ce6ae16c71f4e082191be246d8f5b0925984e.doc       3f6b8276365051fce95e154c128d2cca300b2ea0.doc.txt   7f45558c7ade30fecb2f3c068be633190953fb71.doc.txt   bf807874e9126e9c019490b236288c36d8622f43.pdf             
000ce6ae16c71f4e082191be246d8f5b0925984e.doc.txt   3f6e379f971523a0827788f275ad606d1bec2f93.doc       7f49aafd3f51b330136164aca8170a26ab8c679e.doc       bf807874e9126e9c019490b236288c36d8622f43.pdf.txt         
0011b654ba29d6d4415908f1251245b4f7f3909e.doc       3f6e379f971523a0827788f275ad606d1bec2f93.doc.txt   7f49aafd3f51b330136164aca8170a26ab8c679e.doc.txt   bf82c035df0480b351e5594baf9d55262562d307.doc             
0011b654ba29d6d4415908f1251245b4f7f3909e.doc.txt   3f70de7787371b4b2676a61587a592239311988c.doc       7f5168d009b6c54d1f03f8663d56e5692fe29af1.doc       bf82c035df0480b351e5594baf9d55262562d307.doc.txt         
00139767bba460a2d731e7e08f69d1ad065d9d42.pdf       3f70de7787371b4b2676a61587a592239311988c.doc.txt   7f5168d009b6c54d1f03f8663d56e5692fe29af1.doc.txt   bf8c5d2b8a0ffd03090a25d66791c9a58e1da1f5.doc             
00139767bba460a2d731e7e08f69d1ad065d9d42.pdf.txt   3f7393fbda95cc315b50af2af0f22f11ffbf818a.doc       7f554d50573d1d4fb1106ca921ab810e6e928de1.doc       bf8c5d2b8a0ffd03090a25d66791c9a58e1da1f5.doc.txt         
00195ec0e20d47e36f9dd156652c971d01e996e3.pdf       3f7393fbda95cc315b50af2af0f22f11ffbf818a.doc.txt   7f554d50573d1d4fb1106ca921ab810e6e928de1.doc.txt   bf8f2dc63020f0fd4d354a1780b5393dc88828c0.pdf             
00195ec0e20d47e36f9dd156652c971d01e996e3.pdf.txt   3f77937f5e7cfa9e2019dec8b28cfc1d1e4f091e.doc       7f5bd9eb72d24d7ce3c40b1f8a8885c5cedc606a.doc       bf8f2dc63020f0fd4d354a1780b5393dc88828c0.pdf.txt         

Ótimo! Agora que você já rodou o robô. Está na hora de entendermos mais a fundo algumas partes que fazem isso tudo funcionar.

Python

Python é a linguagem de programação que utilizamos para escrever o nosso robô. Não pretendo me aprofundar muito em Python porque isso é um mundo gigantesco que, por si só, renderia muito e muitos artigos. Mas para ajudar aqueles que estão querendo aprender, sugiro alguns materiais que podem ser encontrados na Internet:

Python para zumbis

Tutorial realizado pelo Grupy Blumenau

Scrapy

Scrapy é a biblioteca que utilizamos para acessar, navegar e encontrar os que queremos nas páginas dos diários oficiais. Podemos dizer que é o coração do nosso robô. Essa lib faz o trabalho sujo que baixar e deixar disponíveis de maneira mais fácil os dados das páginas que estamos vasculhando.

Vamos dar uma olhada como é definido o nosso robô e a maneira de navegar pelas páginas web. Ah! Só para deixar claro, "spider" é o nome dados no scrapy para código que irá acessar e baixar os dados do site do diário oficial. Para exemplificar, vou utilizar a cidade de Florianópolis:

import re
from datetime import date, datetime

from dateparser import parse
from dateutil.relativedelta import relativedelta
from scrapy import FormRequest

from gazette.items import Gazette
from gazette.spiders.base import BaseGazetteSpider


class ScFlorianopolisSpider(BaseGazetteSpider):
    name = "sc_florianopolis"
    URL = "http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial"
    TERRITORY_ID = "4205407"
    AVAILABLE_FROM = date(2015, 1, 1)  # actually from June/2009

    def start_requests(self):
        target = date.today()
        while target >= self.AVAILABLE_FROM:
            year, month = str(target.year), str(target.month)
            data = dict(ano=year, mes=month, passo="1", enviar="")
            yield FormRequest(url=self.URL, formdata=data, callback=self.parse)
            target = target + relativedelta(months=1)

    def parse(self, response):
        for link in response.css("ul.listagem li a"):
            url = self.get_pdf_url(response, link)
            if not url:
                continue

            yield Gazette(
                date=self.get_date(link),
                file_urls=(url,),
                is_extra_edition=self.is_extra(link),
                territory_id=self.TERRITORY_ID,
                power="executive_legislature",
                scraped_at=datetime.utcnow(),
            )

    @staticmethod
    def get_pdf_url(response, link):
        relative_url = link.css("::attr(href)").extract_first()
        if not relative_url.lower().endswith(".pdf"):
            return None

        return response.urljoin(relative_url)

    @staticmethod
    def get_date(link):
        text = " ".join(link.css("::text").extract())
        pattern = r"\d{1,2}\s+de\s+\w+\s+de\s+\d{4}"
        match = re.search(pattern, text)
        if not match:
            return None

        return parse(match.group(), languages=("pt",)).date()

    @staticmethod
    def is_extra(link):
        text = " ".join(link.css("::text").extract())
        return "extra" in text.lower()

Nas primeiras linhas do script estamos dizendo ao Python o que vamos utilizar. Por hora, vamos ignora-las e focar no spider. A brincadeira começa a ficar séria a partir da linha class ScFlorianopolisSpider(BaseGazetteSpider). Não quero entrar em muito detalhes de programação, mas podemos dizer que uma class (ou classe em português), neste caso, é uma representação de de como iremos navegar e baixar os dados das páginas de internet. As linhas seguintes são:

    name = "sc_florianopolis"
    URL = "http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial"
    TERRITORY_ID = "4205407"
    AVAILABLE_FROM = date(2015, 1, 1)  # actually from June/2009

O atributo name diz qual é o nome do spider. Lembra quando rodamos o spider a primeira vez? Então, nós passamos o nome do spider que queríamos rodar. É baseado nesse nome que o scrapy encontra e executa o spider.

URL é um atributo que guarda a primeira página que será visitada quando quisermos pegar os dados de Floripa. Veremos como essa informação é utilizada daqui a pouco. TERRITORY_ID é o código do IBGE da cidade que esse spider está extraindo os dados. AVAILABLE_FROM é algo especifico desse spider. A data colocada nesse atributo diz ao robozinho desde qual data ele deve procurar os arquivos do diário oficial. Uma informação importante, URL, AVAILABLE_FROM e TERRITORY_ID não são campos utilizados pelo scrapy. Eles são usados no código escrito pelas pessoas que criaram esse spider.

    def start_requests(self):
        target = date.today()
        while target >= self.AVAILABLE_FROM:
            year, month = str(target.year), str(target.month)
            data = dict(ano=year, mes=month, passo="1", enviar="")
            yield FormRequest(url=self.URL, formdata=data, callback=self.parse)
            target = target + relativedelta(months=1)

O scrapy espera que o as classes definidas possuam alguns métodos para que possa funcionar corretamente. Quando um spider vai ser executado, o scrapy chama o método start_requests para saber qual é a primeira página que ele deve baixar. Note que o método start_requets retorna um objeto FormRequest. Nesse caso, ele representa uma request que deve ser feita. O scrapy permite que você retorne outros valores nesse método. Mas por motivos de simplicidade, vamos deixar isso para outra hora. O que for baixado da request retornada pelo start_requests é processado e passado para o método parse:

    def parse(self, response):
        for link in response.css("ul.listagem li a"):
            url = self.get_pdf_url(response, link)
            if not url:
                continue

            yield Gazette(
                date=self.get_date(link),
                file_urls=(url,),
                is_extra_edition=self.is_extra(link),
                territory_id=self.TERRITORY_ID,
                power="executive_legislature",
                scraped_at=datetime.utcnow(),
            )

Note que o método parse recebe como parametro um objeto. Esse objeto, que neste caso é chamado de response, é o que contem o que foi baixado pelo scrapy daquela requisão que você retornou anteriormente no método start_requets. É aqui que a mágica acontece. É nesse método que você define como nosso robô vai achar os arquivos do diário oficial. Vamos destrinchar o que está acontecendo aqui.

Logo no início do método é chamado uma função chamada css. CSS é uma forma que podemos utilizar para encontrar elementos na página. Nesse exemplo, estamos procurando listas não numeradas, que tenham a classe (classe do CSS, não é a mesma classe do python) listagem e pegando os links que existem nos itens dessa lista. Sei que é um pouco complicado de entender no primeiro momento. Mas logo a seguir teremos uma sessão só para explicar isso. ;)

Uma vez com os link disponíveis da lista, para cada um deles chamamos o método get_pdf_url que irá extrair os link dos arquivos do diário oficial. Uma vez com o link, nós retornamos um objeto Gazette. Um objeto nada mais é do que uma instanciação em memória de uma classe. Lembra da classe que vimos que ensina o nosso robô a encontrar os arquivos, a ScFlorianopolisSpider? Então, o scrapy instância essa classe e chamar seus métodos. Não se preocupe se não compreendeu isso com 100%. Você vai aprendendo conforme for programando.

Voltando ao Gazette... da mesma forma que o ScFlorianopolisSpider é uma classe que representa a "inteligência" de como o nosso robô sabe baixar os arquivos do diário oficial de Floripa. O Gazette representa um arquivo do diário oficial encontrado. Essa classe possuir alguns atributos que servem para identificar o arquivo. Como a date que é a data do diário oficial, file_urls contem o link para os arquivos do diário, territory_id é o identificador do município no IBGE, power diz de qual Poder é o documento. O scraped_at mostra qual é a data que o arquivo foi baixado.

Não sei se você notou que existem mais métodos não citados até agora. São métodos que foram criados para serem utilizados nos métodos citados. Dê uma olhada nele e veja se consegue entender o que eles fazem. Se estiver com dúvidas, não deixe de perguntar nos canais de comunicação dos embaixadores ou me pergunte pelas redes sociais que você pode encontrar aqui no meu blog.

Continuando... conseguimos baixar a página do diário oficial e encontrar os arquivos. Mas a primeira página do diário não tem todos os arquivos. Precisamos continuar navegando pelas páginas para baixar os que faltam. Temos duas opções:

Retorna um objeto request na função parse.

Quando o scrapy chama o método parse, ele espera de retorno um objeto de que representa uma requisição a outra página, o mesmo tipo de objeto retornado pelo start_requests, um item (como o Gazette), ou ainda uma lista/iterável de ambos. Isso significa que se retornarmos um objeto que represente uma requisição, o scrapy irá baixar a página e chamar a função parse novamente. Se retomarmos um item (Gazette) o scrapy assume que achamos o que estávamos procurando e passa esse item para o próximo passo na nossa linha de execução. Que nesse caso, é transformar os arquivos em texto e gravar as informações em um banco de dados.

Quando o método parse retornar nada, significa que já achamos tudo o que queremos e nosso robô pode parar e recarregar as baterias.

Retornar uma lista/iterável na função start_requets

Dê uma olhada no start_requets novamente. Note que é usado a palavra reservada yield ao invés de return. Isso significa que o método é uma função geradora. O que isso quer dizer? Novamente, simplificando bastante, é como se o retorno da função fosse tratado como uma lista. Isso faz que quando o scrapy chamar o next no retorno da start_request a execução irá continuar a partir de onde o yield anterior foi chamado. Ou seja, enquanto o start_request não retorna None ou uma lista finita de itens, o scrapy continuará baixando as páginas das URL retornadas pela função start_requests. Entenda mais sobre isso no video do Python para Zumbies

Alias! Perceba que a função parse também é uma função geradora. ;)

PS: No momento dessa escrita, o spider do Floripa tem um bug. O spider nunca termina.

Pipeline

Vamos voltar ao que acontece depois que retornamos um item, um arquivo do diário oficial. Uma vez que encontramos algo que estávamos procurando o scrapy passa esse item por uma série de procedimentos definidos em um arquivo de configuração. No scrapy, essa séria de procedimentos é chamada de pipeline. Todos os procedimentos executados nesse pipeline estão definidos no arquivo processing/data_collection/gazette/pipelines.py. Lá você vai ver o PostgreSQLPipeline que é a etapa que grava aquelas informações que você definiu la no objeto Gazette retornado pela função parse no banco de dados. Existe o GazetteDateFilteringPipeline que ignora os arquivos do diário oficial anteriores a uma data determinada no spider. Vai ver também o ExtractTextPipeline é o passo que extrai o texto dos arquivos de PDF, doc e texto.

Se tudo isso funcionar como o esperado. No final você terá diversos arquivos na pasta data/full. :-)

HTML, CSS e XPATH

HTML

HTML é como são definidas as página web. Por exemplo, esse é uma página web muito simples que você pode salvar em uma arquivo HTML local e abrir em seu navegador:

<html>
    <head>
        <title> Esse é o título da nossa página! </title>
    </head>
     <body>
         Esse é corpo da página.
         <a href="https://www.google.com">LInk para o google</a>
     </body>
</html>

É isso com esse tipo de dados que iremos trabalhar dentro do spiders para achar e detectar os arquivos que estamos procurando.

CSS

Anteriormente comentei que CSS poderia ser utilizado para encontrar os arquivos dos diários oficiais que estamos procurando. Isso não é totalmente correto. CSS é muito mais que isso. De uma forma beeeeem resumida, CSS é o que deixa as páginas da rede mundial de computadores bonitas! ;-)

O que nós usamos estamos utilizando são os seletores CSS. Os seletores são utilizados para identificar os elementos da página que queremos modificar. Por exemplo, podemos utilizar seletores para dizer que todas as listas da minha página terão fundo preto e letras verdes. No nosso caso, não utilizamos os seletores para modificar o visual, mas sim, para achar os elementos que nos interessam. Você pode brincar com isso agora mesmo no seu navegador. Vou utilizar o mesmo seletor e página que estamos estudando até agora.

No seu navegador acesse esse link: http://www.pmf.sc.gov.br/governo/index.php?pagina=govdiariooficial.

Se a página não mudou desde quando escrevi esse artigo você deverá estar vendo algo parecido com isso:

Legal, agora vamos brincar com os seletores. Vou utilizar o Firefox, mas o procedimento é muito parecido em outros navegadores. Clique com o botão direito na página e clique na opção "Inspecionar elemento". Agora você deverá estar vendo algo parecido com isso, note que você pode ver o HTML da página(seta 2):

Nesssa tela podemos testar o nosso seletor CSS (seta 1) e ver o que ele encontrou (seta 3). Olha só que legal, encontramos 17 items (seta 4). Note que são exatamente os link para os arquivos que estamos procurando! São esses mesmos itens que são processados na função parse.

XPATH

Seletores CSS não são a única forma de você achar os elementos nas páginas. Você pode encontrar utilizando XPATH. Não vou entrar no XPATH nesse momento. Acho que o artigo já tem bastante coisa para todos que estiverem interessado em brincar com por um bom tempo. Além do que, os seletores CSS já podem resolver muitos, senão a maioria, dos casos. Mas para quem ficou interessado tem o seguinte link para a documentação da Mozilla