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>";
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(someHtml);
// fix images
foreach (var node in doc.DocumentNode.SelectNodes("//img"))
  if (!node.OuterHtml.EndsWith("/>"))
    node.Remove();
Console.WriteLine(doc.DocumentNode.OuterHtml);
Console.ReadLine();

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

Мэппинги

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

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

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

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

class Person
{
  public string Name { get; set; }
  public bool IsMVP { get; set; }
  public bool IsRD { get; set; }
}

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

public class PersonCollection : Collection<Person> {}

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

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

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

xsd -t:PersonCollection "04 Mapping.exe"

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

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

Ну вот, у нас есть левая и правая сторона мэппинга. Сам мэппинг можно создать с помощью такого приложения как 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)
{
  var app = new Application();
  var x = app.XSLT2;
  x.InputXMLFromText = xml;
  x.XSLFromText = xslt;
  return x.ExecuteAndGetResultAsString();
}

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

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

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

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

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

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

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

Вот и все на сегодня. Исходники, как всегда, тут. Comments welcome.

7 responses to “Data acquisition, часть 2”

  1. Спасибо, интересная статья. Я много занимаюсь парсингом информации с сайтов и использую для этого не самые удобные методы/библиотеки, что не может не напрягать. Например, МИЛ.ХТМЛ, не обрабатывает примерно 20 процентов сайтов, да и использовать его очень неудобно. С ВебБраузером тоже проблеммы, особенно когда необходима многопоточность(использую доработанную библиотеку ИЕ), но WatiN я так понял, эту проблему не решает. Но мэпппинг я вообще ни разу не использовал, юзал старые-добрые регулярки:)

    Буду пробовать мэпппинг с использованием Altova, тем более что на торрентах она вроде как есть, и проект у меня не коммерческий. Хотя статья для меня сложная, учитывая мой опыт в постижении нового, в нем больше неудачь,чем приобретений.

    1. Многопоточность – да, это такая проблемка, конечно с WebRequest’ами понятно что делать и как, но вот с WatiN то делать непонятно.

      Рекоммендую скачать просто триал Altova – если вероятность что вы не втянетесь в тему.

  2. А чем генерировали XSD для исходных данных?

    1. Visual Studio умеет генерировать XSD прямо из XML.

      1. Просто сгенерировал
        xsd Input.xml
        но получилось чуть не так, как в примере, или xsd потом руками правится?

      2. Иногда приходится руками править. Например в XML только один элемент а вы точно знаете что их много – тогда приходится прописывать maxOccurs руками.

      3. Спасибо!
        Оперативно вы отвечаете :)

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