Accès clients

Le Blog

Have you ever tried to scrape or harvest data from an existing website — I mean, even ajax-bloated ones? Did you ever attempt to test javascript-dependent interactions within a Web application you built? Well, if you answered yes to one of the questions above, you might be interested in PhantomJS.

PhantomJS is a headless WebKit with JavaScript API. By headless, they mean you can script a real Webkit based browser with no need for a full graphical interface installed.

Installation

On OSX, installation can be achieved using homebrew (note that XCode must be installed on your machine):

$ brew install phantomjs

It can take a bit of time for the binaries to be built, especially because of their dependency to Qt4. When it's done, you can test it this way:

$ phantomjs
Usage: phantomjs [options] script.[js|coffee] [script argument [script argument ...]]
Options:
    --load-images=[yes|no]             Load all inlined images (default is 'yes').
    --load-plugins=[yes|no]            Load all plugins (i.e. 'Flash', 'Silverlight', ...) (default is 'no').
    --proxy=address:port               Set the network proxy.
    --disk-cache=[yes|no]              Enable disk cache (at desktop services cache storage location, default is 'no').
    --ignore-ssl-errors=[yes|no]       Ignore SSL errors (i.e. expired or self-signed certificate errors).

Installation instructions for other platforms and alternative methods can be found on the PhantomJS project wiki.

As a side note, there's also a Python implementation of PhantomJS, PyPhantomJS, which adds plugins support! Also, I've found myself having no segfault using the Python version while the standard one is a bit more unstable on my box (no troll please).

To install PyPhantomJS, let's use pip:

$ pip install PyPhantomJS

The PyPhantomJS executable is named — surprisepyphantomjs:

$ pyphantomjs
usage: pyphantomjs [options] script.[js|coffee] [script argument [script argument ...]]
Minimalistic headless WebKit-based JavaScript-driven tool
positional arguments:
  script.[js|coffee]    The script to execute, and any args to pass to it
optional arguments:
  -h, --help            show this help message and exit
  --disk-cache {yes,no}
                        Enable disk cache (default: no)
  --ignore-ssl-errors {yes,no}
                        Ignore SSL errors (default: no)
  --load-images {yes,no}
                        Load all inlined images (default: yes)
  --load-plugins {yes,no}
                        Load all plugins (i.e. Flash, Silverlight, ...) (default: no)
  --proxy address:port  Set the network proxy
  -v, --verbose         Show verbose debug messages
  --version             show this program's version and license

Usage of the two versions is exactly the same.

Basic usage

PhantomJS scripts can be written in standard JavaScript or in CoffeeScript. Mainly matter of taste here, but CoffeeScript syntax looks really interesting.

So let's write our first script, we want to retrieve the weather forecast for a given city using Google:

// script: meteo.js
var page = new WebPage()
, output = { errors: [], results: null };
if (phantom.args.length == 0) {
    console.log('You must specify a city, eg. "Paris, France"');
    phantom.exit(1);
}
page.open('http://www.google.fr/search?q=meteo+' + phantom.args[0], function (status) {
    if (status !== 'success') {
        output.errors.push('Unable to access network');
    } else {
        var cells = page.evaluate(function(){
            try {
                var cells = document.querySelectorAll('.tpo tr tr')[4].querySelectorAll('td');
                return Array.prototype.map.call(cells, function(cell) {
                    return cell.innerText.replace(/[^0-9]/g, '');
                });
            } catch (e) {
                return [];
            }
        });
        if (!cells || !cells.length > 0) {
            output.errors.push('No valid meteo data found');
        } else {
            output.results = {
                city: phantom.args[0],
                today: {
                    afternoon: cells[1],
                    morning:   cells[2],
                },
                tomorrow: {
                    afternoon: cells[3],
                    morning:   cells[4],
                }
            };
        }
        console.log(JSON.stringify(output, null, '    '));
    }
    phantom.exit();
});

Notice we use the phantom.args Array which contains the parameters passed to the script.

The main magic happens in the page.evaluate() method, we pass it a JavaScript function which will be evaluated within the retrieved page document environment. It's a kind of non-persistent XSS injection just to help you to operate on the page contents =)

Now it's time to launch the script to see how it goes:

$ phantomjs meteo.js "Montpellier, France"
{
    "errors": [],
    "results": {
        "city": "Montpellier, France",
        "today": {
            "afternoon": "29",
            "morning": "17"
        },
        "tomorrow": {
            "afternoon": "28",
            "morning": "17"
        }
    }
}

Now with an invalid city name:

$ phantomjs meteo.js "Unexistent City"
{
    "errors": [
        "No valid meteo data found"
    ],
    "results": null
}

Let's try with another city, an existing one this time:

$ phantomjs meteo.js "Paris, France"
{
    "errors": [],
    "results": {
        "city": "Paris, France",
        "today": {
            "afternoon": "21",
            "morning": "11"
        },
        "tomorrow": {
            "afternoon": "21",
            "morning": "11"
        }
    }
}

As a side note and in case you were wondering, you now understand a bit more why I moved to Montpellier ;)

I CAN HAZ SCREENSHOTS

PhantomJS also allows some nice tricks like injecting scripts to the remote page, very useful when a remote website doesn't ship with your favorite framework (eg. jQuery)… or can render a PNG image of a captured area of the webpage. The example below saves a capture of the weather forecast area:

// script: meteoclip.js
var page = new WebPage();
page.open('http://www.google.fr/search?q=meteo+montpellier,+France', function (status) {
    if (status !== 'success') {
        output.error = 'Unable to access network';
    } else {
        page.clipRect = {
            top: 127,
            left: 170,
            width: 400,
            height: 114
        }
        page.render('meteo.png');
        console.log('Capture saved');
    }
    phantom.exit();
});

Running the meteoclip.js script will get yourself this fancy image stored in meteo.jpg:

There are tons of other cool topics to cover about PhantomJS, like navigation handling, automated logging in, external resources retrieving, functional testing, code organization… so I'll maybe post a bit more about it soon, who knows!

Je travaille actuellement sur une application Django que je compte publier sous licence libre, et je suis confronté au problème classique de l'exposition de la configuration au développeur via les settings de son propre projet.

Classiquement, on a tendance à proposer les settings "à plat", dans le module settings.py du projet :

# settings.py
MY_APP_NAME_FOO = 42
MY_APP_NAME_ENABLE_CHUCK_NORRIZ_MODE = True

Et donc depuis votre appli, vous pouvez récupérer les settings utilisateur de cette façon, en leur assignant une valeur par défaut s'ils ne sont pas déclarés :

# apps/myapp/foo.py
from django.conf import settings

FOO = getattr(settings, 'MY_APP_NAME_FOO', 42)
ENABLE_CHUCK_NORRIZ_MODE = getattr(settings, 'MY_APP_NAME_ENABLE_CHUCK_NORRIZ_MODE', False)

Simple, pratique, suffisant me direz vous. Oui, mais bon, c'est un petit peu verbeux à mon sens, et pas toujours souple pour gérer un catalogue de settings ainsi que leur surcharge. Et puis j'ai l'impression en préfixant systématiquement ces noms de variables de faire insulte à cette merveilleuse fonctionnalité qu'on appelle la gestion des espaces de noms (voire de refaire du PHP < 5.3, ce qui provoque chez moi des bouffées d'angoisse et entame un processus de décapilation douloureux, mais je m'égare).

Qui plus est, personnellement en temps que développeur, j'aurai tendance à préférer gérer les settings correspondant à une application dans un dictionnaire dédié, un peu comme ce que propose la django-debug-toolbar.

Par exemple, en reprenant l'exemple de code initial ou seul le setting ENABLE_CHUCK_NORRIZ_MODE est finalement surchargé :

# settings.py
MY_APP_CONFIG = {
    'ENABLE_CHUCK_NORRIZ_MODE': True,
}

J'ai donc trouvé un moyen assez simple de proposer cette fonctionnalité. Dans le fichier __init__.py de votre module d'application, ajoutez le code suivant :

# apps/my_app/__init__.py
from django.conf import settings

app_settings = dict({
    'FOO': 42,
    'ENABLE_CHUCK_NORRIZ_MODE': False,
}, **getattr(settings, 'MY_APP_CONFIG', {}))

Vous constaterez qu'on fusionne bêtement les settings par défaut et ceux de l'utilisateur qui auront la priorité de surcharge. Ainsi, partout depuis votre application, vous pourrez accéder à ce dictionnaire de settings de cette façon :

# apps/my_app/utils.py
from . import app_settings

if app_settings.get('ENABLE_CHUCK_NORRIZ_MODE'):
    print 'Chuck Norriz is watching you'
else:
    print 'Dance dance, little lamb'

Et bien entendu, pour importer les settings de l'application depuis n'importe où (sous réserve que le module de l'application soit dans votre PYTHON_PATH) :

# foo/bar.py
from my_app import app_settings

print app_settings.get('FOO') # 42

Merci de votre attention, et à bientôt pour de nouvelle aventures.

Sometimes you work on stuff you don't really control, eg. when interacting with some mysterious SOAP server accross the Internets, and you'd appreciate a little help from the Django ecosystem to ease debugging.

That's — you guessed it — my case currently, and I really appreciated being able to create my own custom panel for adding specific debugging capabilities to the awesome Django Debug Toolbar.

Here's how I did, learning mainly from the code of the panels shipping with the DJT. I'm supposing you have installed and configured the DJT in your project already.

First of all, create a panels.py module within one of your app (or wherever you want as it's in your python path) and create a DebugPanel derivated class, like this:

from debug_toolbar.panels import DebugPanel
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _

class MyUsefulDebugPanel(DebugPanel):
    name = 'MyUseful'
    has_content = True

    def nav_title(self):
        return _('Useful Infos')

    def title(self):
        return _('My Useful Debug Panel')

    def url(self):
        return ''

    def content(self):
        context = self.context.copy()
        context.update({
            'infos': [
                {'plop': 'plip'},
                {'plop': 'plup'},
            ],
        })
        return render_to_string('panels/my_useful_panel.html', context)

The debug panel class methods and code should be self-explanatory enough. Just note you have to create a template, here panels/my_useful_panel.html to be stored in your project templates directory, with this kind of contents:

<p>Hey, these are useful informations, I swear:</p>
<ul>
{% for info in infos %}
    <li>Plop is {{ info.plop}}</li>
{% endfor %}
</ul>

Now you have to register the new panel by adding its path to the DEBUG_TOOLBAR_PANELS tuple, in your settings.py (create it if it's not there):

DEBUG_TOOLBAR_PANELS = (
    'debug_toolbar.panels.version.VersionDebugPanel',
    'debug_toolbar.panels.timer.TimerDebugPanel',
    'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
    'debug_toolbar.panels.headers.HeaderDebugPanel',
    'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
    'debug_toolbar.panels.template.TemplateDebugPanel',
    'debug_toolbar.panels.sql.SQLDebugPanel',
    'debug_toolbar.panels.signals.SignalDebugPanel',
    'debug_toolbar.panels.logger.LoggingPanel',
    'my_app_name.panels.MyUsefulDebugPanel',
)

Of course it's up to you to put really useful informations there, but here's the result, tadaa:

result screenshot

Thanks for reading, happy ponying.

Ceux qui me connaissent le savent, je suis partisan d'une gestion intransigeante de la qualité sur le Web, et suis donc — comme souvent dans ce cas — un grand fan du cycle de conférences Paris Web qui a lieu tous les ans en octobre à Paris. C'est l'occasion d'y faire un état des lieux des meilleurs pratiques, d'attraper un rhume, de découvrir de nouvelles techniques, de profiter des joies du métro, ou de rafraîchir ses connaissances (au propre comme au figuré). Surtout, c'est l'occasion d'échanger avec d'autres passionnés de la profession autour de breuvages houblonnés le soir venu en refaisant le Web jusqu'à plus d'heure ni soif.

Mais voila, Paris, c'est loin. Et Paris, ça a tendance à un peu trop vouloir centraliser tout ce qui remue à mon goût. Je m'en rends forcément mieux compte depuis que j'ai déménagé à Montpellier il y a un an et demi (fichtre comme le temps passe). Et surtout, je constate que la région dans laquelle je vis est immensément riche de passion et de compétences autour de ce noble medium qui est le nôtre. Des gens bien, un peu partout autour de moi, qui n'ont pas toujours la possibilité de se déplacer jusqu'à la capitale, de s'y loger, de s'y nourrir, de s'y acheter le nombre d'écharpes nécessaires pour survivre, etc.

Aussi, durant le trajet en voiture qui nous conduisait au dernier Paris Web moi et mes compagnons de route, nous avons une idée assez folle : organiser un évènement du même type que Paris Web, mais dans le sud. C'est à dire plus proche de nous géographiquement et assurant une meilleure compatibilité feinéantique et calorifère. Ainsi naquit l'idée d'un SubWeb.

Après quelques tergiversations, tatônements, études de terrain et autres défrichements des sols argileux du Gard, nous avons fixé la date et le lieu de la première édition de SubWeb qui se tiendra donc le vendredi 27 mai 2011, à l'École des Mines d'Alès de Nîmes.

À toutes fins utiles et pour éviter d'éventuelles déconvenues, il est cependant important de noter que SudWeb ne sera pas un Paris Web transposé plus au sud. Personnellement, j'ai toujours considéré un peu dommage de ne se focaliser que sur l'aspect frontend du Web — certes plein d'enjeux à ne surtout pas négliger — alors que d'autres aspects majeurs, bien que moins visibles des utilisateurs, méritent tout autant notre attention :

  • les aspects backend : architecture technique, hébergement, plateformes, langages, serveur, etc. ;
  • les aspects et enjeux méthodologiques de la gestion technique — voire commerciale — d'un projet Web ;
  • les valeurs métier liées au Web et la production de valeur ajoutée d'un point de vue humain et non plus uniquement mercantile ;
  • les aspects métier à proprement parler, et les enjeux qui accompagnent le changement dans les pratiques et les usages associés au Web.

Tout un programme ! En parlant de programme, un appel à conférence est lancé jusqu'au 7 février inclus, et n'attend que votre proposition d'intervention si vous souhaitez aussi partager un bon moment et votre expérience de faiseur de Web.

Le processus d'inscription, les tarifs et modalités, quand à eux, vous seront communiqués dès que les conditions normales de température et de pression seront optimales, stay tuned ;-)

EDIT : Les inscriptions sont désormais ouvertes, et les tarifs connus :

  • 135 € pour la journée, repas inclus
  • 15 € supplémentaires vous seront demandés si vous désirez participez à la soirée communautaire le soir même

Si vous êtes chômeur ou étudiant, Akei a le plaisir de sponsoriser votre place et prend en charge 45€ sur le prix du ticket d'entrée pour la journée de conférence, ramenant le prix à 90 € au lieu de 135 €.

Enfin, sachez qu'un financement du prix du ticket d'entrée est possible dans le cadre du Droit Individuel à la Formation (DIF).

Ne tardez pas pour commander votre place en ligne, la clôture des inscriptions est prévue pour le 2 mai.

Python is awesome, and so is its native interactive interpreter. I discovered today that it can even provide autocompletion using a very simple trick:

# in your ~/.profile
export PYTHONSTARTUP=$HOME/.pythonrc.py

# in a new ~/.pythonrc.py file
try:
    import readline
except ImportError:
    print("Module readline not available.")
else:
    import rlcompleter
    readline.parse_and_bind("tab: complete")

Note: Don't forget to source your .profile with $ source ~/.profile.

Magic? Well if like me you're running Mac OS X, it won't work, no autocompletion, nada. OS X seems to ship with a very poor (and obsolete) python, and no readline implementation — which is mandatory to achieve our purpose. I even tried to install readline by its own, but it won't solve the problem.

So while being at tweaking up my python setup, let me get rid of the Apple stuff and install a fresh version of python using Homebrew, a great package manager for OSX:

$ brew install readline python

Tadaa! Now you get autocompletion, plus a shiny python 2.7.1 (you could also install latest python3 running brew install python3 by the way).

As a side note, if you work with virtualenvs like me, creating a new env will now involve specifying which python you want to use:

$ mkvirtualenv -p /usr/local/Cellar/python/2.7.1/bin/python \
    --no-site-packages `pwd`/env

That's all folks.

Derniers commentaires

Tweets