Alisson Machado
18 December 2017

Web Scraping: Python, Selenium e BeautifulSoup

Muita gente na internet tem dúvida de como fazer robôs que buscam coisas em sites, baixam conteúdo ou simplesmente executam ações para testar alguma funcionalidade do site, sistema ou algo relacionado.


Sendo assim resolvi fazer esse Post onde eu faço o acesso a um site, analiso os elementos da página HTML e faço a automação da navegação via selenium. Uma vez que a automação da navegação é concluída, pego os dados de uma determinada página, faço uma conversão deles pra um formato CSV e o download desses dados para uma análise posterior, essa técnica é chamada Web Scraping.


O Framework mais famoso em Python para fazer Web Scraping é o Scrapy, porém nele temos alguns problemas para renderizar páginas que usam javascript, como por exemplo SPAs ( Single Page Applications ), essas páginas são todas renderizadas via Javascript que é carregado dinamicamente, isso faz a navegação do site ficar mais amigável, sem a necessidade de ficar recarregando toda a página cada vez que é clicado em um link do site, como acontecia antigamente, para contornar esse problema do Scrapy é necessário utilizar um outro módulo chamado Splash, quem quiser saber mais sobre eles pode acessar esse link: https://github.com/scrapy-plugins/scrapy-splash .


Bom, o site que eu quero pegar os dados segue abaixo:


http://leideacesso.etransparencia.com.br/itaquaquecetuba.prefeitura.sp/Portal/desktop.html?410


Esse site tem os dados referente as receitas e as despesas da prefeitura da Itaquaquecetuba, ele é um site que usa javascript pra tudo, então tem uma série de páginas pra fazer o carregamento o que acaba deixando um pouco mais difícil o uso de frameworks como o Scrapy, sendo assim utilizei o Selenium para fazer a automação da navegação.


O Selenium sempre vai utilizar um navegador para acessar o site que você quer pegar os dados, para isso é necessário instalar um driver que pode ser encontrado no site oficial: http://www.seleniumhq.org/download/


Fiz a instalação do GeckoDriver para o Windows, esse driver irá fazer toda a navegação no navegador Firefox.


Bom, agora que a instalação foi feita, vamos acessar o site e ver qual o caminho que será necessário fazer para chegar nos dados que eu quero coletar.


Acessando a página inicial:



Acima temos a página inicial, os dados que eu quero extrair estão dentro de receitas, após clicar nesse valor será aberta uma segunda página. No selenium para fazer a navegação é necessário saber o nome dos elementos DOM ( Document Object Model ) que eu quero interagir, então vamos fazer uma análise.



Na imagem acima em amarelo está o botão em que eu quero clicar, para clicar nele, é necessário inspecionar o elemento através do navegador.



Qualquer atributo do elemento pode ser usado para interagir com ele, eu escolhi a classe, que tem o nome val01, o ideal é sempre tentar achar alguma tributo que seja único, como por exemplo o ID do elemento, mas durante o post vou mostrar como se seleciona o elemento pelo ID também. Uma vez clicado nesse botão, será carregada essa segunda página:





Nessa segunda página, temos mais elementos para interagir, como o Select, onde posso selecionar o ano de onde quero pegar os dados, assim que selecionado o ano é necessário clicar no botão filtrar.



Agora vamos pegar algum atributo para identificar esses elementos também. Primeiro vou inspecionar o elemento Select, que é nele que vou selecionar o ano antes de clicar no botão.



Neste elemento, vou pegar o atributo name, que possui o valor exe, esse valor será utilizado para fazer a identificação dele via selenium. Agora vou inspecionar o botão

filtrar.


Esse eu vou interagir com ele através do campo ID, que possui o valor imgFiltrar. Agora que já sei como interagir, vou precisar pegar os dados da tabela abaixo:



Agora vou inspecionar a tabela:



A imagem acima mostra que ela está dentro de uma DIV com o ID bd01, esse id que vou usar para interagir. Até agora fizemos somente a análise do que queremos pegar, então vamos entrar em código. Primeiro passo é instalar o módulo do Selenium para Python e o BeautifulSoup.


python3 -m pip install selenium bs4


Agora que já temos o módulo instalado vou colocar embaixo a parte do código que se refere ao selenium e as explicações estão nos comentários:

# O modulo time aqui foi utilizado para esperar o carregamento das paginas atraves do firefox
import time

# o modulo webdriver e necessario para definir qual navegador sera utilizado para fazer a automacao
from selenium import webdriver

# o modulo Select sera utilizado para interagir com a caixa de selecao onde sera definido o ano em que quero buscar os dados
from selenium.webdriver.support.ui import Select

# esse modulo sera utilizado para 
from bs4 import BeautifulSoup

# a linha abaixo define qual e o navegador que queremos utilizar, lembrando que eu instalei somente o driver para conexao com o firefox, mas existem tambem para Chrome e InternetExplorer
driver = webdriver.Firefox()

# abaixo foi definido qual e o site que quero acessar
driver.get("http://leideacesso.etransparencia.com.br/itaquaquecetuba.prefeitura.sp/Portal/desktop.html?410")

# o Sleep abaixo e para aguardar o carregamento da pagina
time.sleep(15)

# Aqui estou buscando o elemento que possui na classe o valor valo01, que e respectivo ao valor da receita onde quero clicar para ir na proxima pagina
receita = driver.find_element_by_class_name("val01")

# aqui e feito um clique no elemento que foi encontrado acima
receita.click()

# aguardando o carregamento da pagina
time.sleep(10)

# agora quero as receitas desde 2013 ate 2017
anos = ["2013","2014","2015","2016","2017"]

for a in anos:
    # aqui e utilizado o modulo Select do selenium para interagir com o ComboBox
    select = Select(driver.find_element_by_name("exe"))

    # aqui e alterado o valor do ComboBox
    select.select_by_value(a)
    # agora e buscado o elemento cujo o ID e igual a imgFiltrar
    filtrar = driver.find_element_by_id('imgFiltrar')
    # retornado o elemento da busca e clicado no botao
    filtrar.click()
    # aguardando o carregamento da tabela
    time.sleep(3)

    # armazenando a div que possui a tabela dentro da variavel dados
    dados = driver.find_element_by_id("bd10")


Se tudo estiver instalado corretamente e você executar esse script, verá que o navegador Firefox irá abrir sozinho e os botões começarão a ser clicados automaticamente através do selenium. Com a parte da automação já feita, agora vamos fazer a raspagem dos dados. O código abaixo está dentro do for no pedaço de código acima, no final vou postar o código completo.

    # aqui e pegado o codigo HTML que esta dentro da div bd01 no codigo que foi mostrado acima
    html = dados.get_attribute("innerHTML")

    # com o codigo HTML dentro da variavel, vamos usar o BeautifulSoup para fazer o parser desse HTML
    soup = BeautifulSoup(html, "html.parser")
    
    # dentro da variavel soup temos o codigo html retornado pelo Selenium ja convertido para o BS
    # vou utilizar o metodo select_one para buscar o elemento table dentro desse codigo
    table = soup.select_one("table")

    # no conteudo dessa tabela temos varias virgulas e espacos, como vou converter esses dados pra csv, vou definir o delimitador com o caracter |
    # na linha abaixo estou buscando todos os elementos tr, que possui a classe Grid_title e os elementos filhos cujo a tag e td
    # e feito um list comprehesion para pegar somente os elementos e eles estao sendo separados pelo caracter |

    headers = [header.text+"|" for header in table.select("tr.Grid_title td")]
   
    # abaixo estou buscando os elementos tr que possuem a classe Grid_line no css
    # e um novo list comprehension para criar uma lista somente com o s elementos que eu quero
    line = []
    data = [d for d in table.select("tr.Grid_line")]
    for d in data:
        linha = ""
        for t in d.select("td"):
            linha += t.text+"|"
        line.append(linha)

    # aqui e a mesma coisa que acima porem com a classe Grid_line_even
    line_even = []
    data = [ d for d in table.select("tr.Grid_line_even")]
    for d in data:
        linha = ""
        for t in d.select("td"):
            linha += t.text+"|"
        line_even.append(linha)

    # agora que os dados ja foram parseados, vou fazer a escrita do arquivo CSV
    with open("%s.csv"%a,"w") as f:
        s = "".join(headers)
        f.write(s+"\n")

        for l in line:
            f.write(l+"\n")

        for l in line_even:
            f.write(l+"\n")

# fim 
time.sleep(10)
driver.close()


Sendo assim, o código completo ficou da seguinte maneira:

# O modulo time aqui foi utilizado para esperar o carregamento das paginas atraves do firefox
import time

# o modulo webdriver e necessario para definir qual navegador sera utilizado para fazer a automacao
from selenium import webdriver

# o modulo Select sera utilizado para interagir com a caixa de selecao onde sera definido o ano em que quero buscar os dados
from selenium.webdriver.support.ui import Select

# esse modulo sera utilizado para 
from bs4 import BeautifulSoup

# a linha abaixo define qual e o navegador que queremos utilizar, lembrando que eu instalei somente o driver para conexao com o firefox, mas existem tambem para Chrome e InternetExplorer
driver = webdriver.Firefox()

# abaixo foi definido qual e o site que quero acessar
driver.get("http://leideacesso.etransparencia.com.br/itaquaquecetuba.prefeitura.sp/Portal/desktop.html?410")

# o Sleep abaixo e para aguardar o carregamento da pagina
time.sleep(15)

# Aqui estou buscando o elemento que possui na classe o valor valo01, que e respectivo ao valor da receita onde quero clicar para ir na proxima pagina
receita = driver.find_element_by_class_name("val01")

# aqui e feito um clique no elemento que foi encontrado acima
receita.click()

# aguardando o carregamento da pagina
time.sleep(10)

# agora quero as receitas desde 2013 ate 2017
anos = ["2013","2014","2015","2016","2017"]

for a in anos:
    # aqui e utilizado o modulo Select do selenium para interagir com o ComboBox
    select = Select(driver.find_element_by_name("exe"))

    # aqui e alterado o valor do ComboBox
    select.select_by_value(a)
    # agora e buscado o elemento cujo o ID e igual a imgFiltrar
    filtrar = driver.find_element_by_id('imgFiltrar')
    # retornado o elemento da busca e clicado no botao
    filtrar.click()
    # aguardando o carregamento da tabela
    time.sleep(3)

    # armazenando a div que possui a tabela dentro da variavel dados
    dados = driver.find_element_by_id("bd10")

    # aqui e pegado o codigo HTML que esta dentro da div bd01 no codigo que foi mostrado acima
    html = dados.get_attribute("innerHTML")

    # com o codigo HTML dentro da variavel, vamos usar o BeautifulSoup para fazer o parser desse HTML
    soup = BeautifulSoup(html, "html.parser")
    
    # dentro da variavel soup temos o codigo html retornado pelo Selenium ja convertido para o BS
    # vou utilizar o metodo select_one para buscar o elemento table dentro desse codigo
    table = soup.select_one("table")

    # no conteudo dessa tabela temos varias virgulas e espacos, como vou converter esses dados pra csv, vou definir o delimitador com o caracter |
    # na linha abaixo estou buscando todos os elementos tr, que possui a classe Grid_title e os elementos filhos cujo a tag e td
    # e feito um list comprehesion para pegar somente os elementos e eles estao sendo separados pelo caracter |

    headers = [header.text+"|" for header in table.select("tr.Grid_title td")]
   
    # abaixo estou buscando os elementos tr que possuem a classe Grid_line no css
    # e um novo list comprehension para criar uma lista somente com o s elementos que eu quero
    line = []
    data = [d for d in table.select("tr.Grid_line")]
    for d in data:
        linha = ""
        for t in d.select("td"):
            linha += t.text+"|"
        line.append(linha)

    # aqui e a mesma coisa que acima porem com a classe Grid_line_even
    line_even = []
    data = [ d for d in table.select("tr.Grid_line_even")]
    for d in data:
        linha = ""
        for t in d.select("td"):
            linha += t.text+"|"
        line_even.append(linha)

    # agora que os dados ja foram parseados, vou fazer a escrita do arquivo CSV
    with open("%s.csv"%a,"w") as f:
        s = "".join(headers)
        f.write(s+"\n")

        for l in line:
            f.write(l+"\n")

        for l in line_even:
            f.write(l+"\n")

# fim 
time.sleep(10)
driver.close()