16 novembre 2011

Lecture ligne par ligne avec NodeJS

Lire un fichier ou plus généralement un flux de données ligne par ligne est une opération triviale pour toutes les personnes connaissant la fonction fgets. Voici 4 méthodes pour résoudre cette problématique basique avec NodeJS :


0. String & Split

C'est la méthode la plus simple, il suffit de charger la totalité du fichier dans une chaîne de caractères puis découper cette chaîne en fonction du caractère \n

#!/usr/bin/env node

var buffer = '';

process.stdin.resume();
process.stdin.setEncoding('utf8');

process.stdin.on('data', function (chunk) {
  buffer += chunk;
});

process.stdin.on('end', function () {
  buffer.split('\n').forEach(function(line) {
    process.stdout.write(line);
    process.stdout.write('\n');
  });
});

1. Module byline


#!/usr/bin/env node

var byline = require('byline');

process.stdin.resume();
process.stdin.setEncoding('utf8');

stream = byline.createLineStream(process.stdin);

stream.on('data', function(line) {
  process.stdout.write(line);
  process.stdout.write('\n');
});
stream.on('end', function() {
  // Ended !
});

2. Module carrier


#!/usr/bin/env node

var carrier = require('carrier');

process.stdin.resume();
process.stdin.setEncoding('utf8');

carrier.carry(process.stdin, function(line) {
  process.stdout.write(line);
  process.stdout.write('\n');
});


3. Module line-reader


#!/usr/bin/env node

var lineReader = require('line-reader');

lineReader.eachLine('/dev/stdin', function(line, last) {
  process.stdout.write(line);
  process.stdout.write('\n');
});

4. Module node-each-line


#!/usr/bin/env node

var each_line = require('./each_line').each_line;

each_line(process.stdin, function (line) {
  process.stdout.write(line);
});


Comparaison

Pour faire son choix, il est intéressant de comparer leur performance pour cela
on commence par les installer:

npm install byline
npm install carrier
npm install line-reader
wget https://raw.github.com/aaronj1335/node-each-line/master/each_line.js

Ensuite, on utilise un fichier qui va générer quelques milliers de lignes :

#!/usr/bin/env node

for(var i = 1; i <= 100000; i++) {
 process.stdout.write(i+'');
 process.stdout.write('\n');
}

Reste à exécuter le tout :

$ time ./input.js | ./00.js >/dev/null

real 0m14.753s
user 0m14.973s
sys 0m1.288s

$ time ./input.js | ./01.js >/dev/null

real 0m11.714s
user 0m11.885s
sys 0m1.204s

$ time ./input.js | ./02.js >/dev/null

real 0m11.495s
user 0m11.761s
sys 0m1.164s

$ time ./input.js | ./03.js >/dev/null

node.js:134
        throw e; // process.nextTick error, or 'error' event on first tick
        ^
Error: Socket is not writable
    at Socket._writeOut (net.js:391:11)
    at Socket.write (net.js:377:17)
    at Object.<anonymous> (/home/thouveni/Tests/node/lines/input.js:4:17)
    at Module._compile (module.js:402:26)
    at Object..js (module.js:408:10)
    at Module.load (module.js:334:31)
    at Function._load (module.js:293:12)
    at Array.<anonymous> (module.js:421:10)
    at EventEmitter._tickCallback (node.js:126:26)

real 0m0.057s
user 0m0.068s
sys 0m0.036s

$ time ./input.js | ./04.js >/dev/null

real 0m5.096s
user 0m5.764s
sys 0m0.624s

Conclusion

Le module line-reader ne fonctionne pas, à ignorer. Les modules carrier et byline sont équivalents. Et le script node-each-line est le plus rapide mais il ne permet pas d'intercepter la fin de fichier. Le choix est donc une affaire de gout. Personnellement, j'utilise byline.

08 septembre 2011

Underscore + JSONSelect = XPath for JSON ?

XML est largement utilisé pour formater, stocker, échanger des informations structurées. Et malgré l’étendu de ses capacités, il n'en reste pas moins qu'un support. Et en tant que support JSON n'a rien à envier à XML. N'importe quelle information peut-être stocker avec JSON. Par contre l'écosystème XML est riche et on peut facilement transformer, valider, émietter nos données, ce qui semble moins évident avec JSON. Mais qui dit JSON dit Javascript et l'écosystème Javascript est également très riche et il permet de faire aussi bien voir mieux que XML dans certain domaine. Voici une technique pour picorer de l'information comme on le ferai avec XPath sur du XML mais sur un objet JSON.

JSONSelect

JSONSelect permet de picorer dans un objet javascript à l'aide de sélecteur CSS. Exemple :
#!/usr/bin/env node

var jsel = require('JSONSelect');
var doc = {
	"id": "0001",
	"type": "donut",
	"name": "Cake",
	"ppu": 0.55,
	"batters":
		{
			"batter":
				[
					{ "id": "1000", "type": "None" },
					{ "id": "1001", "type": "Regular" },
					{ "id": "1002", "type": "Chocolate" },
					{ "id": "1003", "type": "Blueberry" },
					{ "id": "1004", "type": "Devil's Food" }
				]
		},
	"topping":
		[
			{ "id": "5001", "type": "None" },
			{ "id": "5002", "type": "Glazed" },
			{ "id": "5005", "type": "Sugar" },
			{ "id": "5007", "type": "Powdered Sugar" },
			{ "id": "5006", "type": "Chocolate with Sprinkles" },
			{ "id": "5003", "type": "Chocolate" },
			{ "id": "5004", "type": "Maple" }
		]
};

console.log(jsel.match('.name', doc));
// [ 'Cake' ]
console.log(jsel.match('number', doc));
// [ 0.55 ]
console.log(jsel.match('.batter .type', doc));
// [ 'Regular', 'Chocolate', 'Blueberry', 'Devil\'s Food' ]
console.log(jsel.match('.type ~ .id:val("5005")', doc));
// [ '5005' ]
console.log(jsel.match('.type:val("None") ~ .id', doc));
// [ '1000', '5001' ]



Underscore

Underscore fournit ~ 60 fonctions orientées programmation fonctionnelle.
Et partie elles, une trentaine simplifient grandement la manipulation et le traitement des listes et des tableaux. Exemple :
#!/usr/bin/env node

var _ = require('underscore');

var arr = ["1000", "1001", "1002", "1003", "1004"];
console.log(_.first(arr));
// [ '1000' ]
console.log(_.without(arr, "1000"));
// [ '1001', '1002', '1003', '1004' ]
console.log(_(arr).chain().without(arr, "1000").first().value());
// 1001
console.log(_.map(arr, function(value) {
			return '#'+value ;
}));
// [ '#1000', '#1001', '#1002', '#1003', '#1004' ]
console.log(_(arr).chain().map(function(value) { 
			return value.substr(1, 2);
	}).uniq().first().value());
// 00


JSONSelect & Underscore

JSONSelect permet de récupérer des listes d'objet ou des tableaux de valeur, Underscore permet de manipuler des listes et des tableaux.
Coupler les deux offre donc un outil extrêmement puissant pour aller piocher et trier de l'information. Exemple :
#!/usr/bin/env node

var jsel = require('JSONSelect'),
	_ = require('underscore');

//  Underscore + JSONSelect
_.mixin({
		jsel : function(doc, sel) {
			return jsel.match(sel, doc)
		}
});

var data = [
	{ name:'Africa', type:'continent'},
	{ name:'Egypt', type:'country' },
	{ name:'Kenya', type:'country'},
	{ name:'Nairobi', type:'city' },
	{ name:'Mombasa', type:'city' },
	{ name:'Sudan', type:'country'},
	{ name:'Khartoum', type:'city' },
	{ name:'Asia', type:'continent'},
	{ name:'China', type:'country' },
	{ name:'India', type:'country' },
	{ name:'Russia', type:'country' },
	{ name:'Mongolia', type:'country' },
	{ name:'Australia', type:'continent'},
	{ name:'Commonwealth of Australia', type:'country'},
	{ name:'Europe', type:'continent'},
	{ name:'Germany', type:'country' },
	{ name:'France', type:'country' },
	{ name:'Spain', type:'country' },
	{ name:'Italy', type:'country' },
	{ name:'North America', type:'continent'},
	{ name:'Mexico', type:'country'},
	{ name:'Mexico City', type:'city'},
	{ name:'Guadalajara', type:'city'},
	{ name:'Canada', type:'country'},
	{ name:'Ottawa', type:'city'},
	{ name:'Toronto', type:'city'},
	{ name:'United States of America', type:'country' },
	{ name:'South America', type:'continent'},
	{ name:'Brazil', type:'country'},
	{ name:'Argentina', type:'country'}
];

console.log(_(data).chain().jsel('.type').uniq().value());
// [ 'continent', 'country', 'city' ]
console.log(_(data).chain().jsel('.type:val("city") ~ .name').sortBy(function(value) { 
			return value.charCodeAt(0)
	}).value());
// [ 'Guadalajara', 'Khartoum', 'Mombasa', 'Mexico City', 'Nairobi', 'Ottawa', 'Toronto' ]


06 septembre 2011

Installer NodeJS dans son HOMEDIR avec STOW

Il y a quelque temps déjà, j'avais présenté l'utilitaire stow. Mais avec les distributions Linux récentes cet utilitaire avait peu d’intérêt tant l'usage d'apt-get install suffisait la majorité du temps. Mais voilà, avec NodeJS, on ne peut pas attendre que la dernière version soit packagée pour l'utiliser. Il faut donc l'installer soit même, et si il existe plusieurs méthodes, aucune ne permet de changer de version de nodejs en un fragment de seconde. Démonstration !

STOW

Stow permet de recréer une arborescence système à l'aide de liens symboliques. Ces liens pointent sur un répertoire isolé et ils sont générés automatiquement à l'aide d'une simple commande.


Pré requis système

Pour installer et compiler NodeJS, il faut vérifier que les paquets g++ et libssl-dev sont installés.

#!/bin/sh

sudo apt-get install g++ libssl-dev


Installation de STOW

#!/bin/sh

# CHOOSE THE VERSION YOU WANT 
export STOW_VERSION=1.3.3

mkdir ~/local/stow
mkdir -p ~/local/src
mkdir ~/local/bin
mkdir ~/local/info
mkdir ~/local/etc
mkdir ~/local/include
mkdir ~/local/share
mkdir ~/local/share/man
mkdir ~/local/lib
mkdir ~/local/man
mkdir ~/local/man/man1
mkdir ~/local/man/man2
mkdir ~/local/man/man3
mkdir ~/local/man/man4
mkdir ~/local/man/man5
mkdir ~/local/man/man6
mkdir ~/local/man/man7
mkdir ~/local/man/man8

cd ~/local/src
curl http://ftp.gnu.org/gnu/stow/stow-${STOW_VERSION}.tar.gz | tar -xzf -

cd stow-${STOW_VERSION}
./configure --prefix=~/local/stow/stow-${STOW_VERSION}

make
make install

cd ~/local/stow/
./stow-1.3.3/bin/stow stow-1.3.3

echo "export PATH=\$HOME/local/bin:\${PATH:=}" >> ~/.profile
echo "export MANPATH=\$HOME/local/man:\${MANPATH:=}" >> ~/.profile
echo "export LD_LIBRARY_PATH=\$HOME/local/lib:\${LD_LIBRARY_PATH:=}" >> ~/.profile

export PATH=$HOME/local/bin:${PATH:=}
export MANPATH=$HOME/local/man:${MANPATH:=}
export LD_LIBRARY_PATH=$HOME/local/lib:${LD_LIBRARY_PATH:=}


NodeJS

NodeJS doit être compilé manuellement pour bénéficier des dernières versions. On va donc compiler et installer chaque version dans un répertoire spécifique.


Installation de NodeJS pour STOW

#!/bin/sh

# CHOOSE THE VERSION YOU WANT 
export NODEJS_VERSION=0.6.1

mkdir -p ~/local/lib/node_modules
cd ~/local/src

curl http://nodejs.org/dist/v${NODEJS_VERSION}/node-v${NODEJS_VERSION}.tar.gz | tar -xzf -
cd ~/local/src/node-v${NODEJS_VERSION}

./configure --prefix=~/local/stow/node-${NODEJS_VERSION}
make
make install

# Careful, check if stow is installed
cd ~/local/stow
stow node-${NODEJS_VERSION}

echo "export NODE_PATH=\$HOME/local:\$HOME/local/lib/node_modules" >> ~/.profile
export NODE_PATH=$HOME/local:$HOME/local/lib/node_modules


Changer de version facilement

Maintenant tester une nouveau version de NodeJS est très simple :

$ node -v
v0.4.11
$ cd ~/local/stow
$ stow -D node-0.4.11
$ stow node-0.6.1
$ node -v
v0.6.1

26 août 2011

HTTP request loop in NodeJS

Comment envoyer 200000 requêtes HTTP avec NodeJS ? A priori cette question semble facile ? Voyons si c'est le cas ?

Commençons petit

Avant d'envoyer plusieurs centaines de requêtes voyons déjà comment en envoyer une seule :

#!/usr/bin/env node

var http = require('http');

var options = {
  host: '127.0.0.1',
  port: 80,
  path: '/'
};

http.get(options, function(response) {
    console.log(response.statusCode);
});

On boucle !

#!/usr/bin/env node

var http = require('http');

var options = {
  host: '127.0.0.1',
  port: 80,
  path: '/'
};

for(var i = 0; i < 2000000; i++) {
	http.get(options, function(res) {
   	    console.log(res.statusCode);
	});
}

Résultat :


FATAL ERROR: JS Allocation failed - process out of memory

Économiser la mémoire

Ok, on y a été un peu fort, on va optimiser notre boucle en sortant le client HTTP de la boucle, histoire de l'instancier qu'une seule fois.

#!/usr/bin/env node

var http = require('http');


var client = http.createClient(80, '127.0.0.1');


for(var i = 0; i < 200000; i++) {
	var request = client.request("GET", '/')
	request.end();
	request.on('response', function(response) {
			console.log(response.statusCode);
	});
}

Résultat :


node.js:205
        throw e; // process.nextTick error, or 'error' event on first tick
              ^
Error: EMFILE, Too many open files
    at net_legacy.js:741:19
    at Object.lookup (dns_legacy.js:159:5)
    at Socket.connect (net_legacy.js:729:20)
    at Object.createConnection (net_legacy.js:268:5)
    at new ClientRequest (http2.js:1041:23)
    at Client.request (http2.js:1475:11)
    at Object.<anonymous> (/home/touv/tests/node/loop02.js:11:23)
    at Module._compile (module.js:416:26)
    at Object..js (module.js:434:10)
    at Module.load (module.js:335:31)

Oups, NodeJS est-il incapable d'exécuter un traitement aussi simple ? Ou mon code est-il mauvais ?

Pool HTTP Request

Pour lancer nos 200000 requetes, il va falloir économiser la mémoire et les descripteurs, mais surtout s'adapter au mode asynchrone et éviter de vouloir balancer 200000 requêtes en parallèle !

Pour nous aider, il existe le module node-pool qui propose :
"a small but usefull resource pooling/limiting/throttling library".
Voilà donc la solution à notre question toute simple ...

#!/usr/bin/env node

var http = require('http');

var pool = require('generic-pool').Pool({
		name     : 'http_request',
		create   : function(callback) {
			var c = http.createClient(80, '127.0.0.1');
			callback(null, c);
		},
		destroy  : function(client) { },
		max      : 10,
		idleTimeoutMillis : 300,
		log : false
});


for(var i = 0; i < 200000; i++) {
	pool.acquire(function(err, client) {
			var request = client.request("GET", '/');
			request.end();
			request.on('response', function(response) {
					console.log(response.statusCode);
					pool.release(client);
			});
	});

}

19 août 2011

Mapping XML - Array - JSON en PHP

Vouloir établir une correspondance entre JSON et XML n'est pas nouveau. (cf. Converting Between XML and JSON). D'ailleurs, le Zend Framework propose sa solution XML to JSON conversion.
Cependant,

  • le format est-il adapté ?
  • l’opération inverse est-elle possible ?

Vouloir passer du XML au JSON revient en fait à transformer du XML en une structure mémoire de type tableau. Toutes les solutions en PHP que j'ai trouvé passe par cette étape. Aucune ne transforme la chaîne de caractères XML en chaîne de caractères JSON. La véritable question est donc :
Comment faire un mapping entre XML et un tableau PHP ?
Le format JSON n'étant plus qu'une sérialisation du tableau. Du coup, l'opération inverse n'est pas de vouloir transformer du JSON en XML mais bien de vouloir transformer un tableau en XML. Le problème est donc de passer d'une structure mémoire à un fichier XML et réciproquement. CQFD ;-)
La belle affaire me direz-vous ? C'est pareil ! Oui sauf qu'en abordant le problème de cette manière, on doit produire un code capable de transformer toute structure mémoire en XML, et cela a une influence directe sur le format d’équivalence que l'on va choisir. Et à ce petit jeu là, c'est le format proposé par Google qui me semble le plus adapté ! cf. Using JSON in the Google Data Protocol

XML_Array

Voici donc une petite classe PHP, disponible au travers d'un package PEAR qui permet de transformer n'importe quel document XML en tableau PHP. Mais qui permet également l'opération inverse à savoir transformer n'importe quel tableau PHP en fichier XML.

array2xml.php

<?php
require_once 'XML/Array.php';

$array_input = array (
  'rdf:RDF' => 
  array (
    'xmlns:rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
    'xmlns' => 'http://www.exemple.com/fake#',
    'rdf:Description' => 
    array (
      0 => 
      array (
        'rdf:about' => 'urn:id:aaa',
        'name' => 
        array (
          '#text' => 'Dupont T.',
        ),
        'email' => 
        array (
          'type' => 'main',
          '#text' => 'dupont@example.com',
        ),
      ),
      1 => 
      array (
        'rdf:about' => 'urn:id:bbb',
        'name' => 
        array (
          '#text' => 'Dupond D.',
        ),
        'email' => 
        array (
          'type' => 'main',
          '#text' => 'dupond@exeample.com ',
        ),
      ),
    ),
  ),
);

$xml = XML_array::export($array_input);
$array_output = XML_array::import($xml);

assert($array_input == $array_output);

?>

xml2array.php

<?php
require_once 'XML/Array.php';
$xml_input = <<<EOT
<rdf:RDF
	xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
	xmlns="http://www.exemple.com/fake#">
	<rdf:Description rdf:about="urn:id:aaa">
		<name>Dupont T.</name>
		<email type="main">dupont@example.com</email>
	</rdf:Description>
	<rdf:Description rdf:about="urn:id:bbb">
		<name>Dupond D.</name>
		<email type="main">dupond@exeample.com </email>
	</rdf:Description>
</rdf:RDF>
EOT;

$array = XML_Array::import($xml_input);
$xml_output = XML_array::export($array);
 
$xml_input = preg_replace(',\s+,', '', $xml_input);
$xml_output = preg_replace(',\s+,', '', $xml_output);

assert($xml_input == $xml_output);
?>

Tables de correspondance

Règles principales
ARRAY XML
key ⇒ array( key1 ⇒ value, ) <key key1="value/>
key ⇒ array( #text ⇒ value1, ) <key>value1</key>
key ⇒ array( array(…), array(…), ) <key>…</key><key>…</key>
0 ⇒ value <![CDATA[value]]>
key ⇒ value <row key="value" />
#comment ⇒ value <!--value-→
Clefs réservées
XML values key
Text node $t, _t, _text, #text
comment node $c, _c, _comment, #comment
xml:lang xmllang, xml:lang, xml$lang
xml:space xmlspace, xml:space, xml$space
xml:id xmlid, xml:id, xml$id
xml:idref xmlidref, xml:idref, xml$idref

Benchmark

Voici une petite comparaison de performance en chargeant en mémoire séquentiellement 196867 documents XML pour un volume total de 250 Mo.


xml2str   => 53,292876 Seconds 
--------------------------------
xml2array => 178,356342 Seconds 
--------------------------------
xml2dom   => 64,135433 Seconds 

Téléchargement et code source

Le code source est disponible sur GitHub : http://github.com/touv/xml_array

ou on peut directement l'installer avec PEAR en s'abonnant au Channel Respear :


% pear channel-discover pear.respear.net
% pear install respear/xml_array

Webographie