Luiz Felipe P. Figueiredo
  • Home
  • Tech Skills
  • Skill Sets
  • Work Experience
  • Projects
  • Dashboards
  • Blog
  • About me

Web scrapping com Selenium e Tratamento de Dados Não Estruturados

  • Mostrar o código
  • Esconder o código

  • Ver o código fonte
Python
Selenium
Web scraping
EDA
Plotly
Regex
Web scrapping de dados das wikis de FFXIV com Selenium e Tratamento de Dados Não Estruturados
Autor

Luiz Felipe P. Figueiredo

Data de Publicação

22/04/2023

Web scrapping com Selenium e Tratamento de Dados Não Estruturados

Introdução

Este projeto tem como objetivo realizar a extração de dados sobre personagens não jogáveis (NPC) das Wikis de Final Fantasy XIV disponíveis na internet. A proposta central é utilizar técnicas de web scraping, em conjunto com o Selenium, para coletar informações relevantes e detalhadas sobre o universo do jogo. A partir desses dados não estruturados, será realizada uma análise e tratamento minucioso, visando organizar e estruturar essas informações de modo a torná-las mais acessíveis e úteis para diferentes finalidades. O foco reside em oferecer uma abordagem abrangente na captura e tratamento desses dados, contribuindo para a construção de conjuntos de informações mais coerentes e utilizáveis para os entusiastas e jogadores de Final Fantasy XIV.

Para este projeto, decidi utilizar o selenium em conjunto com o Chrome para realizar a raspagem de dados. Existem outras bibliotecas que poderiam ser utilizadas para essa tarefa, como o Beautiful Soup e o Scrapy, e cada uma delas apresenta suas vantagens e desvantagens.

Acabei optando pelo Selenium devido à simplicidade da tarefa em questão e à necessidade de realizar a raspagem em um ambiente específico. Como os dados que eu precisava extrair não eram tão grandes, não foi necessário utilizar um framework de webscraping mais complexo como o Scrapy.

Sobre o Selenium

O Selenium é uma poderosa ferramenta para a automação de testes em navegadores web. Ele é utilizado para automatizar a interação do usuário com uma página web, como clicar em botões, preencher formulários, navegar em menus e outras ações.

O Selenium requer um driver específico para o navegador que será utilizado na raspagem de dados. No caso do Chrome, é necessário fazer o download do ChromeDriver, que é um executável que permite que o Selenium se comunique com o navegador Chrome.

O ChromeDriver deve ser baixado e instalado conforme a versão do Chrome que será utilizada. Após a instalação, é necessário especificar o caminho para o executável do ChromeDriver no código do Selenium, para que o navegador possa ser aberto e utilizado para a raspagem de dados.

Dessa forma, o Selenium e o Chrome trabalham em conjunto para acessar a página web inicial, executar ações como preencher formulários e clicar em botões, e extrair os dados necessários para a análise posterior.

A maneira como os dados serão extraídos depende da estrutura da página web em que eles se encontram e da forma como esses dados estão organizados.

Por exemplo, se os dados estiverem contidos em uma tabela, é possível usar a biblioteca Pandas para extrair diretamente as informações da tabela em um formato de DataFrame. Por outro lado, se os dados estiverem espalhados em diferentes partes da página web, será necessário usar técnicas de raspagem mais avançadas, como a localização de elementos HTML específicos usando seletores de CSS ou XPath.

Com o Selenium, é possível usar várias funções para realizar a raspagem de dados em uma página web. Uma dessas funções é a .find_element(), que pode ser usada em conjunto com a função By para localizar um elemento específico na página usando uma estratégia de pesquisa específica, como XPATH, CSS_SELECTOR, ID, entre outras, depois basta extrair esses elementos com base em algum atributo que ele deveria ter, por exemplo, se eu desejo extrair o link de um elemento, utilizaria o atributo 'href'. Mais sobre o Selenium com o Python na Documentação oficial do Selenium.

Módulos

Código
# biblioteca para automação em navegador web
from selenium import webdriver  
# gerenciador de driver para o navegador Chrome
from webdriver_manager.chrome import ChromeDriverManager
# biblioteca para localizar elementos na página web
from selenium.webdriver.common.by import By
# biblioteca para configurar o serviço do navegador
from selenium.webdriver.chrome.service import Service
# biblioteca para configurar as opções do navegador
from selenium.webdriver.chrome.options import Options

# biblioteca para carregamento e manipulação de dados
import pandas as pd  
# biblioteca para computação numérica
import numpy as np  
# biblioteca para controlar o tempo de execução das tarefas
import time  
# biblioteca para esperar até que determinado elemento seja carregado na página
import plotly.io as pio
pio.renderers.default = "plotly_mimetype+notebook"
import plotly.colors as colors
import plotly.express as px
import plotly.graph_objects as go
# biblioteca para interagir com o sistema operacional
import os  
# biblioteca para registro de informações e depuração do código
import logging  
# biblioteca para trabalhar com expressões regulares (regex)
import re  

Extração

A primeira Wiki a qual irei extrair os dados dos personagens do FFXIV é a Final Fantasy Wiki. Para fazer isso primeiramente foi necessário extrair todas as páginas que continham as informações desejadas. Sendo assim, extraí o link dessas páginas através do botão NEXT na primeira página e nas subsequentes até a ultima, para realizar essa tarefa, optei por usar o seletor CSS do botão em questão. Além disso, a função abaixo, já faz uso destes dados para coletar o nome e o link da página de cada personagem.

Código
def get_pages_fandom_wiki_and_info(starter_page, wait=3):
    """A partir de uma `starter_page` extrai o nome e a url de cada personagem no site 
    `https://finalfantasy.fandom.com/wiki/Category:Characters_in_Final_Fantasy_XIV`, 
    `wait` é o tempo de espera após a página ser acessada"""
    # Configuração do chrome para utilização do Selenium
    chrome_options = Options()

    # Garante que o GUI está desligado (janela do chrome não vai abrir)
    # comente esta linha se quiser que a janela do chrome abra
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    # chrome_options.add_argument("window-size=1920,1080") # Resolução da tela

    # Download dos drivers (silêncioso)
    logging.getLogger("WDM").setLevel(logging.NOTSET)
    os.environ["WDM_LOG"] = "False"

    # Cria o serviço do driver do chrome utilizando o ChromeDriverManager
    webdriver_service = Service(ChromeDriverManager().install())

    # Cria o driver do chrome, janela do navegador só abrir se a opção --headless estiver desabilidade
    driver = webdriver.Chrome(service=webdriver_service, options=chrome_options)
    driver.get(starter_page)  # Abre o site inicial
    time.sleep(3)  # Espera por 3 segundos
    # Resolve popup de cookies. Mude o "ACEITAR" para a linguagem do pop up em caso de erro
    driver.find_element(By.XPATH, '//div[text()="ACCEPT ALL"]').click()
    pages = [starter_page]  # Inicial a lista pages com a primeira pagina

    n = False  # Controlador do loop while
    while not n:  # Loop para encontrar todas as páginas com informações dos personagens
        # Tentar rodar o codigo, caso erro n=True, fecha o loop
        try:
            # Encontra o botão next na página usando o seu seletor css
            b = driver.find_element(
                By.CSS_SELECTOR,
                "#mw-content-text > div.category-page__pagination > a.category-page__pagination-next.wds-button.wds-is-secondary",
            )
            # Acrescenta a cada loop o link do botão next a lista pages
            pages.append(b.get_attribute("href"))
            # Abre o link do botão next(link da próxima página) para continuar com o loop
            driver.get(b.get_attribute("href"))
        except:
            n = True  # Fecha o loop

    character_info = []  # Inicializa a lista character_info

    for page in range(len(pages)):  # Para cada página na lista de paginas

        driver.get(pages[page])  # Abre a page a cada iteração do loop
        time.sleep(wait)  # Espera por 3 segundos
        elems = driver.find_elements(
            By.CLASS_NAME, "category-page__member-link"
        )  # Captura os elementos por nome de Classe css e salva em elems
        for elements in elems:  # Para cada elemento encontrado
            # Extrai o link e salva na lista char_url
            char_url = elements.get_attribute("href")
            char_name = (
                elements.text
            )  # Extrai o nome do personagem, e salva na lista char_name
            # Acrescenta as informações extraidas no loop atual em forma de dicionário na lista character_info
            character_info.append({"char_name": char_name, "url": char_url})
    driver.quit()  # Fecha o Driver
    # Retorna um dataframe pandas com as informações dos personagens
    return pd.DataFrame(character_info)


df = get_pages_fandom_wiki_and_info("https://finalfantasy.fandom.com/wiki/Category:Characters_in_Final_Fantasy_XIV")
Código
df
char_name url
0 13th Order Fugleman Zo Ga https://finalfantasy.fandom.com/wiki/13th_Orde...
1 175th Order Alchemist Bi Bi https://finalfantasy.fandom.com/wiki/175th_Ord...
2 269th Order Mendicant Da Za https://finalfantasy.fandom.com/wiki/269th_Ord...
3 2B (Final Fantasy XIV) https://finalfantasy.fandom.com/wiki/2B_(Final...
4 2P https://finalfantasy.fandom.com/wiki/2P
... ... ...
825 Zirnberk https://finalfantasy.fandom.com/wiki/Zirnberk
826 Zodiark (Final Fantasy XIV) https://finalfantasy.fandom.com/wiki/Zodiark_(...
827 Zozonan https://finalfantasy.fandom.com/wiki/Zozonan
828 Zuiko Buhen https://finalfantasy.fandom.com/wiki/Zuiko_Buhen
829 Zurvan (Final Fantasy XIV) https://finalfantasy.fandom.com/wiki/Zurvan_(F...

830 rows × 2 columns

A raspagem acima demorou cerca de 1:30 min no meu computador, esse tempo pode variar de acordo com as especificações da maquina.

Salvando os dados

Os dados foram salvos utilizando em em um arquivo .csv.

Código

# Exporta o dataframe da função get_pages_fandom_wiki_and_info em um arquivo .csv

df.to_csv('datasets/df.csv')

# Leitura do dataframe
df_characters_ff_wiki = pd.read_csv("datasets/df.csv", index_col=0)
df_characters_ff_wiki
char_name url
0 13th Order Fugleman Zo Ga https://finalfantasy.fandom.com/wiki/13th_Orde...
1 175th Order Alchemist Bi Bi https://finalfantasy.fandom.com/wiki/175th_Ord...
2 269th Order Mendicant Da Za https://finalfantasy.fandom.com/wiki/269th_Ord...
3 2B (Final Fantasy XIV) https://finalfantasy.fandom.com/wiki/2B_(Final...
4 2P https://finalfantasy.fandom.com/wiki/2P
... ... ...
825 Zirnberk https://finalfantasy.fandom.com/wiki/Zirnberk
826 Zodiark (Final Fantasy XIV) https://finalfantasy.fandom.com/wiki/Zodiark_(...
827 Zozonan https://finalfantasy.fandom.com/wiki/Zozonan
828 Zuiko Buhen https://finalfantasy.fandom.com/wiki/Zuiko_Buhen
829 Zurvan (Final Fantasy XIV) https://finalfantasy.fandom.com/wiki/Zurvan_(F...

830 rows × 2 columns

Depois de ter extraído as URLs de cada personagem e o seu nome a próxima etapa é extrair informações básicas de cada um deles. Em cada página, existe um painel ao lado direito com algumas informações básicas.

Código
def get_info_fandom(url_df, wait=19):
    """Para cada url em `url_df`, acessa url e extrai as informações do painel direito da página, `wait` define o tempo de espera para cada nova página acessada"""
    results = []  # Inicializa a lista results
    i = 0

    # Configuração do chrome para utilização do Selenium
    chrome_options = Options()

    # Garante que o GUI está desligado (janela do chrome não vai abrir)
    # comente esta linha se quiser que a janela do chrome abra
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    # chrome_options.add_argument("window-size=1920,1080") # Resolução da tela

    # Download dos drivers (silêncioso)
    logging.getLogger("WDM").setLevel(logging.NOTSET)
    os.environ["WDM_LOG"] = "False"

    # Cria o serviço do driver do chrome utilizando o ChromeDriverManager
    webdriver_service = Service(ChromeDriverManager().install())
    # Cria o driver do chrome, janela do navegador só abrir se a opção --headless estiver desabilitado
    driver = webdriver.Chrome(service=webdriver_service, options=chrome_options)

    for url in url_df:  # Para cada URL na lista de URLs fornecindas

        driver.get(url)  # Abre a URL do loop atual
        # Espera por 19 segundos, número alto para evitar erros, pode ser mudado a depender da velocidade e estalibidade da conexão de internet
        time.sleep(wait)

        if (
            i == 0
        ):  # Condição para resolver o popup de cookies, somente necessário quando o navegador é aberto pela primeira vez no primeiro loop

            # Espera dois segunds até o popup aparecer, e o resolve
            time.sleep(2)
            # Mude o "ACEITAR" para a linguagem do pop up em caso de erro
            driver.find_element(By.XPATH, '//div[text()="ACEITAR"]').click()
        # Captura os elementos por XPATH e salva em elems
        elems = driver.find_elements(
            By.XPATH, '//*[@id="mw-content-text"]/div[1]/aside'
        )

        # Tentar rodar o codigo, caso erro n=True, em caso de erro inputa np.nan e retoma o loop, isso foi necessário por existirem paginas que não são personagens na wiki
        try:

            # Extrai o código do elemento, e separa cada palavra em strings
            info = elems[0].text.splitlines()

        except:

            info = np.nan  # Caso não seja um personagem recebe np.nan
            continue

        # Acrescenta as informações na lista results
        results.append({"info": info})

        i += 1  # Incrementa o i para que não entrar no if acima

    driver.quit()  # Finaliza o driver
    return pd.DataFrame(results)  # Retorna um dataframe da lista results




info_df_uns = get_info_fandom(df["url"])

# Exporta o retorno da função get_info_fandom() para um arquivo .csv

info_df_uns.to_csv("datasets/info_df.csv", sep = "\n")
info
0 ['13th Order Fugleman Zo Ga', '(フューグルマン13 ゾ・ガ,...
1 ['175th Order Alchemist Bi Bi', '(アルケミスト175 ビ・...
2 ['269th Order Mendicant Da Za', '(メンディカント269 ダ...
3 ['2B', '(ツービー, Tsūbī?)', 'Alternate names: YoR...
4 ['2P', '(ツーピー, Tsūpī?)', 'Physical description...
... ...
690 ['Zhloe Aliapoh', '(シロ・アリアポー, Shiro Ariapō?, l...
691 ['Zirnberk', '(ツィルンベルク, Tsirunberuku?)', 'Alte...
692 ['Zodiark', '(ゾディアーク, Zodiāku?)', 'Alternate n...
693 ['Zuiko Buhen', '(ズイコウ・ブヘン, Zuikou Buhen?)', '...
694 ['Zurvan', '(鬼神ズルワーン, Kishin Zuruwān?, lit. Zu...

695 rows × 1 columns

Tratamento dos Não Estruturados

Para tornar os dados não estruturados utilizáveis, é necessário fazer o tratamento apropriado, conforme abaixo:

1 - Remoção das aspas em cada cada da linha e transformação da string ( linha ) em uma lista de strings.

Código
# Importa o dataframe gerado pela função get_info_fandom()
info_df = pd.read_csv("datasets/info_df.csv", index_col=0)

# Limpa as strings das colunas "info"
info_df["info"] = info_df["info"].apply(
    lambda x: x.strip("[']").replace("'", "").replace("\\'", "")
)
# Converte cada linha na coluna "info" em uma lista de strings
info_df["info"] = info_df["info"].apply(lambda x: x.split(", "))

info_df
info
0 [13th Order Fugleman Zo Ga, (フューグルマン13 ゾ・ガ, Fy...
1 [175th Order Alchemist Bi Bi, (アルケミスト175 ビ・ビ, ...
2 [269th Order Mendicant Da Za, (メンディカント269 ダ・ザ,...
3 [2B, (ツービー, Tsūbī?), Alternate names: YoRHa No...
4 [2P, (ツーピー, Tsūpī?), Physical description, Rac...
... ...
690 [Zhloe Aliapoh, (シロ・アリアポー, Shiro Ariapō?, lit....
691 [Zirnberk, (ツィルンベルク, Tsirunberuku?), Alternate...
692 [Zodiark, (ゾディアーク, Zodiāku?), Alternate names:...
693 [Zuiko Buhen, (ズイコウ・ブヘン, Zuikou Buhen?), Biogr...
694 [Zurvan, (鬼神ズルワーン, Kishin Zuruwān?, lit. Zurva...

695 rows × 1 columns

2 - Separação das informações contidas em cada lista de strings em cada linhas do dataset e criação de um novo dataset com essas informações.

Código
# Define as informações a serem pesquisadas em cada linha
required_info = [
    "Race",
    "Age",
    "Gender",
    "Type",
    "Affiliation",
    "Occupation",
    "Hair color",
    "Eye color",
    "Job class",
    "Weapon",
    "Armor",
]


# Cria um dicionário para as informações a serem extraídas
info_dict = {info: [] for info in required_info}
info_dict["name"] = []  # Cria a coluna name


for row in info_df["info"]:  # Para cada linha na coluna info

    # Extrai a primeira strings correspondente ao nome do personagem
    name = row[0]
    info_dict["name"].append(name)  # Acrescenta o nome a coluna name

    # Para cada index e string da linha
    for i, s in enumerate(row):
        # Verifica a ocorrência das informações desejadas na lista required_info
        if s in required_info:
            if i + 1 < len(row):
                # Caso a condição é satisfeita, extrai a proxima string (resposta da informação desejada) e acrescenta no dicionário
                info_dict[s].append(row[i + 1])
            else:
                # Caso a informação não esteja disponivel, ou não exista, imputa "Unknown/Not available"
                info_dict[s].append("Unknown")

    # Caso alguma das informações desejadas não exista, imputa "Unknown/Not available", isso é necessário para evitar erro de index
    for info in required_info:
        if info not in row:
            # Caso a informação não esteja disponivel, ou não exista, imputa "Unknown/Not available"
            info_dict[info].append("Unknown")

# Cria um dataframe a partir do dicionário

info_dataframe = pd.DataFrame(info_dict)

col_order = ["name"] + required_info
info_dataframe = info_dataframe[col_order]  # Reordena o dataframe


# define a function to extract the main number from a string
def extract_main_number(s):
    # use regular expressions to extract the digits before the first non-digit character
    match = re.search(r"\d+", s)
    if match is not None:
        return match.group()
    else:
        return None


info_dataframe["Age"] = info_dataframe["Age"].apply(extract_main_number)
info_dataframe["Age"] = info_dataframe["Age"].fillna(pd.NA)
info_dataframe["Age"] = pd.to_numeric(info_dataframe["Age"], errors="coerce").astype(
    "Int64"
)
info_dataframe["Race"] = info_dataframe["Race"].str.replace('"', "")

# Substituindo termos ambiguos
race_mapping = {
    "Mystel": "Miqo'te",
    "Seekers of the Sun Miqote": "Miqo'te",
    "Miqote / Unknown": "Miqo'te",
    "Keeper of the Moon Miqote": "Miqo'te",
    "Seeker of the Sun Miqote": "Miqo'te",
    "Miqote (formerly)": "Miqo'te",
    "Xaela Au Ra": "Au'ra",
    "Au Ra": "Au'ra",
    "Drahn": "Au'ra",
    "Raen Au Ra": "Au'ra",
    "Au Ra (formerly)": "Au'ra",
    "Highlander Hyur": "Hyur",
    "Hume": "Hyur",
    "Hyur/Garlean": "Hyur",
    "Midlander Hyur": "Hyur",
    "Far Eastern Hyur": "Hyur",
    "Human": "Hyur",
    "Hyur (Lich)": "Hyur",
    "Wildwood Elezen": "Elezen",
    "Ishgardian Elezen": "Elezen",
    "Elezen (formerly)": "Elezen",
    "Elf": "Elezen",
    "Duskwight Elezen": "Elezen",
    "Sea Wolf Roegadyn": "Roegadyn",
    "Roegadyn (formerly)": "Roegadyn",
    "Galdjent": "Roegadyn",
    "Roegadyn (Biggs)": "Roegadyn",
    "Hellsguard Roegadyn": "Roegadyn",
    "Viis": "Viera",
    "Veena Viera": "Viera",
    "Rava Viera": "Viera",
    "Dunesfolk Lalafell": "Lalafell",
    "Dwarf": "Lalafell",
    "Plainsfolk Lalafell": "Lalafell",
    "Plainsfolk Lalafell (formerly)": "Lalafell",
    "Lalafell (formerly)": "Lalafell",
    "Ronso": "Hrothgar",
    "Garlean (as Rullus)": "Garlean",
    "Dragon (Vrtra)": "Dragon",
    "Pixie (formerly)": "Pixie",
    "Porxie": "Familiar",
    "Auspices": "Auspice",
    "Blue Kojin": "Kojin",
    "Hume/Sin eater hybrid": "Hyur/Sin eater hybrid",
}
info_dataframe["Race"] = info_dataframe["Race"].replace(race_mapping)
info_dataframe
name Race Age Gender Type Affiliation Occupation Hair color Eye color Job class Weapon Armor
0 13th Order Fugleman Zo Ga Kobold <NA> Male Non-player character 13th Order Fugleman Unknown Unknown Unknown Unknown Unknown
1 175th Order Alchemist Bi Bi Kobold <NA> Female Non-player character 175th Order Alchemist Unknown Unknown Unknown Unknown Unknown
2 269th Order Mendicant Da Za Kobold <NA> Male Non-player character Unknown Unknown Unknown Unknown Conjurer Unknown Unknown
3 2B Android <NA> Female Non-player character YoRHa Unknown Silver Grey Unknown Virtuous Contract Unknown
4 2P Machine Lifeform <NA> Female Non-player character Unknown Unknown Black Unknown Unknown Unknown Unknown
... ... ... ... ... ... ... ... ... ... ... ... ...
690 Zhloe Aliapoh Miqote 21 Female Non-player character "Menphinas Arms" Unknown Light brown Light blue Unknown Unknown Unknown
691 Zirnberk Roegadyn <NA> Male Non-player character Stone Torches Commander White Blue Dark Knight Lockheart "Halones Armor of Fending"
692 Zodiark Primal <NA> Male Non-player character Unknown Unknown Unknown Unknown Unknown Unknown Unknown
693 Zuiko Buhen Hyur 83 Male Non-player character Buhen clan Leader Unknown Unknown Samurai Tsunakiri Unknown
694 Zurvan Primal <NA> Male Boss Unknown Unknown Unknown Unknown Unknown Unknown Unknown

695 rows × 12 columns

Código
# Criando uma lista com as colunas a serem verificadas
colunas_verificar = [
    "name",
    "Race",
    "Age",
    "Gender",
    "Type",
    "Affiliation",
    "Occupation",
    "Hair color",
    "Eye color",
    "Job class",
    "Weapon",
    "Armor",
]

# Dicionário para armazenar a contagem de valores faltantes em cada coluna
contagem_valores_faltantes = {}

# Loop para verificar cada coluna
for coluna in colunas_verificar:
    # Contagem dos valores faltantes na coluna
    qtd_na = info_dataframe[coluna].isna().sum()
    qtd_unknown = info_dataframe[coluna].astype(str).str.contains("Unknown").sum()
    # Armazenamento da contagem no dicionário
    contagem_valores_faltantes[coluna] = qtd_na + qtd_unknown

# Conversão do dicionário em um dataframe
df_contagem = pd.DataFrame.from_dict(
    contagem_valores_faltantes, orient="index", columns=["qtd_faltante"]
)

# Plot do gráfico de barras com a contagem de valores faltantes por coluna

fig = px.bar(x='index', y='qtd_faltante', data_frame=df_contagem.reset_index().sort_values(by='qtd_faltante', ascending=True), title="Contagem de valores faltantes por coluna", 
       labels={'index':'Variáveis', 'y':'Quantidade de valores faltantes', 'qtd_faltante':'Quantidade Faltante'}, hover_name=df_contagem.index, 
       hover_data={'index':True, 'qtd_faltante':True})

fig.update_traces(hovertemplate='<b>%{x}</b><br>'+'Variáveis: %{x}<br>'+'Quantidade Faltante: %{y}')

fig.show()

Com os dados tratados, podemos verificar que existe uma grande quantidade de dados faltantes pelo gráfico acima. Isso ocorre devido a inconsistência dos dados, onde alguns personagens possuem todas as variáveis acima em sua descrição e outros não.

Segunda Wiki - FFXIV Gamerescape

A FFXIV Gamerescape é uma wiki que conta com muito mais personagens não jogáveis catalogados, e outras informações como informações sobre as missões do jogo e dialogo contido nelas. Fazendo uso disso, decidi por extrair essas informações afim de utiliza-las em um outro momento em um modelo de linguagem natual.

Decidi começar a extrair informações sobre as missões principais do jogo. Para fazer isso, segui uma abordagem semelhante à anterior e extrai as URLs dos botões “next page” na página de quests principais. No entanto, enfrentei um pequeno problema quando a extração entrou em loop após algumas páginas, alternando continuamente entre a última e penúltima página. Isso ocorreu devido à forma como os botões foram implementados. Para resolver esse problema, implementei uma solução que consistia em comparar as URLs e, se fossem iguais, remover todas as URLs seguintes e quebrar o loop.

Código
def get_pages_gamerescape(starter_url="", number_of_pages=10, wait=3):
    """Obtem a próxima página usando o xpath do botão de próxima página no site gamerescape do FFXIV.
    o `starter_url` deve ser fornecido. `number_of_pages` deve ser fornecido, mas se houver muitas páginas para contar, use um `number_of_pages` grande o suficiente para cobrir todas as páginas.
    o argumento `wait` é o tempo de espera até a próxima iteração, aumente esse valor se o site estiver muito lento."""
    # Configuração do chrome para utilização do Selenium
    chrome_options = Options()

    # Garante que o GUI está desligado (janela do chrome não vai abrir)
    # comente esta linha se quiser que a janela do chrome abra
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    # chrome_options.add_argument("window-size=1920,1080") # Resolução da tela

    # Cria o serviço do driver do chrome utilizando o ChromeDriverManager
    webdriver_service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=webdriver_service, options=chrome_options)

    # Inicializa a lista `pages` com a URL inicial fornecida
    pages = [starter_url]

    # Inicia um loop que busca por links adicionais na página atual até que `number_of_pages` seja alcançado
    for i in range(number_of_pages):
        try:
            # Aguarda `wait` segundos antes de buscar por links adicionais
            time.sleep(wait)

            # Acessa a página atual usando o driver do Selenium
            driver.get(pages[i])

            # Encontra o botão de próxima página usando XPATH
            if i == 0:
                elems = driver.find_element(By.XPATH, '//*[@id="mw-pages"]/a[1]')
            else:
                elems = driver.find_element(By.XPATH, '//*[@id="mw-pages"]/a[2]')

            # Verifica se a página atual já foi adicionada à lista `pages` anteriormente.
            # Se sim, remove a página atual da lista `pages`, encerra o loop e fecha o driver do Selenium.
            if len(pages) > 2 and pages[i] == pages[i - 2]:
                pages.pop(-1)
                pages.pop(-1)
                driver.quit()
                break

            # Adiciona o link da próxima página à lista `pages`
            pages.append(elems.get_attribute("href"))
        except:
            # Se ocorrer um erro ao buscar por links adicionais, fecha o driver do Selenium e encerra o loop.
            driver.quit()
            break

    # Fecha o driver do Selenium antes de retornar a lista `pages`
    driver.quit()
    return pages

pages = get_pages_gamerescape(
    "https://ffxiv.gamerescape.com/wiki/Category:Main_Scenario_Quests", 10, 3
)

Com as URLs das páginas que contêm os links para todas as quests principais do jogo, o próximo passo foi extrair o nome e a URL de cada uma dessas quests.

Código
def get_quest_data_gamerescape(pages_list=[], wait=3):
    """Para cada página em `pages_list` extrai a url e nome das quests em cada página, 'wait' é o tempo de espera após a página ser acessada"""

    # Configuração do chrome para utilização do Selenium
    chrome_options = Options()

    # Garante que o GUI está desligado (janela do chrome não vai abrir)
    # comente esta linha se quiser que a janela do chrome abra
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    # chrome_options.add_argument("window-size=1920,1080") # Resolução da tela

    # Cria o serviço do driver do chrome utilizando o ChromeDriverManager
    webdriver_service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=webdriver_service, options=chrome_options)

    results = []  # Inicializa a lista results

    def populate_selector_list():  # cria a função populate_selector_list(), função gera os selectors css de acordo com o padrão da pagina da gamerscape
        selector_list = []
        for k in range(300):
            selector_list.append(
                "#mw-pages > div > div > div:nth-child("
                + str(r)
                + ") > ul > li:nth-child("
                + str(k + 1)
                + ") > a"
            )
        return selector_list

    for i in range(len(pages_list)):  # para cada pagina no dataframe pages
        r = 1  # Setado pra 1 para criar a primeira leva de selectors
        n = 0  # Inicializa o n
        r_changed_consecutive = 0  # inicializa a contagem de erros consecutivos
        # chama a função para criar a primeira leva de selectors com r = 1
        selector_list = populate_selector_list()

        # para cada página em i pages_list o driver abre a página
        driver.get(pages_list[i])
        time.sleep(wait)  # espera wait segundos, default é 3 segundos
        next = False  # controle do while
        while not next:  # enquanto verdade
            # tenta rodar o código abaixo
            try:
                # extrai o elemento com base no selector n da selector_list
                elems = driver.find_element(By.CSS_SELECTOR, selector_list[n])
                # ascrescenta um dicionário contendo a url o nome da quest a lista results
                results.append(
                    {"url": elems.get_attribute("href"), "quest": elems.text}
                )
            # quando os selectors acabam, um erro acontece, com isso o code block do except entra em ação
            except:
                r += 1  # r incrementado para gerar uma nova lista de selectors
                n = 0  # n é definido com 0 novamente, para iteração da selector_list
                # nova lista de selectors criada com base no r do loop atual
                selector_list = populate_selector_list()
                # controle para sair do while em caso de muitos erros consecutivos (acabaram os itens na pagina atual)
                r_changed_consecutive += 1
            else:
                # se o loop é rodado normalmente, r_changed_consecutive volta a ser 0
                r_changed_consecutive = 0
                n += 1  # n é incrementado +1
            if (
                r_changed_consecutive == 3
            ):  # caso mais de 3 erros consecutivos, next = true e o loop while é fechado, partindo para nova iteração loop for
                next = True

    driver.quit()  # Fecha o driver do chrome
    return pd.DataFrame(results)  # retorna o dataframe de results
Código
quest_df = get_quest_data_gamerescape(pages)
quest_df.to_csv('datasets/quest_df.csv', sep = '\t')

quest_df = pd.read_csv("datasets/quest_df.csv", sep="\t")

Em cada painel, há várias guias com informações gerais sobre a missão. Optei por extrair todas as informações de uma vez, devido ao longo tempo necessário para a coleta. Além disso, cada painel contém informações sobre a expansão em que a missão foi lançada, o que também foi extraído. Caso você não esteja familiarizado com o jogo, uma expansão é uma atualização significativa que adiciona novo conteúdo ao jogo, como áreas, personagens e histórias.

Código
def get_quest_data_info(url_df=[], wait=3):
    """Para cada quest em `url_df` extrai o level, expansão, nome da quests, intereações, e dialogo. `wait` é o tempo de espera após a página ser acessada"""

    # Configuração do chrome para utilização do Selenium
    chrome_options = Options()

    # Garante que o GUI está desligado (janela do chrome não vai abrir)
    # comente esta linha se quiser que a janela do chrome abra
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    # chrome_options.add_argument("window-size=1920,1080") # Resolução da tela

    # Cria o serviço do driver do chrome utilizando o ChromeDriverManager
    webdriver_service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=webdriver_service, options=chrome_options)
    results = []
    interactions = []
    quest_name = []
    for i in range(len(url_df)):

        driver.get(url_df[i])
        time.sleep(wait)
        quest_elems = driver.find_element(
            By.CSS_SELECTOR, "#page_content > div.content > div > div > h1"
        )
        quest_name = str(quest_elems.text)
        time.sleep(0.5)
        elems = driver.find_element(
            By.CSS_SELECTOR,
            "#mw-content-text > div > table > tbody > tr:nth-child(1) > td:nth-child(1) > table > tbody > tr > td:nth-child(2) > span",
        )
        levels = elems.text.replace("Lv.", "").replace(" ", "")
        levels = int(levels)
        time.sleep(0.5)
        expansion_elems = driver.find_element(
            By.XPATH,
            '//*[@id="mw-content-text"]/div/table/tbody/tr[1]/td[1]/table/tbody/tr/td[2]/div/a[3]',
        )
        time.sleep(0.5)
        driver.find_element(By.LINK_TEXT, "Dialogue").location_once_scrolled_into_view
        driver.find_element(By.LINK_TEXT, "Dialogue").click()
        time.sleep(0.5)
        dialogue_elems = driver.find_elements(By.CLASS_NAME, "bubble")
        page_dialogues = []
        for k in dialogue_elems:
            page_dialogues.append(k.text)
        dialogue = page_dialogues
        time.sleep(0.5)
        driver.find_element(
            By.LINK_TEXT, "Interactions"
        ).location_once_scrolled_into_view
        driver.find_element(By.LINK_TEXT, "Interactions").click()
        time.sleep(0.5)
        interactions_elems = driver.find_elements(By.CLASS_NAME, "tabbertab")
        interactions = str("".join(interactions_elems[1].text))
        results.append(
            {
                "quest": quest_name,
                "expansion": expansion_elems.text,
                "level": levels,
                "interactions": interactions,
                "dialogue": dialogue,
            }
        )
    driver.quit()
    return pd.DataFrame(results)
Código
#quest_info = get_quest_data_info(quest_df['url'], 3)

#quest_info.to_csv('datasets/quest_info.csv', sep='\t')
quest_info = pd.read_csv("datasets/quest_info.csv", sep="\t", index_col=0)

Transformação dos Dados Não Estruturados

  • Transformação dos nomes de cada patch presente em cada expansão para o nome da expansão em sí.
  • Remoção de caracteres e termos indesejados.
Código
# Um dicionário que mapeia nomes de expansão para os seus equivalentes no jogo
expansion_dict = {
    "Post-Ala Mhigan Liberation": "Stormblood",
    "Newfound Adventure": "Endwalker",
    "Seventh Astral Era": "A Realm Reborn",
    "The Voyage Home": "Shadowbringers",
    "Seventh Umbral Era": "A Realm Reborn",
    "Dragonsong War": "Heavensward",
    "Dark Reprise": "Shadowbringers",
    "Post-Dragonsong War": "Heavensward",
}

# Substituição dos valores na coluna "expansion" do DataFrame usando o expansion_dict
quest_info["expansion"] = quest_info["expansion"].replace(expansion_dict)

# Remoção de caracteres de nova linha na coluna "interactions"
quest_info["interactions"] = quest_info["interactions"].str.replace("\n", "")
quest_info["interactions"] = quest_info["interactions"].str.replace("\r", "")

# Particionamento dos dados após a palavra "Maps" na coluna "interactions"
quest_info["interactions"] = quest_info["interactions"].str.partition("Maps")[2]

# Separação dos elementos na coluna "interactions" com base na vírgula
quest_info["interactions"] = quest_info["interactions"].str.split(",")

# Remoção de aspas simples e duplas na coluna "dialogue"
quest_info["dialogue"] = quest_info["dialogue"].str.replace("'", "")
quest_info["dialogue"] = quest_info["dialogue"].str.replace('"', "")

# Limpeza da coluna "interactions" removendo "Mobs Involved" para cada interação
for index, row in quest_info.iterrows():
    interactions = row["interactions"]
    interactions = [
        x.replace("Mobs Involved", "").strip() if "Mobs Involved" in x else x
        for x in interactions
    ]

# Limpeza da coluna "interactions" removendo "Objects Involved Destination" para cada interação
for index, row in quest_info.iterrows():
    interactions = row["interactions"]
    interactions = [
        re.sub(r'\bMobs\s*Involved\b|\bObjects\s*Involved\s*Destination\b|\bMobs\s*Involved|\bObjects\s*Involved\s*Destination', '', x).strip()
        for x in interactions
    ]
    quest_info.at[index, "interactions"] = interactions

# Retorno do DataFrame quest_info modificado
quest_info
quest expansion level interactions dialogue
0 A Bargain Struck Stormblood 60 [Alisaie, Alphinaud, Lyse, Conrad, M'naago, Me... [Alisaie, Come to take the measure of our frie...
1 A Beeautiful Plan Shadowbringers 74 [Y'shtola, Thancred, Bees] [Thancred, The bag is sealed tight, but Id rat...
2 A Blessed Instrument Shadowbringers 70 [Alphinaud, Weeping Warbler, Thoarich, Dulia-C... [Alphinaud, (Lady Chai has a very particular s...
3 A Blissful Arrival Stormblood 70 [Alphinaud, Wiscar, Watt, Raubahn, Arenvald, P... [Wiscar, Grandad and his friends are already h...
4 A Bold Decision Endwalker 89 [Krile, Tataru, Fourchenault, Barnier, Livingw... [Tataru, A great many of the helping hands tha...
... ... ... ... ... ...
883 Ys Iala’s Errand Shadowbringers 72 [Soldier Crawler] [Ys Iala, Unnngh... Im so hungry I may faint.....
884 Yugiri's Game A Realm Reborn 50 [Alphinaud, Hozan, Yozan, Hiding Child] [Alphinaud, As we speak, the Domans prepare fo...
885 Ziz Is So Ridiculous A Realm Reborn 28 [Aideen Ziz] [Aideen, So, heres what I know about the death...
886 Α Test of Wιll Endwalker 89 [Estinien, Alphinaud, Alisaie, Y'shtola, Urian... [Bereaved Dragon, ......, Estinien, They want ...
887 ┣┨̈//̈ No┨ΦounΔ••• Endwalker 90 [G'raha Tia, Alphinaud, Alisaie, M-017, Sir O... [Alphinaud, Considering the Omicrons single-mi...

888 rows × 5 columns

Código
fig = px.bar(data_frame=quest_info['expansion'].value_counts().reset_index(), x='expansion', y='count', color='expansion', 
             category_orders={'expansion':['A Realm Reborn', 'Heavensward', 'Stormblood', 'Shadowbringers', 'Endwalker']}, 
             hover_name='expansion', labels={'expansion':'Expansão', 'count':'Número de Quests'}, title='Número de Quests por Expansão')
fig.show()

A Realm Reborn, tem a maior quantidade de quests, uma das maiores reclamações dos novos jogadores.

Código


quest_info_filtered = quest_info.groupby('expansion')['level'].value_counts().reset_index()


color_scale = colors.qualitative.Plotly



fig = go.Figure()

for exp in ['A Realm Reborn', 'Heavensward', 'Stormblood', 'Shadowbringers', 'Endwalker']:

    df = quest_info_filtered[quest_info_filtered['expansion']==exp]
    
    fig.add_trace(go.Bar(x=df['level'], y=df['count'],width=0.9,base=0, 
                         name=exp, marker={'color':px.colors.qualitative.Plotly[len(fig.data)]}, 
                         hovertemplate='Level: %{x}<br>Número de Quests: %{y}<br>Expansão: ' + exp, hoverlabel={'namelength': 0}))


dropdown_buttons = [
{'label': "Todas", 'method': "update", 'args': [{"visible": [True, True, True, True, True]}, {"title": "Todas Expansões"}]},
{'label': "ARR", 'method': "update", 'args': [{"visible": [True, False, False, False, False]}, {"title": "A Realm Reborn"}]},
{'label': "HW", 'method': "update", 'args': [{"visible": [False, True, False, False, False]}, {"title": "Heavensward"}]},
{'label': "SB", 'method': "update", 'args': [{"visible": [False, False, True, False, False]}, {"title": "Stormblood"}]},
{'label': "SHB", 'method': "update", 'args': [{"visible": [False, False, False, True, False]}, {"title": "Shadowbringers"}]},
{'label': "EW", 'method': "update", 'args': [{"visible": [False, False, False, False, True]}, {"title": "Endwalker"}]}
]


fig.update_layout({
        'updatemenus': [
        {'showactive': True, 'type':'buttons', 'buttons': dropdown_buttons, 'direction':'right', 'active': 0, 'x':0, 'y':1.01, 'xanchor':'left', 'yanchor':'bottom'}
        ]})
fig.update_layout(barmode='stack')
fig.update_xaxes(title='Levels')
fig.update_yaxes(title='Número de Quests Por Level')

fig.show()

Número de quests por level. Os níveis 50, 60, 70, 80, e 90 se sobrepõem pois todo expansão termina onde a outra parou.

Extração das características dos NPCs

Primeiramente, a função abaixo define uma lista de raças de interesse e cria uma função interna para popular uma lista de XPaths. A lista de XPaths contém os caminhos para as tabelas de cada categoria de personagem no site. A função interna usa loops para gerar esses caminhos.

Então, a função inicializa uma lista de resultados e começa a iterar através da lista de XPaths. Para cada XPath, a função tenta encontrar o elemento correspondente na página utilizando o método find_element do Selenium. Se o elemento for encontrado, a função verifica se o personagem é do gênero feminino ou masculino. Se for feminino, a função define o sexo como “Female”. Se for masculino, a função define o sexo como “Male”.

Por fim, a função adiciona as informações extraídas (URL da categoria, raça e sexo) à lista de resultados. A função retorna a lista de resultados depois de iterar através de todos os XPaths.

Código
def get_character_pages_and_info(url="", wait=2):
    """Partindo da url `https://ffxiv.gamerescape.com/w/index.php?title=Category:NPC&pageuntil=Ailid#Hyur`,
    extrai a url para cada uma das categorias presentes no painel do site, junto com o sexo e raça
    """
    # Lista das raças de interesse

    races = [
        "Hyur",
        "Elezen",
        "Lalafell",
        "Miqo'te",
        "Roegadyn",
        "Au Ra",
        "Viera",
        "Hrothgar",
        "Other",
    ]

    # Função interna para popular a lista de XPaths
    def populate_xpath_list():

        xpath_list = []
        # Loop para percorrer os XPaths das tabelas das categorias presentes na página

        for j in range(1, 9):
            for i in range(2, 4):
                for k in range(1, 4):
                    xpath_list.append(
                        '//*[@id="tabber-7be34c4dba9d92e6cba110a0e80e7d64"]/div['
                        + str(j)
                        + "]/table/tbody/tr["
                        + str(k)
                        + "]/td["
                        + str(i)
                        + "]/a"
                    )

        # Lista com os XPaths das categorias Ancients
        ancients = [
            '//*[@id="tabber-7be34c4dba9d92e6cba110a0e80e7d64"]/div[9]/table[1]/tbody/tr/td[1]/a',
            '//*[@id="tabber-7be34c4dba9d92e6cba110a0e80e7d64"]/div[9]/table[1]/tbody/tr/td[2]/a',
            '//*[@id="tabber-7be34c4dba9d92e6cba110a0e80e7d64"]/div[9]/table[1]/tbody/tr/td[3]/a',
            '//*[@id="tabber-7be34c4dba9d92e6cba110a0e80e7d64"]/div[9]/table[1]/tbody/tr/td[4]/a',
            '//*[@id="tabber-7be34c4dba9d92e6cba110a0e80e7d64"]/div[9]/table[1]/tbody/tr/td[5]/a',
        ]
        xpath_list += ancients

        # Loop para percorrer os XPaths das tabelas das categorias Ancients
        for k in range(2, 4):
            for j in range(1, 6):
                for i in range(1, 7):
                    xpath_list.append(
                        '//*[@id="tabber-7be34c4dba9d92e6cba110a0e80e7d64"]/div[9]/table['
                        + str(k)
                        + "]/tbody/tr["
                        + str(j)
                        + "]/td["
                        + str(i)
                        + "]/a"
                    )

        return xpath_list

    # Chamada da função para popular a lista de XPaths
    xpath_list = populate_xpath_list()

    # Configuração do chrome para utilização do Selenium
    chrome_options = Options()

    # Garante que o GUI está desligado (janela do chrome não vai abrir)
    # comente esta linha se quiser que a janela do chrome abra
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    # chrome_options.add_argument("window-size=1920,1080") # Resolução da tela

    # Cria o serviço do driver do chrome utilizando o ChromeDriverManager
    webdriver_service = Service(ChromeDriverManager().install())
    # instanciar o webdriver do Selenium
    driver = webdriver.Chrome(service=webdriver_service, options=chrome_options)
    # abrir a URL inicial
    driver.get(url)
    # aguardar o tempo especificado
    time.sleep(wait)
    # inicializar a lista de resultados
    results = []
    # para cada xpath na lista de xpaths populada
    for xpath in xpath_list:
        elems = ""
        # tentar encontrar o elemento usando o xpath
        try:
            elems = driver.find_element(By.XPATH, xpath)
        # se não encontrar, continuar para o próximo xpath
        except:
            continue

        # verificar se o gênero é feminino
        if "Female" in elems.get_attribute("title").split(sep=":")[-1].split(sep=" "):
            # definir o sexo como "Female"
            sex = "Female"
            # obter a subraça
            subrace = " ".join(
                [
                    word
                    for word in elems.get_attribute("title")
                    .split(sep=":")[-1]
                    .split()[
                        : elems.get_attribute("title")
                        .split(sep=":")[-1]
                        .split()
                        .index("Female")
                    ]
                ]
            )
            # se a subraça for igual à raça correspondente, definir a subraça como NA (valor ausente do pandas)
            if subrace in races:
                race = races[races.index(subrace)]
                if race == subrace:
                    subrace = pd.NA
        # verificar se o gênero é masculino
        elif "Male" in elems.get_attribute("title").split(sep=":")[-1].split(sep=" "):
            # definir o sexo como "Male"
            sex = "Male"
            # obter a subraça
            subrace = " ".join(
                [
                    word
                    for word in elems.get_attribute("title")
                    .split(sep=":")[-1]
                    .split()[
                        : elems.get_attribute("title")
                        .split(sep=":")[-1]
                        .split()
                        .index("Male")
                    ]
                ]
            )
            # se a subraça estiver na lista de raças fornecida, definir a raça como a subraça correspondente
            if subrace in races:
                race = races[races.index(subrace)]
        # se o gênero não for conhecido
        else:
            # definir o sexo como "Unknown"
            sex = "Unknown"
            # obter a raça
            race = " ".join(
                [
                    word
                    for word in elems.get_attribute("title")
                    .split(sep=":")[-1]
                    .split()[
                        : elems.get_attribute("title")
                        .split(sep=":")[-1]
                        .split()
                        .index("NPC")
                    ]
                ]
            )
            # adicionar o resultado à lista de resultados
        results.append({"url": elems.get_attribute("href"), "race": race, "sex": sex})
    # fechar o webdriver
    driver.quit()
    # retornar um DataFrame do pandas com os resultados
    return pd.DataFrame(results)


characters_pages_and_info_df = get_character_pages_and_info(
    "https://ffxiv.gamerescape.com/w/index.php?title=Category:NPC&pageuntil=Ailid#Hyur"
)
Código
characters_pages_and_info_df.head()
url race sex
0 https://ffxiv.gamerescape.com/wiki/Category:De... Hyur Male
1 https://ffxiv.gamerescape.com/wiki/Category:De... Hyur Male
2 https://ffxiv.gamerescape.com/wiki/Category:De... Hyur Male
3 https://ffxiv.gamerescape.com/wiki/Category:De... Hyur Female
4 https://ffxiv.gamerescape.com/wiki/Category:De... Hyur Female

Esta função recebe três dataframes como entrada: df_url, df_race e df_sex. Esses dataframes têm informações sobre personagens de um jogo, além da URL da página de cada um deles. A função extrai o nome de cada personagem em uma lista de personagens associada a cada URL no df_url, e associa a raça e sexo do personagem com base nas informações nos dataframes df_race e df_sex. O resultado é um novo dataframe com as informações dos personagens.

Código
def get_character_names(df_url, df_race, df_sex):
    """Dado um DataFrame com URLs de páginas da wiki, extrai o nome dos personagens,
    bem como suas raças e sexo, e retorna um novo DataFrame com essas informações."""

    # Cria uma lista com todos os caminhos XPath que serão utilizados para encontrar os nomes dos personagens na página da wiki
    def populate_xpath_list():
        xpath_list = []
        for j in range(1, 5000):
            xpath_list.append(
                '//*[@id="mw-content-text"]/div[1]/table/tbody/tr['
                + str(j)
                + "]/td[1]/a"
            )
        return xpath_list

    xpath_list = populate_xpath_list()

    # Configuração do chrome para utilização do Selenium
    chrome_options = Options()

    # Garante que o GUI está desligado (janela do chrome não vai abrir)
    # comente esta linha se quiser que a janela do chrome abra
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    # chrome_options.add_argument("window-size=1920,1080") # Resolução da tela

    # Cria o serviço do driver do chrome utilizando o ChromeDriverManager
    webdriver_service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=webdriver_service, options=chrome_options)

    # Espera 2 segundos antes de começar a executar o código
    time.sleep(2)
    results = []

    # Percorre as URLs do DataFrame df_url e extrai as informações de nome, raça e sexo de cada personagem
    for i in range(len(df_url)):
        driver.get(df_url[i])
        time.sleep(1)
        race = df_race.loc[i]
        sex = df_sex.loc[i]
        for xpath in xpath_list:
            try:
                elems = driver.find_element(By.XPATH, xpath)
                results.append(
                    {"name": elems.get_attribute("title"), "race": race, "sex": sex}
                )
            except:
                break
    # Encerra a execução do driver do Chrome
    driver.quit()
    # Cria um DataFrame com as informações de nome, raça e sexo dos personagens
    return pd.DataFrame(results)


# df_character = get_character_names(characters_pages_and_info_df["url"], characters_pages_and_info_df["race"], characters_pages_and_info_df["sex"])
Código
# df_character.to_csv('datasets/character_info.csv', sep='\t')
df_character = pd.read_csv("datasets/character_info.csv", sep="\t", index_col=0)

# removendo duplicatas
df_character = df_character.drop_duplicates().reset_index().drop("index", axis=1)
# remove personagens classificados com objetos
df_character = df_character[df_character.race != "Object"]
df_character
name race sex
0 1st Legion Magitek Engineer Hyur Male
1 1st Legion Soldier Hyur Male
2 1st Platoon Soldier Hyur Male
3 3rd Unit Brave Hyur Male
4 4th Legion Soldier Hyur Male
... ... ... ...
7680 Daunting Dragon Non-Humanoid Unknown
7681 Daydreaming Oreias Non-Humanoid Unknown
7682 Defiant Dragon Non-Humanoid Unknown
7683 Delighted Dragonet Non-Humanoid Unknown
7684 Delivery Moogle Non-Humanoid Unknown

7685 rows × 3 columns

Acima, o dataframe de personagens, e abaixo um gráfico com as 10 raças mais prevalentes no jogo em relação as personagens não jogaveis.

Código
top_values = df_character["race"].value_counts().nlargest(10).sort_values(ascending=False)
 
df_filtered = df_character[df_character["race"].isin(top_values.index)]

fig = px.bar(data_frame=df_filtered.race.value_counts().reset_index(), y='race', x='count', color='race', orientation='h', hover_name='race', 
       labels={'race':'Raça', 'count':'Frequência'}, title='As 10 Raças mais Usadas em Final Fantasy XIV')
fig.add_annotation(x=0.15, y=0.21, xref="paper", yref="paper", text='Raças não jogaveis.', showarrow=False)
fig.add_annotation(x=0.03, y=0.15,xref="paper", yref="paper", showarrow=True, arrowhead=1, ax=90, ay=-26)
fig.add_annotation(x=0.04, y=0.34,xref="paper", yref="paper", showarrow=True, arrowhead=1, ax=77, ay=26)
fig.update_layout(dragmode=False)
fig.show()

Hyur é a raça mais utilizada pelos desenvolvedores seguida de Elezen e Roegadyn.

Código
# Raças a manter
races_to_keep = [
    "Hyur",
    "Elezen",
    "Lalafell",
    "Miqo'te",
    "Roegadyn",
    "Au Ra",
    "Viera",
    "Hrothgar",
    "Non-Humanoid",
    "Loporrit",
]

# cria uma nova coluna para classificar outras raças que não estão em races_to_keep como Outras
df_character["race_new"] = df_character["race"].apply(
    lambda x: x if x in races_to_keep else "Outras"
)

df_filtered = df_character[["race_new", "sex"]]
df_filtered = df_filtered.rename(columns={"race_new": "race"})
df_character = df_character.drop("race_new", axis=1)

# grafico do sexo por raça
fig = px.bar(data_frame=df_filtered.value_counts().reset_index(), x='race', y='count', color='sex', barmode='group', hover_name="race", title='Distribuição das raças por sexo em Final Fantasy XIV',
       labels={'sex':'Sexo', 'race':'Raça', 'count':'Frenquência'})
fig.show()

Acima, um gráfico com a distribuição de raça dos personagens por sexo.

This work is licensed under CC BY-SA 4.0
Designed and Developed collaboratively by
Jose C. S. Junior & Luiz F. P. Figueiredo
Built with Quarto