PhantomJS
Integrantes: Bruno Guilherme, Gabriel Pacheco, Guilherme Sousa.
Introdução
PhantomJS é um navegador baseado no WebKit, que é executado em um terminal e é usado para automatizar a interação do usuário com as páginas web. O PhantomJS fornece uma API JavaScript que permite uma navegação automatizada, que os usuários tirem screenshots das páginas web e que os usuários possam manipular os elementos dos sites, como HTML, CSS, Jquery, entre outros. O PhantomJs ainda possui a finalidade de monitoramento, uma vez que, traz a possibilidade para que os seus usuários inspecionem o tráfego do sistema.
Sua linguagem de programação foi construída em C++, porém seus usuários, programam em uma linguagem baseada em javascript. Provavelmente, o PhantomJS foi desenvolvido dessa forma para que assim, ficasse mais fácil para seus colaboradores integrarem o sistema com outras API's, como o CasperJS e o Yalow.
O PhantomJS foi lançado em 23 de Janeiro em 2011 por Ariya Hidayat, e já possui hoje mais de 11 releases. Cada release recebe um nome de alguma flor que brota na estação do ano em que a release é lançada. Das releases mais relevantes podemos destacar:
Cherry Blossom: foi lançado em 27 de abril de 2011. Adiciona suporte para upload de arquivos, screenshots com saída no formato GIF.
Birds of Paradise: foi lançada em 21 de Junho de 2011. Foi implementada uma abstração dos objetos de uma pagina web, implementado a funcionalidade de "rasterization" (descreve uma imagem por vetores gráficos) e monitoramento do tráfego Web.
Water Lily: foi lançado em 23 de setembro de 2011. Apresenta suporte para sistemas de arquivos e eventos com o mouse.
Glory of the Snow: foi lançado em 22 de dezembro de 2011. Essa release teve o objetivo de corrigir bugs das versões anteriores e acrescentar módulos de WebServer.
Blue Winter Rose: foi lançado em 21 de dezembro de 2012 , foi feita a integração com o GhostDriver, e foram implementadas conexões com outras API. Como pode ser visto na figura 1 o GhostDriver funciona como uma ponte entre o Selenium WebDriver e o PhantomJS.
PhantomJS 2.0: foi lançado em 23 de Janeiro de 2015, tem a presença da biblioteca atualizada do WebKit baseado em Qt 5.3.m.
figura 1
Módulos
System: Módulo comum para qualquer plataforma que, no caso do PhantomJS, contém os seguintes atributos:
stdin: o fluxo de entrada padrão, um objeto com a mesma API que um arquivo aberto com o modo "r" sem codificação.
stdout: o fluxo de saída padrão, um objeto com a mesma API que um arquivo aberto com o modo "w" sem codificação.
stderr: o fluxo de erro padrão, um objeto com a mesma API que um arquivo aberto com o modo "w" sem codificação.
Lista de propriedades: args, env, os, pid, platform;
Lista de métodos: -
Lista de callbacks: -
Child Process: Este módulo é útil para tarefas como impressão, envio de e-mail ou até chamar scripts de programas escritos em outra linguagem (não Javascript). Este módulo permite ao usuário, invocar subprocessos e se comunicar com eles através das três conexões de I/O: entrada padrão (stdin), saída padrão (stdout) e erro padrão (stderr).
Lista de propriedades: -
Lista de métodos: spawn( );
Lista de callbacks: -
CookieJar: Este módulo é responsável pelo armazenamento de cookies (que são obtidos ou definidos para qualquer domínio) e pelo fornecimento destes no momento em que o usuário acessar alguma página web pertinente.
Lista de propriedades: -
Lista de métodos: decorateCookieJar();
Lista de callbacks: -
File System: Este módulo fornece um conjunto de funções da API para acessar arquivos e diretórios.
Lista de propriedades: separator, workingDirectory;
Lista de métodos: absolute( ), changeWorkingDirectory( ), copyTree( ), copy ( ), exists( ), isAbsolute( ), isDirectory( ), isExecutable( ), isFile( ), isLink( ), isReadable( ), isWritable( ), lastModified( ), list( ), makeDirectory( ), makeTree( ), move( ), open( ), readLink( ), read( ), removeDirectory( ), removeTree( ), remove( ), size( ), touch( ), write( );
Lista de callbacks: -
Web Page: Este módulo encapsula uma página web.
Lista de propriedades: canGoBack, canGoForward, clipRect, content, cookies, customHeaders, event, focusedFrameName, frameContent, frameName, framePlainText, frameTitle, frameUrl, framesCount, framesName, libraryPath, navigationLocked, offlineStoragePath, offlineStorageQuota, ownsPages, pagesWindowName, pages, paperSize, plainText, scrollPosition, settings, title, url, viewportSize, windowName, zoomFactor;
Lista de métodos: addCookie( ), childFramesCount( ), childFramesName( ), clearCookies( ), close( ), currentFrameName( ), deleteCookie( ), evaluateAsync( ), evaluateJavaScript( ), evaluate( ), getPage( ), goBack( ), goForward( ), go( ), includeJs( ), injectJs( ), openUrl( ), open( ), release( ), reload( ), renderBase64( ), renderBuffer( ), render( ), sendEvent( ), setContent( ), stop( ), switchToChildFrame( ), switchToFocusedFrame( ), switchToFrame( ), switchToMainFrame( ), switchToParentFrame( ), uploadFile( );
Lista de callbacks: Callback Triggers, onAlert, onCallback, onClosing, onConfirm, onConsoleMessage, onError, onFilePicker, onInitialized, onLoadFinished, onLoadStarted, onNavigationRequested, onPageCreated, onPrompt, onResourceError, onResourceReceived, onResourceRequested, onResourceTimeout, onUrlChanged;
Web Server: Para iniciar um servidor web, o PhantomJS faz uso de um módulo de servidor web incorporado chamado Mongoose. Este é um facilitador para a comunicação entre o PhantomJS e o mundo exterior. Obs: Não é recomendado para o uso como um servidor de produção geral.
Lista de propriedades: port;
Lista de métodos: close( ), listen( );
Lista de callbacks: -
Equipe de desenvolvimento
Informações a respeito da equipe de desenvolvimento não são difundidas pelos mesmos. Logo, não foi possível definir exatamente quais são os membros e as suas respectivas funções dentro do projeto atualmente. Segue abaixo dados a respeito dos três usuários que mais colaboraram com o desenvolvimento do PhantomJS desde a sua criação.
ariya: Criador do PhantomJS. Participa ativamente do projeto desde a sua criação em 2011. Atualmente possui mais de 537 commits. 2,975,741 linhas de código adicionadas / 40,233 linhas de código removidas.
detro: Participou ativamente do projeto desde a sua criação em 2011 até meados de 2014. Atualmente possui mais de 170 commits. 109,669 linhas de código adicionadas / 31,210 linhas de código removidas.
vitallium: Participa ativamente do projeto desde o final de 2012. Atualmente possui mais de 125 commits. 8,825,513 linhas de código adicionadas / 11,769,300 linhas de código removidas.
Exemplo - Primeiras Interações
Page Loading Toda página da web pode ser tratada como um objeto web page, e desse modo, as paginas irão ser carregadas no PhantomJs para funcionarem no modo offline, além disso podemos ainda, analisar seu código fonte, HTML, CSS, entre outros. Bem como, podemos também, renderizar as páginas web.
Facilmente, conseguimos demostrar como podemos usar o page object para carregar uma página, e salva lá como uma imagem.png. É valido notar mais uma vez, a similaridade do PhantomJs com a linguagem JavaScript:
var page = require('webpage').create();
page.open('http://example.com', function(status) {
console.log("Status: " + status);
if(status === "success") {
page.render('example.png');
}
phantom.exit();
});
Uma outra característica muito importante do PhantomJs, é a mensuração do tempo em que uma página web é carregada já que, com a oferta de internet crescendo constantemente, alguns minutos/segundos de tempo no carregamento de uma página web pode significar perda de um usuário/cliente. Assim, diversas empresas hoje em dia buscam insistentemente diminuir o tempo de carregamento de suas páginas web. Para mensurar o tempo de carregamento de página web usando o PhantomJs é bastante simples, como iremos mostrar no script abaixo, que irá carregar uma paǵina web e mensurar seu tempo de carregamento:
var page = require('webpage').create(),
system = require('system'),
t, address;
if (system.args.length === 1) {
console.log('Usage: loadspeed.js <some URL>');
phantom.exit();
}
t = Date.now();
address = system.args[1];
page.open(address, function(status) {
if (status !== 'success') {
console.log('FAIL to load the address');
} else {
t = Date.now() - t;
console.log('Loading ' + system.args[1]);
console.log('Loading time ' + t + ' msec');
}
phantom.exit();
});
Uma outra funcionalidade do PhantomJs, que com certeza foi responsável por sua popularidade, é o monitoramento do tráfego da rede. Essa funcionalidade permite que várias análises sobre o comportamento e a performasse da rede sejam analisadas. O PhantomJs permite que estas análises sejam exportadas pelo formato Har(HTTP Archive format), que é um arquivo codificado em JSON, que contém os logs da interação de um web browser, assim, usando o Har viewer os usuários podem visualizar os resultados em diagramas, como observador no código e no exemplo abaixo, que monitora o site da BBC:
"use strict";
if (!Date.prototype.toISOString) {
Date.prototype.toISOString = function () {
function pad(n) { return n < 10 ? '0' + n : n; }
function ms(n) { return n < 10 ? '00'+ n : n < 100 ? '0' + n : n }
return this.getFullYear() + '-' +
pad(this.getMonth() + 1) + '-' +
pad(this.getDate()) + 'T' +
pad(this.getHours()) + ':' +
pad(this.getMinutes()) + ':' +
pad(this.getSeconds()) + '.' +
ms(this.getMilliseconds()) + 'Z';
}
}
function createHAR(address, title, startTime, resources)
{
var entries = [];
resources.forEach(function (resource) {
var request = resource.request,
startReply = resource.startReply,
endReply = resource.endReply;
if (!request || !startReply || !endReply) {
return;
}
// Exclude Data URI from HAR file because
// they aren't included in specification
if (request.url.match(/(^data:image\/.*)/i)) {
return;
}
entries.push({
startedDateTime: request.time.toISOString(),
time: endReply.time - request.time,
request: {
method: request.method,
url: request.url,
httpVersion: "HTTP/1.1",
cookies: [],
headers: request.headers,
queryString: [],
headersSize: -1,
bodySize: -1
},
response: {
status: endReply.status,
statusText: endReply.statusText,
httpVersion: "HTTP/1.1",
cookies: [],
headers: endReply.headers,
redirectURL: "",
headersSize: -1,
bodySize: startReply.bodySize,
content: {
size: startReply.bodySize,
mimeType: endReply.contentType
}
},
cache: {},
timings: {
blocked: 0,
dns: -1,
connect: -1,
send: 0,
wait: startReply.time - request.time,
receive: endReply.time - startReply.time,
ssl: -1
},
pageref: address
});
});
return {
log: {
version: '1.2',
creator: {
name: "PhantomJS",
version: phantom.version.major + '.' + phantom.version.minor +
'.' + phantom.version.patch
},
pages: [{
startedDateTime: startTime.toISOString(),
id: address,
title: title,
pageTimings: {
onLoad: page.endTime - page.startTime
}
}],
entries: entries
}
};
}
var page = require('webpage').create(),
system = require('system');
if (system.args.length === 1) {
console.log('Usage: netsniff.js <some URL>');
phantom.exit(1);
} else {
page.address = system.args[1];
page.resources = [];
page.onLoadStarted = function () {
page.startTime = new Date();
};
page.onResourceRequested = function (req) {
page.resources[req.id] = {
request: req,
startReply: null,
endReply: null
};
};
page.onResourceReceived = function (res) {
if (res.stage === 'start') {
page.resources[res.id].startReply = res;
}
if (res.stage === 'end') {
page.resources[res.id].endReply = res;
}
};
page.open(page.address, function (status) {
var har;
if (status !== 'success') {
console.log('FAIL to load the address');
phantom.exit(1);
} else {
page.endTime = new Date();
page.title = page.evaluate(function () {
return document.title;
});
har = createHAR(page.address, page.title, page.startTime, page.resources);
console.log(JSON.stringify(har, undefined, 4));
phantom.exit();
}
});
}
Exemplo - Artigos sobre Engenharia de Software
Abaixo podemos ver um exemplo de uso do phantomjs. Neste exemplo é feito uma busca por artigos relacionados com Engenharia de Software em uma revista eletrônica especializada em cursos na área de programação.
var steps=[];
var testindex = 0;
var loadInProgress = false;
/*********SETTINGS*********************/
var webPage = require('webpage');
var page = webPage.create();
page.settings.userAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36';
page.settings.javascriptEnabled = true;
page.settings.loadImages = false;
phantom.cookiesEnabled = true;
phantom.javascriptEnabled = true;
console.log('All settings loaded, start with execution');
page.onConsoleMessage = function(msg) {
console.log(msg);
};
steps = [
//Step 1 - Abrindo a pagina DevMedia
function(){
console.log('Step 1 - Abrindo a pagina DevMedia');
page.open("http://www.devmedia.com.br/artigos/", function(status){
});
},
//Step 2 - Preenchendo o campo de busca e clicando no botao pesquisar
function(){
console.log('Step 2 - Preenchendo o campo de busca e clicando no botao pesquisar');
page.evaluate(function() {
$("nav.busca-pocket input").val("Engenharia de Software");
$("nav.busca-pocket button").click();
});
page.render('Busca.png');
},
//Step 3 - Esperando pelo resultado da busca
function(){
console.log("Step 3 - Esperando pelo resultado da busca ");
var fs = require('fs');
var result = page.evaluate(function() {
return document.querySelectorAll("html")[0].outerHTML;
});
fs.write('DevMedia.html',result,'w'); //Salvando a página carregada em um arquivo html
page.render('DevMedia.png'); //Salvando uma foto da página carregada
},
];
//Executando os passos um por um com um tempo de intervalo
interval = setInterval(executeRequestsStepByStep,5000);
function executeRequestsStepByStep(){
if (loadInProgress == false && typeof steps[testindex] == "function") {
steps[testindex]();
testindex++;
}
if (typeof steps[testindex] != "function") {
console.log("Test complete!"); //Teste finalizado
phantom.exit();
}
}
//Controlando se a página está sendo carregada
page.onLoadStarted = function() {
loadInProgress = true;
console.log('Loading started');
};
//Verificando se a página já foi completamente carregada
page.onLoadFinished = function() {
loadInProgress = false;
console.log('Loading finished');
};
page.onConsoleMessage = function(msg) {
console.log(msg);
};
É interessante notar que o phantomjs permite renderizar a página html carregada em formato de foto e assim obter uma imagem. Isto pode ser útil para verificar a disposição dos elementos na página e até mesmo ser usado para debugar o código, visualizando o que está sendo carregado. Também é possível salvar a página carregada em um arquivo html contendo todas as tags, facilitando assim buscar algum conteúdo específico. Abaixo podemos ver a imagem gerada da página carregada no exemplo acima e também os logs que foram exibidos durante a execução do script.
figura 2
figura 3
Exemplo - Cotação atual do dólar
Neste exemplo uma busca é realizada a fim de coletar o preço do dólar no momento em que o script é executado. Logo, vê-se uma aplicação prática da ferramenta, visto que com essa informação é possível desenvolver outras ferramentas que precisam manter o preço do dólar atualizado.
var steps=[];
var testindex = 0;
var loadInProgress = false;
/*********SETTINGS*********************/
var webPage = require('webpage');
var page = webPage.create();
page.settings.userAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36';
page.settings.javascriptEnabled = true;
page.settings.loadImages = false;
phantom.cookiesEnabled = true;
phantom.javascriptEnabled = true;
console.log('All settings loaded, start with execution');
page.onConsoleMessage = function(msg) {
console.log(msg);
};
steps = [
//Step 1 - Abrindo a pagina DolarHoje
function(){
console.log('Step 1 - Abrindo a pagina DolarHoje');
page.open("http://dolarhoje.com/", function(status){
});
},
//Step 2 - Preenchendo o campo de busca e clicando no botao pesquisar
function(){
console.log('Step 2 - Buscando o valor do dolar hoje');
page.evaluate(function() {
var dolar = document.getElementById("nacional").value;
console.log(dolar);
});
page.render('Dolar.png');
},
];
//Executando os passos um por um com um tempo de intervalo
interval = setInterval(executeRequestsStepByStep,5000);
function executeRequestsStepByStep(){
if (loadInProgress == false && typeof steps[testindex] == "function") {
steps[testindex]();
testindex++;
}
if (typeof steps[testindex] != "function") {
console.log("Test complete!"); //Teste finalizado
phantom.exit();
}
}
//Controlando se a página está sendo carregada
page.onLoadStarted = function() {
loadInProgress = true;
console.log('Loading started');
};
//Verificando se a página já foi completamente carregada
page.onLoadFinished = function() {
loadInProgress = false;
console.log('Loading finished');
};
page.onConsoleMessage = function(msg) {
console.log(msg);
};
Abaixo podemos ver o campo de onde foi retirado o valor do dólar no momento em que o script fora executado e também uma tela com os logs do momento da execução.
figura 4
figura 5