XML 观察

追溯 RDF 数据的源头

RDF 工具正逐渐成熟起来

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: XML 观察

敬请期待该系列的后续内容。

此内容是该系列的一部分:XML 观察

敬请期待该系列的后续内容。

一年前,我为 developerWorks写了两篇有关 朋友的朋友(Friend-of-a-Friend,FOAF)项目的文章。FOAF 是一种 XML/RDF 词汇表,用于以计算机可读的形式描述您通常可以放在主 Web 页上的某种个人信息,如您的姓名、即时信使昵称和工作地点等。

在我有关 FOAF 的第二篇文章(请参阅 参考资料)的清单 6 中,我演示了 FOAFbot,一个我编写的聚集人们的 FOAF 文件并回答相关问题的社区支持代理。FOAFbot 能够记录谁说了关于谁的什么事情。当问及我叫什么名字时,FOAFbot 答道:

edd@xml.com's name is 'Edd Dumbill',
according to Dave Beckett, Edd Dumbill,
Jo Walsh, Kip Hampton, Matt Biddulph,
Dan Brickley, and anonymous source Anon47

FOAFbot 背后的思想是,如果您能够验证不同的几个人(您所信任的人)都记录了一个事实,那么您很可能相信这是真实的。

以下是追溯这样的元数据源头的另一个运用。搜索引擎在其早期历史中被滥用的一个主要表现是恶意元标记(meta tag spamming)。网站会把错误的元数据放到它们的页面中以提高它们的搜索引擎排名。由此,搜索引擎不再关注元标记,因为它们极有可能提供虚假信息。事实上,象 Google 这样的搜索引擎找到了其它更高级的度量来评定页面关联性。

展望 Web 的未来,避免诸如恶意元标记这样的滥用将变得至关重要。Tim Berners-Lee 对 Semantic Web 的展望(请参阅 参考资料)是希望出现这样一个 Web:其上大多数的数据都是机器可读的,从而使得目前由人类完成的大多数信息处理自动进行。

元数据滥用在 Semantic Web 上可能造成的麻烦甚至更大:网站不再限于仅仅造自己站点的假。它还可以造其它站点的假。例如,一家书店对竞争者的报价作假,这是有可能的。

我不会探究各种安全性和可信机制(这些机制将防止这种语义的破坏行为)的细节,而是将重点讨论这样的机制之所以成为可能的基础: 追溯源头

存储 RDF

处理 RDF 然后存储它的应用程序使用 三元组存储(triple store)来做到这一点。RDF/XML 输入文档被分解成一列包含 主语、谓语和宾语 的三元组。然后后续处理就是操作和查询存储中的三元组。只有在涉及到交换时,才会用 RDF 的 XML 语法。由于将数据直接分解成三元组,所以 XML 工具(象 XPath 或 XQuery)就没有多少用处了。已经有人编写了直接使用 XQuery 操作 XML 语法的 RDF 处理工具,但我认为对于通用的 RDF 处理,这多少有点多此一举。

为了进行演示,我将向您展示如何将简单的 RSS 1.0 文档用作测试数据。最近我建立了一个 weblog 站点,其中,我对不知情的公众强加了我的观点。为了将我涂鸦式的元数据放在一起,我生成了一个 RSS 1.0 文件(请参阅 参考资料获取链接)。该文件的开始部分类似于清单 1,曾接触过 RSS 的任何人应该对它非常熟悉。

清单 1. 摘自 RSS 1.0 文件的开始部分
<rdf:RDF
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns="http://purl.org/rss/1.0/"
>
  <channel rdf:about="http://usefulinc.com/edd/blog">
    <title>Edd Dumbill's Weblog: Behind the Times</title>
 
    <description>
      Thoughts and comment from Edd Dumbill, technology writer
      and free software hacker.
    </description>
    <link>http://usefulinc.com/edd/blog</link>

在 RDF 处理器将这一部分解析成三元组时,我获得了清单 2 中显示的数据。

清单 2. 与清单 1 的开始部分对应的 RDF 三元组
[http://usefulinc.com/edd/blog,
    http://www.w3.org/1999/02/22-rdf-syntax-ns#type,
    http://purl.org/rss/1.0/channel]
[http://usefulinc.com/edd/blog,
    http://purl.org/rss/1.0/title,
    "Edd Dumbill's Weblog: Behind the Times"]
[http://usefulinc.com/edd/blog,
    http://purl.org/rss/1.0/description,
    "Thoughts and comment from Edd Dumbill,
     technology writer and free software hacker."]
[http://usefulinc.com/edd/blog,
    http://purl.org/rss/1.0/link,
    "http://usefulinc.com/edd/blog"]

随后 RDF 应用程序所要操作的就是 清单 2中显示的这列三元组了。至此,一切都很顺利。但在存储这个数据时,您会丢失一些重要信息的线索,即数据的出处和其它相关数据,如我是何时对数据拍了快照。要记录这个信息,我需要通过一些方式使该信息与我所找到的 RDF 语句相关联。

首先,对于 清单 2中的数据,我需要模拟出一个描述,记录下对于这些数据我可能要说的内容。清单 3 就包含了这样一个示例描述,它使用了专为这一目的而虚构出的一个示例名称空间。

清单 3. 检索清单 1 和清单 2 中数据的示例描述
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:me="http://example.org/ns/mymeta#">
<rdf:Description rdf:about="http://example.org/retrieval/52365">
    <me:origin rdf:resource="http://usefulinc.com/edd/blog/rss" />
    <me:retrieved>2003-06-24T08:59:55.00Z</me:retrieved>
</rdf:Description>
</rdf:RDF>

您可以从 清单 3中看出:我已经(任意)虚构了一个 URI 来表示对一个文件进行的第 52365 次检索。这似乎是一个合理的方法,如同任何对远程资源的每次轮询所命名的那样。这个资源的出处是这个 RSS 1.0 文件的 URI,而时间戳记表明何时找到它的。

现在,剩下的就是存储四元组,而不是存储三元组。清单 4 展示了我如何修改 清单 2以显示我现在添加到存储中的所有数据。

清单 4. 增加了上下文 URI 和上下文元数据的三元组
[http://usefulinc.com/edd/blog,
    http://www.w3.org/1999/02/22-rdf-syntax-ns#type,
    http://purl.org/rss/1.0/channel,
    http://example.org/retrieval/52365]
[http://usefulinc.com/edd/blog,
    http://purl.org/rss/1.0/title,
    "Edd Dumbill's Weblog: Behind the Times",
    http://example.org/retrieval/52365]
[http://usefulinc.com/edd/blog,
    http://purl.org/rss/1.0/description,
    "Thoughts and comment from Edd Dumbill,
     technology writer and free software hacker.",
    http://example.org/retrieval/52365]
[http://usefulinc.com/edd/blog,
    http://purl.org/rss/1.0/link,
    "http://usefulinc.com/edd/blog",
    http://example.org/retrieval/52365]
[http://example.org/retrieval/52365,
    http://example.org/ns/mymeta#origin,
    http://usefulinc.com/edd/blog/rss,
    <NULL>]
[http://example.org/retrieval/52365,
    http://example.org/ns/mymeta#retrieved,
    "2003-06-24T08:59:55.00Z",
    <NULL>]

清单 4中演示的思想是每个 RDF 语句都增加了一个 URI,它将该 RDF 语句链接到我想要存储的有关该 RDF 语句的元数据上。这个简单机制给我带来了无穷的力量。除了使我能够检索有关该 RSS 文件的元数据以外,它还提供了从存储中除去该信息的便利方法。

实际实现

在运用这个思想时,我已将上面四元组的第四个元素称为 上下文。这个术语并不规范,但它对我而言非常有效。而且,Redland RDF 应用程序框架中也使用了这个术语,我将使用该框架来演示这个追溯源头的应用程序。

(顺便提一下,我要对 Dave Beckett(Redland 的创建者)表示感谢。在我去年编写 FOAFbot 时,Redland 不支持上下文,所以我最终只能以一种非常繁冗的格式来实现上下文。Dave 回应了我的请求,他将上下文的支持添加到了他的工具箱中。)

Redland 是一个基于 C 的工具箱,带有许多语言绑定,包括 Python、Perl 和 Java。它由一个 RDF 解析器、raptor 和一个数据存储组成。该存储目前使用 Berkeley DB 文件,不过还在开发对底层 SQL 存储的支持。对于我的示例而言,我将使用到 Redland 的 Python 绑定。

这里的目标是要创建一个简单的 RSS 1.0 聚集器。使用聚集器的目的是对多个 RSS 馈送进行重复拍快照,并允许您以有趣的方式组合它们。随着新内容项的加入,RSS 文件会随着时间的变化而发生更改 - 我想避免多个冗余项,而仍能保留历史项。在本文后面,我将开发所需的一些功能。

您将在 参考资料中找到一个到这个项目的 Python 代码(fraggle.tar.gz)的链接。还有一个到 Redland 工具箱的链接,您也需要安装这个工具箱。

Aggregator 类会处理访存和存储 RDF 数据这个有趣的工作。清单 5 摘自这个类的 load_uri 方法,它从 aggregate.py 的第 189 行开始。

清单 5. 追溯检索到的 RDF 的上下文
stream = self._parser.parse_as_stream(
        RDF.Uri(string="file:./%s" % fname),
        base_uri=urinode.uri)
if stream:
    channel = None
        context = self.context_uri_node()
    timestamp = RDF.Node(literal=
        time.strftime("%Y-%m-%dT%H:%M:%S.00Z"))
    while not stream.end():
        statement = stream.current()
        # add the statement to the model, with context
        self._model.add_statement(statement, context)
        # if it's a <rss:channel> remember the URI
        if ( statement.predicate == _rdfType and
            statement.object == _rssChannel ):
            channel = RDF.Node(node=statement.subject)
        # move on
        stream.next()
	# now to add the context information
	# first, the source URI
	
        self._model.add_statement(RDF.Statement(
	subject=context, predicate=_fraggieSource,
	object=urinode), _globalContext)
	# second, the channel URI
	
        self._model.add_statement(RDF.Statement(
	subject=context, predicate=_fraggieChannel,
	object=channel), _globalContext)
	# third, the timestamp
	
        self._model.add_statement(RDF.Statement(
	subject=context, predicate=_fraggieTimestamp,
	object=timestamp), _globalContext)
	# fourth, the checksum
	
        self._model.add_statement(RDF.Statement(
	subject=context, predicate=_fraggieChecksum,
	object=RDF.Node(literal=checksum)), _globalContext)
	self.register_fetch(urinode, context)

该清单中用粗体显示的几行与追溯上下文有关。首先,使用 context_uri_node() 生成上下文的 URI。这返回形式为 http://usefulinc.com/fraggie/global/1 的 URI 结果。这个上下文然后被追加到在检索到的 RSS 中找到的每个语句中。一旦存储了 RSS 数据,我随后就将有关上下文 URI 的数据添加到存储中。在本例中,我存储了作为 RSS 文件来源的 URI、RSS 通道本身的 URI( rss:channel 元素中的 rdf:about 值)、访存文件的时间以及文件的 MD5 校验和(以便以后确定 RSS 文件是否随时间变化而发生了更改)。

如果您下载了源代码,那么正如您将看到的, Aggregator 类的其余部分实现了两个功能:第一个是 RSS 搜索所需的内务处理方法;第二个向聚集器的询问者提供了查询方法。请注意,所有的内务处理变量(如 fetch 计数)都是用 RDF 表示的,并且保存在 RDF 存储中。清单 6 展示了将计数变量表示为 RDF/XML 语句。

清单 6. 用 RDF 表示的内部计数器
<rdf:Description
    rdf:about="http://usefulinc.com/fraggie/counter"
    rdf:value="0" />

我发现在 RDF 存储中持久地保存任何拥有全局作用域的变量是很有意义的。这种方法的一个直接优点是它在多次调用后仍能保持状态。

该演示归档文件包括两个您可以运行的示例 RSS 文件。使用 python fraggle.py 可以调用该演示。该演示首先使聚集器装入这两个示例 RSS 文件,它们都引用了 Mark Pilgrim 最近在 XML.com 上发表的一篇文章。这个练习的目的是找到谁对这篇文章作了什么评价。运行该演示产生的输出如清单 7 所示(为可读性起见,已对一些输出行重新进行了格式处理)。

清单 7. fraggle.py 的输出
Links to http://www.xml.com/pub/a/2003/07/02/dive.html
 
From : Meerkat: An Open Wire Service: XML.com
       <http://meerkat.oreillynet.com/>
Time : 2003-07-05T15:39:26.00Z
Title: The Vanishing Image: XHTML 2 Migration Issues
Desc :  In Mark Pilgrim's latest Dive Into XML column,
       Pilgrim examines XHTML 2.0 <tt>object</tt>
       element, which is a replacement for the more
       familiar and widely supported <tt>img</tt>.
 
From : paranoidfish.org/links
       <http://www.paranoidfish.org/links/>
Time : 2003-07-05T15:39:25.00Z
Title: XML.com: The Vanishing Image: XHTML 2 Migration
       Issues [Jul. 02, 2003]
Desc : using <object> as a replacement for <img>
       is not a safe bet right now

摘自 XML.com 的第一部分展示了对这篇文章的正式描述。第二部分摘录展示了由 paranoidfish.org 网站所有者提供的摘要。

如果您研究过 fraggle.py 的源代码,那么将明白所有查询都围绕上下文进行。首先,会询问聚集器哪些上下文提到了这篇文章的 URI。然后根据上下文请求文章元数据并建立索引。请注意,上下文在时间上对应于一个馈送的快照。如果我的示例随时间变化要获得更多 RSS 文件的快照,那么您可能会看到有几项都是来自同一来源的 - 可能在描述上稍作了更改(人们常常会随时修正拼写错误)。对 fraggle.py 中查询的一个明显改进是根据源进行分组,而不是单单根据时间顺序进行显示。

尽管从浏览、自我冲浪(ego-surfing)的角度而言,weblog 和其它因特网站点的 RSS 馈送很有趣,但我认为象这样一个项目的真正价值很有可能体现在企业中。许多组织常常会生成大量以时间为序的数据流。举一个简单的示例,URI 分配给了客户或项目,然后就可以生成并聚集活动的 RSS 流。

然后可以很轻松的为那些对这样的聚集数据感兴趣的任何人划分和切割这些数据。例如,管理者可能希望知道每个工人在做什么,项目经理可能希望获得最近的三个状态更新,更高级的管理层可能希望获得整个部门的快照视图,等等。不难想象,这种工具很可能会在客户关系管理(CRM)这一领域中产生最佳收益。

结束语

本文中演示的简单示例只触及了追溯 RDF 源头的皮毛。在 Web 上,信息来自哪里与信息本身同样重要。源头追溯的 RDF 工具刚开始兴起,当它们的使用变得更广泛时,它们的能力无疑会更加完善。

毫无疑问,Redland RDF 应用程序框架是一个值得进一步研究的工具箱。它具有到您最喜爱的脚本编制语言的接口;它运行在 UNIX、Windows 和 Mac OS X 上;而且它是一个开放源码项目,所以您所做的任何改进会使整个社区受益。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=21476
ArticleTitle=XML 观察: 追溯 RDF 数据的源头
publish-date=03012003