Magazine

Linq to Xml (XLinq) vs. XPath

Publié le 22 janvier 2009 par Olivier Duval

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

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, pour aimer traiter du XML, il faut être un peu maso.

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.

 
 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:idcontains(.,'post-') and not(followi...", 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|http://msdn.microsoft.com/en-us/library/system.xml.linq.xdocument.descendants.aspx] des éléments entry qui représentent un billet. Pour chaque billet, on prend les commentaires associés.

///[csharp]

[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}) \n {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("\n","")
                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}) \n {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)});

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.

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


Retour à La Une de Logo Paperblog

A propos de l’auteur


Olivier Duval 4 partages Voir son profil
Voir son blog

l'auteur n'a pas encore renseigné son compte l'auteur n'a pas encore renseigné son compte