Neste post eu mostro como obter e fazer os parser dos dados dos jogos de tabuleiro do BoardGameGeek, obtidos através de sua API XML. O processo apresentado aqui faz uso e complementa o que já foi apresentado no scrapper do ranking do BGG, e servirá de base para alguns posts que penso em escrever no futuro.
Um dos primeiros posts do blog foi sobre um scrapper para obtermos as informações da página do ranking do BoardGameGeek, um portal especializado em jogos de tabuleiro. Meu principal objetivo com isso não foi o de extrair o ranking em si, mas o de extrair o identificador único de cada jogo para usá-lo junto à API XML oferecida pelo portal. Essa API nos dá acesso à (praticamente) todas as informações sobre os jogos que estão disponíveis em suas respectivas páginas e, portanto, fornece um caminho mais simples para obtermos os dados sobre cada um deles de forma programática. No entanto, o mais importante para mim é que essas informações têm o potencial de facilitar uma das coisas que eu mais tenho dificuldade: encontrar os jogos que combinam com aquilo que eu curto.
Neste post eu mostro como interagir com a API XML do BGG e, também, fazer o parser das informações que obtemos a partir dela. Esse processo vai ser importante para entendermos os tipos de dados que temos disponíveis e as possíveis formas de usá-los nos próximos posts.
A primeira coisa que precisaremos fazer para interagir com a API do BGG é carregar a base que contém o identificador único de cada jogo. Estes dados podem ser obtidos a partir do scrapper disponível neste link, ou você pode simplesmente usar a base de dados disponível junto a esse post. A informação que precisaremos está na coluna id
na tabela abaixo.
# carregando os pacotes
library(tidyverse) # core
library(httr) # para fazer o scraping
library(xml2) # para o parser
library(fs) # para manipular paths
## lendo os dados do ranking da BGG para pegar o identificador unico de cada jogo
ranking <- read_rds(file = 'data/ranking_bgg.rds') %>%
# dropando os jogos que não possuem game_id
drop_na(id)
## visualizando a tabela
rmarkdown::paged_table(x = ranking)
A API do BGG funciona através de requisições do tipo GET
, que retornam um arquivo xml
(eXtensible Markup Language) cujos parâmetros e valores seguem o padrão de tags e atributos comum à páginas HTML
. Não é necessário fazer qualquer tipo de autenticação para usar essa API, bastando fazer àquela requisição diretamente para o endpoint selecionado e empregando os métodos desejados. Falando nisso, não tenho certeza se essa API é RESTful ou não (REpresentational State Transfer), uma vez que (1) apenas requisições do tipo GET
são suportadas, (2) as requisições para um determinado tipo de informação são todas feitas através de um único endpoint e (3) os nomes dos métodos utilizados devem ser passados diretamente para a url
da requisição. De toda forma, a documentação da API pode ser encontrada aqui e, se você souber me dizer se ela é RESTful ou não, eu agradeceria.
Existem diversos tipos de informação disponíveis no site do BGG que podem ser obtidas através dessa API, tais como os dados dos jogos (i.e., thing
), das coleções (i.e., collection
) e dos próprios usuários (i.e., user
) e do fórum (i.e., forum
). Além disso, dentro de cada tipo de informação desta, podemos buscar detalhes específicos, tais como tudo o que há no marketplace para um jogo ou os resultados das partidas dos jogos registrados por cada usuário. Embora haja uma infinidade de informalões, neste post vamos focar apenas nas relativas às características dos jogos e, portanto, vamos usar o endpoint https://www.boardgamegeek.com/xmlapi2/thing
.
Existem diversos métodos que podemos ser usados junto daquele endpoint. Entretanto, vamos usar aqui o id
do jogo (que vamos extrair da tabela que carregamos acima), as estatísticas relacionadas à cada jogo (setando stats=1
) e uma amostra de, no máximo, 100 comentários associados à cada jogo (setando ratingcomments=1
e pagesize=100
). Existem outros métodos associados a esse endpoint, como os vídeos que falam sobre o jogo (i.e., videos=1
) e as informações dos anúncios do marketplace (i.e., marketplace=1
), mas não vamos trabalhar com eles aqui. De toda forma, precisamos juntar o endereço do endpoint e os métodos para compor a url
para o GET
: fazemos isso separando a url
do endpoint das strings dos métodos utilizando o ?
e cada método do outro usando o &
. Com isso, a url
ficará assim: https://www.boardgamegeek.com/xmlapi2/thing?id=<game_id>&stats=1&ratingcomments=1&pagesize=100
- onde o game_id
vai ser um número correspondente ao identificador numérico do jogo.
A função abaixo cuidará de fazer essa requisição, salvando uma cópia do HTML da resposta em disco caso o valor passado para o argumento path_to_save
não seja NULL
.
## funcao para pegar o XML de um jogo
pega_jogo <- function(game_id, path_to_save = NULL) {
# url base para pegar um jogo, as estatisticas e a primeira pagina de comentarios
base_url <- str_glue('https://www.boardgamegeek.com/xmlapi2/thing?id={game_id}&stats=1&ratingcomments=1&pagesize=100')
# fazendo o request e salvando o codigo da resposta se o path nao for nulo
if(!is.null(path_to_save)){
GET(url = base_url,
write_disk(path = sprintf(fmt = '%s/%08d.html', path_to_save, game_id),
overwrite = TRUE)
)
} else {
GET(url = base_url)
}
}
Vamos usar a função para fazer a requisição de um jogo, salvando o xml
resultante em disco. Para isso, vou criar um diretório temporário para armazenar o xml
e, também, extrair o identificador numérico do jogo que utilizaremos nesse exemplo: o Ticket to Ride. Esse é um jogo de construir rotas com trenzinhos, e é muito legal e divertido.
## setando o path onde vamos jogar os arquivos
path_scrapped_data <- 'temporario'
## criando pasta se ela nao existir
if(!dir_exists(path_scrapped_data)){
dir_create(path = path_scrapped_data, recurse = TRUE)
}
# pegando o id do jogo que vamos usar no exemplo
id_do_jogo <- ranking %>%
# pegando o jogo wingspan
filter(titulo == 'Ticket to Ride') %>%
# pegando o id do jogo
pull(id) %>%
# parseando o id para um numero
parse_number()
# fazendo o request do XML do jogo
xml_do_jogo <- pega_jogo(game_id = id_do_jogo, path_to_save = path_scrapped_data)
Podemos pegar o content
da resposta da requisição e usar a função xml_structure
para entender a sua estrutura. Isso nos ajuda bastante a identificar as tags que precisamos buscar, bem como o tipo de resultado que deve estar associado à cada uma delas. Como o output dessa função é bastante longo, resolvi usar outra abordagem aqui no post só para dar uma ideia do que existe dentro da resposta: usei a função as_list
do pacote xml2
para parsear o código para uma lista do R. O resultado disso, é uma lista aninhada, começando pelo elemento único items
que, por sua vez, tem outro elemento único chamado item
dentro dela e, finalmente, os elementos que estamos buscando. Para obter esses objetos e extrair o nome delas, usei a função pluck
, seguida da função names
para extrair o nome das tags e table
para contar quantas vezes cada uma aparece. O resultado que obtemos com isso é apresentado abaixo, onde podemos ver quantas vezes cada tag aparece dentro do xml
: algumas aparecem uma única vez, outras aparecem dezenas ou centenas de vezes (i.e., links
, name
e pool
).
xml_do_jogo %>%
# pega o conteudo da response
content() %>%
# coloca o conteudo como uma lista
as_list() %>%
# pega os elementos da lista que estao dentro de item
pluck('items', 'item') %>%
# olha os nomes dos elementos na sublista
names %>%
# contando quantas vezes cada nome aparece
table
.
comments description image link maxplayers
1 1 1 200 1
maxplaytime minage minplayers minplaytime name
1 1 1 1 16
playingtime poll statistics thumbnail yearpublished
1 3 1 1 1
E com isso fechamos a parte da obtenção dos dados da API do BGG, que se mostrou bastante simples: sem autenticação, só usar o enpoint e os métodos que queremos. Por outro lado, veremos a seguir que fazer o parser dessas informações é bem mais trabalhoso.
response
O conteúdo da requisição é bastante heterogêneo, e fazer o parser dele fica mais fácil de ser entendido se pensarmos em quatro tipos de informação: (1) àquelas de cunho geral e que são representadas por uma única quantidade (e.g., idade mínima, tempo de jogo,…) ou facilmente resumidas à uma única informação (e.g., nome do jogo), (2) os comentários associados à cada jogo, (3) as que trazem uma opinião específica da comunidade em torno do jogo e que são representadas por tabelas consolidando o resultado de votações (e.g., idade recomendada, dependência do idioma) e (4) os metadados que trazem detalhes específicos do jogo e que são apresentadas como tabelas contendo cada um destes metadados de forma discriminada (e.g., mecânicas, autores,…). Veremos como fazer o parser de cada informação desta e o que obtemos como resultado em cada caso.
A primeira informação geral que vamos parsear é àquela que está dentro da tag name
. Para isso, precisaremos pegar tudo o que está associado à essa tag usando um xml_find_all
e extrair os atributos de cada elemento com xml_attrs
. O resultado dessa operação é uma lista onde cada elemento é um data.frame
com uma única linha, contendo um nome do jogo e um indicador se esse nome é o oficial (i.e., primary) ou o não-oficial (i.e., alternate). Juntaremos essas linhas usando um bind_rows
seguido de um select
para organizar o resultado. Aplicando essa função ao conteúdo do xml
, obtemos uma tabelinha com todos os nomes do jogo, onde podemos ver que o os nomes não-oficiais são normalmente aqueles em outras línguas. Essa tabela é bastante útil pois, com base nela, podemos fazer um de-para das informações do BGG para àquelas da Ludopedia, o que nos permite responder à algumas perguntas que havíamos aberto anteriormente. De toda forma, se quiséssemos extrair apenas o nome oficial do jogo, bastava usar um filter
pelo valor primary
, seguido de um pull
da coluna título.
## funcao para parsear a lista de nomes do jogo
parser_nome <- function(arquivo_xml) {
# pega o arquivo HTML
arquivo_xml %>%
# extrai todas as tags name
xml_find_all(xpath = '*//name') %>%
# extrai todo os atributos dessas tags
xml_attrs() %>%
# junta todos os atributos em uma tibble
bind_rows() %>%
# remove a coluna sortindex
select(titulo = value, metadado = type)
}
# parseando os nomes
parser_nome(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 16 × 2
titulo metadado
<chr> <chr>
1 Ticket to Ride primary
2 Les Aventuriers du Rail alternate
3 Jízdenky, prosím! alternate
4 Menolippu alternate
5 Ticket to Ride سكة سفر alternate
6 Wsiąść do Pociągu alternate
7 Zug um Zug alternate
8 ¡Aventureros al Tren! alternate
9 Τρενάκια alternate
10 เกมต่อรถไฟ alternate
11 チケット・トゥ・ライド・アメリカ alternate
12 乗車券 alternate
13 鐵道任務 alternate
14 鐵道任務 Ticket to Ride alternate
15 铁路环游 alternate
16 티켓 투 라이드 alternate
A próxima informação que vamos parsear é a descrição do jogo, que é um string contendo toda a estorinha e contexto sobre o jogo. Essa informação está dentro da tag description
, e basta usarmos o xml_text
para pegá-la.
## funcao para parsear a descricao do jogo
parser_descricao <- function(arquivo_xml) {
# pega o arquivo HTML
arquivo_xml %>%
# extrai todas as tags description
xml_find_first(xpath = '*//description') %>%
# extrai o texto da descricao
xml_text()
}
# parseando a descricao
parser_descricao(arquivo_xml = content(x = xml_do_jogo))
[1] "With elegantly simple gameplay, Ticket to Ride can be learned in under 15 minutes. Players collect cards of various types of train cars they then use to claim railway routes in North America. The longer the routes, the more points they earn. Additional points come to those who fulfill Destination Tickets – goal cards that connect distant cities; and to the player who builds the longest continuous route. "The rules are simple enough to write on a train ticket – each turn you either draw more cards, claim a route, or get additional Destination Tickets," says Ticket to Ride author, Alan R. Moon. "The tension comes from being forced to balance greed – adding more cards to your hand, and fear – losing a critical route to a competitor." Ticket to Ride continues in the tradition of Days of Wonder's big format board games featuring high-quality illustrations and components including: an oversize board map of North America, 225 custom-molded train cars, 144 illustrated cards, and wooden scoring markers. Since its introduction and numerous subsequent awards, Ticket to Ride has become the BoardGameGeek epitome of a "gateway game" -- simple enough to be taught in a few minutes, and with enough action and tension to keep new players involved and in the game for the duration. Part of the Ticket to Ride series. "
O próximo parser estrutura diversas informações relacionadas ao ano de publicação do jogo, quantidade de jogadores, tempo de jogo e idade mínima. Todas essas informações estão como tags soltas dentro do xml
, portanto tive que usar o self
dentro do xpath para pegar cada uma delas e colocar dentro do mesmo resultado. Entretanto, uma vez que conseguimos extrair essas informações, usamos um map_dfr
para as colocarmos como um tibble
e, depois, um pivot_wider
para organizá-las entre colunas.
## funcao para parsear as informacoes do jogo
parser_informacoes <- function(arquivo_xml) {
# pega o arquivo HTML
arquivo_xml %>%
# extrai todas as tags relacionadas às informações sobre o ano de publicacao, quantidade de jogadores
# idade minima para o jogo e tempo de jogo
xml_find_all(xpath = '*//*[self::yearpublished or self::minplayers or self::maxplayers
or self::playingtime or self::minplaytime or self::maxplaytime or self::minage]') %>%
# colocando todos os atributos dessa tag em um tibble
map_dfr(
~ list(
caracteristica = xml_name(.x),
valor = xml_attrs(.x, 'value')
)) %>%
# parseando tudo para numerico
mutate(valor = parse_number(x = valor)) %>%
# passando o tibble do formato longo para o largo
pivot_wider(names_from = caracteristica, values_from = valor)
}
# parseando as informacoes
parser_informacoes(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 1 × 7
yearpublished minplayers maxplayers playingtime minplaytime
<dbl> <dbl> <dbl> <dbl> <dbl>
1 2004 2 5 60 30
# … with 2 more variables: maxplaytime <dbl>, minage <dbl>
Quando passamos o método stats=1
para o endpoint também coletamos as informações sobre as estatísticas relacionadas ao ranking do jogo que escolhemos. Com isso vamos ter acesso: (a) às informações relativas às notas e (b) àquelas diretamente relacionadas aos rankings em que cada jogo aparece. A função abaixo dá conta de parsear o primeiro destes, nos dando acesso à quantidade de votos que cada jogo recebeu, a nota média (arimética e bayesiana) e outras informações relacionadas ao interesse dos usuários pelo jogo.
## funcao para parsear todas as informacoes relacionadas à avaliação de cada jogo
parser_avaliacoes <- function(arquivo_xml) {
# extrai outras informacoes das avaliacoes e junta com as informacoes de rankings e contagem de comentarios
arquivo_xml %>%
# extrai todas as tags relacionadas dentro das avaliacoes que nao estejam relacionadas ao rankeamento
xml_find_all(xpath = '*/statistics/ratings/*[not(self::ranks)]') %>%
# coloca todas as informacoes dentro de um tibble
map_dfr(
~ list(
estatistica = xml_name(.x),
valor = xml_attrs(.x, 'value')
)
) %>%
# parseando tudo para numerico
mutate(valor = parse_number(x = valor)) %>%
# passando o tibble do formato longo para o largo
pivot_wider(names_from = estatistica, values_from = valor)
}
# parseando as avaliacoes
parser_avaliacoes(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 1 × 12
usersrated average bayesaverage stddev median owned trading wanting
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 76598 7.41 7.30 1.30 0 106496 941 693
# … with 4 more variables: wishing <dbl>, numcomments <dbl>,
# numweights <dbl>, averageweight <dbl>
Já a função abaixo parseia os rankings em que cada jogo aparece. Além do ranking geral, cada jogo também pode estar posicionado dentro da família de jogos à que pertence e/ou aos tipos de mecânica associados a ele. No nosso exemplo, podemos ver que o Ticket to Ride ocupa a 193º posição do ranking geral e a 40º posição quando o assunto são os jogos familiares.
## funcao para parsear todas as informacoes relacionadas aos rankings em que cada jogo esta
parser_rankings <- function(arquivo_xml) {
# pega o arquivo html
arquivo_xml %>%
# extrai todas as tags que estejam relacionadas ao ranking
xml_find_all(xpath = '*/statistics/ratings/ranks/rank') %>%
# extrai todo os atributos dessas tags
xml_attrs('value') %>%
# junta todos os atributos em uma tibble
bind_rows() %>%
# renomeando colunas
rename(nivel = type, tipo = name, nome = friendlyname,
posicao = value, media_bayesiana = bayesaverage) %>%
# parseando os numericos para tal
mutate(
posicao = parse_number(x = posicao),
media_bayesiana = parse_number(x = media_bayesiana)
)
}
# parseando os rankings
parser_rankings(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 2 × 6
nivel id tipo nome posicao media_bayesiana
<chr> <chr> <chr> <chr> <dbl> <dbl>
1 subtype 1 boardgame Board Game Rank 193 7.30
2 family 5499 familygames Family Game Rank 41 7.30
O método ratingcomments=1
faz com que tenhamos acesso aos comentários e notas individuais associadas à cada jogo. Essas informações estão dentro da tag comment
, aninhada em comments
, e podemos usar a função abaixo para extrair os 100 comentários que virão junto do xml
. O resultado dela é um tibble
onde temos uma coluna com o identificador único, a nota dada ao jogo e o comentário feito por um dado usuário. Esse identificador do usuário pode até ser usado em outros endpoints disponíveis na API, como aquele que nos dá acesso às coleções.
## funcao para parsear os comentarios sobre o jogo
parser_comentarios <- function(arquivo_xml) {
# pega o arquivo HTML
arquivo_xml %>%
# extrai todas as tags de comentario
xml_find_all(xpath = '*/comments/comment') %>%
# extrai todo os atributos dessas tags
xml_attrs() %>%
# junta todos os atributos em uma tibble
bind_rows() %>%
# renomeando as colunas
rename(usuario = username, nota = rating, comentario = value) %>%
# parseando as notas para numerico
mutate(nota = parse_number(x = nota))
}
# parseando os comentarios
parser_comentarios(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 100 × 3
usuario nota comentario
<chr> <dbl> <chr>
1 Steffen S. 10 ""
2 harry the horse 10 ""
3 zapator 10 "Very accessible for all kind of gamers. A f…
4 madouc 10 "TThis is the game of the year ! 5 good re…
5 southj95 10 "Excellent Game! Very easy to teach (took li…
6 Jabberwock 10 ""
7 Skyjack 10 ""
8 Sunfox 10 "Just as good as the comments say it is! Eve…
9 SteffenS 10 ""
10 Babluit 10 ""
# … with 90 more rows
Um ponto importante é que estamos limitados à obter um máximo de 100 comentários por requisição à API (usamos o método pagesize=100
para garantir que isso vá acontecer). Desta forma, se quisermos pegar os comentários de 101 à 200 (e assim sucessivamente), devemos correr entre as ‘páginas’ dos comentários usando o método page=numero
(onde número
é o número da página). No entanto, se fossemos fazer isso precisávamos ter noção de quantos comentários existem para poder chegar ao número máximo de páginas que podermos passar para o método. É possível extrairmos estas informações olhando diretamente a tag comments
, e extraindo o atributo totalitems
. Assim, para obtermos todos os comentários para esse jogo, bastaria dividirmos essa quantidade por 100, e iterar da página 1 até este máximo usando o método page
junto do endpoint.
## funcao para parsear a quantidade total de comentarios que um jogo tem
parser_comentarios_total <- function(arquivo_xml) {
# pega o arquivo html
arquivo_xml %>%
# pega tudo o que está sobre a tag comments
xml_find_all(xpath = '*/comments') %>%
# pega apenas o valor correspondente ao total de comentarios
xml_attr('totalitems') %>%
# parseando o string para numero
parse_number()
}
# parseando a descricao
parser_comentarios_total(arquivo_xml = content(x = xml_do_jogo))
[1] 76974
Existem três tags com o nome pool
dentro do xml
, e as funções a seguir tratam de fazer o parser deste conteúdo. Essas tags nada mais são do que os resultados de votações abertas sobre o número de jogadores, idade sugerida e dependência do idioma, nas quais os usuários do portal do BGG puderam opinar em torno desses três grupos de informação.
O parser abaixo pega os resultados da votação relacionada ao número de jogadores, retornando as informações sobre a melhor e a pior quantidade de jogadores, além da quantidade recomendada per se. Se quiséssemos colocar essas informações em suas próprias colunas, faria bastante sentido agrupar a tabela abaixo pela coluna voto
e pegar o num_jogadores
que tivesse a maior quantidade de votos usando um slice_max
com n = 1
, passando o resultado disso para um pivot_wider
depois. Não mostro como fazer isso aqui, mas seria uma opção viável para resumirmos essas informações em torno dessas três recomendações (i.e. a pior quantidade de jogadores, a melhor quantidade de jogadores, e a quantidade recomendada de jogadores).
## funcao para parsear os resultados da votacao do melhor numero de jogadores para se jogar
parser_votacao_n_jogadores <- function(arquivo_xml) {
# pega o arquivo html
arquivo_xml %>%
# extrai todas as tags com os resultados da votação relacionada ao melhor numero de jogadores
xml_find_all(xpath = '*/poll[@name="suggested_numplayers"]/results') %>%
# coloca tudo dentro de um tibble
map_dfr(
~ xml_find_all(.x, xpath = 'result') %>%
xml_attrs() %>%
bind_rows() %>%
mutate(numplayers = xml_attrs(.x))
) %>%
# renomeia e organiza as colunas
select(num_jogadores = numplayers, voto = value, num_votos = numvotes) %>%
# parseando votos para numerico
mutate(num_votos = parse_number(x = num_votos))
}
# parseando o resultado da votacao da melhor quantidade de jogadores
parser_votacao_n_jogadores(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 18 × 3
num_jogadores voto num_votos
<chr> <chr> <dbl>
1 1 Best 1
2 1 Recommended 7
3 1 Not Recommended 620
4 2 Best 73
5 2 Recommended 504
6 2 Not Recommended 224
7 3 Best 244
8 3 Recommended 545
9 3 Not Recommended 41
10 4 Best 634
11 4 Recommended 231
12 4 Not Recommended 13
13 5 Best 293
14 5 Recommended 434
15 5 Not Recommended 54
16 5+ Best 6
17 5+ Recommended 20
18 5+ Not Recommended 457
O parser seguinte olha o resultado da votação sobre a idade mínima sugerida para o jogo. Novamente, se quiséssemos resumir as informações dessa tabela à uma única linha (i.e., qual a idade recomendada pela comunidade para o jogo), bastaria que usássemos um filter
para reter a linha que tivesse a maior quantidade de votos (i.e., num_votos == max(num_votos)
) e, então, usar um pull
na coluna idade_ideal
.
## funcao para parsear os resultados da votacao da idade recomendada para o jogo
parser_votacao_idade <- function(arquivo_xml) {
# pega o arquivo html
arquivo_xml %>%
# extrai todas as tags com os resultados da votação relacionada à idade recomendada para o jogo
xml_find_all(xpath = '*/poll[@name="suggested_playerage"]/results') %>%
# coloca tudo dentro de um tibble
map_dfr(
~ xml_find_all(.x, xpath = 'result') %>%
xml_attrs()
) %>%
# renomeia e organiza as colunas
select(idade_ideal = value, num_votos = numvotes) %>%
# parseando votos para numerico
mutate(num_votos = parse_number(x = num_votos))
}
# parseando o resultado da votacao da melhor idade para o jogo
parser_votacao_idade(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 12 × 2
idade_ideal num_votos
<chr> <dbl>
1 2 0
2 3 0
3 4 3
4 5 5
5 6 67
6 8 173
7 10 40
8 12 4
9 14 2
10 16 0
11 18 1
12 21 and up 0
Finalmente, o último parser relacionado às votações fala da dependência do idioma para o jogo. Essa informação indica o quanto dependemos de entender o que está escrito no livro de regras, cartas e etc de forma a conseguir jogar. O resultado desse parser é mais uma vez uma tabelinha e, para extrair a recomendação da comunidade, bastaria que repetimos o processo descrito acima, mas usando um pull
na coluna voto
.
## funcao para parsear os resultados da votacao sobre a dependencia do idioma para jogar o jogo
parser_votacao_idioma <- function(arquivo_xml) {
# pega o arquivo html
arquivo_xml %>%
# extrai todas as tags com os resultados da votacao sobre a dependencia do idioma para jogar o jogo
xml_find_all(xpath = '*/poll[@name="language_dependence"]/results') %>%
# coloca tudo dentro de um tibble
map_dfr(
~ xml_find_all(.x, xpath = 'result') %>%
xml_attrs()
) %>%
# renomeia e organiza as colunas
select(voto = value, num_votos = numvotes) %>%
# parseando votos para numerico
mutate(num_votos = parse_number(x = num_votos))
}
# parseando o resultado da votacao da dependencia do idioma
parser_votacao_idioma(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 5 × 2
voto num_votos
<chr> <dbl>
1 No necessary in-game text 182
2 Some necessary text - easily memorized or small crib sheet 26
3 Moderate in-game text - needs crib sheet or paste ups 1
4 Extensive use of text - massive conversion needed to be p… 0
5 Unplayable in another language 1
Para fechar essa seção, acho importante ressaltar que o processo que sugeri fazer aqui resume as informações em cada uma dessas três tabelas em torno de uma única quantidade - i.e., a informação com mais votos. No entanto, acredito ser possível criar algum tipo de embedding ou feature a partir das informações em cada uma dessas tabelas, que descreva de que forma a opinião dos usuários variou para aquele jogo. Isto talvez seja interessante para fazer uma caracterização mais refinada dos mesmos.
O parser que deixei para o final é aquele relacionado aos metadados do jogo, que estavam dentro da tag link
. Se você puder lembrar, essa é àquela tag que tinha umas 200 ocorrências no xml
e, assim, já podemos imaginar a quantidade de informação que existe nela. De fato, quando parseamos essas informações através da função abaixo, vemos que existe uma diversidade de informações sobre cada jogo. Como o resultado dessa operação são várias list-columns
dentro de um tibble
, vamos precisar de mais um pouquinho de trabalho para deixar esse dado organizado.
## funcao para parsear os metadados do jogo
parser_metadados <- function(arquivo_xml) {
# pega o arquivo HTML
arquivo_xml %>%
# extrai todas as tags link
xml_find_all(xpath = '*//link') %>%
# extrai todo os atributos dessas tags
xml_attrs() %>%
# junta todos os atributos em uma tibble
bind_rows() %>%
# organiza as colunas
select(id_metadado = id, metadado = value, tipo_metadado = type) %>%
# removendo o padrao boardgame do tipo de metadado
mutate(
tipo_metadado = str_replace(string = tipo_metadado,
pattern = 'boardgame', replacement = 'tbl_')
) %>%
# aninhando informacoes pelo tipo de metadado
nest(data = -tipo_metadado) %>%
# passando o dado para o formato largo
pivot_wider(names_from = tipo_metadado, values_from = data)
}
# parseando os metadados
parser_metadados(arquivo_xml = content(x = xml_do_jogo))
# A tibble: 1 × 9
tbl_category tbl_mechanic tbl_family tbl_expansion tbl_compilation
<list> <list> <list> <list> <list>
1 <tibble [1 × 2]> <tibble [7 … <tibble [… <tibble [155… <tibble [1 × 2…
# … with 4 more variables: tbl_implementation <list>,
# tbl_designer <list>, tbl_artist <list>, tbl_publisher <list>
O resultado que obtivemos no parser dos metadados não é muito útil pois ele não está em um formato tidy
. Apesar de termos uma informação diferente por coluna, ela está totalmente colapsada dentro de tibbles
, o que dificulta (mas não impede) realizarmos muitas operações que seriam úteis para entender melhor os dados. Como a informação dentro de cada uma das list-columns
deve ter uma estrutura diferente das demais, vamos começar tratando esse dado passando a base do formato largo para o formato longo, usando a função pivot_longer
. A partir daí, vamos utilizar a função split
para quebrar o tibble
resultante em uma lista de tibbles
por tipo de informação, desaninhando eles na sequência usando um unnest
- eu descrevo o que são cada uma dessas informações e todas as demais no final desse post.
## expandindo cada uma das tabelas que contem multiplas informacoes sobre cada jogo
tabelas <- parser_metadados(arquivo_xml = content(x = xml_do_jogo)) %>%
# passando a base para o formato longo
pivot_longer(cols = everything(), names_to = 'tabela', values_to = 'dados') %>%
# separando a base em listas de acordo com a dimensao
split(.$tabela) %>%
# desaninhando cada tabela
map(.f = unnest, cols = dados) %>%
# dropando a coluna do id da tabela
map(.f = select, -tabela) %>%
# ordenando as tabelas por jogo em ordem alfabetica do metadado
map(.f = arrange, metadado)
tabelas
$tbl_artist
# A tibble: 2 × 2
id_metadado metadado
<chr> <chr>
1 12519 Cyrille Daujean
2 11886 Julien Delval
$tbl_category
# A tibble: 1 × 2
id_metadado metadado
<chr> <chr>
1 1034 Trains
$tbl_compilation
# A tibble: 1 × 2
id_metadado metadado
<chr> <chr>
1 160069 Ticket to Ride: 10th Anniversary
$tbl_designer
# A tibble: 1 × 2
id_metadado metadado
<chr> <chr>
1 9 Alan R. Moon
$tbl_expansion
# A tibble: 155 × 2
id_metadado metadado
<chr> <chr>
1 185197 Alaska (fan expansion for Ticket to Ride)
2 184013 Alice in Wonderland (fan expansion for Ticket to Ride)
3 120037 Alsace (fan expansion for Ticket to Ride)
4 189445 Ancient Greece (fan expansion for Ticket to Ride)
5 185198 Ancient Sicily (fan expansion for Ticket to Ride)
6 197040 Antarctica (fan expansion for Ticket to Ride)
7 252142 Antarctica (fan expansion for Ticket to Ride)
8 223979 Barsoom (fan expansion for Ticket to Ride)
9 211230 Belarus (fan expansion for Ticket to Ride)
10 289366 Biblical Israel (fan expansion for Ticket to Ride)
# … with 145 more rows
$tbl_family
# A tibble: 4 × 2
id_metadado metadado
<chr> <chr>
1 64960 Components: Map (Continental / National scale)
2 61646 Continents: North America
3 14835 Country: USA
4 17 Game: Ticket to Ride (Official)
$tbl_implementation
# A tibble: 12 × 2
id_metadado metadado
<chr> <chr>
1 258140 Les Aventuriers du Rail Express
2 244525 Ticket to Ride Demo
3 309113 Ticket to Ride: Amsterdam
4 14996 Ticket to Ride: Europe
5 205125 Ticket to Ride: First Journey (U.S.)
6 225244 Ticket to Ride: Germany
7 276894 Ticket to Ride: London
8 21348 Ticket to Ride: Märklin
9 253284 Ticket to Ride: New York
10 31627 Ticket to Ride: Nordic Countries
11 202670 Ticket to Ride: Rails & Sails
12 34127 Ticket to Ride: The Card Game
$tbl_mechanic
# A tibble: 7 × 2
id_metadado metadado
<chr> <chr>
1 2041 Card Drafting
2 2912 Contracts
3 2875 End Game Bonuses
4 2040 Hand Management
5 2081 Network and Route Building
6 2661 Push Your Luck
7 2004 Set Collection
$tbl_publisher
# A tibble: 17 × 2
id_metadado metadado
<chr> <chr>
1 23043 ADC Blackfire Entertainment
2 47848 Asmodee China
3 934 Bandai
4 6784 Bergsala Enigma (Enigma)
5 34501 Boardgame Space
6 1027 Days of Wonder
7 2973 Edge Entertainment
8 15605 Galápagos Jogos
9 5530 Giochi Uniti
10 8439 Happy Baobab
11 18852 Hobby World
12 8291 Korea Boardgames Co., Ltd.
13 3218 Lautapelit.fi
14 11107 Nordic Games GmbH
15 7466 Rebel Sp. z o.o.
16 33998 Siam Board Games
17 9234 Swan Panasia Co., Ltd.
Como podemos ver, todas essas informações já são tidy
por si só, pois temos uma informação diferente por coluna e cada observação em uma linha distinta. Se o nosso objetivo for a manipulação e visualização de dados, então salvar cada uma dessas tabelas no formato em que estão já estaria ok. Por outro lado, se quisermos juntar essas informações para colocar todas as informações sobre um determinado jogo em uma única linha, usar elas para fazer um join
acabaria replicando todas as informações em muitas linhas, o que não é desejável. Nesse contexto, teríamos duas opções para prevenir que isto ocorra, novamente dependendo do objetivo final que formos dar aos dados:
tibble
e juntar tudo em uma tabela só. Para isso, podemos usar um paste0
dentro de um summarise
para colapsar as informações de cada linha em um string só, seguido de um bind_rows
para juntar cada tibble
resultante linha a linha e, finalmente, um pivot_wider
para colocar cada informação em uma coluna diferente. Para fins ilustrativos, apresento este tratamento abaixo.## colocando as tabelas no formato tidy
tabelas_tidy <- tabelas %>%
# sumarizando todas as informacoes de forma a termos uma linha por jogo
map(.f = summarise, metadado = paste0(unique(metadado), collapse = ';')) %>%
# colocando tudo em uma unica tabela
bind_rows(.id = 'informacao') %>%
# removendo o prefixo tbl
mutate(informacao = str_remove(string = informacao, pattern = 'tbl_')) %>%
# passando a base para o formato largo
pivot_wider(names_from = informacao, values_from = metadado)
tabelas_tidy
# A tibble: 1 × 9
artist category compilation designer expansion family implementation
<chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 Cyril… Trains Ticket to … Alan R.… Alaska (… Compo… Les Aventurie…
# … with 2 more variables: mechanic <chr>, publisher <chr>
Pronto! Já temos uma visão de como colocar as informações dos metadados em um único tibble
no formato tidy
, seja para consolidar tudo em uma tabela só ou para a análise de dados. Vamos fechar esse post criando uma função para consolidar todo o parser.
Todas as funções que criamos para parsear as informações recebem como input o xml
do jogo e retornam um tibble
, uma lista de tibbles
ou um vetor com um único elemento. Portanto, vou criar uma única função para fazer o parser chamando todas essas outras funções que criamos, mas aproveitando para carregar o arquivo xml
que queremos parsear dentro dela mesmo, facilitando a nossa vida. Com isso, essa função receberá como argumento apenas o path para o arquivo xml
que foi salvo.
# funcao para parsear o arquivo xml inteiro do jogo
parser_do_jogo <- function(path_arquivo_xml) {
## lendo o arquivo xml
xml_do_jogo <- read_xml(x = path_arquivo_xml)
## parseando o arquivo xml
tibble(
tbl_nomes = list(parser_nome(arquivo_xml = xml_do_jogo)),
tbl_comentarios = list(parser_comentarios(arquivo_xml = xml_do_jogo)),
tbl_rankings = list(parser_rankings(arquivo_xml = xml_do_jogo)),
tbl_votacao_idade = list(parser_votacao_idade(arquivo_xml = xml_do_jogo)),
tbl_votacao_idioma = list(parser_votacao_idioma(arquivo_xml = xml_do_jogo)),
tbl_votacao_jogadores = list(parser_votacao_n_jogadores(arquivo_xml = xml_do_jogo))
) %>%
# juntando informacoes que ja estao no formato esperado
bind_cols(
parser_metadados(arquivo_xml = xml_do_jogo),
descricao = parser_descricao(arquivo_xml = xml_do_jogo),
total_comentarios = parser_comentarios_total(arquivo_xml = xml_do_jogo),
parser_avaliacoes(arquivo_xml = xml_do_jogo),
parser_informacoes(arquivo_xml = xml_do_jogo)
)
}
## fazendo o parser do jogo
jogos <- parser_do_jogo(path_arquivo_xml = 'temporario/00009209.html')
## olhando a tabela
jogos
# A tibble: 1 × 36
tbl_nomes tbl_comentarios tbl_rankings tbl_votacao_ida…
<list> <list> <list> <list>
1 <tibble [16 × 2]> <tibble [100 × 3]> <tibble [2 × 6]> <tibble [12 × 2…
# … with 32 more variables: tbl_votacao_idioma <list>,
# tbl_votacao_jogadores <list>, tbl_category <list>,
# tbl_mechanic <list>, tbl_family <list>, tbl_expansion <list>,
# tbl_compilation <list>, tbl_implementation <list>,
# tbl_designer <list>, tbl_artist <list>, tbl_publisher <list>,
# descricao <chr>, total_comentarios <dbl>, usersrated <dbl>,
# average <dbl>, bayesaverage <dbl>, stddev <dbl>, median <dbl>, …
Como podemos ver, temos todas as informações que já cobrimos até aqui nessa tabela, inclusive àquelas list-columns
. Poderíamos salvar essa tabela como está, ou descartar àquelas list-columns
e usar àquelas que preparamos acima, criar novas colunas ou fazer qualquer coisa que nos ocorrer - mas acredito que qualquer decisão daqui para a frente depende muito de como esses dados seriam usados. Deixo todas essas opções em aberto por aqui, mas mostrarei como fazer algumas delas em posts futuros, quando pretendo usar esses dados para gerar algumas visualizações, dashboards e análises. Um ponto importante a se notar é que temos apenas um jogo neste exemplo e, portanto, essa tabela acima tem uma só linha. Todavia, caso tivéssemos mais de um, jogo teríamos tantas linhas quanto fossem os jogos, e ainda poderíamos colocar uma coluna com o identificador numérico do jogo para referência.
Eu mostro como generalizar essa função para vários jogos através do o código que acompanha este post, acessível por esse link.
Como pretendo usar esses dados em mais de um post, vou deixar aqui um pequeno dicionário para explicar as tabelas e o que temos de informação em cada uma delas. Vou usar como referência as informações que estão dentro do objeto jogos
, uma vez que o que vier dele vai ser o output padrão do parser que criamos.
titulo
: nomes que o jogo pode assumir, dependendo da língua;metadado
: indicam se o nome correspondente é aquele oficial (primary
) ou se é uma variação dada em outra língua (alternate
).usuario
: nome do usuário que fez o comentário;nota
: nota dada pelo usuário ao jogo;comentario
: comentário feito pelo usuário sobre o jogo.nivel
: nível geral do ranking avaliado;id
: identificador numérico único do tipo de ranking;tipo
: tipo de ranking analisado;nome
: nome do ranking;posicao
: posição no ranking em questão;media_bayesiana
: média bayesiana da nota do jogo.idade_ideal
: categoria de idade votada pelos usuários;num_votos
: quantidade de votos.voto
: categoria do grau de dependência do idioma votada pelos usuários;num_votos
: quantidade de votos.num_jogadores
: categoria da quantidade de jogadores votada pelos usuários;voto
: categorias existentes associadas à pior, à melhor e à quantidade recomendada de jogadores;num_votos
: quantidade de votos.id_metadado
: identificador numérico único da temática do jogo;metadado
: nome da temática associada ao jogo.id_metadado
: identificador numérico único da mecânica;metadado
: nome da mecânica de jogo.id_metadado
: identificador numérico único da família;metadado
: nome da família associada ao jogo.id_metadado
: identificador numérico único da expansão do jogo;metadado
: nome da expansão do jogo.id_metadado
: identificador numérico único da edição especial do jogo;metadado
: nome da edição especial do jogo.id_metadado
: identificador numérico único da implementação do jogo;metadado
: nome da variante que faz a implementação do jogo base.id_metadado
: identificador numérico único da pessoa autora do jogo;metadado
: nome da pessoa do autora do jogo.id_metadado
: identificador numérico único da pessoa artista responsável pela arte jogo;metadado
: nome da pessoa artista responsável pela arte jogo.id_metadado
: identificador numérico único da editora responsável por publicar o jogo;metadado
: nome da editora responsável por publicar o jogo.A ideia deste post foi criar o scrapper das informações dos jogos de tabuleiro disponíveis no portal do BoardGameGeek, fazendo uso de sua API XML para isso. Essa API é bastante simples e fácil de usar, agilizando o trabalho de obtenção dos dados, apesar das dificuldades impostas para o seu processamento e organização. Neste intuito, desenvolvemos uma série de funções para fazer a requisição destes dados e parseá-los, de forma a chegarmos em uma estrutura de dados tal que tenhamos uma linha de um data.frame
/tibble
com as informações de cada jogo. A partir desta estrutura podemos então pensar em extensões do processamento de dados que melhor atendam aos objetivos de visualização e análise de dados, algo que acabaremos focando em posts futuros. Neste contexto, acredito que esse post ficará mais como uma referência futura sobre o método de obtenção dos dados do que qualquer outra coisa em si.
Um ponto importante é que resolvi explorar neste post apenas o endpoint com as informações sobre cada jogo, e nem toquei nos demais. Assim, algumas outras possibilidades permanecem inexploradas (e.g., raspar as informações sobre as coleções de cada usuário), mas podem se provar úteis para outras finalidades relacionadas ao aprendizado.
A obtenção desses dados é um passo bastante importante para seguirmos em frente com diversas questões que já foram colocadas em outros posts, e muitas outras que podemos começar a pensar agora que sabemos que dados que temos em mãos:
Dúvidas, sugestões ou críticas? É só me procurar pelo e-mail ou GitHub!
A solução aqui seria basicamente um pivot_wider
em cada tibble
, seguida de um bind_cols
entre elas e delas com todas as demais informações.↩︎
If you see mistakes or want to suggest changes, please create an issue on the source repository.
Text and figures are licensed under Creative Commons Attribution CC BY 4.0. Source code is available at https://github.com/nacmarino/codex/, unless otherwise noted. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".
For attribution, please cite this work as
Marino (2022, Jan. 23). Codex: Interagindo com a API XML do BoardGameGeek. Retrieved from https://nacmarino.github.io/codex/posts/2022-01-23-interagindo-com-a-api-do-boardgamegeek/
BibTeX citation
@misc{marino2022interagindo, author = {Marino, Nicholas}, title = {Codex: Interagindo com a API XML do BoardGameGeek}, url = {https://nacmarino.github.io/codex/posts/2022-01-23-interagindo-com-a-api-do-boardgamegeek/}, year = {2022} }