Менеджер MP3 файлов на основе RDF и C# — Часть 1

Эндрю Мэттьюс
Оригинал: 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:   .
@prefix daml:  .
@prefix log:  .
@prefix rdfs:  .
@prefix owl:   .
@prefix xsdt: .
@prefix :  .
 
: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:

 
 
     "History 5 | Fall 2006 | UC Berkeley" ; 
     "Thomas Laqueur" ;
     "History 5 | Fall 2006 | UC Berkeley" ;
     "2006" ;
     "History 5 | Fall 2006 | UC Berkeley" ;
     " (C) Copyright 2006, UC Regents" ;
     "C:\\Users\\andrew.matthews\\Music\\hist5_20060829.mp3" .
 

Вы можете видеть как конструируются URI из базового URI, и что все свойства принадлежат экземпляру Track_1. Дальше, наверно, следует использовать префиксы, чтобы избавиться от этих длинных URI. Затем я покажу вам, как делать запросы к хранилищу, чтобы извлечь максимум из вашей музыкальной коллекции.

Перевод: Михаил Навернюк