Центром библиотеки является класс HtmlDocument. В юнит-тестах у меня сплошь да рядом встречалась такая коснтрукция
[Test]
public void Test1()
{
var doc = new HtmlDocument();
doc.LoadHtml(SomeResource.SomePredefinedHtmlString);
...
}
Чтобы избавиться от лишней строчки, я добавил в HtmlDocument статический метод CreateFromHtml:
public static HtmlDocument CreateFromHtml(string html)
{
var document = new HtmlDocument();
document.LoadHtml(html);
return document;
}
Тогда тест приобретет следующий вид:
[Test]
public void Test1()
{
var doc = HtmlDocument.CreateFromHtml(SomeResource.SomePredefinedHtmlString);
...
}
Каждая нода документа позволяет выполнить XPath-запрос посредством методов SelectNodes и SelectSingleNode. Давайте остановимся на текущей реализации второго метода:
public IHtmlBaseNode SelectSingleNode(string xpath)
{
if (xpath == null)
{
throw new ArgumentNullException("xpath");
}
HtmlNodeNavigator nav = new HtmlNodeNavigator(_ownerdocument, this);
XPathNodeIterator it = nav.Select(xpath);
if (!it.MoveNext())
{
return null;
}
HtmlNodeNavigator node = (HtmlNodeNavigator)it.Current;
return node.CurrentNode;
}
Хм... Так не пойдет. Single - это один и только один. Давайте исправим логику метода SelectSingleNode, а для удобства реализуем еще и SelectSingleNodeOrDefault, SelectFirstNode и SelectFirstNodeOrDefault - в лучших традициях LINQ:
public IHtmlBaseNode SelectSingleNode(string xpath)
{
var it = CreateXPathNodeIterator(xpath);
if (!it.MoveNext()) // 0
throw new InvalidOperationException("Sequence contains no elements");
var nodeNavigator = (HtmlNodeNavigator)it.Current;
var currentNode = nodeNavigator.CurrentNode;
if (it.MoveNext()) // >1
throw new InvalidOperationException("Sequence contains more than one element");
return currentNode;
}
public IHtmlBaseNode SelectSingleNodeOrDefault(string xpath)
{
var it = CreateXPathNodeIterator(xpath);
if (!it.MoveNext()) // 0
return null;
var nodeNavigator = (HtmlNodeNavigator)it.Current;
var currentNode = nodeNavigator.CurrentNode;
if (it.MoveNext()) // >1
throw new InvalidOperationException("Sequence contains more than one element");
return currentNode;
}
public IHtmlBaseNode SelectFirstNode(string xpath)
{
var it = CreateXPathNodeIterator(xpath);
if (!it.MoveNext()) // 0
throw new InvalidOperationException("Sequence contains no elements");
var nodeNavigator = (HtmlNodeNavigator)it.Current;
return nodeNavigator.CurrentNode;
}
public IHtmlBaseNode SelectFirstNodeOrDefault(string xpath)
{
var it = CreateXPathNodeIterator(xpath);
if (!it.MoveNext()) // 0
return null;
var nodeNavigator = (HtmlNodeNavigator)it.Current;
return nodeNavigator.CurrentNode;
}
private XPathNodeIterator CreateXPathNodeIterator(string xpath)
{
if (xpath == null)
{
throw new ArgumentNullException("xpath");
}
var nav = new HtmlNodeNavigator(_ownerdocument, this);
return nav.Select(xpath);
}
Обратите внимание на то, что методы SelectNodes, SelectSingleNode, SelectSingleNodeOrDefault, SelectFirstNode и SelectFirstNodeOrDefault возвращают объекты типа IHtmlBaseNode. Однако зачастую после выполнения запроса приходится приводить IHtmlBaseNode к HtmlNode (тег), AttributeNode (атрибут) или HtmlTextNode (текст). Давайте сделаем так, чтобы можно было задавать тип возвращаемой ноды/нод. Для примера возьмем SelectSingleNode, сделаем его generic и создадим ряд методов-оберток:
public T SelectSingleNode(string xpath) where T: IHtmlBaseNode
{
var it = CreateXPathNodeIterator(xpath);
if (!it.MoveNext()) // 0
throw new InvalidOperationException("Sequence contains no elements");
var nodeNavigator = (HtmlNodeNavigator) it.Current;
var currentNode = (T)nodeNavigator.CurrentNode;
if (it.MoveNext()) // >1
throw new InvalidOperationException("Sequence contains more than one element");
return currentNode;
}
public IHtmlBaseNode SelectSingleNode(string xpath)
{
return SelectSingleNode(xpath);
}
public HtmlNode SelectSingleHtmlNode(string xpath)
{
return SelectSingleNode(xpath);
}
public HtmlAttribute SelectSingleAttributeNode(string xpath)
{
return SelectSingleNode(xpath);
}
public HtmlTextNode SelectSingleTextNode(string xpath)
{
return SelectSingleNode(xpath);
}
Аналогичный рефакторинг необходимо провести и для оставшихся четырех методов.
P. S. Из уважения к законам жанра я оставлю здесь эту ссылку на тему того, почему не стоит парсить HTML при помощи регулярных выражений.
8 комментариев:
Подскажите, в чем суть перехода на второй эксперементальный HAP? И как будет выглядеть конструкция, которая раньше выглядела примерно так:
HtmlNode _HN = doc.DocumentNode.SelectSingleNode("//td[h1]");
foreach (HtmlNode hn in _HN.SelectNodes("table[1]/tr[2]//th"))
т.е. как применить методы XPath к IHtmlBaseNode для получения второго уровня группировки? Только через LINQ?
Я невнимательно прочел Ваш пост, все ответы в нем есть, спасибо.
Судя по вашему второму комментарию, вы подумали, что приведенный в статье код содержится в экспериментальной ветке. Это не так. Это просто мои изменения.
Я уже заметил что во 2м экспериментальном билде это не реализовано. Пока выкручиваюсь так: HtmlNode hn = (HtmlNode)doc.DocumentNode.SelectSingleNode("//td[h1]");
Но душа болит что что-то не так ))
Так скачайте исходники экспериментальной бранчи, наложите описанные в посте изменения - и будет вам душевное спокойствие :)
Фраза "Анонимный комментирует..." - подбор слов как-то на нехорошие мысли наводит, может изменить немного? ))
Ну так авторизуйтесь :)
не совсем понял что такое версия 2 из бранчи, и где таки ее взять ?
Отправить комментарий