Linq to Xml (XLinq) vs. XPath

Posted by – January 22, 2009

Préambule

Sous Blogger, on peut obtenir l’ensemble d’un blog sous forme d’un flux Atom, à des fins de sauvegarde (c’est d’actualités). La configuration, le thème, les billets et les commentaires sont inclus dans le flux.

Atom (voir RFC 4287) est un format ouvert de syndication et concurrent de RSS, qui lui, reste propriétaire (damned !). Le namespace utilisé dans le schéma XML d’Atom est http://www.w3.org/2005/Atom.

Atom permet d’étendre son format et d’intégrer des spécificités supplémentaires, ce qui le rend plus souple que RSS. Par exemple pour la plateforme blogger, les commentaires d’un billet, cette extension qu’on appellera threads est décrite dans le RFC 4685. Le namespace pour cette extension est http://purl.org/syndication/thread/1.0

On va s’intéresser ici au traitement XML d’un flux Atom, avec XPath et les fonctions du DOM et avec XLinq ou Linq To Xml.

Le souci sous Blogger, c’est qu’il n’y a pas de page qui récapitule l’ensemble des commentaires, comme on peut le voir sur d’autres blogware (Dotclear ou Typo).

Les commentaires sont accolés à chaque message. Cela peut-être intéressant d’avoir une interface (client “lourd” ou Web) pour les lire, ou d’autres personnes auraient également l’accès.

Un autre besoin, pourrait être d’extraire l’ensemble des billets / posts, et les commentaires associés afin de les réinjecter dans un autre moteur.

Traitement XML

On va traiter le fichier blog-od.xml (en annexe), qui est l’export du blog, avec 2 méthodes : l’habituelle, avec XPath, et la nouvelle, avec XLinq. Une difficulté ici est d’interroger les éléments en intégrant les namespaces Atom et threads, cela sera différent selon la technique utilisée.

Le fichier blog-od.xml est tiré d’un 1er essai de blog sur Blogger il y a quelques temps déjà, et est donc obsolète.

On recherche les billets, dont l’id contient la chaine “post-”, et pour chacun, les commentaires éventuels. On s’aidera de l’extension threads :

in-reply-to : représente un élément qui est une réponse à un billet. Le billet est pointé grâce à l’attribut ref, qui est l’id de ce dernier.

NB : Il y a certainement plus simple ou mieux pour chacune des techniques, à améliorer le cas échéant. Avouons-le, aimer traiter du XML, il faut être un peu maso (ou bourré).

La version XPath : on prend tous les éléments ayant pour id contenant post- et n’étant pas un commentaire (qu’une entry Atom n’ait pas d’élément thr:in-reply-to), on a ainsi tous les billets. Pour chacun d’eux, on recherche les entry constituant une réponse à ce dernier, c’est à dire une ref sur l’id du billet courant.

Code également disponible sous Pastie (en attendant de trouver une solution correcte pour son affichage sur le blog).

// // Affichage d'une entry // private string _post(XmlNode entry, XmlNamespaceManager nsmgr) {     return string.Format("{0} {1} {2}",          entry.SelectSingleNode("atom:title", nsmgr).InnerText,          entry.SelectSingleNode("atom:content", nsmgr).InnerText,          entry.SelectSingleNode("atom:link[@rel='alternate']", nsmgr) == null                ? string.Empty                : entry.SelectSingleNode("atom:link[@rel='alternate']", nsmgr).Attributes["href"].InnerText); }     [Test] public void XDoc_parse() {     Stopwatch sw = Stopwatch.StartNew();       var doc = new XmlDocument();     doc.Load("blog-od.xml");       // namespace par défaut : Atom + pour les threads     var nsmgr = new XmlNamespaceManager(doc.NameTable);     nsmgr.AddNamespace("atom", "http://www.w3.org/2005/Atom");     nsmgr.AddNamespace("thr", "http://purl.org/syndication/thread/1.0");       // recherche billets seulement                 var nodeList = doc.SelectNodes("//atom:id[contains(.,'post-') and not(following-sibling::thr:in-reply-to)]", nsmgr);     Console.WriteLine("# posts : {0}", nodeList.Count);     foreach (XmlNode id in nodeList)     {         var entry = id.ParentNode;         Console.WriteLine("POST : {0}", _post(entry, nsmgr));         // total dans le thread                       var total = entry.SelectSingleNode("thr:total", nsmgr);         if (total != null && int.Parse(total.InnerText) > 0)         {             Console.WriteLine("total réponses : {0}", total.InnerText);             // recherche commentaires             var reponses =                 doc.SelectNodes(string.Format("//thr:in-reply-to[@ref='{0}']", id.InnerText.Trim()),  nsmgr);             if (reponses != null)                 foreach (XmlNode rep in reponses)                     Console.WriteLine("REPONSE : {0}", _post(rep.ParentNode, nsmgr));         }       }       sw.Stop();     Console.WriteLine("{0} ms",sw.ElapsedMilliseconds); }

La version XLinq : on prend tous les Descendants des éléments entry qui représentent un billet. Pour chaque billet, on prend les commentaires associés.

Code sous Pastie.

[Test] public void XLinq_can_read_Feed() {     Stopwatch sw = Stopwatch.StartNew();       XNamespace nsatom = "http://www.w3.org/2005/Atom";     XNamespace nsthr = "http://purl.org/syndication/thread/1.0";       var xdoc = XDocument.Load("blog-od.xml");       // posts     var posts = from elt in xdoc.Descendants(nsatom + "entry")             where elt.Element(nsatom + "id").Value.IndexOf("post-") != -1             && elt.Element(nsthr + "in-reply-to")==null             select new             {              Id = elt.Element(nsatom + "id").Value,              Title = elt.Element(nsatom + "title").Value,              Summary = elt.Element(nsatom + "content").Value,              Link = (elt.Elements(nsatom + "link").                            Where(l => l.Attribute("rel").Value == "alternate").                             Select(l => l.Attribute("href").Value)).FirstOrDefault(),               DatePub = elt.Element(nsatom + "published")==null ?                     DateTime.Now :                     DateTime.Parse(elt.Element(nsatom + "published").Value),               NbComments=Convert.ToInt32(elt.Element(nsthr + "total").Value)              };       Console.WriteLine("# posts : {0}", posts.Count());     foreach (var e in posts)     {         Console.WriteLine(@"<a href=""{0}"">{1}</a> [{2}] ({3}) \  {4} ",                           e.Link,                           e.Title,                           e.NbComments,                           e.DatePub, e.Summary);         var r = from elt in xdoc.Descendants(nsatom + "entry")                 where elt.Element(nsatom + "id").Value.IndexOf("post-") != -1                 && elt.Element(nsthr + "in-reply-to") != null                 && elt.Element(nsthr + "in-reply-to").Attribute("ref").Value == e.Id.Trim().Replace("\ ","")                 select new                 {                     Id = elt.Element(nsatom + "id").Value,                     Title = elt.Element(nsatom + "title").Value,                     Summary = elt.Element(nsatom + "content").Value,                         Link = (elt.Elements(nsatom + "link").Where(l => l.Attribute("rel").Value == "alternate").                         Select(l => l.Attribute("href").Value)).FirstOrDefault(),                     DatePub = elt.Element(nsatom + "published") == null ?                      DateTime.Now :                      DateTime.Parse(elt.Element(nsatom + "published").Value)                 };         Console.WriteLine("REPONSE : {0}", r.Count());         foreach (var resp in r)             Console.WriteLine(@"<a href=""{0}"">{1}</a> ({2}) \  {3} ",                           resp.Link,                           resp.Title,                           resp.DatePub,                           resp.Summary);     }       sw.Stop();     Console.WriteLine("{0} ms",sw.ElapsedMilliseconds); }

On devrait certainement pouvoir n’écrire qu’une requête pour les posts et les réponses du post, en joignant les deux (avec les group, join), à approfondir.

Resharper nous transformera notre requête Linq en l’équivalent réécrit avec les méthodes d’extensions – sucre syntaxique nous dira-t-on – qui permet de lire la requête de façon naturelle (fluent interface), mais pas forcément tout aussi lisible quand la requête devient conséquente :

var posts = xdoc.Descendants(nsatom + "entry").                Where(elt => elt.Element(nsatom + "id").Value.IndexOf("post-") != -1                      && elt.Element(nsthr + "in-reply-to") == null).                Select(elt => new {                        Id = elt.Element(nsatom + "id").Value,                       Title = elt.Element(nsatom + "title").Value,                       Summary = elt.Element(nsatom + "content").Value,                       Link = (elt.Elements(nsatom + "link").                                     Where(l => l.Attribute("rel").Value == "alternate").                                         Select(l => l.Attribute("href").Value)).FirstOrDefault(),                         DatePub = elt.Element(nsatom + "published") == null                             ? DateTime.Now : DateTime.Parse(elt.Element(nsatom +"published").Value),                       NbComments =Convert.ToInt32(elt.Element(nsthr + "total").Value)});

Conclusion

Pour les habitués du SQL, Linq est plus naturel à écrire, même si une gymnastique est nécessaire au début. Il ne reste plus qu’à enrober tout ça d’une interface présentable, à suivre.

Google offre une API en javascript (pour ceux qui aiment) ou dans d’autres langages (.NET, JAVA, PHP, Python, Obj-C) pour gérer un blog sous blogger. Cette API se nomme gdata, et permet non seulement d’interroger blogger mais aussi tout un ensemble de services, voir sur la page gdata API. Une interface en ligne vous permettra de tester l’ensemble des APIs Google

Ressources

  • pour tester en ligne vos requêtes XPath : XPath test,
  • calculer combien coûte votre code en temps d’exécution même si dans notre cas, R# nous donne le résultat d’exécution du “test” en ms – au passage XLinq semble plus lent (mais l’exemple n’est pas forcément optimisé)

resharper-time.png

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>