Ich versuche, einen Abschnitt einer Webseite mit HtmlAgilityPack in einem C # -Programm zu analysieren. Unten ist eine vereinfachte Version dieses Abschnitts der Seite (bearbeitet 30/30/2015 2:40 PM EST):
<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>
Ich versuche, das erste und einzige erste "a" -Tag mit dem GET-Parameter "a" im 3. oder 4. Kinddiv jedes div mit der Klasse "row-box" (die mit dem Wort "Correct") zu bekommen in ihnen im obigen Beispiel). Ich habe den folgenden XPath erstellt, der diese Knoten und nur diese Knoten sowohl im Inspektor von Chrome als auch im Firefath-Add-On für Firefox (zur besseren Lesbarkeit) enthält:
//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]
Wenn ich diese Seite jedoch mit HttpWebRequest lade, den Antwortstream in ein HtmlDocument-Objekt lade und SelectNodes (xpath) in seiner DocumentNode-Eigenschaft mit diesem XPath aufruft, gibt es nicht nur die drei korrekten Knoten zurück, sondern auch die beiden Tags mit dem Text "Falsch ausgewählt" im obigen Beispiel. Mir ist aufgefallen, dass dies effektiv dasselbe ist, als wenn ich den obigen XPath verwenden würde, außer ohne das letzte "[1]", wie dieses (zur besseren Lesbarkeit eingepackt):
//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=")]
Ich habe sichergestellt, dass ich die neueste Version von HtmlAgilityPack verwende, verschiedene Varianten meines XPath versucht, um festzustellen, ob es vielleicht eine willkürliche maximale Länge oder andere einfache Probleme hatte, und versuchte, ähnliche Probleme ohne Erfolg zu recherchieren. Ich habe versucht, eine noch einfachere HTML-Struktur mit dem gleichen Grundkonzept zu testen, konnte das Problem damit aber nicht reproduzieren, daher vermute ich, dass es ein subtiles Problem damit ist, wie HtmlAgilityPack etwas in dieser Struktur parst.
Wenn jemand weiß, was dieses Problem verursachen könnte, oder einen besseren Weg hat, einen XPath-Ausdruck zu schreiben, der die richtigen Knoten erhält und hoffentlich keine Probleme in HtmlAgilityPack verursacht, wäre ich sehr dankbar.
BEARBEITEN
Wie vorgeschlagen, hier ist eine vereinfachte Version des C # -Codes, den ich verwende, was ich bestätigt habe, reproduziert das Problem für mich.
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!");
}
Neue Antwort basierend auf aktualisiertem HTML
Wir können den Filter //a[contains(@href,'a=')][1]
, da dies das erste Element <a>
von seinem direkten Elternelement auswählt.
Wir müssen Klammern hinzufügen, um den Nachfahren-Operator in den Filter aufzunehmen, d
(//a[contains(@href,'a=')])[1]
Wenn wir jedoch diesen erweitern, um den ersten Nachkommenfilter auf jeden Knoten in einem anderen Knotensatz anzuwenden, ist der resultierende XPath-Ausdruck ungültig:
//div[contains(@class,'row-box')](//a[contains(@href,'a=')])[1]
Ich denke, wir müssen es in zwei Schritte aufteilen:
In C # sieht das so aus:
// 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]"));
}
Mit dieser Seite habe ich den xpath-Ausdruck zusammengestellt:
Wenn ich es in HtmlAgilityPack ausführe, bekomme ich nur diese drei Elemente zurück:
<a href = "/test/path?a=123">
<a href = "/test/path?a=abc&b=123">
<a href = "/test/path?a=ghi">
Hier ist eine Aufschlüsselung des Ausdrucks:
//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
Ich glaube, der Hauptunterschied zum xpath in deiner Frage ist /descendant::a[contains(@href,'a=') and position()=1]
abcendant /descendant::a[contains(@href,'a=') and position()=1]
vs /descendant::a[contains(@href,'a=')][1]
. Wenn Sie [1]
separat anwenden, filtern Sie als erstes Kind anstelle des ersten Nachkommens.