Parsowanie dokumentu HTML w .NET

Pewnie nie raz zastanawialiście się jak szybko i bez nadmiernego wysiłku wyciągnąć informacje z dokumentu HTML. Niedomknięty znacznik czy brak apostrofów to już standard w większości stron. XHTML po części rozwiązuje niektóre problemy, ale stron w pełni walidowanych też za dużo nie uświadczymy. Mimo podobieństw większość stron HTML nie  możemy traktować jak dokumentów XML. Platforma .NET nie dostarcza nam narzędzi do parsowania dokumentów HTML, pozostaje nam wiec korzystanie z zewnętrznych bibliotek.

W niniejszym wpisie chciałbym przedstawić bibliotekę którą poznałem już jakiś czas temu: Html Agility Pack, która nie raz już ułatwiła mi pracę. Główną zaletą tej biblioteki jest możliwość poruszanie sie po dokumencie HTML jak po dokumencie XML. Do wybierania elementów dokumentu mozemy uzyci języka XPath lub korzystając z LINQ (od wersji 1.4.0). Sam proces używania i posługiwania się wspomnianą biblioteką jest dość prosty i pokrótce zaprezentują go poniżej.

Oczywiście pierwszą czynnością jaką musimy wykonać aby móc korzystać z tej biblioteki jest dodanie do projektu referencji do pliku HTMLAgilityPack.dll. Po tej czynnosci mozemy korzystac z elementow jakie dostarcza nam biblioteka.

Klasą reprezentującą nasz dokument HTML jest HtmlDocument. Obiekt tej klasy tworzymy korzystając z domyślnego konstruktora.

[csharp]
WebClient client = new WebClient();
string html = client.DownloadString("http://blog.pietowski.com");

HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(html);
[/csharp]

Dokument HTML możemy wczytać korzystając z metody Load do której możemy przekazać strumień, obiekt klasy TextReader lub ścieżkę do pliku. Alternatywą jest użycie medody LoadHtml, którą wykorzystałem w powyższym przykładzie, wczytującej dokument bezpośrednio z obiektu klasy System.String. Przed wczytaniem dokumentu możemy ustawić odpowiednie opcje parsowania ustawiając odpowiednie wartości polom o nazwach w formacie:  OptionsXXX.

W celu pobranie błędów parsowania korzystamy z  dostępnej właściwości ParseErrors:

[csharp]
Console.WriteLine("Parse errors:");
foreach(HtmlParseError error in doc.ParseErrors)
{
Console.WriteLine(error.Reason);
}
[/csharp]

Główny węzeł dokumentu dostępny jest pod właściwością DocumentNode korzystając z tego obiektu możemy przeglądać kolejne węzły wczytanego dokumentu. W celu pobrania elementu na podstawie identyfikatora używamy metody GetElementbyId.

[csharp]
HtmlNode blogDescription = doc.GetElementbyId("blog-description");
if(blogDescription != null)
{
Console.WriteLine("Blog description: {0}",blogDescription.InnerText);
}
[/csharp]

Jeśli chcemy wyszukać konkretne węzły w naszym dokumencie możemy skorzystać z LINQ:

[csharp]
IEnumerable<HtmlNode> links = from link in doc.DocumentNode.DescendantNodes()
where link.Name == "a" && link.Attributes["href"] != null
select link;

IEnumerable<HtmlNode> links2 = doc.DocumentNode.DescendantNodes()
.Where(x=>x.Name == "a" && x.Attributes["href"] != null);
[/csharp]

lub wykorzystując język XPath:

[csharp]
HtmlNodeCollection xpathLinks =
doc.DocumentNode.SelectNodes("//a[@href]");

Console.WriteLine("Links:");
foreach(var link in links)
{
Console.WriteLine(link.Attributes["href"].Value);
}
[/csharp]

Najnowszą wersję opisywanej biblioteki można znaleźć na stronie http://htmlagilitypack.codeplex.com/

Projekt demonstrujący wykorzystanie HtmlAgilityPack można pobrać tutaj.