20 May 2010

Data acquisition, часть 2

.NET
В первой части моего рассказа про 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.
Tags:c.netmappingdata mappingaltovamapforce
Hubs: .NET
+13
1.9k 41
Comments 24