Implementar IGP-M
cuducos opened this issue · 23 comments
Implementar adaptador para o IGP-M.
Vou pegar isso. Por favor, me coloca como assignee. :)
Só para documentar (e lembrar no futuro): a fonte de IGP-M é a FGV, mas eles não liberam série histórica —pelo menos eu não achei.
Mas achei no Banco Central. Por isso, a fonte é esta: https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?method=downLoad&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=[SGSFW2301]
Com isso se baixa um csv
com os dados atualizados.
Só para documentar (e lembrar no futuro): a fonte de IGP-M é a FGV, mas eles não liberam série histórica —pelo menos eu não achei.
Achei nessa URL aqui: http://www14.fgv.br/fgvdados20/visualizaconsulta.aspx
Clicando no "Acesse aqui" e, depois em "Séries institucionais" parece funcionar (falei em off isso, mas registrando aqui).
Parece que este bloco funciona:
import requests
ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
URL = ROOT + SERIE + ARGS
session = requests.Session()
cookie_jar = requests.cookies.RequestsCookieJar()
cookie_jar.set('dtcookie', 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ')
session.cookies = cookie_jar
r = session.get(URL)
print(r.text)
Mais tarde vou usar BS4
para parsear o html e pegar apenas table
.
Parece que este bloco funciona [...]
Hell yeah! Funcionou aqui sim : )
Mais tarde vou usar BS4 para parsear o html e pegar apenas
table
.
Posso sugerir outro caminho? Instale a rows
com pip install rows[html,xls]
(ao invés de só rows[xls]
como está hoje) e vamos testar o rows.import_from_html
. A vantagem seria código mais em alto nível, uma dependência direta a menos, e, se funcionar direitinho, podemos deixar a serializaçào dos dados a cargo da rows
; )
Cheguei bêbado e testei rows
. Estamos quase lá...
import requests
from io import BytesIO
import rows
ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
URL = ROOT + SERIE + ARGS
session = requests.Session()
cookie_jar = requests.cookies.RequestsCookieJar()
cookie_jar.set('dtcookie', 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ')
session.cookies = cookie_jar
r = session.get(URL)
r.encoding = 'ISO-8859-1'
html = r.content
html_decoded = html.decode('iso-8859-1').encode('utf8')
table = rows.import_from_html(BytesIO(html_decoded), index=4)
for row in table:
print(row)
(Tive um probleminha de encoding
, mas contornei...)
Sensacional.
A parte do código tb. Hahaha...
Pensando aqui em como encapsular isso no Adapter
e no Download
, rascunhei esse pseudo código pq me empolguei. Escrevi aqui mesmo, não rodei nem nada! Compartilho só para tentar ajudar. Mas pode ignorar se quiser!
class Adapter(meta...):
@property
def cookies(self) -> dict:
return getattr(self, "COOKIES", {})
Aé rola passar essas infos na hora de instanciar o Download
. E, lá no Downloads
, algo como:
def http(self, path -> Path) -> Path:
session = Session()
cookie_jar = cookiejar_from_dict(self.cookies)
session.cookies = cookie_jar
response = session.get(self.url)
path.write_bytes(response.content)
return path
Não usei o encoding de propósito, pois acho que isso ficaria no Adapter
, como kwarg na hora de abrir o arquivo. Também teria que mudar uma linha para que o protocolo https
seja lido como http`, mas deixemos esses detalhes pro PR : )
Só vim dar m toque aqui pois com o d628e8e:
- O
Adapter
base agora suporta URL com HTTP e também HTTP com cookies! - No
README.md
tem um mini-guia para criar novos adaptadores ; )
Opa. Deixei essa issue "descansando" dada a minha inaptidão para OOP. rs...
Vou ver o README.md
e ver se sigo. :)
@cuducos , acho que preciso de uma ajuda...
O IGP-M vem com aquele encoding zoado. É preciso decode
de ISO-8859-1
e, em seguida, encode
de UTF-8
. Quando fiz o rascunho, ficou assim:
import requests
from io import BytesIO
import rows
ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
URL = ROOT + SERIE + ARGS
session = requests.Session()
cookie_jar = requests.cookies.RequestsCookieJar()
cookie_jar.set('dtcookie', 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ')
session.cookies = cookie_jar
r = session.get(URL)
r.encoding = 'ISO-8859-1'
html = r.content
html_decoded = html.decode('iso-8859-1').encode('utf8')
table = rows.import_from_html(BytesIO(html_decoded), index=4)
for row in table:
print(row)
Tentei reproduzir isso em download.py
[a partir da linha 44], ou não conseguimos ler. Fiz isso:
def http(self, path: Path) -> Path:
session = Session()
if self.cookies:
session.cookies = cookiejar_from_dict(self.cookies)
response = session.get(self.url)
if response.encoding != 'utf-8':
raw_encoding = response.content
decoded = raw_encoding.decode('iso-8859-1').encode('utf-8')
path.write_bytes(decoded)
else:
path.write_bytes(response.content)
return path
Ainda assim, não consegui. Deu UnicodeDecodeError
:
In [14]: igpm = Igpm()
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-14-a5c7534a4bc0> in <module>()
----> 1 igpm = Igpm()
/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in __init__(self)
31 raise AdapterNoImportMethod(msg)
32
---> 33 self.data = {key: value for key, value in self.download()}
34 if self.data:
35 self.most_recent_date = max(self.data.keys())
/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in <dictcomp>(.0)
31 raise AdapterNoImportMethod(msg)
32
---> 33 self.data = {key: value for key, value in self.download()}
34 if self.data:
35 self.most_recent_date = max(self.data.keys())
/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in download(self)
108 with download() as path:
109 for kwargs in self.import_kwargs:
--> 110 for data in self.read_from(path, **kwargs):
111 yield from (row for row in self.serialize(data) if row)
/home/rodolfo/.local/lib/python3.7/site-packages/rows/plugins/plugin_html.py in import_from_html(filename_or_fobj, encoding, index, ignore_colspan, preserve_html, properties, table_tag, row_tag, column_tag, *args, **kwargs)
77 """Return rows.Table from HTML file."""
78 filename, fobj = get_filename_and_fobj(filename_or_fobj, mode="rb")
---> 79 html = fobj.read().decode(encoding)
80 html_tree = document_fromstring(html)
81 tables = html_tree.xpath("//{}".format(table_tag))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xea in position 6160: invalid continuation byte
Tem alguma sugestão?
Sugestão: você pode passar um encoding para a função import_from_html
, dessa forma não precisa fazer o decode (e depois o re-encode para UTF-8) do HTML. Além disso, é mais robusto usar response.apparent_encoding
em vez de response.encoding
. Sugiro essa alteração:
# ...
response = session.get(URL)
html = response.content
table = rows.import_from_html(BytesIO(html), encoding=response.apparent_encoding, index=4)
for row in table:
print(row)
Caso queiram detectar o encoding de maneira mais rápida e confiável, vejam esse vídeo que criei.
@turicas , valeu pela sugestão. :)
Em download.py
, porém, não chegamos a chamar a função import_from_html
; isso fica em base.py
, no __init__
da classe:
class Adapter(metaclass=ABCMeta):
def __init__(self) -> None:
functions = {"html": import_from_html, "xls": import_from_xls}
try:
self.read_from = functions[self.file_type]
except KeyError:
msg = (
f"Invalid file type {self.file_type}. "
f"Valid file types are: {', '.join(functions)}."
)
raise AdapterNoImportMethod(msg)
self.data = {key: value for key, value in self.download()}
if self.data:
self.most_recent_date = max(self.data.keys())
if self.should_aggregate:
self.aggregate()
Então ficaria "estranho" checar o encoding em download.py
e fazer qualquer alteração de encoding em base.py
, não?
( Na verdade, eu não faço ideia de como implementar isso ou mesmo o io.BytesIO
necessário...)
Talvez seja o caso de alguém mais experiente pegar o IGP-M para eu não cagar tudo... :/
@rodolfo-viana basta dá pra setar o IMPORT_KWARGS
na classe do adapter (veja: https://github.com/cuducos/calculadora-do-cidadao/blob/master/calculadora_do_cidadao/base.py#L110).
@turicas não rolou. Setei no IMPORT_KWARGS
, mas ele vira NoneType
e não pode ser iterado.
Rola mostrar como tá teu adapter e essa linha do IMPORT_KWARGS
? Seria esse o caminho mesmo: download faz tudo em bytes sem se preocupar com encoding, que seria configurado no IMPORT_KWARGS
… mas posso ter deixado algum bug sem querer ¯_(ツ)_/¯
@cuducos , para mim parece ser o seguinte caso: download.py
está ok; é possível, em igpm.py
, indicar o encoding de entrada; como não há conversão para utf-8
, fica NoneType
.
Repare que, antes de criar igpm.py
, quando estava apenas testando o link, tive de usar io.BytesIO
:
import requests
from io import BytesIO
import rows
ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
URL = ROOT + SERIE + ARGS
session = requests.Session()
cookie_jar = requests.cookies.RequestsCookieJar()
cookie_jar.set('dtcookie', 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ')
session.cookies = cookie_jar
r = session.get(URL)
r.encoding = 'ISO-8859-1'
html = r.content
html_decoded = html.decode('iso-8859-1').encode('utf8')
table = rows.import_from_html(BytesIO(html_decoded), index=4)
for row in table[:-1]:
value = getattr(row, "field_189_monthly_var")
date = getattr(row, "date_monthyyyy")
month = date[0:3]
year = date[4:]
print(year)
Esse processo i/o não é repetido na função serialize()
. Não sendo repetido, não é possível iterar.
Mas como está tem igpm.py
? Estou acompanhando do celular, será que perdi no fio?
@cuducos , conforme falamos, aqui os arquivos em que mexi.
adapters/igpm.py
:
from datetime import date
from typing import NamedTuple
from calculadora_do_cidadao.base import Adapter
from calculadora_do_cidadao.fields import PercentField
from calculadora_do_cidadao.months import MONTHS
from calculadora_do_cidadao.typing import MaybeIndexesGenerator
class Igpm(Adapter):
ROOT = 'https://www3.bcb.gov.br/sgspub/consultarvalores/consultarValoresSeries.do?'
SERIE = 'method=consultarValores&optSelecionaSerie=189&dataInicio=30%2F06%2F1989&'
ARGS = 'selTipoArqDownload=1&hdOidSeriesSelecionadas=189&hdPaginar=false&bilServico=%5BSGSFW2301%5D'
url = ROOT + SERIE + ARGS
COOKIES = {'dtcookie': 'EB62E3A5ABDDF04A5F354D7F23CC2681|c2dzfDF8X2RlZmF1bHR8MQ'}
file_type = "html"
SHOULD_AGGREGATE = True
IMPORT_KWARGS = tuple({
"index": index,
"encoding": 'iso-8859-1'
} for index in range(1, 4))
def serialize(self, row: NamedTuple) -> MaybeIndexesGenerator:
print(row)
#value = getattr(i, "field_189_monthly_var")
#date = getattr(i, "date_monthyyyy")
#month = date[0:3]
#year = date[4:]
# if value is None:
# continue
# yield self.round_date(date(year, month, 1)), value
adapters/__init__.py
:
from calculadora_do_cidadao.adapters.ipca import Ipca # noqa
from calculadora_do_cidadao.adapters.selic import Selic # noqa
from calculadora_do_cidadao.adapters.igpm import Igpm # noqa
E aqui, o TypeError
:
In [1]: from datetime import date
In [2]: from decimal import Decimal
In [3]: from calculadora_do_cidadao.adapters import Igpm
In [4]: igpm = Igpm()
Row(field_0='Period', csv_file='Function')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-a5c7534a4bc0> in <module>()
----> 1 igpm = Igpm()
/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in __init__(self)
31 raise AdapterNoImportMethod(msg)
32
---> 33 self.data = {key: value for key, value in self.download()}
34 if self.data:
35 self.most_recent_date = max(self.data.keys())
/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in <dictcomp>(.0)
31 raise AdapterNoImportMethod(msg)
32
---> 33 self.data = {key: value for key, value in self.download()}
34 if self.data:
35 self.most_recent_date = max(self.data.keys())
/home/rodolfo/Documents/Github/calculadora-do-cidadao/calculadora_do_cidadao/base.py in download(self)
109 for kwargs in self.import_kwargs:
110 for data in self.read_from(path, **kwargs):
--> 111 yield from (row for row in self.serialize(data) if row)
TypeError: 'NoneType' object is not iterable
Teu IMPORT_KWARGS
é uma tupla, ou seja, é uma sequência com alguns dicionários:
({'index': 1, 'encoding': 'iso-8859-1'},
{'index': 2, 'encoding': 'iso-8859-1'},
{'index': 3, 'encoding': 'iso-8859-1'})
Como explicado no README.md
, isso faz com que o import_from_html
seja chamo 3 vezes, cada uma um com desses dicionários como kwargs. Nos teus exemplos aqui você não precisa chamar o import_from_html
3 vezes, só precisa chamar uma com {'index': 4, 'encoding': 'iso-8859-1'}
.
Ou seja, tu precisa de um IMPORT_KWARGS
mais simples. Teste aí com:
IMPORT_KWARGS = {"encoding": "iso-8859-1", "index": 4}
E nos conte se rola progresso ; )