CasperJS permet de scripter des scenarios automatisés de navigation et d'interaction avec des applications Web utilisant massivement JavaScript.
Utiliser Homebrew, un gestionnaire de paquet pour OS X, est la solution la plus simple :
$ ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"
$ brew update
$ brew install casperjs --devel
$ casperjs --version
1.1-beta2
Note : PhantomJS sera alors installé en tant que dépendance.
PhantomJS s'installe facilement via npm, tandis que CasperJS s'installera via
git
:
$ npm install -g phantomjs
$ git clone https://github.com/n1k0/casperjs.git
$ cd casperjs
$ ln -sf `pwd`/bin/capserjs /usr/local/bin/casperjs
$ git co 1.1-beta2
$ casperjs --version
1.1-beta2
C:\casperjs
;C:\casperjs\bin
à votre variable d'environnement PATH
Vous pouvez désormais utiliser la commande casperjs.exe
:
C:> casperjs.exe myscript.js
Notes :
Dans un nouveau fichier casper-commits.js
:
require("casper").create({pageSettings: {loadImages: false, loadPlugins: false}})
.start("https://github.com/n1k0", function() {this.clickLabel("casperjs")})
.waitForSelector(".commits a").thenClick(".commits a")
.waitForSelector(".commit-group-heading").run(function() {
this.echo("Latest commits from CasperJS:\n\n" + this.evaluate(function() {
var nodes = document.querySelectorAll('.commit-title');
return [].map.call(nodes, function(node) {
return node.textContent.replace("…", "").trim();
});
}).join("\n - ")).exit();
});
$ casperjs casper-commits.js
Latest commits from CasperJS:
- fixed jshint issue
- updated obsolete doclinks in README
- fixes #704 - added --auto-exit=no test runner option
- added missing test file - refs #721
- refs #546 - even better 1.1 strategy for storing successes/failures
- fixes #704 - provide an 'exit' tester event to hook before runner exits
- refs #546 - documented fallback strategies for getPasses/getFailures
- fixes #546 - updated 1.1 upgrade docs regarding getPasses/getFailures
- minor docs fixes for clientutils
- Merge pull request #718 from phillipalexander/fix-readme-typo
…
Oui, c'était une blague.
Mais à la fin vous pourrez produire vous-même ce genre de script
(En beaucoup plus beau)
function hello() {
var target = "world";
return "hello, " + target;
}
console.log(hello()) // "hello, world"
console.log(target) // ReferenceError: target is not defined
function hello() {
target = "world";
return "hello, " + target;
}
console.log(hello()) // "hello, world"
console.log(target) // "world"
Ici, l'absence d'utilisation du mot-clé var
stocke target
dans le scope
global, c'est à dire attache la propriété target
à l'objet window
.
function hello() {
"use strict";
target = "world";
return "hello, " + target;
}
console.log(hello()) // ReferenceError: assignment to undeclared variable target
console.log(target) // undefined
La directive "use strict"
effectue une vérification stricte du contexte dans
lequel nous déclarons nos variables. Utilisez-la aussi souvent que possible!
function hello(target) {
function capitalize() {
return target.toUpperCase();
}
return "hello, " + capitalize();
}
console.log(hello("chuck")) // "hello, CHUCK"
console.log(target) // undefined
Un fonction imbriquée accède au scope de sa fonction parente. Les arguments reçus par une fonction ne sont pas exposés hors de son scope.
function hello(target) {
function capitalize() {
target = target.toUpperCase();
}
capitalize();
return "hello, " + target;
}
console.log(hello("chuck")) // "hello, CHUCK"
Une fonction imbriquée peut altérer une variable du scope parent, mais cette pratique est vivement déconseillée, car ce type d'effets de bords sont toujours plus difficiles à contrôler et à tester.
function add(x) {
return function(y) {
return x + y;
};
}
var add5 = add(5);
console.log(add5(3)); // 8
console.log(add(5)(3)); // 8
Une closure est une fonction qui effectue des opérations sur ou à partir des
éléments de son scope parent. Ici, la fonction add
prend un argument x
et
retourne elle-même une autre fonction prenant un paramètre y
qui sera ajouté
à x
.
L'aspirine est dans le petit placard blanc de la salle de bain.
var functions = [];
for (var i = 0; i < 10; i++) {
functions.push(function() {
console.log(i);
});
}
functions.forEach(function(fn) {
fn();
});
Cela affiche dix fois le nombre 10
. Que s'est-il passé ?
var functions = [];
for (var i = 0; i < 10; i++) {
functions.push(function(i) {
return function() {
console.log(i);
};
}(i));
}
functions.forEach(function(fn) {
fn();
});
Grace à une closure, nous avons isolé la valeur de i
utilisée par chacune des
fonctions de notre tableau de fonctions.
var functions = [];
for (var i = 0; i < 10; i++) {
(function(i) {
functions.push(function() {
console.log(i);
});
})(i);
}
functions.forEach(function(fn) {
fn();
})
Ici nous utilisons une IIFE — Immediately Invoked Function Expression — qui aboutit au même résultat.
>>> typeof "foo"
???
>>> typeof null
???
>>> typeof NaN
???
>>> typeof new Date()
???
>>> Object.prototype.toString.call(new Date())
???
>>> typeof "foo"
"string"
>>> typeof null
object
>>> typeof NaN
"number"
>>> typeof new Date()
"object"
>>> Object.prototype.toString.call(new Date())
"[object Date]"
>>> function Cow(name) {
this.name = name;
}
>>> Cow.prototype.say = function(what) {
console.log(this.name + ' says ' + what);
};
>>> var cow = new Cow('Geneviève');
>>> cow.say('Meuh.');
// => "Geneviève says Meuh."
>>> var cow = new Cow('Geneviève');
[Object object]
>>> cow = JSON.parse(JSON.stringify(cow));
[Object object]
>>> cow.says('Halp.');
???
>>> var cow = new Cow('Geneviève');
[Object object]
>>> cow = JSON.parse(JSON.stringify(cow));
[Object object]
>>> cow.says('Halp.');
TypeError: cow.says is not a function
Un objet cloné de cette façon renverra toujours un objet natif dépossédé de toutes les méthodes que vous y avez ajouté.
Array#forEach
[1, 2, 3].forEach(function(i) {
console.log(i);
});
// => 1
// => 2
// => 3
Array#map
[1, 2, 3].map(function(x) {
return x * 2;
});
// [2, 4, 6]
Array#reduce
[1, 2, 3].reduce(function(acc, x) {
return acc + x;
});
// 6
Array#filter
[1, 2, 3].filter(function(x) {
return x > 1 && x < 3;
});
// [2]
Arguments
function adder() {
return [].reduce.call(arguments, function(acc, x) {
return acc + x;
});
}
adder(1, 2) // 3
adder(1, 2, 3) // 6
Function#apply
function adder() {
return [].reduce.call(arguments, function(acc, x) {
return acc + x;
});
}
adder.apply(null, [1, 2])
adder.apply(null, [1, 2, 3])
Function#call
function adder() {
return this.reduce(function(acc, x) {
return acc + x;
});
}
adder.call([1, 2])
adder.call([1, 2, 3])
Function#bind
function test() {
"use strict";
console.log(this, JSON.stringify(arguments));
}
>>> test();
undefined '{}'
>>> test.bind("foo")();
foo {}
>>> test.bind("foo", 1, 2)();
foo {"0":1,"1":2}
document.querySelector
>>> typeof document.querySelector('body')
???
>>> document.querySelector('body') instanceof HTMLBodyElement
???
>>> document.querySelector('body') instanceof HTMLElement
???
>>> document.querySelector('#nonexistent')
???
>>> typeof document.querySelector('#nonexistent')
???
document.querySelector
>>> typeof document.querySelector('body')
"object"
>>> document.querySelector('body') instanceof HTMLBodyElement
true
>>> document.querySelector('body') instanceof HTMLElement
true
>>> document.querySelector('#nonexistent')
null
>>> typeof document.querySelector('#nonexistent')
"object"
document.querySelectorAll
>>> typeof document.querySelectorAll('a')
???
>>> document.querySelectorAll('a') instanceof HTMLElement
???
>>> document.querySelectorAll('a') instanceof NodeList
???
>>> document.querySelectorAll('a')[0] instanceof HTMLElement
???
>>> document.querySelectorAll('a') instanceof Array
???
document.querySelectorAll
>>> typeof document.querySelectorAll('a')
"object"
>>> document.querySelectorAll('a') instanceof HTMLElement
false
>>> document.querySelectorAll('a') instanceof NodeList
true
>>> document.querySelectorAll('a')[0] instanceof HTMLElement
true
>>> document.querySelectorAll('a') instanceof Array
false
NodeList
>>> document.body.innerHTML = '<ul><li>1</li><li>2</li><li>3</li></ul>'
>>> document.querySelectorAll('li').length
???
>>> document.querySelectorAll('li').map(function(node) {
>>> return parseInt(node.textContent, 10);
>>> })
???
NodeList
>>> document.body.innerHTML = '<ul><li>1</li><li>2</li><li>3</li></ul>'
>>> document.querySelectorAll('li').length
3
>>> document.querySelectorAll('li').map(function(node) {
>>> return parseInt(node.textContent, 10);
>>> })
TypeError: document.querySelectorAll(...).map is not a function
NodeList
La solution, utiliser la fonction prototypale Array#map
en lui passant le
contexte approprié :
>>> Array.prototype.map.call(document.querySelectorAll('li'), function(node) {
>>> return parseInt(node.textContent, 10);
>>> })
[1, 2, 3]
Ou encore :
>>> [].map.call(document.querySelectorAll('li'), function(node) {
>>> return parseInt(node.textContent, 10);
>>> })
[1, 2, 3]
Le Web scraping (parfois appelé Harvesting) est une technique d'extraction du contenu de sites Web, via un script ou un programme, dans le but de le transformer pour permettre son utilisation dans un autre contexte
Extraire tous les liens depuis la page fr.wikipedia.org/wiki/Web_Scraping.
Créer un fichier exercices/1/1.js
:
// exercices/1/1.js
var casper = require('casper').create();
casper.start('http://fr.wikipedia.org/wiki/Web_scraping', function() {
this.debugHTML();
});
casper.run();
Lancer le script :
$ casperjs exercices/1/1.js
On crée un object casper
:
var casper = require('casper').create();
Notez l'utilisation du pattern module, comme dans node.js
On lance le navigateur sur l'url :
casper.start('http://fr.wikipedia.org/wiki/Web_scraping');
Considérez la méthode start()
comme l'ouverture d'un nouvel onglet et le
chargement d'une url dans un navigateur classique.
On définit le comportement à la réception de la réponse HTTP attendue ; ici, on affiche simplement le contenu HTML brut de la page :
casper.start('http://fr.wikipedia.org/wiki/Web_scraping', function() {
this.debugHTML();
});
La méthode debugHTML()
est très utile pour savoir exactement quel contenu
HTML est utilisé dans la page courante.
Enfin, on lance le traitement de l'ensemble des opérations programmées :
casper.run();
Une erreur fréquente des débutants est d'omettre l'appel à run()
, résultant sur
un script figé.
On pourrait aussi écrire :
var casper = require('casper').create();
casper.start('http://fr.wikipedia.org/wiki/Web_scraping');
casper.then(function() {
this.debugHTML();
});
casper.run();
Ou encore :
require('casper')
.create()
.start('http://fr.wikipedia.org/wiki/Web_scraping')
.then(function() {
this.debugHTML();
})
.run();
Plus une affaire de goûts et de couleurs qu'autre chose…
// exercices/1/2.js
var casper = require('casper').create();
var links = [];
casper.start('http://fr.wikipedia.org/wiki/Web_scraping', function() {
links = this.evaluate(function() {
var nodes = document.querySelectorAll('a');
return [].map.call(nodes, function(node) {
return node.getAttribute('href');
});
});
});
casper.run(function() {
this.echo(links).exit();
});
evaluate()
Regardons plus en détail ce qui s'est passé…
Une fois la page Wikipedia ouverte, nous appelons la méthode evaluate()
depuis
le context casper afin de récupérer une valeur depuis le DOM de la page en
question :
var links = [];
casper.start('http://fr.wikipedia.org/wiki/Web_scraping', function() {
links = this.evaluate(function() {
// ce qui sera retourné ici sera affecté à la variable links
});
});
Et que voulons-nous affecter à notre variable links
? La liste des liens
hypertextes de la page courrante ; aussi nous commençons par récupérer la
liste des éléments DOM correspondants :
var nodes = document.querySelectorAll('a');
Que nous mappons afin d'en extraire le contenu de l'attribut href
:
return [].map.call(nodes, function(node) {
return node.getAttribute('href');
});
Note: Nous ne pouvons pas directement mapper la liste d'éléments car le type
NodeList
n'implémente pas complètement le prototype Array
; nous appliquons
donc directement la méthode Array#map
à notre objet NodeList
au moyen de
Function#call
.
Enfin, nous lançons l'exécution des opérations et affichons le contenu de notre
variable links
:
casper.run(function() {
this.echo(links).exit();
});
Casper#evaluate()
Pour évaluer du code dans le contexte de la page Web que vous scrapez, il faut utiliser la méthode Casper#evaluate().
Par sécurité, cette fonction est sandboxée, il n'y a aucun moyen pour le code qui lui est passé d'accéder aux objets et variables JavaScript en dehors du contexte de la page en question.
Comme pour PhantomJS, l'utilisation du sandboxing implique de ne pouvoir transférer que des objets sérializables de l'environnement JavaScript de la page à l'environnement d'exécution CasperJS.
Astuce : considérez une valeur comme sérializable lorsque l'expression suivante est vraie :
value === JSON.parse(JSON.stringify(value)) // true
Casper#evaluate()
Ceci fonctionnera :
var href = casper.evaluate(function() {
return document.querySelector('a').href; // String
});
Ceci ne fonctionnera pas :
var href = casper.evaluate(function() {
return document.querySelector('a'); // HTMLElement, non serializable
});
Casper#evaluate()
Ce diagramme illustre le principe d'étanchéïté entre les deux environnements :
Casper#evaluate()
Une fonction passée à evaluate()
n'a pas accès au scope de l'environnement
CasperJS ; ceci ne fonctionnera pas :
var toto = 42;
this.echo(this.evaluate(function() {
return toto; // undefined
}));
On peut cependant passer des arguments inline à evaluate()
:
var toto = 42;
this.echo(this.evaluate(function(toto) {
return toto; // 42
}, toto));
Vous connaissez sans doute la fonctionnalité de Google Suggest, qui permet de proposer des suggestions pour un terme de recherche particulier :
Allons scraper tout ça pour rigoler un coup (ou pas)
L'idée est de développer un script permettant, pour un terme de recherche passé, d'afficher la liste des suggestions correspondantes depuis la ligne de commande :
$ casperjs google-suggest.js pourquoi
pourquoi la guerre au mali
pourquoi pas coline
pourquoi je me suis marié
pourquoi le ciel est bleu
pourquoi la france intervient au mali
pourquoi je me suis marié aussi
pourquoi j'ai mangé mon père
pourquoi on baille
pourquoi le pape démissionne
Certains outils peuvent également s'avérer utiles :
Une fois l'objet casper
créé, il dispose d'un attribut cli
qui permet de
récupérer les informations passées en ligne de commande ; ici, nous souhaitons
récupérer le premier argument :
var casper = require('casper').create();
var word = casper.cli.get(0);
La méthode Casper#sendKeys() permet de simuler la saisie d'un texte sur un champ de formulaire ; Nous allons saisir le mot passé en argument du script dans la zone de recherche google :
casper.start('http://www.google.com/', function() {
this.sendKeys('input[name=q]', word);
});
L'affichage des suggestions de recherche Google étant asynchrone, nous utilisons
la méthode Casper#waitFor afin d'attendre que le sélecteur correspondant
existe et commence par le mot que nous recherchons ; ici, le sélecteur CSS3
correspondant est .gsq_a table span
:
casper.waitFor(function() {
return this.fetchText('.gsq_a table span').indexOf(word) === 0
}, function() {
// la boite d'autocomplétion est disponible
// récupération des suggestions
});
Une fois la zone affichant les suggestions est visible, nous pouvons procéder à la récupération des textes pour chaque élément de la liste correspondante :
suggestions = this.evaluate(function() {
var nodes = document.querySelectorAll('.gsq_a table span');
return [].map.call(nodes, function(node){
return node.textContent;
});
});
Enfin, nous affichons la liste des résultats que nous avons stocké :
casper.run(function() {
this.echo(suggestions.join('\n')).exit();
});
Utiles pour :
Le framework de test fonctionnel a été refondu en version 1.1, et propose
désormais des fonctionnalités avancées de reporting, les méthodes setUp()
et
tearDown()
et propose une architecture plus découplée.
La documentation de la version de développement est située à l'adresse docs.casperjs.org.
Installer la version 1.1 de développement :
$ git clone https://github.com/n1k0/casperjs.git
$ cd casperjs
$ git co master
$ casperjs --version
1.1.0-DEV
casperjs test
L'utilisation des fonctionnalités de test implique l'utilisation d'une
sous-commande particulière, casperjs test
:
$ casperjs test mytest.js
Testons la recherche google :
// exercices/3/1.js
casper.test.begin('Google search tests', 4, function suite(test) {
casper.start("http://www.google.fr/", function() {
test.assertTitle("Google", "google homepage title is the one expected");
test.assertExists('form[action="/search"]', "main form is found");
this.fill('form[action="/search"]', {
q: "casperjs"
}, true);
});
casper.waitFor(function() {
return this.getTitle() === "casperjs - Recherche Google";
}, function() {
test.assertUrlMatch(/q=casperjs/, "search term has been submitted");
test.assertEval(function() {
return document.querySelectorAll("h3.r").length >= 10;
}, "google search for \"casperjs\" retrieves 10 or more results");
});
casper.run(function() {
test.done();
});
});
Vous devriez obtenir quelque chose d'approchant :
En faisant volontairement échouer le test :
casper.test.begin('Google search tests', 4, function suite(test) {
Nous démarrons un nouveau test en spécifiant :
test
contenant une référence à l'objet Tester. casper.start("http://www.google.fr/", function() {
test.assertTitle("Google", "google homepage title is the one expected");
test.assertExists('form[action="/search"]', "main form is found");
this.fill('form[action="/search"]', {
q: "casperjs"
}, true);
});
Nous ouvrons la page d'accueil de Google, puis :
<title>
) casper.waitFor(function() {
return this.getTitle() === "casperjs - Recherche Google";
}, function() {
test.assertUrlMatch(/q=casperjs/, "search term has been submitted");
test.assertEval(function() {
return document.querySelectorAll("h3.r").length >= 10;
}, "google search for \"casperjs\" retrieves 10 or more results");
});
Nous attendons que le titre de la page ait changé, puis nous vérifions :
casper.run(function() {
test.done();
});
Enfin, nous executons l'ensemble des opérations définies et appelons la méthode Tester#done() afin de signifier que le test est terminé.
L'option --xunit
de la commande casper test
permet d'exporter le résultat
d'une suite de tests au format XUnit :
$ casperjs test exercices/3/1.js --xunit=log.xml
Ces logs dont utiles dans la perspective de la mise en place d'intégration continue avec un serveur comme Jenkins.
Le fichier log.xml
contient alors :
<?xml version="1.0" encoding="UTF-8"?>
<testsuites duration="0.94">
<testsuite errors="0" failures="0" name="Google search tests"
package="exercices/3/1" tests="4" time="0.94"
timestamp="2013-04-29T16:12:01.211Z">
<testcase classname="exercices/3/1"
name="google homepage title is the one expected" time="0.55"/>
<testcase classname="exercices/3/1"
name="main form is found" time="0.001"/>
<testcase classname="exercices/3/1"
name="search term has been submitted" time="0.388"/>
<testcase classname="exercices/3/1"
name="google search for 'casperjs' retrieves 10 or more results"
time="0.001"/>
<system-out/>
</testsuite>
</testsuites>
1 / 91
#