[C#] Введение в XPath на примере простого парсера

Вступление.

Уверен, что не раз и не два приходилось сливать какой-нибудь контент со страниц сайта. Естественно, первое, что приходит на ум — это впихнуть регулярные выражения где только можно и побольше. Люди не просто так придумали афоризм:

Некоторые люди, когда сталкиваются с проблемой, думают «Я знаю, я решу её с помощью регулярных выражений.» Теперь у них две проблемы.

Вот и мы не будет останавливаться на них, а пойдем дальше. У нас есть DOM. Почему бы не обрабатывать документ как подобает, а не просто как строку html-кода? Будем использовать библиотеку HtmlAgilityPack (http://htmlagilitypack.codeplex.com/). Официальная страничка HAP говорит нам следующее:
» This is an agile HTML parser that builds a read/write DOM and supports plain XPATH or XSLT (you actually don’t HAVE to understand XPATH nor XSLT to use it, don’t worry…). It is a .NET code library that allows you to parse «out of the web» HTML files. The parser is very tolerant with «real world» malformed HTML. The object model is very similar to what proposes System.Xml, but for HTML documents (or streams).»
В меру вольном переводе это звучит так:

Это парсер HTML, который строит доступный для чтения / записи DOM и поддерживает простой XPATH или XSLT (Вам не нужно понимать XPATH ни XSLT, чтобы использовать его, не волнуйтесь …). Это. NET библиотека, которая позволяет работать с HTML файлами «вне сети». Анализатор терпим к «реальному» неправильному HTML. Объектная модель очень похожа на ту, что предлагает System.Xml, но для HTML документов (или потоков).

Хоть автор и говорит, что понимать XPath не обязательно, необходимо хотя бы знать, что это. Всезнающая Википедия дает такое определение (http://ru.wikipedia.org/wiki/XPath):

XPath (XML Path Language) — язык запросов к элементам XML-документа. Разработан для организации доступа к частям документа XML в файлах трансформации XSLT и является стандартом консорциума W3C. XPath призван реализовать навигацию по DOM в XML. В XPath используется компактный синтаксис, отличный от принятого в XML.

Начало работы

Выбираем сайт для экспериментов. Я взял сайт http://rabota-i-trud.com.ua/.
Качаем HAP с официального сайта.
Открываем нашу Visual Studio и создаем новый проект.

1

Здесь и далее разработка ведется на Visual Studio 2010 Express Edition под .NET 3.5

Теперь надо добавить в проект HAP.
2
В появившемся окошке выбираем вкладку «Browse» и находим библиотеку.
3
В Solution Explorer в References можно увидеть такое:
6
Осталось только прописать using HtmlAgilityPack; вначале и готово.

Исследуем целевой сайт

Всякие резюме находятся по ссылке http://rabota-i-trud.com.ua/shortres.php. Тут же можно провести поиск по критериям. Все как у людей.
8
Парсинг этого сайта состоит из двух частей:

  • Найти ссылки на интересующие нас страницы.
  • Выбрать из этих страниц необходимую информацию.

Посмотрим на «отличительные» черты ссылок на страницы с резюме.
9
Сразу бросается в глаза, что ссылки на резюме имеют атрибут class со значением newtitle. Просмотрев код страницы, приходим к выводу, что ни у каких других элементов на страницы атрибута class с таким значением нет. Так что можем использовать его как критерий поиска. А что если посмотреть с другой стороны? Что если взять полный путь от корня документа?
10
На рисунке он показан в самом низу. При программной реализации к этому еще вернемся.
Теперь надо посмотреть на страницу с резюме и выбрать критерии поиска DOM-узлов.
11
Тут тоже все оказалось не сложно. Нам подходят элементы td (ячейки таблицы), у который class = pad_resume.
В такой выборке мы получим
12
Отбросить левый столбец можно просто обходя список DOM-узлов с шагом +2 (начиная со второго элемента).

Кодим. Кодим. Кодим!

Для начала набросаем на форму элементы ввода/вывода. Пользователь должен вводить url с критериями выборки, первую страницу поиска и последнюю (это 3 TextBox’a). На выходе пользователь получаем много-много напарсенной информации (для простоты поставим RichTextBox). Еще парочка строк с описанием полей ввода и кнопка запуска парсинга. Получается что-то такое:
13
Пунктирные линии — это отголосок моей «любви» к «резиновым» дизайнам.

Теперь все-таки кодим.
Нам понадобится подключить несколько namespace:

using System.Net;
using System.Web;
using System.IO;
using System.Threading;

Парсинг будет идти в отдельном потоке. Подробнее о том, как грамотно писать многопоточное приложение, можно прочитать в статье deface (https://forum.xaknet.ru/showthread.php?t=11675). Посему, не буду останавливаться на этом.

Нам нужен метод для получения HTML-кода страницы. В Интернете существуют сотни примеров как это можно сделать. Возьмем один из них и немного допилим:

public string getRequest(string url)
        {
            try
            {
                var httpWebRequest = (HttpWebRequest) WebRequest.Create(url);
                httpWebRequest.AllowAutoRedirect = false;//Запрещаем автоматический редирект
                httpWebRequest.Method = "GET"; //Можно не указывать, по умолчанию используется GET.
                httpWebRequest.Referer = "http://google.com"; // Реферер. Тут можно указать любой URL
                using (var httpWebResponse = (HttpWebResponse) httpWebRequest.GetResponse())
                {
                    using (var stream = httpWebResponse.GetResponseStream())
                    {
                        using (var reader = new StreamReader(stream, Encoding.GetEncoding(httpWebResponse.CharacterSet)))
                        {
                            return reader.ReadToEnd();
                        }
                    }
                }
            }
            catch
            {
                return String.Empty;
            }
        }

Готово.

Теперь надо полученный HTML-код обработать. Для начала создаем объект класса HtmlDocument:

HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument();

Загружаем в doc полученный HTML:

doc.LoadHtml(getRequest(url));

Надо сказать несколько слов о синтаксисе XPath, который мы будем использовать. Базой языка XPath являются оси(ниже приведены только те, которые были использованы мной):

  • attribute:: — Возвращает множество атрибутов текущего элемента (сокращенно — @).
  • descendant-or-self:: — Возвращает полное множество потомков и текущий элемент (сокращенно — //).

Полный список можно найти по ссылке — http://ru.wikipedia.org/wiki/XPath#.D0.9E.D1.81.D0.B8

Так же есть ряд функций для работы с множествами:

  • [] — дополнительные условия выборки
  • / — определяет уровень дерева

Полный список можно найти по ссылке —
http://ru.wikipedia.org/wiki/XPath#….B0.D0.BC.D0.B8
Итак, необходимо составить правило для выборки ссылок на страницы с резюме. Мы говорили, что это элементы a с атрибутом class равным newtitle.

  • Для начала: //
  • Теперь надо указать, что выбираются ссылки: //a
  • Но надо конкретизировать, какие ссылки мы выбираем: //a[]
  • Надо ссылки с атрибутом class равным newtitle: //a[@class=’newtitle’]

Есть.
При просмотре страниц мы говорили, что можно взять полный путь от корня документа:
/html/body/center/div/div[2]/table[2]/tr/td/table[3]/tr/table/tr/td[2]/b/a
Оба правила отвечают одинаковым наборам элементов.
У созданного объекта doc есть свойство DocumentNode (указывает на верхний узел документа). У него же в свою очередь есть методы SelectNodes и SelectSingleNode. Первый выбирает коллекцию элементов, а второй — только один. Нам нужен первый метод.

HtmlNodeCollection c = doc.DocumentNode.SelectNodes("//a[@class='newtitle']");

Важно! Этот метод может вернуть null, если не будет найдено элементов.
Значит надо сделать проверку:

if (c != null)

Далее в цикле обрабатываем каждый элемент коллекции. У этих элементов нас интересует атрибут href (доступ к нему можно получить через массив атрибутов Attributes). И, если он не null, то загружаем страничку по этой ссылке. На загруженной страничке с резюме нас интересуют ячейки таблицы с атрибутом class равным pad_resume.
Правило для выборки будет таким:
//td[@class=’pad_resume’]
Как говорилось раньше, нам нужны только четные ячейки.
Да, если взять правило //td[@class=’list_info_resume’], то можно получить другие данные на странице с резюме (Образование, Опыт работы, Дополнительная информация, Пожелания к будущей работе).

Полный код метода приведен ниже:

private void start()
        {
            my_delegate = new add_text(add_text_method);
            // В цикле обрабатываем странички с first_page по last_page (те, что указали в полях ввода)
            for (int i = this.first_page - 1; i < this.last_page; i++)
            {
                string content = getRequest(tb_url.Text + this.param_separator + "p=" + i);
                HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument();
                doc.LoadHtml(content);
                // Получаем список ссылок на страницы с резюме
                HtmlNodeCollection c = doc.DocumentNode.SelectNodes("//a[@class='newtitle']");
                if (c != null)
                {
                    // Обрабатываем каждую страницу (парсим из нее выбранные данные)
                    foreach (HtmlNode n in c)
                    {
                        if (n.Attributes["href"] != null)
                        {
                            // Загружаем страничку с резюме
                            string u = main_url + n.Attributes["href"].Value;
                            string res_cn = getRequest(u);
                            // Парсим страничку с резюме
                            HtmlAgilityPack.HtmlDocument d = new HtmlAgilityPack.HtmlDocument();
                            d.LoadHtml(res_cn);
                            // Выбираем ячейки с нужными данными
                            HtmlNodeCollection pads = d.DocumentNode.SelectNodes("//td[@class='pad_resume']");
                            if (pads != null)
                            {
                                // Нам нужны только четные ячейки
                                int j = 1;
                                while (j < pads.Count) {
                                    // Записываем текст из ячейки в RTB (убираем все лишнее через trim)
                                    rtb_output.Invoke(my_delegate, new object[] { pads[j].InnerText.Trim() + ";" });
                                    j = j + 2;
                                }
                                rtb_output.Invoke(my_delegate, new object[] { "\n" });
                            }
                        }
                    }
                }
            }
            a_delegate = new set_text(set_text_method);
            // Убираем дубли
            rtb_output.Invoke(a_delegate, new object[] { array_unique(rtb_output.Lines) });\
            // Делаем элементы формы активными
            tlp_main.Enabled = true;
            // Завершаем поток
            tr.Abort();
        }

Результат работы программы видно на рисунке ниже:
14

Выводы

Как видим, работа с Xpath очень проста и позволяет получить требуемый результат без особых усилий. В этом небольшом мануале показана малая часть возможностей XPath и HAP. Сокрытое остается на рассмотрение читателей.
Скачать исходники
Скачать текст

(с) K_S for XakNet.Ru

, , , , , ,

16 комментариев
  1. freeaces Said:

    Спасибо!

  2. KronuS Said:

    freeaces, я рад, что сия небольшая статья Вам помогла 🙂

  3. Skami Said:

    Прикольно. На заказ парсеры, часом, не пишите? 🙂

  4. KronuS Said:

    Пишу.
    Но зависит от сайта, с которого надо парсить.

  5. Vlad Said:

    Если тема все еще актуальная, не мог бы автор написать/заснять инструкция, по создания парсера для браузерного приложения, например для какой нибудь браузерной игры.
    _____
    Заранее благодарен http://kronus.me/wp-includes/images/smilies/icon_smile.gif

  6. KronuS Said:

    Vlad, увы, но тема про написание парсеров на заказ уже не актуальна.

  7. Masha Said:

    Эх, если бы Вы написали как еще формы эти делать, было бы вообще супер

  8. KronuS Said:

    Masha, ничего хитрого. Одна форма создается сразу по созданию нового проекта. А если в проект надо добавить еще одну форму, то «Проект -> Добавить форму Windows».

  9. Тоха Said:

    Здравствуйте,а не могли бы вы,сделать или изменить текущий пример под такую задачу —
    сайт — http://www.057.ua/dosug/446
    нужно взять информацию о кафе и барах с первой по последнюю страницы
    Название,адрес,телефон и вывести в ричбокс с последующим сохранением в csv.
    Огромное пожалуйста)

  10. Иван Said:

    Огромное спасибо за статью. Очень многое стало понятно.

  11. Serge Said:

    XPath получаю с помощью браузера Chrome или FireFox.
    Если вручную прописать XPath — HAP работает, если из браузера скопировать — HAP даёт исключение.

    И ещё вопрос — с HTML5 будет работать?

  12. Иван Said:

    У вас ссылка вида — <a class="newtitle"…… ,а у меня что-то вроде <a href="site.com/…" class="tut-class" и т.д. и выборка с помощью "//a[@class='tut-class']" не помогает. Подскажите верное решение.

  13. KronuS Said:

    Иван, должно с любым классом работать. Скорее всего, проблема в другом.

  14. Иван Said:

    Нашел выражение, которое у меня сработало)) «.//*[@id=’page-wrapper’]/main/div/section/a»

  15. Евгений Said:

    Спасибо за урок ,хорошая работа и всё понятно объяснили!Даже и не думал ,что всё так не сложно ,обязательно воспользуюсь вашим примером и напишу парсер ,класс! P.S. Рад ,что зашёл на эту страницу 🙂 ,добавлю ка я её в закладки 😉

  16. Aleks Said:

    Статья супер,жалко что сейчас код не работает,мб руки кривые,а может логику проверки страниц надо переписывать ибо сайты уже не те+ в основном JS подгрузка идет.
    Есть статьи, с рабочим кодом?

Оставить комментарий

Top ↑ | Main page | Back