.NET

16 mar, 2018

.NET Core 2.0 + Selenium + MongoDB: extraindo dados de páginas Web

Publicidade

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:

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:

Referências