
Nem sempre dispomos de APIs REST públicas – ou até mesmo privadas – para o consumo de informações relativas a um tópico específico. Uma prática comum para superar tal barreira consiste na construção de processos para a extração automatizada de dados de páginas Web, prática esta conhecida como Web Scrapping.
Neste novo artigo, demonstro como implementar uma aplicação .NET Core 2.0 para a coleta de dados, fazendo uso do Selenium WebDriver e do MongoDB. O exemplo em questão permitirá a obtenção da classificação atualizada da NBA, a liga de basquete norte-americana.
E aproveito este artigo para deixar aqui um convite.
Dia 19/03/2018 (segunda-feira) às 22h00 – horário de Brasília – teremos mais um hangout no Canal .NET. O assunto desta vez será o uso do Docker Compose como solução para simplificar o deployment de aplicações. A transmissão acontecerá via YouTube em um link a ser disponibilizado em breve.
Para efetuar a sua inscrição acesse a página do evento no Facebook ou então o Meetup. A transmissão acontecerá via YouTube, em um link a ser divulgado em breve.
Origem dos dados
A seguir está o endereço da página utilizada para a extração de dados:
Nas próximas imagens estão destacados em vermelho os itens que contêm as informações a serem coletadas:


Os elementos HTML correspondentes foram indicados nos trechos de código apresentados nas próximas seções.
Implementando a aplicação para extração de dados
Para a coleta dos dados com a classificação da NBA, será criada uma Console Application baseada no .NET Core:

Será necessário incluir neste projeto os seguintes packages:
- Microsoft.Extensions.Configuration.Json e Microsoft.Extensions.Options.ConfigurationExtensions, ambos empregados na manipulação de configurações definidas no arquivo appsettings.json;
- MongoDB.Driver para a gravação dos dados extraídos em um banco do MongoDB;
- Selenium.WebDriver: solução que possibilitará o carregamento da página-alvo e a manipulação de seus diferentes elementos HTML.

A listagem a seguir traz o conteúdo do arquivo appsettings.json, já considerando as configurações para uso do MongoDB e do Selenium WebDriver (este último em conjunto com o Mozilla Firefox):
{
"ConnectionStrings": {
"BaseNBA": "mongodb://localhost:27017"
},
"SeleniumConfigurations": {
"CaminhoDriverFirefox": "C:\\Selenium\\FirefoxDriver\\",
"UrlPaginaClassificacaoNBA": "http://www.espn.com.br/nba/classificacao",
"Timeout": 60
}
}
O tipo SeleniumConfigurations será utilizado para carregar configurações de uso do Selenium Web Driver e que foram especificadas no arquivo appsettings.json (caminho do driver do Firefox, URL da página que servirá de base para a extração e timeout em segundos para carregamento de tal HTML):
namespace CargaDadosNBA
{
public class SeleniumConfigurations
{
public string CaminhoDriverFirefox { get; set; }
public string UrlPaginaClassificacaoNBA { get; set; }
public int Timeout { get; set; }
}
}
Já na próxima listagem, estão as implementações das classes Conferencia e Equipe:
- O tipo Conferencia corresponde à estrutura de cada documento a ser armazenado na base de dados (incluindo toda a classificação por conferência/região). A propriedade _id é controlada pelo MongoDB, dispensando assim os desenvolvedores da necessidade de preenchimento da mesma;
- A classe Equipe contém a identificação, a classificação e estatísticas de um time, com suas instâncias estando vinculadas à propriedade Equipes em Conferência.
using System;
using System.Collections.Generic;
using MongoDB.Bson;
namespace CargaDadosNBA
{
public class Conferencia
{
public ObjectId _id { get; set; }
public string Temporada { get; set; }
public string Nome { get; set; }
public DateTime DataCarga { get; set; }
public List<Equipe> Equipes { get; set; } = new List<Equipe>();
}
public class Equipe
{
public int Posicao { get; set; }
public string Nome { get; set; }
public int Vitorias { get; set; }
public int Derrotas { get; set; }
public string PercentualVitorias { get; set; }
}
}
O tipo PaginaClassificacao é uma implementação baseada no pattern Page Object, fazendo uso de um objeto baseado na interface IWebDriver (namespace OpenQA.Selenium) para a execução do Firefox em modo headless (sem a abertura de janelas). A página contendo a classificação atualizada da NBA será carregada por meio desta estrutura que emprega o Selenium WebDriver, com a mesma extraindo os dados necessários (método ObterClassificacao) para a carga posterior em uma base do MongoDB:
using System;
using System.Linq;
using System.Collections.Generic;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
namespace CargaDadosNBA
{
public class PaginaClassificacao
{
private SeleniumConfigurations _configurations;
private IWebDriver _driver;
public PaginaClassificacao(SeleniumConfigurations configurations)
{
_configurations = configurations;
FirefoxOptions options = new FirefoxOptions();
options.AddArgument("--headless");
_driver = new FirefoxDriver(
_configurations.CaminhoDriverFirefox,
options);
}
public void CarregarPagina()
{
_driver.Manage().Timeouts().PageLoad =
TimeSpan.FromSeconds(_configurations.Timeout);
_driver.Navigate().GoToUrl(
_configurations.UrlPaginaClassificacaoNBA);
}
public List<Conferencia> ObterClassificacao()
{
DateTime dataCarga = DateTime.Now;
List<Conferencia> conferencias = new List<Conferencia>();
string temporada = _driver
.FindElement(By.ClassName("automated-header"))
.FindElement(By.TagName("h1"))
.Text.Split(new char[] { ' ' }).Last();
var dadosConferencias = _driver
.FindElements(By.ClassName("responsive-table-wrap"));
var captions = _driver
.FindElements(By.ClassName("table-caption"));
for (int i = 0; i < captions.Count; i++)
{
var caption = captions[i];
Conferencia conferencia = new Conferencia();
conferencia.Temporada = temporada;
conferencia.DataCarga = dataCarga;
conferencia.Nome =
caption.FindElement(By.ClassName("long-caption")).Text;
conferencias.Add(conferencia);
int posicao = 0;
var conf = dadosConferencias[i];
var dadosEquipes = conf.FindElement(By.TagName("tbody"))
.FindElements(By.TagName("tr"));
foreach (var dadosEquipe in dadosEquipes)
{
var estatisticasEquipe =
dadosEquipe.FindElements(By.TagName("td"));
posicao++;
Equipe equipe = new Equipe();
equipe.Posicao = posicao;
equipe.Nome =
estatisticasEquipe[0].FindElement(
By.ClassName("team-names")).GetAttribute("innerHTML");
equipe.Vitorias = Convert.ToInt32(
estatisticasEquipe[1].Text);
equipe.Derrotas = Convert.ToInt32(
estatisticasEquipe[2].Text);
equipe.PercentualVitorias =
estatisticasEquipe[3].Text;
conferencia.Equipes.Add(equipe);
}
}
return conferencias;
}
public void Fechar()
{
_driver.Quit();
_driver = null;
}
}
}
Maiores detalhes sobre recursos do Selenium WebDriver podem ser encontrados nos seguintes artigos:
- Testando aplicações Web com Selenium WebDriver, .NET Core 2.0, .NET Standard 2.0 e xUnit
- .NET Core 2.0 + Selenium WebDriver: testes em modo headless com Firefox e Chrome
A interação com o MongoDB acontecerá através da classe ClassificacaoRepository, a qual será responsável pelo carregamento das classificações relativas às duas conferências da NBA (Leste e Oeste):
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
namespace CargaDadosNBA
{
public class ClassificacaoRepository
{
private MongoClient _client;
private IMongoDatabase _db;
public ClassificacaoRepository(
IConfiguration configuration)
{
_client = new MongoClient(
configuration.GetConnectionString("BaseNBA"));
_db = _client.GetDatabase("NBA");
}
public void Incluir(List<Conferencia> conferencias)
{
_db.DropCollection("Classificacao");
var classificacaoNBA =
_db.GetCollection<Conferencia>("Classificacao");
classificacaoNBA.InsertMany(conferencias);
}
}
}
Para informações complementares sobre o uso de MongoDB com o .NET Core 2.0 acesse:
Por fim, a classe Program utilizará as estruturas definidas anteriormente para a extração de dados (classe PaginaClassificacao) e carga do resultado obtido (classe ClassificacaoRepository) na base de dados do MongoDB:
using System;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace CargaDadosNBA
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Carregando configurações...");
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(quot;appsettings.json");
var configuration = builder.Build();
var seleniumConfigurations = new SeleniumConfigurations();
new ConfigureFromConfigurationOptions<SeleniumConfigurations>(
configuration.GetSection("SeleniumConfigurations"))
.Configure(seleniumConfigurations);
Console.WriteLine(
"Carregando driver do Selenium para Firefox em modo headless...");
var paginaClassificacao = new PaginaClassificacao(
seleniumConfigurations);
Console.WriteLine(
"Carregando página com classificações da NBA...");
paginaClassificacao.CarregarPagina();
Console.WriteLine(
"Extraindo dados...");
var classificacao = paginaClassificacao.ObterClassificacao();
paginaClassificacao.Fechar();
Console.WriteLine("Gravando dados extraídos...");
new ClassificacaoRepository(configuration)
.Incluir(classificacao);
Console.WriteLine(
"Carga de dados concluída com sucesso!");
Console.ReadKey();
}
}
}
As fontes do projeto descritos nesta seção já foram disponibilizados no GitHub:
Testes
A imagem a seguir traz o resultado da execução da Console Application detalhada na seção anterior:

Consultando o banco NBA via Robo 3T (utilitário de gerenciamento do MongoDB), será possível notar a presença da coleção Classificacao, além de dois documentos vinculados à mesma:

Já as próximas imagens trazem a visualização destes documentos no formato JSON:






