В первой части моего рассказа про 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.
Оставить комментарий