|
Эндрю Мэттьюс Оригинал: Using RDF and C# to Create an MP3 Manager - Part 1 Эта статья является продолжением предыдущей публикации о разработке приложений Semantic Web на C#. Я опять воспользуюсь библиотекой SemWeb, но в этот раз я хочу продемонстрировать возможности RDF на примере простого менеджера MP3 файлов. Я его еще не закончил, и буду работать над ним в течении следующих нескольких дней, для того, чтобы показать вам насколько легко стало работать с RDF/OWL на C# в наши дни.
Программа довольно проста, меня натолкнул на мысль написать ее сайт RDF-izers, где вы можете найти множество инструментов для преобразования данных из разных форматов в RDF. Пока я осваивался с LINQ, я создал простую систему тэгов для файлов, я просто сканировал файлы и извлекал из них все метаданные, которые мог, и сохранял их в базе данных тегов на SQL сервере. Менеджер MP3 файлов не слишком сильно отличается от такой системы. Я просто извлек ID3 тэги из файлов MP3 и сохранил полученную метаинформацию в объектах Track. Затем я написал простой конвертер для того, чтобы сохранить извлеченные данные в RDF хранилище в памяти. Все вместе это заняло у меня 3-4, часа включая поиск подходящего API для чтения ID3. Я не буду показывать (пока не попросят) код для тестирования или для обхода файловой системы. Вместо этого я покажу вам код, который я написал для сохранения объектов в RDF хранилище. Прежде всего, мы имеем класс Track. Я выкинул большую часть реализации свойств для краткости.
[OntologyBaseUri("file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/")]
[OwlClass("Track", true)]
public class Track : OwlInstanceSupertype
{
[OwlProperty("title", true)]
public string Title /* … */
[OwlProperty("artistName", true)]
public string ArtistName /* … */
[OwlProperty("albumName", true)]
public string AlbumName /* … */
[OwlProperty("year", true)]
public string Year /* … */
[OwlProperty("genreName", true)]
public string GenreName /* … */
[OwlProperty("comment", true)]
public string Comment /* … */
[OwlProperty("fileLocation", true)]
public string FileLocation /* … */
private string title;
private string artistName;
private string albumName;
private string year;
private string genreName;
private string comment;
private string fileLocation;
public Track(TagHandler th, string fileLocation)
{
this.fileLocation = fileLocation;
title = th.Track;
artistName = th.Artist;
albumName = th.Album;
year = th.Year;
genreName = th.Genere;
comment = th.Comment;
}
}
Вообще то здесь нечего комментировать за исключением указания нескольких ключевых атрибутов, которые используются для того, чтобы предоставить механизму сериализации дополнительную информацию о том, как генерировать URI для класса, его свойств и их значений. Очевидно, что это предварительная версия, поэтому мы не указываем большое количество дополнительной информации о типах XSD, версиях и т.д. Но я уверен, что вы уловили идею: мы можем сделать для RDF многое из того, что LINQ для SQL делает в отношении реляционных баз данных. Классы атрибутов также очень просты:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property)]
public class OwlResourceSupertypeAttribute : Attribute
{
public string Uri
{
get { return uri; }
}
private readonly string uri;
public bool IsRelativeUri
{
get { return isRelativeUri; }
}
private readonly bool isRelativeUri;
public OwlResourceSupertypeAttribute(string uri)
: this(uri, false){}
public OwlResourceSupertypeAttribute(string uri, bool isRelativeUri)
{
this.uri = uri;
this.isRelativeUri = isRelativeUri;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property)]
public class OwlClassAttribute : OwlResourceSupertypeAttribute
{
public OwlClassAttribute(string uri)
: base(uri, false){}
public OwlClassAttribute(string uri, bool isRelativeUri)
: base(uri, isRelativeUri){}
}
[AttributeUsage(AttributeTargets.Property)]
public class OwlPropertyAttribute : OwlResourceSupertypeAttribute
{
public OwlPropertyAttribute(string uri)
: base(uri, false){}
public OwlPropertyAttribute(string uri, bool isRelativeUri)
: base(uri, isRelativeUri){}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class OntologyBaseUriAttribute : Attribute
{
public string BaseUri
{
get { return baseUri; }
}
private string baseUri;
public OntologyBaseUriAttribute(string baseUri)
{
this.baseUri = baseUri;
}
}
OwlResourceSupertypeAttribute - это базовый класс для всех атрибутов, имеющих отношение к ресурсам в онтологии, то есть ко всему, что имеет URI. Поэтому он имеет свойство Uri, и кроме того, он имеет свойство isRelativeUri, которое определяет, является ли URI абсолютным или указывается относительно базового URI определенного где-то еще. Хотя я пока не реализовал эту функциональность, я предполагаю разрешить ресурсам ссылаться на определение базового пространства имен в RDF хранилище или в файле. OwlClassAttribute наследует OwlResourceSupertype и может быть использован только с классами или структурами. Вы используете его (или базовый тип, если хотите) для того чтобы указать URI OWl класса, в виде которого будет сохраняться ваш тип. Так что для класса Track мы будем иметь соответствующий OWL класс "Track". В онтологии этот Track будет задаваться относительно некоторого базового URI, который я определяю, используя атрибут OntologyBaseUriAttribute. Этот атрибут задает URI онтологии относительно которого указываются URI классов и свойств. (например: "file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/"). Для свойств класса Track я определил другой подкласс OwlResourceSupertype - OwlPropertyAttribute, который применим только к свойствам. Еще одно упрощение по сравнению с OWL, которое я допускаю, заключается в том, что я не делаю различий между объектными свойствами (ObjectProperty) и свойствами данными (DatatypeProperty). Это будет не трудно добавить, и я уверен, что сделаю это в следующие несколько дней. Так что теперь я обеспечил механизм сериализации аннотациями о том, как создавать из моего класса утверждения, которые я могу добавлять в RDF хранилище. Эти аннотации могут быть прочитаны механизмом сериализации и использованы для формирования подходящих URI для ресурсов. Мы все еще нуждаемся в инструменте для создания экземпляров объектов. Я решил эту проблему самым простым способом: я просто завел счетчик в сканере, и стал формировать URI экземпляра, добавляя значение счетчика к URI класса. Так что первый экземпляр будет иметь URI: "file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/Track_1", и т.д. Этот подход простой, но должен быть улучшен в любом серьезном приложении. Далее мне нужно получить из экземпляра класса Track набор утверждений, который можно будет сохранить в RDF хранилище. Для этого я воспользовался новым средством C# 3.5 - методом расширения (extension method), который позволил мне писать такой код:
foreach (Track t in GetAllTracks(txtFrom.Text))
{
t.InstanceUri = GenTrackName(t);
store.Add(t);
}
Здесь store - это RDF хранилище, GetAllTracks - это итератор, который выдает файлы из директории, указанной в txtFrom.Text. GenTrackName - создает URI для экземпляров треков. Я бы мог использовать более изощренную схему с использованием хешей из местоположения треков, или что-нибудь еще, но я торопился ;-). Код механизма сериализации также не сложен:
public static class MemoryStoreExtensions
{
public static void Add(this MemoryStore ms, OwlInstanceSupertype oc)
{
Debug.WriteLine(oc.ToString());
Type t = oc.GetType();
PropertyInfo[] pia = t.GetProperties();
foreach (PropertyInfo pi in pia)
{
if(IsPersistentProperty(pi))
{
AddPropertyToStore(oc, pi, ms);
}
}
}
private static bool IsPersistentProperty(PropertyInfo pi)
{
return pi.GetCustomAttributes(typeof (OwlPropertyAttribute), true).Length > 0;
}
private static void AddPropertyToStore(OwlInstanceSupertype track, PropertyInfo pi, MemoryStore ms)
{
Add(track.InstanceUri, track.GetPropertyUri(pi.Name), pi.GetValue(track, null).ToString(), ms);
}
public static void Add(string s, string p, string o, MemoryStore ms)
{
if(!Empty(s) && !Empty(p) && !Empty(o))
ms.Add(new Statement(new Entity(s), new Entity(p), new Literal(o)));
}
private static bool Empty(string s)
{
return (s == null || s.Length == 0);
}
}
Add - метод расширения, который перебирает свойства класса OwlInstanceSupertype. OwlInstanceSupertype - базовый класс для всех классов, которые могут быть сохранены в хранилище. Как вы можете видеть, Add проверяет каждое свойство, является ли оно сохраняемым. И если да, то свойство сохраняется с помощью вызова AddPropertyToStore. AddPropertyToStore создает URI для субъекта (экземпляр трека в хранилище), предиката (объект свойство в классе Track) и объекта (который является строковым литералом содержащим значение свойства). Это утверждение добавляется в хранилище. Вот и все. Почти. Небольшая онтология, которую я создал для музыкальных треков, выглядит следующим образом:
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix daml: <http://www.daml.org/2001/03/daml+oil#> .
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsdt: <http://www.w3.org/2001/XMLSchema#>.
@prefix : <http://aabs.purl.org/ontologies/2007/04/music#> .
:ProducerOfMusic a owl:Class.
:SellerOfMusic a owl:Class.
:NamedThing a owl:Class.
:TemporalThing a owl:Class.
:Person a owl:Class;
owl:subClassOf :NamedThing.
:Musician owl:subClassOf :ProducerOfMusic, :Person.
:Band a :ProducerOfMusic.
:Studio a :SellerOfMusic, :NamedThing.
:Label = :Studio.
:Music a owl:Class.
:Album a :NamedThing.
:Track a :NamedThing.
:Song a :NamedThing.
:Mp3File a owl:Class.
:Genre a :NamedThing.
:Style = :Genre.
:title
rdfs:domain :Track
rdfs:range xsdt:string.
:artistName
rdfs:domain :Track
rdfs:range xsdt:string.
:albumName
rdfs:domain :Track
rdfs:range xsdt:string.
:year
rdfs:domain :Album
rdfs:range xsdt:integer.
:genreName
rdfs:domain :Track
rdfs:range xsdt:string.
:comment
rdfs:domain :Track
rdfs:range xsdt:string.
:isTrackOn
rdfs:domain :Track
rdfs:range :Album.
:fileLocation
rdfs:domain :Track
rdfs:range xsdt:string.
Когда я запустил мой менеджер на директорию с подкастами, то получил на выходе следующий N3:
<file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/Track_1>
<file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/title> "History 5 | Fall 2006 | UC Berkeley" ;
<file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/artistName> "Thomas Laqueur" ;
<file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/albumName> "History 5 | Fall 2006 | UC Berkeley" ;
<file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/year> "2006" ;
<file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/genreName> "History 5 | Fall 2006 | UC Berkeley" ;
<file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/comment> " (C) Copyright 2006, UC Regents" ;
<file:///C:/dev/prototypes/semantic-web/src/Mp3ToRdf/fileLocation> "C:\\Users\\andrew.matthews\\Music\\hist5_20060829.mp3" .
Вы можете видеть как конструируются URI из базового URI, и что все свойства принадлежат экземпляру Track_1. Дальше, наверно, следует использовать префиксы, чтобы избавиться от этих длинных URI. Затем я покажу вам, как делать запросы к хранилищу, чтобы извлечь максимум из вашей музыкальной коллекции. Перевод: Михаил Навернюк |