У меня есть довольно большой XML-файл, который я пытаюсь проанализировать с помощью приложения C # и HtmlAgilityPack. XML выглядит примерно так:
...
<tr>
<td><b>ABC-123</b></td>
<td>15</td>
<td>4</td>
</tr>
<tr>
<td>AB-4-320</td>
<td>11</td>
<td>2</td>
</tr>
<tr>
<td><b>ABC-123</b></td>
<td>15</td>
<td>4</td>
</tr>
<tr>
<td>AB-4-320</td>
<td>11</td>
<td>2</td>
</tr>
<tr>
<td>CONTROLLER1</td>
<td>4</td>
<td>3</td>
</tr>
<td>CONTROLLER2</td>
<td>4</td>
<td>3</td>
</tr>
...
В основном серия строк и столбцов таблицы, которая повторяется. Я сначала делаю поиск контроллера, используя:
string xPath = @"//tr/td[starts-with(.,'CONTROLLER2')]";
HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes(xPath);
foreach (HtmlNode link in nodes) { ... }
Что возвращает правильный узел. Теперь я хочу искать назад (вверх) для первого (ближайшего) совпадающего узла <td>
который начинается с текста «ABC»:
string xPath = @link.XPath + @"/parent::tr/preceding-sibling::tr/td[starts-with(.,'ABC-')]";
Это возвращает все совпадающие узлы, а не только ближайшие. Когда я попытался добавить [1] в конец этой строки XPath, он, похоже, не работал, и я не нашел примеров, показывающих, что предикат используется с такой функцией осей. Или, скорее, я делаю это неправильно. Какие-либо предложения?
Вы можете использовать этот XPath:
/parent::tr/preceding-sibling::tr[td[starts-with(.,'ABC-')]][1]/td[starts-with(.,'ABC-')]
Это приведет к поиску ближайшего предыдущего <tr>
, у которого есть дочерний <td>
начинающийся с 'ABC-'. Затем получим этот особый элемент <td>
.
Существует как минимум два подхода, которые вы можете выбрать при использовании HtmlAgilityPack:
foreach (HtmlNode link in nodes)
{
//approach 1 : notice dot(.) at the beginning of the XPath
string xPath1 =
@"./parent::tr/preceding-sibling::tr[td[starts-with(.,'ABC-')]][1]/td[starts-with(.,'ABC-')]";
var n1 = node.SelectSingleNode(xPath1);
Console.WriteLine(n1.InnerHtml);
//approach 2 : appending to XPath of current link
string xPath2 =
@"/parent::tr/preceding-sibling::tr[td[starts-with(.,'ABC-')]][1]/td[starts-with(.,'ABC-')]";
var n2 = node.SelectSingleNode(link.XPath + xPath2);
Console.WriteLine(n2.InnerHtml);
}
Если вы используете LINQ-to-XML вместо HAP, то это работает:
var node = xml.Root.Elements("tr")
.TakeWhile(tr => !tr.Elements("td")
.Any(td => td.Value.StartsWith("CONTROLLER2")))
.SelectMany(tr => tr.Elements("td"))
.Where(td => td.Value.StartsWith("ABC-"))
.Last();
Я получил этот результат:
<td>
<b>ABC-123</b>
</td>
(Который я проверил, был второй соответствующий узел в вашем примере, а не первый.)