Webscraping + PostgreSQL: Extraindo dados da web e criando uma base de dados com o base stats de pokémons

Deborah Bertoldo da Silva
Todas as Letras
Published in
14 min readApr 18, 2022

Neste artigo irei ensinar como extrair dados via python de uma página web que contém informações dos status dos pokémons, criar um banco de dados no PostgreSQL dentro de um container Docker e gerenciar este através do Docker-compose, por fim aprenderemos como mandar os dados extraídos para este banco de dados.

Figura 1: Serperior Shiny. Suas cores azuis e amarelas lembram o ícone do Python :)

Observação: O programa e suas dependências foram criadas no Linux, comandos para Windows podem ser diferentes. Nenhum pokémom foi ferido durante o projeto.

Ferramentas utilizadas:

  • Python
  • Docker
  • Docker Copose
  • PostgreSQL

Bibliotecas do Python utilizadas:

  • requests
  • bs4 (BeautifulSoup)
  • SQLAlchemy

Antes de partimos para nosso código, vamos passar por alguns fundamentos sobre o python e seu paradigma como linguagem de programação,

Python e programação orientada a objetos

Definição

As linguagens de programação seguem diferentes paradigmas, e o python segue o paradigma de orientação a objetos (POO). Mas o que é isso? Um objeto é um elemento computacional que representa, no domínio da solução, alguma entidade (abstrata ou concreta) do domínio de interesse do problema sob análise. Objetos similares são agrupados em classes [1]. A utilização das classes é algo constante no desenvolvimento dos programas, de forma que também poderíamos também considerar uma linguagem orientada a classes. O paradigma do POO se baseia em quatro pilares: abstração, encapsulamento, herança e polimorfismo.

  • abstação: o objeto deve possuir uma identidade que deve ser única no sistema, não podendo haver repetição ou conflito. As suas características são tratadas como propriedades do objeto, e as ações que o objeto irá executar são chamadas de métodos.
  • encapsulamento: técnica que esconde as propriedades privadas do objeto, deve-se instanciar métodos para retornar e setar o valor da propriedade.
  • herança: objetos filhos herdam as propriedades e métodos de seus objetos pais.
  • polimorfismo: de forma semelhante a herança, o polimorfismo permite herdar métodos da classe pai, e adicionalmente permite atribuir implementação de novo método.

Mais informações podem ser encontradas aqui [2].

Vamos ver como podemos trabalhar com classses. Suponha desejamos declarar uma classe Pessoa:

class Pessoa():# Atributos e métodos da classe

Como vimos anteriormente, uma classe é representada por seus atributos e métodos. Para declarar um atributo definimos seu nome com um método chamado __init__, este método define o constutor da classe, ou seja, definimos como uma nova pessoa será criada neste programa.

Para definir os atributos de uma classe em seu construtor, basta passá-los como parâmetro, como podemos ver abaixo:

class Pessoa:
def __init__(self, nome, gênero, cpf):
self.nome = nome
self.genero = gênero
self.cpf = cpf

Agora, estamos indicando que toda pessoa que for criada em nosso programa e que utilize como base a classe Pessoa deverá possuir um nome, gênero e cpf.

Sempre que precisamos criar “algo” com base em uma classe, dizemos que estamos “instanciando objetos”. O ato de instanciar um objeto significa que estamos criando a representação de uma classe em nosso programa.

Para instanciar um objeto no Python com base em uma classe previamente declarada, basta indicar a classe que desejamos utilizar como base e, caso possua, informar os valores referentes aos seus atributos, como podemos ver abaixo:

class Pessoa:
def __init__(self, nome, sexo, cpf):
self.nome = nome
self.genero = genero
self.cpf = cpf
if __name__ == "_main_":
pessoa1 = Pessoa("João","M","123456")
print(pessoa1.nome)

Ao executar a linha pessoa1 = Pessoa("Joao", "M", "123456")estamos criando um objeto do tipo pessoa com nome “João”, genero “M” e cpf “123456”. Desta forma o programa retornará:

João

Com isso, agora possuímos uma forma de criar diversas pessoas utilizando a mesma base, a classe Pessoa.

Como vimos anteriormente, uma classe possui atributos (que definem suas características) e métodos (que definem seus comportamentos).

Imagine que possuímos um atributo ativo na classe Pessoa. Toda pessoa criada em nosso sistema é inicializado como ativo, porém, imagine que queremos alterar o valor deste atributo e, assim, “desativar” a pessoa em nosso sistema e, além disso, exibir uma mensagem de que a pessoa foi “desativada com sucesso”.

Para isso, precisamos definir um comportamento para essa pessoa, assim, agora, ela poderá ser “desativada”.

Sendo assim, precisamos definir um método chamado “desativar” para criar este comportamento na classe Pessoa, como podemos ver abaixo:

class Pessoa:
def __init__(self, nome, gênero, cpf, ativo):
self.nome = nome
self.genero = gênero
self.cpf = cpf
self.ativo = True
def desativar(self):
self.ativo = False
print("A pessoa foi desativada com sucesso")
if __name__ == "_main_":
pessoa1 = Pessoa("João","M","123456",True)
pessoa1.desativar()

Para criarmos este “comportamento” na classe Pessoa, utilizamos a palavra reservada def, que indica que estamos criando um método da classe, além do nome do método e seus atributos, caso possuam.

Depois disso, é só definir o comportamento que este método irá realizar. Neste caso, o método vai alterar o valor do atributo “ativo” para “False” e imprimir a mensagem “A pessoa foi desativada com sucesso”

Aparentemente o código acima funciona normalmente. Porém, temos um pequeno problema com o atributo “ativo”: ele é acessível para todo mundo. Ou seja, mesmo possuindo o método “desativar”, é possível alterar o valor do atributo “ativo” sem qualquer problema. Este comportamento do nosso programa dá brechas para que um usuário possa ser ativado ou desativado sem passar pelo método responsável por isso. Por isso, para corrigir este problema, devemos recorrer a um pilar importantíssimo da Orientação à Objetos: o encapsulamento.

Para definir um atributo público, não há necessidade de realizar nenhuma alteração, por padrão, todos os atributos e métodos criados no Python são definidos com este nível de visibilidade.

Já se precisarmos definir um atributo como privado, adicionamos dois underlines (__) antes do nome do atributo ou método:

class Pessoa:
def __init__(self, nome, gênero, cpf):
self.nome = nome
self.genero = gênero
self.cpf = cpf
self.__ativo = ativo
def desativar(self):
self.__ativo = False
print("A pessoa foi desativada com sucesso")
if __name__ == "_main_":
pessoa1 = Pessoa("João","M","123456",True)
pessoa1.desativar()
pessoa1.ativo = True
print(pessoa1.ativo)

Porém, caso precisemos acessar os atributos privados de uma classe, o Python oferece um mecanismo para construção de propriedades em uma classe e, dessa forma, melhorar a forma de encapsulamento dos atributos presentes. É comum que, quando queremos obter ou alterar os valores de um atributo, criamos métodos getters e setters para este atributo:

class Pessoa:
def __init__(self, nome, sexo, cpf, ativo):
self.__nome = nome
self.__genero = genero
self.__cpf = cpf
self.__ativo = ativo
def desativar(self):
self.__ativo = False
print(“A pessoa foi desativada com sucesso”)
def get_nome(self):
return self.__nome
def set_nome(self, nome):
self.__nome = nome
if __name__ == “__main__”:
pessoa1 = Pessoa(“João”, “M”, “123456”, True)
pessoa1.desativar()
pessoa1.ativo = True
print(pessoa1.ativo)
# Utilizando geters e setterspessoa1.set_nome(“José”)
print(pessoa1.get_nome()

A fonte destas definições podem ser acessadas através daqui. [3]

Extraindo informação de páginas

Uma dica bacana nesse primeiro momento é utilizar o google colab para testar os comandos desta seção. Você pode verificar em:

Vamos considerar que queremos os status mais recentes dos pokemons. Podemos encontrar estas informações em:

https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_base_stats_(Generation_VIII-present)

Biblioteca Requests e Beautiful Soup

Para o início do nosso programa, devemos ser capazes de mandar solicitações HTTP em Python. Para isto, utilizamos a biblioteca requests.

Se desejar instalar em sua máquina, é possível inserir o seguinte comando em seu terminal:

pip install requests

Uma vez com a biblioteca disponível no seu ambiente, devemos importa-la e criar a requisição.

import requests
url = "https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_base_stats_(Generation_VIII-present)"
r = requests.get(url)
print(r)

Se imprimirmos esta variável receberemos a resposta da requisição. Para verificar o que foi extraído da página podemos imprimir:

print(r.text)

Este comando nos retorna o código HTML da página. Basicamente estamos vendo uma sopa de letrinhas. E não é por menos que nossa outra biblioteca se chama Beautiful Soup, este pacote será responsável pela análise do documento HTML. Para instalar em sua máquina utilize o seguinte comando:

pip install bs4

Agora com a biblioteca disponível, podemos recriar nosso código. Renomearei a biblioteca BeatifulSoup como bs como atalho e utilizarei o formato lxml para realizar o parse:

import requestsimport bs4from bs4 import BeautifulSoup as bsurl = "https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_base_stats_(Generation_VIII-present)"r = requests.get(url)soup = bs(r.text, 'lxml')print(soup)

Muito mais elegante, não? :)

Agora como caçar pokemons selvagens nas graminhas, vamos caçar nossas variáveis de interesse. Para isto vamos inspecionar a página. No Google Chrome você pode clicar na página com o botão direito na página e ir em especionar, ou utilizar o atalho Ctrl+Shift+I (Linux e Windows).

No primeiro ícone da aba de inspeção, podemos selecionar a área que desejamos visualizar a tag, ou podemos utilizar o atalho Ctrl+Shift+C.

Print da janela inspecionar, circulado em vermelho está o ícone para seleção de elemento da página a ser inspecionado.

Vamos clicar no quadro onde está o nome do fofíssimo Bulbassauro.

<td class=”l”><a href=”/wiki/Bulbasaur_(Pok%C3%A9mon)” title=”Bulbasaur (Pokémon)”>Bulbasaur</a>
<td>

Para obter o valor do nome do pokemon devemos portanto procurar o elemento td com classe “l”. Queremos o texto que está incluso nele.

pokemon = soup.find(‘td’, class_=’l’)
print(pokemon.text)

Como resposta, teremos “Bulbasaur”. Legal, né? Mas como faço para pegar o nome de todos os pokemons dessa tabela? Para isto, vamos criar uma lista chamada list_pokemon.

list_pokemon = soup.find_all('td', class_='l')for element in list_pokemon:print(element.text)

Agora temos uma lista com o nome de nossos pokémons. De forma semelhante, podemos criar uma lista com as outras informações desejadas. Por exemplo, o HP do pokemon. Ao inspecionar o quadro de HP verificamos que não possui mais uma classe "l", mas um style com o código da cor. Não é um problema, apenas algumas pequenas adaptações são necessárias:

list_hp = soup.find_all("td", style="background:#FF5959")for element in list_hp:print(element.text)

Legal, porém os códigos retornam apenas o texto, se verificarmos a lista em si verificamos a existência de uma quebra de linha após a informação, e a lista de hp é uma lista de strings quando queremos uma lista de inteiros. Vamos atualizar nossas listas retirando o elemento \n e transformando os valores de hp em int. Para isto, criaremos uma interação no tamanho da lista (len()) que percorre todos os elementos da lista e a atualiza com o valor desejado.

Vamos começar a construir nossa tabela criando dataframes com a biblioteca pandas. No caso, iremos criar um objeto com duas dimensões (linha e coluna) denominado DataFrame. Esta biblioteca é ideal para manipulação de DataFrames. Para instalar a biblioteca em sua máquina utilize o comando:

pip install pandas

Com o pacote instalado, vamos importar a biblioteca e criar nosso dataframe.

import pandas as pdimport pandas as pddf = pd.DataFrame(columns=['pokemon', 'hp'])

Temos uma lista vazia, desejamos inserir as linhas de pokemon e hp. Vamos percorrer interações adicionando linhas a este dataframe criando outro dataframe auxiliar de uma linha de valores com os dados do pokemon e hp de deteminada linha. Concateno os dois dataframes com a função pd.concat, que será responsável por inserir a linha no dataframe vazio criado anteriormente. Ignoraremos o index das linhas das colunas.

for i in range(len(list_pokemon)):df2 = pd.DataFrame({'pokemon' : [list_pokemon[i]],'hp': [list_hp[i]],})df = pd.concat([df,df2],ignore_index=True)for element in df:print(df[element])

Adicionando os outros itens da lista nas colunas podemos criar um dataframe com as informações da nossa url. Mas e que tal deixarmos esse código mais explicativo, utilizando o conceito de classes detalhado anteriormente?

Então mãos à obras!

Criando a classe Scrapper:

Como dito anteriormente os pilares da POO consiste em abstração, encapsulamento, herancça e polimorfismo. Vamos utilizar estas propriedades para criar um projeto separado, nomeado webscraping.py.

Começaremos importando nossas bibliotecas a serem utilizadas e definindo nossa classe Scrapper que recebe como atributo a url.

from bs4 import BeautifulSoup as bsimport pandas as pdimport requestsclass Scrapper: def __init__(self) -> None:  self.url = "https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_base_stats_(Generation_VIII-present)"

Para a visualização da tabela/dataframe não é importante para o usuário ter acesso a lista de pokemons ou hp individualmente. Utilizando o conceito de encapsulamento, vamos definir métodos privados para retornar estas listas apenas dentro da minha classe e que possuam apenas uma única função, pensando no princípio de responsabilidade única:

def _get_pokemon(self, soup: bs) -> list: """Return list_pokemon Args: soup (bs): BeautifulSoup Returns: list: list_pokemon[string] """ list_pokemon = soup.find_all("td", class_="l") for i in range(len(list_pokemon)):  list_pokemon[i] = list_pokemon[i].text.rstrip("\n") return list_pokemon

Desta forma esse método tem a única função de retornar as lista de nome dos pokémons. Note que são métodos que implementam novas funcionalidades, explicitando a propriedade de polimorfismo. Para criar nosso dataframe, podemos criar um construtor, que por simplicidade construirá um arquivo JSON.

def _build_json(self, soup: bs) -> pd.DataFrame:"""built dataframe of pokemon's stats ['pokemon','hp']Args:soup (bs): BeautifulSoupReturns:pd.DataFrame: df_pokemon_stats""" df = pd.DataFrame( columns=["pokemon","hp",]) list_pokemon = self._get_pokemon(soup) list_hp = self._get_hp(soup)
for i in range(len(list_id)): df2 = pd.DataFrame({ "pokemon": [list_pokemon[i]], "hp": [list_hp[i]], }) df = pd.concat([df, df2], ignore_index=True) return df

Agora vamos por fim criar a função para a chamada no programa principal, que nos retorna o dataframe criado anteriormente. Note que por herança, o argumento soup necessário nas outras funções será criado nesta função, que passará para as demais como herança.

def scrapping_pokemon(self) -> pd.DataFrame:"""Return dataframe to be consumed in main programReturns:pd.DataFrame: dataframe with pokemon stats""" r = requests.get(self.url) soup = bs(r.text, "lxml") return self._build_json(soup)

Agora vamos construir nossa função principal, chamada main.py. Devemos chamar nossa classe Scrapper construída anteriormente, e iremos chamar o método o contrutor scrapping_pokemon criado. Note que as funções não necessitam de argumentos.

from webscraping import Scrapperscrap = Scrapper()df = scrap.scrapping_pokemon()

Com penas essas 3 linhas extraímos os dados da página com o status dos pokemons e construímos nosso Dataframe! :D

O próximo passo é criar um banco de dados para salvar esses dados.

PostgreSQL

O PostgreSQL, popularmente denominado Postgres é um sistema de gerenciamento de banco de dados objeto relacional de código aberto. Outros exemplos que poderiam ser utilizados incluem: MySQL, OracleDB, SQLServer, MariaDB.

Por ora, vamos criar um banco de dados em nossa máquina local. Por padrão o Postgres é acessado localmente e roda na porta 5432. Você pode obter mais informações de como instalar o Postgres e configurá-lo aqui [4].

Pessoalmente utilizo o aplicativo Dbeaver, que pode ser baixado aqui [5], posteriormente mostrarei como criar a conexão nele.

O Postgres permite a criação e execução de instâncias por meio de containers Docker [6].

Figura 3: Ícone do PostgreSQL, um elefante azul, á esquerda, e á direita, phanpy, seu representante no mundo pokemon.

Docker e Docker Compose

Docker é um serviço de contêiner de software que se tornou bastante popular na implantação de aplicativos. Basicamente, ele permite que você empacote o software de uma forma conceitualmente semelhante a uma máquina virtual. Ao contrário das VMs, os contêineres não contêm software de plataforma, portanto, são muito leves e portáteis. O software empacotado sempre será executado da mesma forma em qualquer plataforma em que o Docker seja executado.

A imagem é um arquivo e o contêiner é um processo.. Uma imagem é um pacote de sistema de arquivos que contém todas as dependências necessárias para executar um processo: arquivos de biblioteca, arquivos no sistema de arquivos, pacotes instalados, recursos disponíveis, processos em execução e módulos do kernel.

Como os arquivos executáveis são a base para os processos em execução, as imagens são a base para contêineres em execução. Os contêineres em execução usam uma visão imutável da imagem, permitindo vários recipientes para reutilizar a mesma imagem simultaneamente. Como as imagens são arquivos, elas podem ser gerenciadas por sistemas de controle de versão, melhorando a automação do contêiner e o provisionamento.

As imagens do contêiner precisam estar disponíveis localmente ou armazenadas e mantidas em um repositório de imagens. Um repositório de imagens é apenas um serviço público ou privado onde as imagens podem ser armazenadas, pesquisadas e recuperadas [7].

Outro recurso muito interessante que podemos utilizar é o gerenciador de containers do Docker, o Docker Compose. Ele nos permite descrer a infraestrutura como código e como ela vai se comportar ao ser iniciado.

Figura 4: Ícones do Docker (baleia) e docker-compose (polvo) segurando e administrando seus containers.

Outro ponto interessante para comentar, são as variáveis de ambiente, podemos configurar no Compose usando o environment, passando as variáveis que serão usadas por nossa aplicação em determinado ambiente, quando os serviços estiverem subindo.

Para criar um banco de dados, passamos o host, porta, usuário e senha do banco de dados que o Postgres vai usar para poder instalar e depois funcionar [8].

Para instalar o docker em nossa máquina Linux siga os seguintes passos:

  • docker:
  • docker-compose:

Uma vez com o docker e docker-compose em nossas máquinas, vamos criar a imagem do nosso banco de dados.

Antes de mais nada, devemos criar um script SQL para iniciar a nossa tabela. Vamos criar uma tabela chamada pokemon.stats.

CREATE TABLE PUBLIC.pokemonstats ( id INT ,pokemon VARCHAR ,hp INT ,attack INT ,defense INT ,sp_defense INT ,sp_attack INT ,speed INT ,"total" INT ,"average" FLOAT);

Salvarei este arquivo como init.sql numa pasta denominada scripts dentro da pasta raíz do meu projeto. Este script servirá de parâmetro para a construção de nossa imagem do Postgres.

Criarei um arquivo docker-compose.yml na pasta raíz com o seguinte trecho:

version: "3"services: db:  image: postgres  container_name: "pokestats_container"  environment:   - POSTGRES_USER=root   - POSTGRES_PASSWORD=root   - POSTGRES_DB=pokemon_stats  ports:   - "5432:5432"  volumes:   - ".db:/var/lib/postgresql/data/"   - ".scripts:/docker-entrypoint-initdb.d/"

Assim, crio uma imagem do meu serviço de banco de dados pokemon_stats do postgres com o container nomeado pokestats_container. Necessito criar uma pasta denominada ‘db’ para salvar o volume do banco de dados. Note que as variáveis em ‘environment’ são as variáveis ambientes utilizadas como credencial para nossa conexão ao banco de dados. Escolhi um user e senha simples pelo fato de ser uma execução local.

Uma forma mais segura de manter estas credenciais, é criar um arquivo .env, desta forma usuários externos não terão acesso a suas credenciais.

POSTGRES_USER=rootPOSTGRES_PASSWORD=rootPOSTGRES_DB=pokemon_statsPOSTGRES_ADDRESS=localhost

Agora iremos utilizar o comando a seguir no terminal para executar o pull da imagem:

docker-compose up

Caso queira que meu terminal permaneça livre, utilizo a tag -d no fim do comando.

docker-compose up -d

Com o comando a seguir podemos visualizar quais são os container ativos:

docker-compose ps

Agora devemos criar a conexão com o banco de dados com as credenciais salvas em .env. Abaixo, a conexão criada no DBeaver:

Figura 5: Tela para tentar nova conexão no software DBeaver.

Quando a conexão for conectada com sucesso, devemos ver em nosso navegador de banco de dados (aba da esquerda) o banco de dados e a tabela pokemonstats em public:

Figura 6: Tabela pokemonstats criada com sucesso

Assim, iniciamos nosso banco de dados com sucesso. Agora resta mandar a nossa tabela para o nosso banco de dados.

Inserindo dados no banco de dados

Antes de mais nada, precisamos criar uma conexão do nosso programa python com banco de dados. Para isto, utilizaremos a biblioteca SQLAlchemy, que é uma biblioteca de mapeamento SQL.

Para instala-la em nossa máquina utilizaremos o comando:

pip install -U Flask-SQLAlchemy

Como salvamos o arquivo .env com nossas credenciais, devemos importar os em nosso programa principal. Nosso programa principal então se tornará:

import osfrom webscraping import Scrapperfrom sqlalchemy import create_engine# Crio conexão banco de dados Postgresengine = create_engine(  "postgresql+psycopg2://{}:{}@{}/{}".format(    os.environ.get("POSTGRES_USER"),    os.environ.get("POSTGRES_PASSWORD"),    os.environ.get("POSTGRES_ADDRESS"),    os.environ.get("POSTGRES_DATABASE"),))connection = engine.connect()
scrap = Scrapper()df = scrap.scrapping_pokemon()df.to_sql("pokemonstats", connection, if_exists="append", index=False)

Executando o programa acima, após a atualização no DBeaver temos por fim nosso banco de dados com os status dos pokémons. Finalmente pegamos todos! :)

Figura 8: Caso tudo ocorra normalmente, você deverá obter uma tabela no DBeaver semelhante a minha, com todas as linhas e colunas preenchidas.

O projeto completo pode ser encontrado em:

[1] https://www.dca.fee.unicamp.br/cursos/PooJava/objetos/conceito.html

[2] https://www.digitalhouse.com/br/blog/programacao-orientada-a-objetos-o-que-e/

[3] https://www.treinaweb.com.br/blog/orientacao-a-objetos-em-python

[4] https://www.devmedia.com.br/instalando-postgresql

[5] https://dbeaver.io/download/

[6] https://hub.docker.com/_/postgres

[7] http://www.tecnisys.com.br/noticias/2021/qual-e-a-diferenca-entre-uma-imagem-docker-e-um-container

[8] https://imasters.com.br/banco-de-dados/docker-compose-o-que-e-para-que-serve-o-que-come

--

--

Deborah Bertoldo da Silva
Todas as Letras

Física, mestranda em tecnologia nuclear e entusiasta em ciência da computação.