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: