PHP SP

PHP WebDriver

Dos ultimos anos pra cá foi possível notar que o Facebook adora PHP, este artigo exalta mais uma prova disso: O PHP WebDriver.

O que é o WebDriver?

PHP WebDriver é uma API que permite automatizar interações com navegadores Web através de código PHP. Suas utilidades são, por exemplo, testes – aceitação, fluxo, integrações… – e, meu favorito, Crawlers.

Antes de explicar sobre o WebDriver, precisamos falar sobre Selenium e você também deverá saber que estamos lidando com o Java. Sim, este é o primeiro ponto a se notar: Não é uma implementação nativa com PHP, trata-se de uma integração.

Selenium é uma ferramenta de automação de navegador. Basicamente ele possui um Servidor de Controle Remoto que é responsável por abrir e fechar instâncias de navegadores e também servir de proxy para estas.

Para interagir com o servidor Selenium, utilizamos WebDrivers: drivers específicos capazes de enviar comandos aos navegadores que oferecem suporte. No momento em que escrevo este artigo, pode-se verificar nove drivers desenvolvidos.

E para interagir com com os drivers utilizamo-nos de uma API, e foi aí que o Facebook nos deu mais uma forcinha. – Estou ignorando completamente o fato de que eles estão entre os editores da RFC que descreve o WebDriver.

Facebook’s PHP WebDriver

Com o primeiro release em Outubro de 2013, o perfil Facebook no github conta com o repositório php-webdriver, também disponível no packagist para usuários de composer.

Trata-se de uma API similar às existentes em outras linguagens – Java, Python, Ruby, JavaScript, .NET – com o maior intuito de oferecer a mesma interface de programação.

Sua utilização é muito simples, porém exige um Selenium Server rodando, portanto dê uma olhada nos dois primeiros tópicos presentes no repositório php-webdriver.

use FacebookWebDriverRemoteRemoteWebDriver;
use FacebookWebDriverRemoteDesiredCapabilities;
use FacebookWebDriverWebDriverBy;

// endereço do selenium server
$host = 'http://localhost:4444/wd/hub';

// WebDriver capaz de trabalhar com instâncias do firefox/iceweasel
$webdriver = DesiredCapabilities::firefox();

// Nosso driver que está tomando conta do navegador
$driver = RemoteWebDriver::create($host, $webdriver);

// Acessando o phpsp.org.br
$driver->get('http://www.phpsp.org.br');

// O que temos na barra de título do navegador?
var_dump($driver->getTitle());

$driver->close();

O resultado, hoje, foi:

string(93) "Grupo de Desenvolvedores de PHP de São Paulo | Grupo de Desenvolvedores de PHP de São Paulo"

É importante também se certificar de que seu Selenium Server possui o driver que você procura.

Na prática

Existem outros artigos e até mesmo ferramentas que indicarão o webdriver como ferramenta de testes. Incluindo este texto.

No TDC 2015 recebemos, inclusive, uma excelente apresentação sobre uma ferramenta chamada Codeception, que também se baseia no Selenium. Com uma ferramenta assim, eu não correria atrás de escrever testes no PHPUnit usando o WebDriver diretamente.

O exemplo prático que lhes apresentarei, portanto, será o desenvolvimento de um crawler.

Nosso projeto de exemplo

O site do jogo Tibia possui um ranking de jogadores por servidor, para consulta via web-page. Se quisessemos disponibilizar, de alguma forma, isto à outras aplicações não existiria forma limpa. Optaremos, portanto, pela criação de um crawler que irá acessar estes rankings e obter os dados de maneira organizada.

O ranking pode ser acessado através deste link e não requer qualquer tipo de autenticação, o que torna nossa vida um pouco mais fácil.

Com o Selenium já de pé, iniciaremos nossa missão configurando o navegador:

use FacebookWebDriverRemoteRemoteWebDriver;
use FacebookWebDriverRemoteDesiredCapabilities;
use FacebookWebDriverWebDriverBy;


$host = "http://localhost:4444/wd/hub";

$driver = RemoteWebDriver::create(
			$host,
			DesiredCapabilities::firefox()
		);

$driver->get('https://secure.tibia.com/community/?subtopic=highscores');

Até aqui não há muita novidade, apenas acessamos a página utilizando o Firefox/Iceweasel. Acessando o link você poderá notar que requer um filtro inicial: devemos escolher qual servidor queremos visualizar através de uma select-box. Depois deve-se clicar no botão “Submit”.

Com toda astúcia do mundo descobrimos que o select-box pode ser encontrado com um seletor CSS:

select[name=world]

 , assim como o botão Submit:

input[name=Submit]

 .

Nossa vida vem ficando cada vez mais fácil:

$selectMundo = $driver->findElement(
  WebDriverBy::cssSelector('select[name=world]')
);

$botaoSubmit = $driver->findElement(
  WebDriverBy::cssSelector('input[name=Submit]')
);

// Selecionando o mundo Nerana
$selectMundo->sendKeys('nerana');
$botaoSubmit->click();

Após enviar o click, a página vai ser atualizada (realizamos uma submissão de formulário) e, quando terminar, existirá em si agora uma tabela abaixo do filtro. No caso desta página, não existem muitas classes ou IDs CSS que facilitem nossa vida, optei então por buscar através de características das TRs: São zebradas que variam entre as cores

#F1E0C6

  e

#D4C0A1

 .

Obtemos as linhas de resultado assim:

$linhasResultado = $driver->findElements(
  WebDriverBy::cssSelector('tr[bgcolor="#F1E0C6"], tr[bgcolor="#D4C0A1"]')
);

$linhasResultado

 é um array que contém objetos do tipo 

FacebookWebDriverRemoteRemoteWebElement

 . Como nem tudo é perfeito, nesta página existe outra linha que possui a cor 

#F1E0C6

 e, portanto, entraria na nossa lista de elementos. Para contornar isto verifiquei o número de elementos do tipo coluna dentro de cada linha, pois o Ranking se apresenta sempre com 4 colunas: Posição, Jogador, Nível e Pontuação. (Talvez exista um seletor CSS que verifique o número de filhos…)

A unica característica marcante estre estas colunas foi o tamanho em porcentagem:

foreach ($linhasResultado as $resultado) {
  $by = WebDriverBy::tagName('td');

  // Se o número de colunas for 4, estamos no ranking :D
  if (count($resultado->findElements($by)) == 4) {
    $posicao = $resultado->findElement(
      WebDriverBy::cssSelector('td[width="10%"]')
    )->getText();

    $jogador = $resultado->findElement(
      WebDriverBy::cssSelector('td[width="55%"]')
    )->getText();

    $nivel = $resultado->findElement(
      WebDriverBy::cssSelector('td[width="15%"]')
    )->getText();

    $pontos = $resultado->findElement(
      WebDriverBy::cssSelector('td[width="20%"]')
    )->getText();

    echo "[{$posicao}] => {$jogador}, Lv. {$nivel} ({$pontos})n";
  }
}

É interessante notar que os nossos comandos rodam de forma totalmente síncrona, aguardando o carregamento e término de interações com o navegador a cada linha.

Com isso temos o seguinte produto final:

use FacebookWebDriverRemoteRemoteWebDriver;
use FacebookWebDriverRemoteDesiredCapabilities;
use FacebookWebDriverWebDriverBy;


$host = "http://localhost:4444/wd/hub";

$driver = RemoteWebDriver::create(
			$host,
			DesiredCapabilities::firefox()
		);

$driver->get('https://secure.tibia.com/community/?subtopic=highscores');

$selectMundo = $driver->findElement(
  WebDriverBy::cssSelector('select[name=world]')
);

$botaoSubmit = $driver->findElement(
  WebDriverBy::cssSelector('input[name=Submit]')
);

// Selecionando o mundo Nerana
$selectMundo->sendKeys('nerana');
$botaoSubmit->click();

// Realizamos o filtro, agora selecionemos a tabela de resultado
$linhasResultado = $driver->findElements(
  WebDriverBy::cssSelector('tr[bgcolor="#F1E0C6"], tr[bgcolor="#D4C0A1"]')
);

$ranking = array();

foreach ($linhasResultado as $resultado) {
  $by = WebDriverBy::tagName('td');
  if (count($resultado->findElements($by)) == 4) {
    $posicao = $resultado->findElement(
      WebDriverBy::cssSelector('td[width="10%"]')
    )->getText();

    $jogador = $resultado->findElement(
      WebDriverBy::cssSelector('td[width="55%"]')
    )->getText();

    $nivel = $resultado->findElement(
      WebDriverBy::cssSelector('td[width="15%"]')
    )->getText();

    $pontos = $resultado->findElement(
      WebDriverBy::cssSelector('td[width="20%"]')
    )->getText();

    $ranking[$posicao] = array(
      'player' => $jogador,
      'nivel' => $nivel,
      'pontos' => $pontos
    );
  }
}

foreach ($ranking as $posicao => $dados) {
  echo "[{$posicao}] => {$dados['player']}, Lv. {$dados['nivel']} ({$dados['pontos']})n";
}

$driver->close();

Após a execução, o código acima deverá gerar uma saída semelhante à esta (executado em 14/09/2015):

[1] => Maurolkit, Lv. 809 (8788565507)
[2] => Roxed, Lv. 647 (4482528033)
[3] => Brus, Lv. 588 (3354368746)
[4] => Darkside the ghost, Lv. 584 (3286773922)
[5] => Borro, Lv. 565 (2981196516)
[6] => Mrocznny, Lv. 557 (2858362161)
[7] => Fire Blood Brother, Lv. 548 (2721845171)
[8] => Zadzioreq, Lv. 546 (2688489509)
[9] => Inwander, Lv. 544 (2660628665)
[10] => Climahazzard, Lv. 539 (2582067124)
[11] => Samir Agresor, Lv. 533 (2496223354)
[12] => Alucarz, Lv. 531 (2471971159)
[13] => Gugus Uhmejker, Lv. 519 (2310170793)
[14] => Saganin, Lv. 513 (2226539931)
[15] => Ementhesthii Theallihextium, Lv. 509 (2179536356)
[16] => Jay Xeler, Lv. 506 (2134713534)
[17] => Godlhazzard, Lv. 505 (2124543834)
[18] => Itsume, Lv. 503 (2099977061)
[19] => Superbird, Lv. 500 (2066993814)
[20] => Dark Spauw, Lv. 488 (1923652055)
[21] => Gwircel, Lv. 484 (1870709893)
[22] => Xaed, Lv. 481 (1839103878)
[23] => Crazy Maya, Lv. 478 (1799318553)
[24] => Dalamaar, Lv. 477 (1790197351)
[25] => Lock Lost, Lv. 475 (1763805386)

 

Conclusão

O php webdriver é mais um ótimo item para se guardar da caixa de ferramentas, tanto para testes quanto para obtenção de dados não disponíveis de forma comum. Sendo a segunda opção mais interessante, pois para testes existem outras ferramentas específicas para tanto.

Desenvolver crawlers utilizando o WebDriver é interessantíssimo porque, diferente de o que estamos acostumados, ele não apenas obtém os dados por alguma forma de cURL, mas trabalha a interação como um usuário de verdade, passando por cada validação de formulário, interação do navegador, atualização do DOM, execução de JavaScript e outros itens que só acontecem no navegador. Torna-se util a partir do momento que nosso crawler não busca apenas páginas estáticas. Além disso, o resultado das interações é visual e o Selenium permite tomar até mesmo obter capturas de tela do navegador em tempo de execução.

Powered by WPeMatico