HtmlAgilityPack - пожалуй, самый популярный парсер HTML под .NET. В транке SVN'а лежит версия 1.4, но я бы посоветовал использовать версию 2 из бранчи. Я внес некоторые изменения в эту ветку и зарепортил их в виде патча. Пока неизвестно, попадут эти изменения в репозиторий или нет, поэтому вкратце опишу их в этом посте: может, тебе, мой дорогой читатель, эти изменения тоже пригодятся.
Центром библиотеки является класс 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 при помощи регулярных выражений.