Пользователь
0,0
рейтинг
22 января 2011 в 09:06

Разработка → Html Agility Pack — удобный .NET парсер HTML

.NET*
Всем привет!
Как-то раз мне пришла в голову идея проанализировать вакансии размещенные на Хабре. Конкретно интересовало, есть ли зависимость между размером зарплаты и наличия высшего образования. А еще сейчас у студентов идет сессия (в том числе и у меня), то возможно кому-то уже надоело трепать нервы на экзаменах и этот анализ будет полезен.
Так как я программист на .Net, то и решать эту задачу — парсить объявления на Хабре я решил на C#. Вручную разбирать строки html мне не хотелось, поэтому было придумано найти html-парсер, который помог бы осуществить задачу.
Забегая вперед скажу, что из анализа ничего интересного не вышло и сессию придется сдавать дальше :(
Но зато немножко расскажу про весьма полезную библиотеку Html Agility Pack

Выбор парсера


Вышел на эту библиотеку я через обсуждение на Stackoverflow. В комментариях предлагались еще решения, например библиотека SgmlReader, которая переводит HTML в XmlDocument, а для XML в .Net инструментов полный набор. Но почему-то меня это не подкупило и я пошел качать Html Agility Pack.

Беглый осмотр Html Agility Pack


Справку по библиотеке можно скачать на странице проекта. Функционал на самом деле очень радует.
Всего нам доступно двадцать основных классов:



Названия методов соответствуют интерфейсам DOM (замечание k12th) + плюшки: GetElementbyId(), CreateAttribute(), CreateElement() и т.д., так что работать будет особенно удобно, если приходилось сталкиваться с JavaScript
Похоже, что html все же перегоняется в Xml, а HtmlDocument и др. классы это обертка, ну и ничего страшного в этом, ввиду этого доступны такие возможности как:
  • Linq to Objects (via LINQ to Xml)
  • XPATH
  • XSLT

Парсим хабр!


Вакансии на хабре представлены в виде таблицы, в строках дана информация о требуемой специальности и зарплате, но так как нам нужна информация об образовании, то придется переходить на страницу вакансии и разбирать ее.
Итак, начнем, нам нужна таблица, чтобы вытащить оттуда ссылки и инфу о позиции с зарплатой:
  1. static void GetJobLinks(HtmlDocument html)
  2. {
  3.     var trNodes = html.GetElementbyId(«job-items»).ChildNodes.Where(=> x.Name == «tr»);
  4.  
  5.     foreach (var item in trNodes)
  6.     {
  7.         var tdNodes = item.ChildNodes.Where(=> x.Name == «td»).ToArray();
  8.         if (tdNodes.Count() != 0)
  9.         {
  10.             var location = tdNodes[2].ChildNodes.Where(=> x.Name == «a»).ToArray();
  11.  
  12.             jobList.Add(new HabraJob()
  13.             {
  14.                 Url = tdNodes[0].ChildNodes.First().Attributes[«href»].Value,
  15.                 Title = tdNodes[0].FirstChild.InnerText,
  16.                 Price = tdNodes[1].FirstChild.InnerText,
  17.                 Country = location[0].InnerText,
  18.                 Region = location[2].InnerText,
  19.                 City = location[2].InnerText
  20.             });
  21.         }
  22.  
  23.     }
  24.  
  25. }

А после осталось пройти по каждой ссылке и вытащить инфу об образовании и заодно еще и занятость — здесь есть небольшая проблема в том, что если таблица с ссылками на вакансию лежала в div-е с известным id, то информация о вакансия лежит в таблице без всяких id, поэтому пришлось немножко поизвращаться:
  1. static void GetFullInfo(HabraJob job)
  2. {
  3.     HtmlDocument html = new HtmlDocument();
  4.     html.LoadHtml(wClient.DownloadString(job.Url));
  5.     // html.LoadHtml(GetHtmlString(job.Url));
  6.  
  7.     // так делать нельзя :-(
  8.     var table = html.GetElementbyId(«main-content»).ChildNodes[1].ChildNodes[9].ChildNodes[1].ChildNodes[2].ChildNodes[1].ChildNodes[3].ChildNodes.Where(=> x.Name == «tr»).ToArray();
  9.  
  10.     foreach (var tr in table)
  11.     {
  12.         string category = tr.ChildNodes.FindFirst(«th»).InnerText;
  13.  
  14.         switch (category)
  15.         {
  16.             case «Компания»:
  17.                 job.Company = tr.ChildNodes.FindFirst(«td»).FirstChild.InnerText;
  18.                 break;
  19.             case «Образование:»:
  20.                 job.Education = HabraJob.ParseEducation(tr.ChildNodes.FindFirst(«td»).InnerText);
  21.                 break;
  22.             case «Занятость:»:
  23.                 job.Employment = HabraJob.ParseEmployment(tr.ChildNodes.FindFirst(«td»).InnerText);
  24.                 break;
  25.             default:
  26.                 continue;
  27.         }
  28.     }
  29. }

Результаты


Ну а дальше, сохраняем результаты в XML и смотрим в Excel-e, что же получилось… и видим, что ничего хорошего не получилось, потому что большинство компаний либо не указывают размер зарплаты, либо не указывают информацию об образовании (забывают, указывают в теле вакансии, или действительно неважно), либо не указывают все сразу.
Кому интересно, вот результаты в xlsx и xml, а здесь исходник

P.S.


При парсинге возникла такая проблема — страницы скачивались очень медленно. Поэтому я сначала попробовал WebClient, а потом WebRequest, но разницы не было. Поиск в гугле указал на то, что следует явно отключать Proxy в коде, и тогда все будет хорошо, однако это тоже не помогло.
Nail Salikhov @kastaneda
карма
5,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (60)

  • –1
    Когда мне надо было парсить html, я пользовался регулярными выражениями. А можно было проще…
    • +12
      • 0
        Спасибо. Теперь я тоже это знаю.
        • +2
          как мне кажется, через это проходили если не все, то многие :-)
          года с полтора назад я писал качалку музыки с известного ресурса — ссылки парсились регулярками, а когда появилась идея про вакансии на хабре, мысль с регулярками как-то даже и не пришла в голову — все приходит с опытом
          • +1
            Я ненормальный — когда все использовали регулярные выражения, я использовал конечные автоматы :)
            • 0
              Надеюсь это не патология, ибо нас таких минимум двое)
      • +4
        «asking regexes to parse arbitrary HTML is like asking Paris Hilton to write an operating system» © от туда (-:
      • 0
        Всему надо знать меру. Если у вас есть html и надо получить, к примеру, title, не ужели будете подключать сторонние библиотеки и парсить весь html? Regex в данном случае продуктивнее. А то что парень по ссылке с пеной у рта отстаивает свою правоту, так это, извините, диагноз.
        • 0
          Это не пена у рта, это сатира.
          • 0
            Если бы актеру пришлось переиграть это обращение к народу, ему бы пришлось использовать «пену», иначе получилось бы не правдоподобно)
            Юмор, и сатира в частности, служит высмеиванию чего то не правильного, а, как говорилось, Regex все же уместен в некоторых случаях, поэтому как то не очень смешно.
        • 0
          Посмотрите второй ответ.
          • 0
            Не загоняйте меня в цикл) Я всего лишь хочу сказать, что не стоит так насмехаться над регулярками. Они в сотнях случаях эффективнее любых парсеров, поэтому сравнивать с пэрис это кощунство!)
            • 0
              Регулярки лучше только в одном случае — нужно очень быстро вытащить относительно небольшой кусок простых данных в неизменяемых и известных исходных данных. Я сам был большой любитель регулярных выражений, пока не столкнулся с необходимостю вытаскивать title(в том числе) из тысяч страниц — вот тут то и стало понятно, что весь код надо нафиг переделать.
              • 0
                А вот у меня такая ситуация: В одном проекте надо получать title, favicon и description страницы:
                • через HttpWebResponse получаю первые 5000 байт
                • регуларкой получаем charset encoding
                • регуляркой получаем нужние данные + иногда indexOf
                Согласен, что немного спартанские методы, но зато уже длительное время все работает шустро и стабильно. И только исxодя из этого, я и говорю, что иногда не стоит отказываться от регулярныx выражений, потому что у самого нету с ним негативного опыта. Конечно, весь документ парсить таким образом мне и в голову не приxодило.
                • 0
                  регуларкой получаем charset encoding

                  Могу предположить что у вас либо только английский язык, либо постоянные кракозябры в результатах.

                  Мне приходится парсить русско-/англо-/украино- язычные сайты и «простая» процедура получения кодировки сводится к тонне кода и собственному бранчу chardet.

                  В одном проекте надо получать title, favicon и description

                  Опять-же, могу предположить что часть фавиконок у вас не находится, потому что люди прописывают фавикон как минимум десятком неправильных, но понимаемых браузером способов(если вообще прописываются).
                  • 0
                    На вxод, в массе, кириллица — проблем ни разу не было! Если в коде указан charset encoding его и используем, если нет, тогда — Ude.

                    С title, description и favicon ничего предполагать не надо, так как я все же вижу результаты и это все пишу не просто «выпендриться», а посоветовать, что бы люди не списывали со счетов Regex. Если вас устраивают другие варианты, то я только рад;). Меня же тот, который описал!
                    • 0
                      Покажите пожалуйста для образовательных целей регэксп для фавикона.
                      • +1
                        var link = Regex.Match(document, "(<([^>]*link[^>]+(rel[\\s]*=[\\s]*('|\"|)(shortcut|icon))[^>]+)>)", RegexOptions.IgnoreCase).Value;
                              var favicon = Regex.Match(link, "(href[\\s]*=[\\s]*('|\"|)([^'\"\\s]+)('|\"|))", RegexOptions.IgnoreCase).Value;
                        • +1
                          Извините убирал лишнее и пропустил: в получении урл используем 3 найденую группу.
                    • 0
                      Если в коде указан charset encoding его и используем

                      Цифра по памяти, но у меня процента 3 страниц указывает кодировку одну, а использует другую и процентов 6 указывают кодировку почти правильно, т.е. неправильно пишут название кодировки.
                      • 0
                        6% отпадают, так как Encoding.GetEncoding(encodingName) не обработает не верное имя -> заюзаем тогда Ude. За 3% ваша правда, но пока что все в пределаx нормы, если что, так перейду полностью на Ude.
    • +3
      Some people, when confronted with a problem, think «I know, I'll use regular expressions.» Now they have two problems

      (С) Jamie Zawinski
      • +1
        Эта цитата обязательно появляется в любом месте, где упоминались регулярные выражения.
        И самое интересное, что она не надоедает.
        • 0
          Потому что это правда.
          • 0
            Это точно.
  • +3
    Очень хороший dom-парсер, долго использую его. Особенно удобно, что умеет dom-модель достраивать и XPath выполнять.
    • +1
      из минусов я выделил только то, что нет родных методов GetElement(s)ByTag / ByName / ByClass — хотя это все легко реализуется с помощью LINQ
      • 0
        Хм… а можно же extension написать.
      • 0
        Тогда уж сразу querySelector бы…
  • +1
    Единственная проблема, автор не спешит принимать патчи, да и вообще как-то затерялся. Так что многое приходится патчить вручную.
  • +3
    Названия методов заимствованы из JavaScript + плюшки: GetElementbyId(), CreateAttribute(), CreateElement() и т.д.

    Справедливости ради отмечу, что это не заимствование, а интерфейсы DOM. В JS они просто взяты оттуда же:)
    • 0
      спасибо за замечание, сейчас поправлю :-)
    • 0
      GetElementbyId() вещь конечно хорошая, только у нее есть косяк в том, что этот метод есть только у HtmlDocument…
      А при работе уже с HtmlNode такого метода нет. И это крайне не удобно, если делаешь не поиск 1 div в огромном документа, а когда делаешь довольно большой разбор
      • 0
        Согласно стандарту, id должен быть уникальным в пределах документа. Так что как бы и нет смысла искать по id в пределах определенной ноды (хоть это и не совсем так). В JS, например, у нод тоже нет этого метода — всё согласно стандарту.
        DOM-методы — не самая удобная вещь на свете.
        • 0
          Стандарты стандартами, к ним очень вольно относятся разработчики.
          Я тут парсил вики на выходных, и дубляж обнаружил даже там где-то.
          По этому стандарт стандартом, а парсер должен работать.
          • 0
            Вики? Википедию? Неуникальные id'ы? плохо:(

            Все-таки искать по id внутри ноды это одно, а его неуникальность — другое. Если мы предположим, что атрибут id может быть не уникальным, то нам придется из метода, название которого говорит, что он возвращает один элемент, возвращать коллекцию.

            Если вам действительно, на практике, необходимо такое поведение, то можно отнаследоваться и поступить так, как в js-библиотеке MooTools:
            function (id) {
                var founded = document.getElementById(id);
                if(!founded) {
                    return null;
                }
                for (var parent = founded.parentNode; parent != this; parent = parent.parentNode) {
                    if (!parent) {
                        return null;
                    }
                }
                return founded
            }
            
  • +1
    Я этот парсер использую для фильтрации того хлама, что пишут в форму написания поста/комментария пользователи. Достаточно хорошая штука, ни разу не подводила.
  • 0
    >возникла такая проблема — страницы скачивались очень медленно.

    Такую проблему решал распараллеливанием. На некоторых сайтах 25 потоков спокойно работают. Хотя хабр, как понимаю, ограничения накладывает на кол-во запросов. Получится около 1-2секунды на стр
  • –4
    Ничего полезного — в смысле те кто имеют высшее получают больше или как?
    • 0
      в смысле, что недостаточно данных для того чтобы вести речь о какой-либо зависимости.
      хотя один вывод пожалуй сделать можно — серьезные отечественные компании (например Rambler, СКБ Контур, QIWI Кошелек, Parallels) требуют наличия высшего образования.
      впрочем, вы можете и сами взглянуть на отчете, который приложен в результатах топика
  • 0
    Зачем такие страшные конкструкции, если заявлена поддержка xpath?
    • 0
      сам с xpath никогда не сталкивался и узнал про него когда страшная конструкция уже была написана. поэтому, оставлю на будущее
      • 0
        Год назад были проблемы с поддержкой xpath — простые конструкции он обрабатывал верно, но на что-нибудь более-менее сложное возвращал пустой результат.
  • 0
    Как со скоростью обработки тегов?
    По сравнению с регулярками медленней?
    • 0
      документ слишком маленький, пожалуй, на нем серьезного сравнения не провести.
      в любом случае, разбирать DOM-деревья регулярными выражениями — плохой выбор (см. комментарий выше)
    • 0
      Да, если планируете использовать его на продакшене, под нагрузкой — не советую. Будет медленнее раз в 5-10, чем регулярки. Но это недостаток всех dom-парсеров.
      • 0
        Так это смотря на «продакшене» ЧЕГО.
        Если не большого массива информации за короткое время, а обычных сайтов, периодически обновляемой информации, то удобство окупается.
        Вспоминаю парсинг html регулярками как страшный сон.
        • 0
          ну я написал, под нагрузкой. предполагал, что то, о чём вы говорите, будет понятно
  • 0
    В HtmlAgilityPack есть класс HtmlWeb, с помощью которого можно сразу получить HtmlDocument, вызвав метод экземпляра Load и передав в него в качестве параметра url страницы, так что можно было обойтись возможностями лишь HtmlAgilityPack в рамках этой задачи. У вас на скриншоте этот класс, кстати, есть.
    • 0
      спасибо за наводку, я это пропустил
    • 0
      Спасибо
  • 0
    Я для парсинга субтитров с ted.com использовал эту штуку, удобно очень.
  • +2
    Тоже пытались использовать. Удобно бесспорно, но у нас были очень большие объемы (много страниц парсились одновременно), а он к сожалению выжирает очень много памяти, так как хранит на момент работы всю Dom структуру документа. И к сожалению пришлось вернуться к регулярным выражениям. Т.е. на больших объемах обрабатываемых параллельно, готовьтесь к большим утечкам памяти. Но, на мой взгляд, лучшее решение на небольших объемах или если нужно выдирать много различных данных, и регулярки использовать накладно в плане нагрузки процессора, так как операции поиска по dom отрабатывают быстрее (проверенно на парсинге ссылок из документа) и есть возможность жертвовать памятью.
    • –1
      а вам было критично знать всю структуру? или последовательного чтения достаточно?
      если да, то по идее можно использовать SAX (Simple Api XML) — в .net это обеспечивает класс XmlReader.
      написать обертку для HTML и вперед
  • 0
    Конечно лучше всего использовать xpath, а не городить такой монструозный код. В файерфоксе есть куча плагинов, облегчающих работу с xpath, т.е. достаточно кликнуть на нужный элемент, скопировать выражение и вставить в код. Только нужно знать некоторые особенности, например когда файерфокс создает DOM, после тэга table добавляется tbody, при верстке страницы его как правило опускают. Я написал для себя маленькую утилитку, позволяющую тестить xpath именно для HtmlAglityPack и статического html, получилось очень удобно.
  • –1
    А аналогов nokogiri для .net нет?

    Ну там «tr.hot» и дальше разбор полетов…
  • +1
    А как библиотека справляется с неправильным html (нет закрытия тега и т.д.)?
    • +2
      Вполне отлично — закрывает их! :)
  • 0
    Я в свое время для разбора сайтов SgmlReader юзал. Всех плюшек уже не помню, но то что он «неправильный» html делал «правильным» и спокойно работал с xpath, точно было.
  • 0
    Забавно, что по прошествии уймы лет, критический баг Incorrect parsing of HTML4 optional end tags до сих пор не пофиксен. Это означает, что совершенно валидный документ HTML будет неверно обработан, если он пользуется опциональностью закрытия тегов. Это же курам на смех! Пять лет! Пять лет прошло!

    Не понимаю, почему у этой библиотеки такая популярность, хотя сейчас есть достойные конкуренты. Я разобрал некоторые на StackOverflow:

    Как распарсить HTML в .NET?

    Короче, я бы советовал переходить на CsQuery или AngleSharp, а не пользоваться этим ископаемым глюкалом (некоторые ещё и XPath умудряются пользоваться, хотя даже для HAP есть Fizzler, я уж молчу про хождение по ChildNodes индексами — ещё одна загадка природы).

    P. S. Пишу комментарий к старой статье, потому что она в топе гугла, и, подозреваю, многие ей доверятся. Про недостатки HAP надо знать.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.