Data acquisition, часть 2

    В первой части моего рассказа про data acquisition, я написал про то, какой инструментарий используется для получения HTML из интернета. В этом посте я более детально расскажу про то, как из этого HTML получать нужные данные, и как эти данные трансформировать в нужный нам формат.

    Сформированность HTML


    Когда вы получаете HTML из какого-то ресурса, у вас могут быть два варианта – либо идеально сформированный HTML который можно сразу конвертировать в XML (то есть брать и использовать), либо плохо сформированный HTML. Большинство HTML, к сожалению, сформировано плохо. В этой ситуации есть два варианта: либо использовать HTML Agility Pack для того чтобы вытащить все нужные данные, либо использовать эту же библиотеку для того чтобы “скорректировать” полученный HTML и сделать его более XML-образным. Вот самый минимальный пример того, как можно удалить все незакрытые элементы IMG:

    var someHtml = "<p><img src='a.gif'>hello</p>";<br/>
    HtmlDocument doc = new HtmlDocument();<br/>
    doc.LoadHtml(someHtml);<br/>
    // fix images
    foreach (var node in doc.DocumentNode.SelectNodes("//img"))<br/>
      if (!node.OuterHtml.EndsWith("/>"))<br/>
        node.Remove();<br/>
    Console.WriteLine(doc.DocumentNode.OuterHtml);<br/>
    Console.ReadLine();<br/>

    Кому-то может показаться, что фиксинг HTML является ненужной задачей – ведь используя тот же метод SelectNodes() можно получить любой элемент, даже если этот элемент плохо сформирован (malformed). Но существует одно приемущество, которое не следует забывать – если вы получили правильный XML, то а) вы можете сделать (или сгенерировать) XSD для этого кусочка XML; и б) получив XSD, можно генерировать мэппинги из XML-структуры на POCO, с которыми намного легче работать.

    Мэппинги


    Мэппинг данных обычно фигурирует в интеграционных системах вроде BizTalk. Идея в том, чтобы преобразовать набор данных во что угодно – обычно это правда просто другой набор данных. На самом деле, во многих случаях это сопоставление один-к-одному, но часто нужны разные конверсии – например, весь HTML это текст, а чтобы получить число, нужно делать конверсию (int.Parse() и т.п). Давайте посмотрим на то, как это делается.

    Допустим мы получили следующую (примитивную) структуру при разборе:

    <table><br/>
      <tr><br/>
        <td>Alexander</td><br/>
        <td>RD</td><br/>
      </tr><br/>
      <tr><br/>
        <td>Sergey</td><br/>
        <td>MVP, RD</td><br/>
      </tr><br/>
      <tr><br/>
        <td>Dmitri</td><br/>
        <td>MVP</td><br/>
      </tr><br/>
    </table><br/>

    А теперь представим что нам нужно замэпить эти данные на следующую структуру:

    class Person<br/>
    {<br/>
      public string Name { get; set; }<br/>
      public bool IsMVP { get; set; }<br/>
      public bool IsRD { get; set; }<br/>
    }<br/>

    Для этого класса лучше сразу создать класс-коллекцию:

    public class PersonCollection : Collection<Person> {}<br/>

    Теперь мы сгенерируем XSD для исходных данных. Результат выглядит примерно вот так:

    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><br/>
      <xs:element name="table"><br/>
        <xs:complexType><br/>
          <xs:sequence><br/>
            <xs:element name="tr" maxOccurs="unbounded"><br/>
              <xs:complexType><br/>
                <xs:sequence><br/>
                  <xs:element name="td" type="xs:string"/><br/>
                  <xs:element name="td" type="xs:string"/><br/>
                </xs:sequence><br/>
              </xs:complexType><br/>
            </xs:element><br/>
          </xs:sequence><br/>
        </xs:complexType><br/>
      </xs:element><br/>
    </xs:schema><br/>

    Это легко – наверное слишком легко. Что сложнее так это получить схему для нашего класса коллекций. (N.b.: вместо схемы можно использовать, например, базу данных напрямую, но я пожалуй воспользуюсь XSD.) Внимание, магический трюк: компилируем сборку с типом PersonCollection а потом запускаем следующую команду:

    xsd -t:PersonCollection "04 Mapping.exe"<br/>

    Не поверите – эта комманда генерирует XSD на основе CLR-типа! Замечу что запускать XSD имеет смысл только в “битности” вашей системы. Не смотря на то, что у меня все компилируется для x86, чтобы заработал XSD пришлось сделать 64-битную сборку. Получился следующий XSD-файл, с помощью которого можно делать мэппинг:

    <xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"><br/>
      <xs:element name="ArrayOfPerson" nillable="true" type="ArrayOfPerson" /><br/>
      <xs:complexType name="ArrayOfPerson"><br/>
        <xs:sequence><br/>
          <xs:element minOccurs="0" maxOccurs="unbounded" name="Person" nillable="true" type="Person" /><br/>
        </xs:sequence><br/>
      </xs:complexType><br/>
      <xs:complexType name="Person"><br/>
        <xs:sequence><br/>
          <xs:element minOccurs="1" maxOccurs="1" name="Name" type="xs:string" /><br/>
          <xs:element minOccurs="1" maxOccurs="1" name="IsMVP" type="xs:boolean" /><br/>
          <xs:element minOccurs="1" maxOccurs="1" name="IsRD" type="xs:boolean" /><br/>
        </xs:sequence><br/>
      </xs:complexType><br/>
    </xs:schema><br/>

    Ну вот, у нас есть левая и правая сторона мэппинга. Сам мэппинг можно создать с помощью такого приложения как Stylus Studio или MapForce. Мэппинги создаются визуально, но процесс создания неинтуитивен, так что если вы никогда не работали с визуальными мэппингами, придется в начале немного помучаться.

    Для того чтобы создать свой мэппинг, я воспользовался программой Altova MapForce. Если коротко, то эта программа может делать много разных мэппингов, в том числе и XSD-на-XSD, что нам и нужно. Мэппинги генерируются для языков XSLT1/2, XQuery, Java, C# и C++. Лично я для своих целей использую XSLT2, а для запуска трансформаций использую бесплатный движок AltovaXML, т.к. все что дает компания Microsoft в .Net для XSLT – настоящее убожество. А XQuery вообще в .Net нету. И нет, библиотека Mvp.Xml тоже не особо помогает, хотя приз за усилия разработчикам полагается.

    Первое, что мы делаем – это визуально описываем мэппинг с помощью доступных нам примитивов. Выглядит результат примерно так:




    Теперь мы генерируем для мэппинга XSLT. Все что осталось, так это определиться с тем как его вызывать. Если учесть что мы используем AltovaXML для трансформации, сам код выглядит вот так:

    public static string XsltTransform(string xml, string xslt)<br/>
    {<br/>
      var app = new Application();<br/>
      var x = app.XSLT2;<br/>
      x.InputXMLFromText = xml;<br/>
      x.XSLFromText = xslt;<br/>
      return x.ExecuteAndGetResultAsString();<br/>
    }<br/>

    Для того, чтобы десериализовать XML в коллекцию, мы используем следующий метод:

    public static T FromXml<T>(string xml) where T : class<br/>
    {<br/>
      var s = new XmlSerializer(typeof(T));<br/>
      using (var sr = new StringReader(xml))<br/>
      {<br/>
        return s.Deserialize(sr) as T;<br/>
      }<br/>
    }<br/>

    Вот собственно и все – получив наш XML, его можно смело трансформировать:

    string xml = File.ReadAllText("Input.xml");<br/>
    string xslt = File.ReadAllText("../../output/MappingProjectMapToPersonCollection.xslt");<br/>
    string result = XsltTransform(xml, xslt);<br/>
    var pc2 = FromXml<PersonCollection>(result);<br/>

    Лирика о мэппингах


    Кому-то может показаться что мэппинги излишни, и для простых случаев возможно это действительно так. Но хочу заметить что мэппинги, будучи дополнительным уровнем абстрации, позволяют лучше контролировать результат и адаптировать его к изменяющимся условиям – а в случае с меняющимся дизайном сайта это действительно актуально.

    Мэппинги и работа с XML в целом не бесплатна – Visual Studio (даже 2010) крайне плохо с ней справляется, поэтому я воспользовался специализированной, платной программой. Хотя нет, я вру конечно, ведь мэппинги поддерживаются в BizTalk (а следовательно в VS2008). И естественно наша задача может быть “транспонирована”, в каком-то смысле, на BizTalk. А что, для личного использования можно и попробовать, если сидите на MSDN-подписке.

    Вот и все на сегодня. Исходники, как всегда, тут. Comments welcome.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

    Комментарии 24

      +1
      Расскажите об анализе данных на странице. Допустим поиск нерегулярного шаблона, например, цены на страницах разных интернет-магазинов с неизвестной структурой dom дерева.
        0
        Открываю FireBug, смотрю что где. Запоминаю нужный элемент либо по id либо по классу либо по позиции. Выдираю этот кусочек HTML, смотрю well-formed или нет. Корректирую ошибки, делаю мэппинг. Ну и дальше все как обычно.
          0
          с неизвестной структурой dom дерева
            0
            ну обобщенным поиском я вообще не занимаюсь. это затратно, и если информация не структурирована, вы просто теряете время.
              0
              а тот, кто сможет не теряя время структурировать получает — profit)
                +1
                мне кажется что задача о которой вы говорите решается совершенно иными способами чем описано в статье
                например таким — habrahabr.ru/blogs/artificial_intelligence/93641/
        0
        все что дает компания Microsoft в .Net для XSLT – настоящее убожество
        Можно поподробнее, в чём убогость? Недавно кидал распарсенные данные в SQL-дамп через XSLT — всё было в порядке.

        А для того чтобы замапить валидный XML в структуру данных достаточно воспользоваться LINQ to XML и не городить кучу платных инструментов. Ну да, нужно .NET 3.5.
          0
          Убогость в двух вещах: плохой поддержке XSLT и отсутствия поддержки XQuery. Мне кажется очень плохо что этого нет в .Net-е.

          Да, мапить можно через Linq to XML. Да можно вообще вручную через XPath от HTML Agility Pack. Но я так не делаю т.к. мне нужно более организованно управлять мэппингами, быстро реагировать на изменения и по возможности сохранять накопленный ноу-хау.
            0
            Ну так я и пытаюсь выяснить у вас, в чём заключается плохая поддержка XSLT в .NET. А то может я чего-то не знаю :)
            мне нужно более организованно управлять мэппингами, быстро реагировать на изменения
            Опять не понял. Много красивых слов вижу, сути не вижу. Вы вообще LINQ пользовались? Вместо того, чтобы написать элементарный код, чрезвычайно похожий (внимание!) на XQuery, вы городите преобразование CLR в XSD, потом прогоняете это через сторонний XSLT-парсер при этом пишете в статье в каждом абзаце какой это геморрой. А всего-то и надо, что заполнить данными экземпляр класса. Может быть я всё-таки немного не понимаю, чего вы хотите добиться, но навскидку — LINQ тут был бы идеальным и бескровным решением.
              0
              В .Net-овом XSLT не поддерживается очень много функций и фич, без которых в XSLT играть неинтересно.

              Да, я знаю что можно использовать LINQ. Но мэппинг — это серьезное, промышленное решение, а не быстрая отписка. Мэппинги себя окупают, да и создание их никаких проблем не вызывает.
                –1
                Ну приведите пример того, что не поддерживается в .NET для XSLT. Я уже в который раз пытаюсь услышать конкретику.

                То есть вы считаете, LINQ это не серьёзное промышленное решение, а ваш самописный геморрой с толпой преобразований и кучей ненужных платных инструменов — да? Забавно :)
          +1
          А вот бесплатная штука для трансформации данных — www.talend.com
          Мощная и удобная!
            +1
            Занимался написанием грабберов сайтов, с последующим парсингом содержимого. Могу посоветовать следующий код который нравится лично мне:
            public static XmlDocument LoadFromUrlAsXML(string url, CookieContainer contayner)
                {
                  using (SgmlReader reader = new SgmlReader { InputStream = new StringReader(CleanString(WebDownloader.GetString(EncodeCyrUrl(url), null, (byte[])null, contayner))) })
                  {
                    if (!url.Contains("://")) url = String.Format("http://{0}", url);
                    XmlDocument doc = new XhtmlDocument(reader.NameTable);
                    doc.Load(reader);
                    reader.Close();
                    return doc;
                  }
                }


            * This source code was highlighted with Source Code Highlighter.


            В итоге получим XML документ, с которым приятно работать.
              0
              То есть вы хотите сказать что если документ по Url плохо сформирован то связка SgmlReader-XhtmlDocument его выправят?
                0
                Я хочу сказать что используя данный метод у меня ни разу не возникло проблем. Сграббил 6 сайтов, которые были написаны на 3-х разных движках. Я не гарантирую на 100% что такой метод сработает на всех сайтах сети, но на большинстве думаю точно.
                  0
                  Стоп, а что за метод CleanString()? Читинг? =)
                    0
                    Промазал, код ниже в комментах.
              0
              Есть немного=)
              /// <summary>
              /// Используется чтобы оставить только содержимое HTML части, для более удобного приведения к XML виду
              /// </summary>
              private static string CleanString(string str)
              {
              return str.Substring(str.IndexOf("<html", StringComparison.OrdinalIgnoreCase), str.IndexOf("</html>", StringComparison.OrdinalIgnoreCase) - str.IndexOf("<html", StringComparison.OrdinalIgnoreCase) + 6);
              }


              * This source code was highlighted with Source Code Highlighter.
                0
                Вооот, а раз так, ваш метод загрузки в XmlDocument не нужен — это вообще вчерашний день, сегодня есть XElement.Parse()
                  0
                  Ну тогда спасибо за наводку, изучу на досуге.
                0
                Лично я всегда пользовался watin-ом для подобных целей. watin.sourceforge.net/
                Возможно немного не то (открывается окно браузера), но быстро используемое и очень удобное средство
                  0
                  Вы наверное не читали первую часть серии =)
                  0
                  mezastel, спасибо! интересный обзор
                    0
                    mezastel, а для валидации данных в xml по определенным правилам что используете? (минимальная длинна строки, дата в будущем итд)

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

                    Самое читаемое