XPath donnant différents résultats dans les navigateurs et HtmlAgilityPack

c# html-agility-pack xpath

Question

J'essaie d'analyser une section d'une page Web à l'aide de HtmlAgilityPack dans un programme C #. Vous trouverez ci-dessous une version simplifiée de cette section de la page (modifiée le 1/30/2015 à 14 h 40 HNE):

<html>
    <body>
        <div id="main-box">
            <div>
                <div>...</div>
                <div>

                    <div class="other-classes row-box">
                        <div>...</div>
                        <div>...</div>
                        <div>
                            <p>
                                <a href="/some/other/path">
                                    <img src="/path/to/img" />
                                </a>
                            </p>
                            <p>
                                ...
                                <a href="/test/path?a=123">Correct</a> extra text
                            </p>
                        </div>
                        <div>
                            ...
                            <p>
                                <ul>
                                    ...
                                    <li>
                                        <span>
                                            <a href="/test/path?a=456&b=123">Never Selected</a>
                                            and <a href="/test/path?a=789">Never Selected</a>.
                                        </span>
                                    </li>
                                </ul>
                            </p>
                        </div>
                        ...
                    </div>

                    <div class="other-classes row-box">
                        <div>...</div>
                        <div>...</div>
                        <div>
                            <p>
                                No "a" tag this time
                            </p>
                        </div>
                        <div>
                            <p>
                                <ul>
                                    <li>
                                        <span>
                                            <span style="display:none;">
                                                <a href="/some/other/path">Never Selected</a>
                                            </span>
                                        </span>
                                    </li>
                                    <li>
                                        <span>
                                            <a href="/test/path?a=abc&b=123">Correct</a>
                                            and <a href="/test/path?a=def">Wrongly Selected</a>.
                                        </span>
                                    </li>
                                </ul>
                            </p>
                        </div>
                        ...
                    </div>

                    <div class="other-classes row-box">
                        <div>...</div>
                        <div>...</div>
                        <div>
                            <p>
                                <span>
                                    <a href="/test/path?a=ghi">Correct</a>
                                </span>
                            </p>
                            <p>
                                ...
                                <a href="/test/path?a=jkl">Wrongly Selected</a> extra text
                            </p>
                        </div>
                        <div>
                            <p>
                                <ul>
                                    ...
                                    <li>
                                        <span>
                                            <a href="/test/path?a=mno&b=123">Never Selected</a>
                                            and <a href="/test/path?a=pqr">Never Selected</a>.
                                        </span>
                                    </li>
                                </ul>
                            </p>
                        </div>
                        ...
                    </div>

                </div>
            </div>
        </div>
    </body>
</html>

J'essaie d'obtenir le premier et le seul premier "a" tag avec le paramètre GET "a" dans le 3ème ou le 4ème div enfant de chaque div avec la classe "row-box" (ceux avec le mot "Correct" en eux dans l'exemple ci-dessus). Je suis arrivé avec le XPath suivant qui obtient ces nœuds et uniquement ces nœuds dans l'inspecteur de Chrome et l'add-on Firepath pour Firefox (enveloppés pour la lisibilité):

//div[@id="main-box"]/div/div[2]/div[contains(@class, "row-box")]/div[
  (position() = 3 or position() = 4) and descendant::a[
    contains(@href, "a=")
  ]
][1]/descendant::a[contains(@href, "a=")][1]

Toutefois, lorsque je charge cette page à l'aide de HttpWebRequest, chargez le flux de réponse dans un objet HtmlDocument et appelez SelectNodes (xpath) sur sa propriété DocumentNode à l'aide de cette XPath, elle renvoie non seulement les trois nœuds corrects, mais également les deux balises contenant le texte. "Incorrectement sélectionné" dans l'exemple ci-dessus. J'ai remarqué que c'est effectivement la même chose que si je devais utiliser le XPath ci-dessus, sauf sans le dernier "[1]", comme ceci (enveloppé pour la lisibilité):

//div[@id="main-box"]/div/div[2]/div[contains(@class, "row-box")]/div[
  (position() = 3 or position() = 4) and descendant::a[
    contains(@href, "a=")
  ]
][1]/descendant::a[contains(@href, "a=")]

Je me suis assuré d'utiliser la dernière version de HtmlAgilityPack, j'ai essayé plusieurs variantes de mon XPath pour déterminer si elle atteignait peut-être une longueur maximale arbitraire ou d'autres problèmes simples de ce type, et j'ai essayé de rechercher des problèmes similaires sans succès. J'ai essayé de combiner une structure HTML encore plus simple en utilisant le même concept de base, mais je ne pouvais pas reproduire le problème avec cela. Je suppose donc que la façon dont HtmlAgilityPack analyse quelque chose dans cette structure peut être un problème subtil.

Si quelqu'un sait ce qui pourrait causer ce problème ou a un meilleur moyen d'écrire une expression XPath qui obtiendra les bons nœuds et n'espère pas causer de problèmes dans HtmlAgilityPack, je vous en serais très reconnaissant.

MODIFIER

Comme suggéré, voici une version simplifiée du code C # que j'utilise, qui, j’ai confirmé, reproduit le problème pour moi.

using System;
using System.Net;
using HtmlAgilityPack;

...

static void Main(string[] args)
{
    string url = "http://www.deerso.com/test.html";
    string xpath = "//div[@id=\"main-box\"]/div/div[2]/div[contains(@class, \"row-box\")]/div[(position() = 3 or position() = 4) and descendant::a[contains(@href, \"a=\")]][1]/descendant::a[contains(@href, \"a=\")][1]";
    int statusCode;
    string htmlText;

    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);

    request.Accept = "text/html,*/*";
    request.Proxy = new WebProxy();
    request.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0";

    using (var response = (WebResponse)request.GetResponse())
    {
        statusCode = (int)((HttpWebResponse)response).StatusCode;
        using (var stream = response.GetResponseStream())
        {
            if (stream != null)
            {
                using (var reader = new System.IO.StreamReader(stream))
                {
                    htmlText = reader.ReadToEnd();
                }
            }
            else
            {
                Console.WriteLine("Request to '{0}' failed, response stream was null", url);
                htmlText = null;
                return;
            }
        }
    }

    HtmlNode.ElementsFlags.Remove("form"); //fix for forms
    HtmlDocument doc = new HtmlDocument();
    doc.LoadHtml(htmlText);

    HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes(xpath);

    foreach (HtmlNode node in nodes)
    {
        Console.WriteLine("Node Found:");
        Console.WriteLine("Text: {0}", node.InnerText);
        Console.WriteLine("Href: {0}", node.Attributes["href"].Value);
        Console.WriteLine();
    }

    Console.WriteLine("Done!");
}

Réponse populaire

Nouvelle réponse basée sur le HTML mis à jour

Nous ne pouvons pas utiliser le filtre //a[contains(@href,'a=')][1] car il sélectionne le premier <a> élément de son parent direct.

Nous devons ajouter des crochets pour inclure l'opérateur descendant dans le filtre, c'est-à-dire

(//a[contains(@href,'a=')])[1]

Cependant, si nous développons cela pour appliquer le premier filtre descendant à chaque nœud d'un autre groupe de nœuds, l'expression xpath résultante n'est pas valide:

//div[contains(@class,'row-box')](//a[contains(@href,'a=')])[1]

Je pense que nous devons le diviser en deux étapes:

  1. Obtenez le groupe d'éléments div contenant le lien particulier que nous voulons.
  2. Obtenir le premier élément de lien descendant de chaque élément de ce groupe

En C # cela ressemble à:

// Get the <div> elements we know are ancestors to the <a> elements we want
HtmlNodeCollection topDivs = doc.DocumentNode.SelectNodes("//a[contains(@href,'?a=')]/ancestor::div[contains(@class,'row-box')]");

// Create a new list to hold the <a> elements
List<HtmlNode> linksWeWant = new List<HtmlNode>(topDivs.Count)

// Iterate through the <div> elements and get the first descendant
foreach(var div in topDivs)
{
    linksWeWant.Add(div.SelectSingleNode("(//a[contains(@href,'?a=')])[1]"));
}

Ancienne réponse

En utilisant cette page comme guide, j'ai assemblé l'expression xpath:

Lorsque je l'exécute dans HtmlAgilityPack, seuls les trois éléments suivants sont renvoyés:

<a href = "/test/path?a=123">
<a href = "/test/path?a=abc&b=123">
<a href = "/test/path?a=ghi">

Voici une ventilation de l'expression:

//div[contains(@class,'row-box')]        -> Get nodeset of <div class="*row-box*"> elements
/descendant::a                           -> From here get all descendant <a> elements
[contains(@href,'a=') and position()=1]  -> Filter according to href value and element being the first descendant

Je crois que la différence essentielle avec xpath dans votre question est /descendant::a[contains(@href,'a=') and position()=1] vs /descendant::a[contains(@href,'a=')][1] . Appliquer le [1] séparément filtre en tant que premier enfant au lieu du premier descendant.



Related

Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow