Web Scraping da B3 com Python
Buscando os ativos do IBX-100 e salvando no banco de dados.
Olá pessoal,
Começou ontem uma série de lives desenvolverei alguma coisa relacionada ao QuantBrasil, ao vivo. Se você não pôde assistir na hora, não se preocupe: as lives ficam salvas no canal. O código desenvolvido está disponível no GitHub.
Após os episódios do Code Night, resumirei nesse espaço os destaques. Assim, se você prefere um conteúdo mais resumido e por escrito, fique de olho nessa newsletter também.
Tarefa do dia
O objetivo do dia era salvar o portfólio IBX-100 no banco de dados do QuantBrasil. O IBX-100 é rebalanceado a cada 4 meses, o que significa que é interessante ter uma automação onde eu consiga rodar um script e garantir que tenho sua versão mais atualizada.
Realizando um scraping de dados
No próprio site da B3 existe a composição da carteira em uma tabela, o que torna-a uma boa candidata ao processo de web scraping. Basicamente um web scraper é um código que carrega uma página web e extrai seus dados.
Esse método é particularmente relevante quando se deseja obter dados que não estão estruturados de forma conveniente como uma API, por exemplo.
Nesse caso em particular, existiam algumas dificuldades:
A tabela é gerada dinamicamente. Isso significa que fazer uma simples requisição GET (carregar a página via código) não irá resolver, pois o conteúdo da página é gerado posteriormente via JavaScript.
Além disso, a tabela está dentro de um iframe, que é basicamente uma forma de se enxertar uma página HTML dentro de outra página HTML.
Por fim, a tabela é paginada com um default de 20 linhas. Isso significa que nem todos os elementos estão visíveis na página e é necessária uma interação do usuário para revelá-los.
Para resolver esses problemas precisamos utilizar uma ferramenta como o Playwright.
Playwright
Originalmente criado como uma ferramenta de end-to-end testing, o Playwright é um fork do Puppeteer e se assemelha a outras ferramentas mais tradicionais como Selenium e Cypress.
Essas ferramentas se especializam em simular um uso real da página web – não simplesmente a requisição HTTP. Assim, você simula um usuário que abre a página, clica num botão, digita um valor no formulário, etc.
Como o site da B3 é dinâmico, isso é basicamente o que precisamos.
Instalando o Playwright
No meu caso, utilizei a versão Python do Playwright. A instalação foi feita do seguinte modo, através do Poetry (um gerenciador de bibliotecas melhor que o pip
) :
poetry add playwright
poetry run playwright install --with-deps chromium
Para mais instruções de instalação, veja o site oficial.
Nesse caso, estamos instalando o driver do Chromium, ou seja, o Playwright irá fazer as simulações nessa engine.
Criando um script Python
Com o Playwright instalado, vamos ao código. Como precisamos de uma forma de visualizar toda a tabela, existiam 3 soluções para o problema:
Fazer o scraping da primeira página, clicar na próxima, fazer o scraping da segunda página, e assim sucessivamente até o final.
Clicar no dropdown e selecionar a opção de mostrar até 120 elementos de uma vez. Como o IBX-100 possui 100 ativos, garantimos que todos estarão visíveis.
Clicar no botão de download, baixar a planilha e retirar os dados de lá.
A primeira opção me pareceu mais trabalhosa, e a 3ª mais direta. No entanto, ela requer que se faça um download, o que pode gerar alguns problemas se esse script rodar em um ambiente serverless. Sendo assim, optei pela opção 2.
Fazendo o scraping do HTML
A primeira etapa então consiste em adquirir os dados, remover o que é preciso do site de modo a manipulá-los depois. Eis o código:
b3_url = "https://sistemaswebb3-listados.b3.com.br/indexPage/day/IBXX?language=pt-br"
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(b3_url)
# wait for the table to load
table = page.wait_for_selector("table.table-responsive-sm.table-responsive-md")
select = page.locator("#selectPage")
select.select_option("120")
# wait a few seconds
time.sleep(2)
# wait for the table to load
table = page.wait_for_selector("table.table-responsive-sm.table-responsive-md")
table_html = table.inner_html()
Vamos aos detalhes:
Utilizamos a URL do iframe (que também carregava uma página dinâmica) em vez do link original. Essa URL foi descoberta inspecionando a página. Assim, conseguimos acessar os elementos presentes.
Para “selecionarmos” a tabela, utilizamos as classes que as estilizavam.
Depois localizamos o select com a opção de elementos por página e em seguida selecionamos a opção 120, ou seja, que mostra 120 linhas na tabela.
Uma requisição é feita, portanto precisamos esperar que ela termine. Eis a razão do
sleep
.Em seguida, garantimos que a tabela foi carregada novamente, e pegamos o seu HTML.
Manipulando o HTML
Uma vez que o HTML foi extraído, não há mais nada a ser feito com o Playwright. Com a ajuda do Claude geramos um parser utilizando a famosa biblioteca BeautifulSoup.
def parse_table_beautifulsoup(table_html):
"""
Parse HTML table using BeautifulSoup
Returns a list of dictionaries with the parsed data
"""
soup = BeautifulSoup(table_html, "html.parser")
# Extract headers
headers = []
for th in soup.thead.find_all("th"):
headers.append(th.text.strip())
# Extract rows
rows = []
for tr in soup.tbody.find_all("tr"):
row = {}
for idx, td in enumerate(tr.find_all("td")):
# Clean and format the data
value = td.text.strip()
# Convert numeric values
if idx == 3: # Qtde. Teórica column
value = int(value.replace(".", ""))
elif idx == 4: # Part. (%) column
value = float(value.replace(",", "."))
row[headers[idx]] = value
rows.append(row)
return rows
Salvando no banco de dados
Por fim, restou salvar a composição da carteira no banco de dados do QuantBrasil (que utiliza PostgreSQL). Por brevidade, omitirei os detalhes da implementação, mas é basicamente uma inserção numa relação m:n entre asset
e portfolio
.
tickers = []
weights = []
for row in data:
ticker = row.get("Código")
weight = row.get("Part. (%)")
tickers.append(ticker)
weights.append(weight)
portfolio_id = create("IBX100")
print(f"Created portfolio with id {portfolio_id}")
remove_all(portfolio_id)
print(f"Removed all assets from portfolio {portfolio_id}")
ticker_map = get_ticker_map(tickers)
asset_ids = [ticker_map[ticker] for ticker in tickers]
add_assets(asset_ids, portfolio_id, weights)
print(f"Added {len(asset_ids)} assets to portfolio")
Conclusão
O resultado final foi um script que em poucos segundos é capaz de abrir o site da B3, interagir com a tabela de modo a encontrar todos os ativos, criar ou atualizar o portfólio no banco de dados.
O script completo pode ser visto aqui. Não deixe de se inscrever no YouTube para não perder os próximos episódios!
Abraços,
Rafael