-
-
Notifications
You must be signed in to change notification settings - Fork 382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Raspador de Suzano-SP #983
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oi @carlosfrodrigues ! Esse site deve ter dado dor de cabeça, deu em mim também. Fizemos a revisão do código no GT de Raspadores dessa última terça 21/11/23 então está bem completo. Testamos ao vivo e até conseguimos coletar mais arquivos do que originalmente pois não coleta apenas PDF. Deixo aqui várias dicas e proposta de melhorias. Fica a vontade pra tirar dúvida ou questionar qualquer coisa!
Por documentação:
Algumas datas não são confiáveis como na edição 049 onde o título apresenta publicação em 14/02/2018 mas deveria ser de 14/03/2018 e nesse caso daria pra corrigir pela URL. Porém em outras edições o título está correto e a URL não como na edição 102 de 01/06/2019.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Esse arquivo pode ser removido do PR. Não precisava subir ele no conteúdo do PR, bastava linkar na descrição mesmo hehehe
name = "sp_suzano" | ||
TERRITORY_ID = "3552502" | ||
start_date = datetime.date(2017, 8, 1) | ||
start_urls = ["https://suzano.sp.gov.br/web/transparencia/imprensa-oficial/"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Foi percebido que no início da raspagem dois redirects aconteciam:
2023-11-21 19:34:34 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (301) to <GET https://suzano.sp.gov.br/transparencia/imprensa-oficial> from <GET https://suzano.sp.gov.br/web/transparencia/imprensa-oficial/>
2023-11-21 19:34:35 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (301) to <GET https://suzano.sp.gov.br/transparencia/imprensa-oficial/> from <GET https://suzano.sp.gov.br/transparencia/imprensa-oficial>
2023-11-21 19:34:35 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://suzano.sp.gov.br/transparencia/imprensa-oficial/> (referer: None)
Pra evitar essas requisições extras toda vez que formos raspar o dia anterior, é melhor substituir o start_urls
já com o endereço final:
start_urls = ["https://suzano.sp.gov.br/web/transparencia/imprensa-oficial/"] | |
start_urls = ["https://suzano.sp.gov.br/transparencia/imprensa-oficial/"] |
info = response.xpath("/html/body/div[2]/div/div/div[2]/div/div/*/*/span[2]") | ||
data = response.xpath( | ||
"/html/body/div[2]/div/div/div[2]/div/div/*/div[@class='desc']" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sugiro não usar o xpath completo nos seletores pois são muito rígidos, ou seja, propensos a erros por mudanças pequenas na estrutura do html. Temos uma aula no curso de Python p/ Inovação Cívica da Escola de Dados sobre seletores. Lá cobrimos um pouco de xpath. Vale dar uma conferida.
Mas sugiro uso de css aqui pois nesse caso as âncoras de seleção que temos no html usam bastante o atributo class
e css lida melhor com ele do que xpath:
info = response.xpath("/html/body/div[2]/div/div/div[2]/div/div/*/*/span[2]") | |
data = response.xpath( | |
"/html/body/div[2]/div/div/div[2]/div/div/*/div[@class='desc']" | |
) | |
titles = response.css("div.box-editais-licitacoes-2015 div.g-el2 > p > span.b::text") | |
# | |
# sugiro usar um nome diferente para a variável (titles > info) para ficar um pouco mais descritivo | |
url_boxes = response.css( | |
"div.box-editais-licitacoes-2015 div.g-el2 > p + div" | |
) # também não usamos xpath completo, passamos a usar css e sugerir url_boxes > data | |
Além disso, sugiro mudar os nomes das variáveis pra representarem um pouco melhor o que estamos coletando, info
e data
ficaram um pouco genéricos.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As próximas sugestões de modificação vão usar essa nomenclatura de titles
e url_boxes
.
) | ||
file_pattern = r'href="(https://[^"]+\.pdf)"' | ||
date_pattern = r"(\d{1,2}[./]\d{1,2}[./]\d{4})" | ||
for index, element in enumerate(data): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Não é necessário usar índices ao lidar com duas lista de mesmo tamanho. Podemos usar zip
no lugar
for index, element in enumerate(data): | |
for title_raw, url_box in zip(titles, url_boxes): | |
title = info[index].get() | ||
matches_url = re.findall(file_pattern, element.get()) | ||
matches_date = re.findall(date_pattern, title) | ||
if matches_url and matches_date: | ||
url = matches_url[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aqui seria bom mudar algumas coisas:
- Estamos usando
re.findall
para buscar a url, quando poderíamos usar um seletor pro atributohref
(isso até faz com que possamos coletar arquivos não-PDF como .docx na edição 20.2 de 2021-01-28)
1.1. Imagino que você já tenha detectado isso, mas agora seria necessário verificar se há ou não urls (casos onde não aparecem urls ou aparece apenas uma ao invés do comum que seria a url da imagem primeiro e a url do arquivo por último) - Outra coisa também é que estávamos usando o
re.findall
pra pegar apenas a primeira ocorrência. Nesse caso é melhor usarre.search
. Usar o método.re_first
de uma resposta de seletor também é uma boa alternativa pois às vezes nem precisaríamos importar a bibliotecare
. - Achei que valia adicionar um log de debug para linhas sem data
- Ao invés de verificar
if matches_url and matches_date:
e continuar com o código dentro da verificação é interessante fazer esses curto-circuitos em Python pro código não expandir demais horizontalmente (nesse caso fazia sentido separar em 2 verificações distintas, mas também poderia verificar a negaçãonot matches_url or not matches_date
) - Apesar de o pipeline estar dando conta nesse caso, temos dado preferência a manter isso no código dos raspadores por padrão
title = info[index].get() | |
matches_url = re.findall(file_pattern, element.get()) | |
matches_date = re.findall(date_pattern, title) | |
if matches_url and matches_date: | |
url = matches_url[0] | |
urls = url_box.css("a::attr(href)").getall() | |
if not urls: | |
continue | |
url = urls[-1] | |
date_raw = title_raw.re_first(r"\d{1,2}[./]\d{1,2}[./]\d{4}") | |
if not date_raw: | |
self.logger.debug(f"Não é possível encontrar a data do elemento com título '{title_raw.get()}'. URL de referência: {url}") | |
continue | |
if self.start_date > date > self.end_date: | |
continue | |
if "/" in matches_date[0]: | ||
date = datetime.datetime.strptime( | ||
matches_date[0], "%d/%m/%Y" | ||
).date() | ||
else: | ||
date = datetime.datetime.strptime( | ||
matches_date[0], "%d.%m.%Y" | ||
).date() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Não é necessário usar dois casos de formato de data, podemos substituir um para o outro e apenas considerar um dos casos.
if "/" in matches_date[0]: | |
date = datetime.datetime.strptime( | |
matches_date[0], "%d/%m/%Y" | |
).date() | |
else: | |
date = datetime.datetime.strptime( | |
matches_date[0], "%d.%m.%Y" | |
).date() | |
date = datetime.datetime.strptime( | |
date_raw.replace(".", "/"), "%d/%m/%Y" | |
).date() | |
date = datetime.datetime.strptime( | ||
matches_date[0], "%d.%m.%Y" | ||
).date() | ||
is_extra = True if (title.find("Extra") != -1) else False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seria bom usar lowercase pra poder pegar outras possíveis formas de escrita e também fazer verificação de substring usando o in
me parece um pouco mais legível. Além disso, não é necessário a ternária True if ... else False
pois o retorno já é booleano (tanto no código original quanto na sugestão).
is_extra = True if (title.find("Extra") != -1) else False | |
is_extra = "extra" in title_raw.get().lower() | |
|
||
yield Gazette( | ||
date=date, | ||
file_urls=[url], | ||
is_extra_edition=is_extra, | ||
power="executive", | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vi que tínhamos as edições disponíveis pra coleta então quis fazer esse agrado. Me arrependi amargamente pois existem muitos casos de fronteira uawheauwehauweuhaweuawe
yield Gazette( | |
date=date, | |
file_urls=[url], | |
is_extra_edition=is_extra, | |
power="executive", | |
) | |
edition_regex = re.compile(r""" | |
(edição|edital) # o padrão inicia com 'edição' mas há caso de 'edital' | |
(\s+extra)? # pode ser uma edição 'extra' | |
(\s+n°)? # pode ou não ter um 'nº' | |
[\s–-]* # às vezes vezes o número de edição não é separado do que vem antes ou depois, antes geralmente pode vir espaço, traço ou travessão | |
([.\d]+) # número de edição | |
.+? # qualquer coisa entre o número de edição e a data de publicação, caso contrário a data pode ser capturada como número de edição | |
\d{1,2}[./]\d{1,2}[./]\d{4} # data de publicação | |
""", flags=re.IGNORECASE | re.VERBOSE) # dá pra pegar edition_number e isso é um inferno pois escrevem manualmente e é cheio de casos de fronteira | |
edition_number = re.search(edition_regex, title_raw.get()) | |
yield Gazette( | |
date=date, | |
file_urls=[url], | |
is_extra_edition=is_extra, | |
edition_number=edition_number.group(4) if edition_number else '', | |
power="executive", | |
) |
Segue o commit com o raspador e o log.
Fecha a issue #973