blog.virgule.info

Ce blog est écrit à quatre mains et autant de pieds par Mathieu Pillard (mat, billets bleus) et Muriel Méric-Nay (kerdekel, billets verts). Si vous n'arrivez pas à comprendre qui poste quoi, c'est soit que vous êtes atteint d'une forme rare de daltonisme soit que vous n'avez pas été lire notre introduction, donc allez zou, allez y, nous on en a marre de tout re-expliquer :-)

TreeWalker, XPath, getElementsByTagName et compagnie sont dans un bateau (Openweb)

Dans mon précédent post sur le sujet, je suis resté quelque peu évasif sur l'interet de mon code et sur les diverses optimisations possibles: je voulais juste exposer le bug que j'avais rencontré avec Konqueror.

Ici, nous allons reprendre tout depuis le début. Le probleme est donc le suivant: Soit un article de linuxfr, avec zero, un ou plusieurs commentaires. Chaque commentaire est dans un <div class="comment ...">. Il faut effectuer diverses actions sur chaque commentaire trouvé, tel le cacher ou non selon son score, le mettre a part si il est nouveau, etc.

Pour répondre a ce probleme, j'ai pris 4 approches différentes. On considere pour la démonstration que nos élements n'ont qu'une seule class (ce qui n'est pas forcement le cas en pratique), et que on doit commencer a chercher des la racine du document, pas apres. On ne prend pas non plus en compte les différences en terme d'occupation mémoire, ca sera pour un autre article.

Note: tous ces examples ont été testés uniquement avec Firefox. Ils devraient marcher avec des modifications mineures dans tous les autres navigateurs, a l'exception d'IE.sci

getElementsByTagName()

C'est la méthode la plus naive. On fait un getElementsByTagName('div'), et on regarde l'attribut .className de chaque résultat pour oui ou non l'ajouter dans un tableau final. Le code:

function getComments()
{
	var a   = document.getElementsByTagName('div');
	var len = a.length;
	var out = new Array();
	
	for (var i = 0; i < len; i++)
	{
		if (a[i].hasAttribute('class') && a[i].className == "comment")
			out.push(a[i]);
	}
	return out;
}

Parseur maison

Code écrit a l'origine par Daniel Glazman un jour ou j'avais donné la méthode naive a quelqu'un qui cherchait un getElementsByClassName(). On parse notre document recursivement et quand on trouve ce qu'on cherche, on l'ajoute directement.

function getComments()
{
	function _GetElementsByClass(outArray, seed)
	{
		while (seed)
		{
			if (seed.nodeType == Node.ELEMENT_NODE)
			{
				if (seed.hasAttribute("class") && seed.className == "comment")
					outArray.push(seed);
				_GetElementsByClass(outArray, seed.firstChild)
			}
			seed = seed.nextSibling;
		}
	}
	
	var outArray = new Array();
	_GetElementsByClass(outArray, document.documentElement);
	return outArray;
}

TreeWalker

A mon avis le plus lisible et le plus flexible: meme sans connaitre on devine ce que fait le code du premier coup d'oeil, et c'est simple a modifier Par ailleurs, on utilise un truc implémenté nativement par le navigateur, on devrait donc etre rapide. Voir plus bah pour la meme chose en plus complexe.

function getComments()
{
	function acceptNode(node)
	{
		if (node.hasAttribute("class") && node.className == "comment")
		{
				return NodeFilter.FILTER_ACCEPT;
		}
		return NodeFilter.FILTER_SKIP;
	}
	
	var out        = new Array();
	var treeWalker = document.createTreeWalker(document.body, 
	                                           NodeFilter.SHOW_ELEMENT,
	                                           acceptNode,
	                                           false);
	if (treeWalker)
	{
		var node = treeWalker.nextNode();
		
		while (node)
		{
			out.push(node);
			node = treeWalker.nextNode();
		}
	}
	return out;
}

XPath

Donné par Dylan Schiemann en réponse au post de Daniel sur les différentes méthodes de getElementsByClassName, et redonné par Dam lors de mon récent post sur le TreeWalker et Konqueror, on profite du fait que on manipule du XML, et on utilise XPath.

function getComments()
{
	var xpathResult = document.evaluate('//*[@class = "comment"]', document, null, 0, null);
	var outArray = new Array();
	var item; 
	
	while (item = xpathResult.iterateNext())
	{
		outArray[outArray.length] = item;
	}
	return outArray;
}

Voyons ce que cela donne. Chaque script a été relancé 10 fois sur un gros (> 700 commentaires) article de linuxfr tres legement nettoyé, et le cache vidé entre chaque essai. C'est loin d'etre une méthode tres scientifique, mais ca suffira ici. Les temps sont exprimés en millisecondes, meme si cela n'est pas tres important vu que par flemme je ne donnerais ni les pages ni les specs de la machine sur laquelle j'ai fait tourner tout :-)

  1. Résultat pour getElementsByTagName(): 200
  2. Résultat pour Parseur maison: 5000
  3. Résultat pour TreeWalker: 2400
  4. Résultat pour XPath: 180

(Rappel: je ne teste pas les différentes implémentations de getElementsByClassName. Je teste différentes manieres d'utiliser le DOM pour récupérer des commentaires sur linuxfr, en simpliant grandement. Ces chiffres sont donc a prendre avec des pincettes, ils servent surtout a annoncer d'autres méthodes dans la suite de ce post :-)

Les résultats sont plutot interessants. J'ai bien tout verifié pour etre sur que je n'hallucinais pas. Vu que c'est pas tellement ce que j'attendais, les conclusions attendront: je vais passer un peu de temps à regarder ce que je peux faire sur la DLFPToolbar au vu de ces résultats.

Edit: Visiblement, je suis pas le seul à m'interesser au probleme. La version svn de prototype a l'air excellente.


Commentaires

  1. En valà un billet qu'il est bien.

    Merci :)

    Posté le jeudi 15 décembre 2005 à 11:50 par Thanh

  2. Les résultats sont très surprenants en effet. Je m'interroge juste sur la pertinence du test "naïf". En effet, la méthode getElementByTagName récupère l'ensemble des éléments div contrairement aux autres méthodes qui ne présume pas de l'élément. Cette supposition permet-elle de gagner de précieuses millisecondes ? Quel sont les temps avec un getElementByTagName("*") ?

    Posté le mardi 20 décembre 2005 à 09:14 par Pierre

  3. J'y ai pensé, mais j'ai oublié de tester avec '*'. (J'ai banni cette construction de mon esprit :-) Je retesterais ce soir si j'y pense.

    Par contre, je pensais pouvoir gagner du temps dans le TreeWalker en utilisant NodeFilter.FILTER_REJECT, vu que je sais pas mal de trucs sur ce que j'ai a parser, mais les gains sont minimes, meme en rejettant des grosses parties complexes du document.

    Posté le mardi 20 décembre 2005 à 12:38 par mat

  4. Je suis allé de mon petit test aussi :-)

    Il semble que la différence avec un getElementsByTagName("*") ne soit pas vraiment significative.

    J'ai également testé avec un nodeIterator (identique au TreeWalker mais initialisé avec un document.createNodeIterator). Le client utilisé correspond à Opera puisque Firefox ne les implémente pas correctement. Le résultat est que j'obtiens un facteur trois entre la mode TreeWalker et la façon NodeIterator qui est plus rapide. Par contre avec la version 9p1 d'Opera c’est toujours la version XPAth qui mène la dance.

    Posté le mardi 20 décembre 2005 à 14:51 par Pierre

Les commentaires pour ce billet sont fermés.