Webscraping tabela completa fundos BB

O link abaixo da acesso a uma tabela atualizada com todos os fundos de investimento do Banco do Brasil, com rentabilidades, taxas de administração, etc,

link<- “https://www37.bb.com.br/portalbb/tabelaRentabilidade/rentabilidade/gfi7,802,9085,9089,1.bbx?tipo=1&nivel=400&_ga=1.8723766.2018557100.1481650921&pk_vid=1ee53400dc7060fa1614624360a41470&pk_vid=1ee53400dc7060fa1614624360a41470

A ideia é fazer um webscraping da tabela e transformar isto

Em algo como isto,

O que permitiria fazer filtros e comparações sobre tipos de fundos, rentabilidade, taxa de administração, etc.

Agradeço a quem puder auxiliar.

Olá, Pedro. Tudo bem?

O código abaixo funcionou aqui. De resto, é um trabalhinho de manipulação de dados.

library(rvest)
library(tidyverse)

url <- "https://www37.bb.com.br/portalbb/tabelaRentabilidade/rentabilidade/gfi7.bbx"

tabelas <- read_html(url) %>% 
  html_table()

Solução trabalhosa de tratamento de dados

# -- 1. Carregar pacotes
library(rvest)
#> Carregando pacotes exigidos: xml2
library(tidyverse)

# -- 2. Salvar a URL da página
url <- "https://bit.ly/3rrMoPP"

# -- 3. Ler a página do site
tabela <- read_html(url) %>% 
  
  html_table() 

# -- 4. Tratamento de Dados
df <- tabela %>% 
  
  map(.f = ~ janitor::row_to_names(dat = .x, row_number = 1) %>% 
        
        tibble::as_tibble() %>%  
        
        dplyr::mutate(tipo = names(.) %>% magrittr::extract(1), .before = everything()) %>% 
        
        janitor::clean_names(numerals = "left") %>% 
        
        dplyr::rename(fundo = 2) %>% 
        
        dplyr::rename_all(.funs = funs(str_remove_all(string = ., pattern = "\\_r"))) %>%   
        
        dplyr::mutate(fundo = fundo %>% str_squish() %>% str_remove_all(pattern = " ?\\([0-9]\\)")) %>%   
        
        dplyr::mutate(class = dia, .before = fundo) %>% 
        
        dplyr::mutate(class = str_replace(class, "[[:digit:]]|\\-", NA_character_)) %>% 
        
        tidyr::fill(class) %>% 
        
        dplyr::mutate(class = replace(class, is.na(class), "Ações")) %>% 
        
        dplyr::filter(class != fundo)) %>% 
  
  dplyr::bind_rows() %>% 
  
  dplyr::mutate(
    
    dplyr::across(
      
      .cols = c(dia:taxa_de_adm_aa, cota), 
      
      .fns = parse_number, locale = locale(decimal_mark = ',')), 
    
    dplyr::across(
      
      .cols = starts_with("data"), 
      
      .fns = lubridate::dmy))
#> Warning: `funs()` is deprecated as of dplyr 0.8.0.
#> Please use a list of either functions or lambdas: 
#> 
#>   # Simple named list: 
#>   list(mean = mean, median = median)
#> 
#>   # Auto named with `tibble::lst()`: 
#>   tibble::lst(mean, median)
#> 
#>   # Using lambdas
#>   list(~ mean(., trim = .2), ~ median(., na.rm = TRUE))
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_warnings()` to see where this warning was generated.
#> Warning: Problem with `mutate()` input `..1`.
#> i 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> 
#> i Input `..1` is `dplyr::across(...)`.
#> Warning: 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> Warning: Problem with `mutate()` input `..1`.
#> i 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> 
#> i Input `..1` is `dplyr::across(...)`.
#> Warning: 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> Warning: Problem with `mutate()` input `..1`.
#> i 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> 
#> i Input `..1` is `dplyr::across(...)`.
#> Warning: 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> Warning: Problem with `mutate()` input `..1`.
#> i 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> 
#> i Input `..1` is `dplyr::across(...)`.
#> Warning: 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> Warning: Problem with `mutate()` input `..1`.
#> i 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> 
#> i Input `..1` is `dplyr::across(...)`.
#> Warning: 13 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  79  -- a number      -
#>  80  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> Warning: Problem with `mutate()` input `..1`.
#> i 19 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  76  -- a number      -
#>  77  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> 
#> i Input `..1` is `dplyr::across(...)`.
#> Warning: 19 parsing failures.
#> row col expected actual
#>  27  -- a number      -
#>  28  -- a number      -
#>  29  -- a number      -
#>  76  -- a number      -
#>  77  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> Warning: Problem with `mutate()` input `..1`.
#> i 36 parsing failures.
#> row col expected actual
#>  23  -- a number      -
#>  24  -- a number      -
#>  25  -- a number      -
#>  26  -- a number      -
#>  27  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.
#> 
#> i Input `..1` is `dplyr::across(...)`.
#> Warning: 36 parsing failures.
#> row col expected actual
#>  23  -- a number      -
#>  24  -- a number      -
#>  25  -- a number      -
#>  26  -- a number      -
#>  27  -- a number      -
#> ... ... ........ ......
#> See problems(...) for more details.

Created on 2021-03-03 by the reprex package (v1.0.0.9002)

Se alguém quiser ajudar no tratamento dos dados, é super bem-vindo. Rola uma ajuda ou um comentário sobre o código, @clente?

Abs,

Rafael.

1 curtida

Rafael,

A lógica do seu código estava perfeita, então, só porque você pediu, vou deixar alguns comentários sobre a forma (e não o conteúdo) do programa:

  • Não precisa de uma linha em branco entre cada linha da pipeline. Isso deixa o código muito esparso;
  • Só use a notação lambda do map() para funções de no máximo uma linha, senão fica difícil entender o que está acontecendo;
  • Se você vai dar library(tidyverse), não é necessário invocar as funções com dplyr::;
  • Não faça pipelines horizontais como fundo %>% str_squish(). Uma pipeline serve para encadear manipulações complexas, então nesse caso é melhor fazer str_squish(fundo);
  • Ainda no mesmo tema, names(.) %>% magrittr::extract(1) é equivalente a names(.)[1]. Nem só de tidyverse é feito o R;
  • Respeite os alertas e mensagens porque eles dão informações importantes sobre o código. No seu caso, a função funs() foi deprecada e você não lidou com os “-” das colunas numéricas;
  • Atente-se às porcentagens (no caso, a coluna pl_medio_taxa_de_adm_aa), pois elas precisam ser divididas por 100 quando viram números;
  • Seja consistente quanto às aspas, ou seja, use somente aspas duplas ou somente aspas simples e
  • Quando não forem necessários, prefira omitir os nomes dos argumentos. Em str_remove_all(string = ., pattern = "\\_r"), por exemplo, minha sugestão seria o muito mais compacto str_remove_all(., "\\_r").

Aqui vai a minha solução:

# Pacotes necessários
library(rvest)
library(tidyverse)
library(janitor)

# Locale para datas e números em português
brasil <- locale(date_format = "%d/%m/%Y", decimal_mark = ",")

# Função para parsear uma tabela
parse_table <- . %>%
  html_table(FALSE) %>%
  as_tibble() %>%
  mutate(
    across(.fns = ~ifelse(row_number() == 2, paste(lag(.x), .x), .x)),
    categoria = ifelse(row_number() == 3, lag(X1), NA)
  ) %>%
  row_to_names(2) %>%
  rename(fundo = 1, categoria = 14) %>%
  set_names(str_remove, "\\(.*?\\)|R[$]") %>%
  clean_names(numerals = "middle") %>%
  mutate(tipo = ifelse(str_starts(cota, "[A-Z]"), cota, NA)) %>%
  fill(tipo, categoria) %>%
  replace_na(list(tipo = "Ações")) %>%
  mutate(across(.fns = str_squish)) %>%
  filter(tipo != cota) %>%
  relocate(categoria, tipo) %>%
  mutate(
    pl_medio_taxa_de_adm_aa = str_remove(pl_medio_taxa_de_adm_aa, "%"),
    categoria = ifelse(categoria == "Ações Fundo", "Ações", categoria)
  ) %>%
  type_convert(na = c("-", ""), locale = brasil) %>%
  mutate(pl_medio_taxa_de_adm_aa = pl_medio_taxa_de_adm_aa/100)

# Parsear as tabelas
"https://bit.ly/3rrMoPP" %>%
  read_html() %>%
  xml_find_all("//table") %>%
  map_dfr(parse_table) %>%
  glimpse(width = 70)
#> Rows: 133
#> Columns: 15
#> $ categoria                  <chr> "Fundos com carteira de curto pra…
#> $ tipo                       <chr> "Curto Prazo", "Renda Fixa", "Ref…
#> $ fundo                      <chr> "BB Fluxo Automático", "RF Simple…
#> $ rentabilidade_dia          <dbl> 0.003, 0.003, 0.007, 0.008, 1.439…
#> $ rentabilidade_acum_mes     <dbl> 0.014, 0.014, 0.023, 0.028, 2.971…
#> $ rentabilidade_fevereiro    <dbl> 0.062, 0.033, 0.040, 0.062, 1.995…
#> $ rentabilidade_2021         <dbl> 0.145, 0.106, 0.146, 0.196, 11.25…
#> $ rentabilidade_12_meses     <dbl> 1.347, 0.865, 1.319, 1.622, 25.30…
#> $ rentabilidade_24_meses     <dbl> 6.023, 4.438, 5.905, 6.538, 53.95…
#> $ rentabilidade_36_meses     <dbl> 11.688, 8.981, 11.593, 12.595, 82…
#> $ pl_medio_pl_medio_12_meses <dbl> 2549, 12889, 26008, 6894, 522, 26…
#> $ pl_medio_taxa_de_adm_aa    <dbl> 0.0100, 0.0125, 0.0100, 0.0070, 0…
#> $ data_cotacao               <date> 2021-03-04, 2021-03-04, 2021-03-…
#> $ cota                       <dbl> 1.207624, 1.362333, 4.975702, 1.7…
#> $ data_inicio                <date> 2017-02-16, 2015-10-01, 2003-12-…

Created on 2021-03-04 by the reprex package (v1.0.0)

2 curtidas

@clente, que aula! Muito obrigado pelas dicas, com certeza irei incorporar nos próximos códigos. Valeu mesmo!

1 curtida

Oi, @clente. Tudo bem? Surgiu uma dúvida aqui nessa parte do código.

O que o xml_find_all("//table") faz? Além disso, no código abaixo:

  mutate(
    pl_medio_taxa_de_adm_aa = str_remove(pl_medio_taxa_de_adm_aa, "%"),
    categoria = ifelse(categoria == "Ações Fundo", "Ações", categoria)
  ) %>%
  type_convert(na = c("-", ""), locale = brasil)

Aparece aqui como A��es, eu tentei colocar

brasil <- locale(date_format = "%d/%m/%Y", decimal_mark = ",", encoding = 'UTF-8')

porém, naõ funcionou. Tem ideia de como resolver?

xml_find_all("//table") retorna todos os nodes do tipo <table> da página. Sobre o problema de encoding, isso me parece coisa do Windows… Minha sugestão é, no RStudio, clicar em File > Save with Encoding… e selecinar o UTF-8.

Oi, @clente. Tudo bem? Muito obrigado pela ajuda até aqui! Valeu mesmo!

O problema de encoding é no output do tibble e não no script em si. Ele ocorre ao incluir a linha de comando

type_convert(na = c("-", ""), locale = brasil)

Não sei como resolver :confused: .

Caio Lente, seus scripts são &*(@oda! Com alguns poucos comentários a mais poderiam compor um ótimo livro de consulta/cookbook. Já pensou nisso?

Abs.

Mário.

1 curtida

@marrut Não sei se vc conhece o maravilhoso livro escrito pelo Lente: https://curso-r.github.io/zen-do-r/

1 curtida

Obrigado pelo elogio! Eu tenho escrito o Zen do R aos poucos e espero que um dia tenha várias dicas de programação… Vamos ver se dá certo :slight_smile:

Esse tipo de coisa é complicado de resolver sem ter acesso ao computador :frowning: Infelizmente não consigo imaginar o que pode estar acontecendo.

1 curtida