使用 microformats 分离数据与格式

为语义 Web 创建简单实用的格式

微格式(Microformat)是在标准 XHTML 代码中嵌入结构化数据的一种新方法。阅读本文,了解如何读写 Web 这种全新的微格式。

Jack D Herrington (jherr@pobox.com), 高级软件工程师, Leverage Software Inc.

Jack D. Herrington 是一位有着二十余年经验的高级软件工程师。他撰写了三本图书:Code Generation in ActionPodcasting Hacks PHP Hacks他写过的文章超过三十篇。您可通过电子邮件联系 Jack:jherr@pobox.com



2006 年 8 月 21 日

每当我偶尔看到有人灵光一闪产生的某个理念,我就会自言自语:“我要是能想到这个该多好,真是天才!” 微格式正是这样一种理念。您知道,人们一直在尝试从非结构化的 Web 中提取结构化数据。在人们讨论 “语义 Web”(数据与格式分离的一种 Web)时,您会听到一些与此相关的内容。而无论如何,语义 Web 尚未消失,所有在非结构化世界中寻找结构化数据的问题依然存在。

迄今为止,微格式是向着导出 Web 上的结构化数据这一方向迈进的一小步。理念非常简单。获得一个包含某些事件信息的页面:开始时间、结束时间、位置、主题、Web 页面,等等。过去的方法是将这些信息放置在页面的超文本标记语言(HTML)中,微格式摒弃了这种做法,而是添加一些标准化 HTML 标记和层叠样式表(CSS)类名。页面依然可以保留您选择的任何外观,但对于寻找这些格式化的(或者应该说,是微格式化的)HTML 片段的浏览器来说,变化无处不在。

或许查看一段微格式化的 HTML 能帮助您理解这种概念。清单 1 展示了格式化为 hCalendar 微格式标准的一个事件。

清单 1. 一个简单的事件页面
<html>
  <body>
    <div class="vevent">
      <a class="url" href="http://myevent.com">
        <abbr class="dtstart" title="20060501">May 1</abbr> - 
        <abbr class="dtend" title="20060502">02, 2006</abbr>
        <span class="summary">My Conference opening</span> - at
        <span class="location">Hollywood, CA</span>
      </a>
      <div class="description">The opening days of the conference</div>
    </div>
  </body>
</html>

单独的一个页面上可以有多个事件,各事件均包含于一个用 vevent 类进行标记的 <div> 标记中。在这个 <div> 标记中是使用不同的标记进行标记的事件数据,有些人用特定的类来标记事件数据。url 类是转向 Web 页面的定位点。dtstartdtend 类位于 title 元素中编码了开始时间和结束时间的标记内。summarylocationdescription 类均属于包含相应内容的标记。

现在,这些条目所在的页面即可使用 CSS 任选方式来编码这些条目了。因此,卡片的外观对于站点来说依然是惟一的。类的命名规范才是最重要的。

这就是微格式要处理的全部。微格式的 Web 站点就像是一个维基百科,您可为想展示的任何数据添加您自己的格式。在我上一次查看该站点时,那里有用于事件、联系信息、评论及其他许多方面的格式。站点中甚至还提供了一个方便的格式创建程序,如 图 1 所示。

图 1. hCalendar 创建程序页面
hCalendar 创建程序页面

在这篇文章中,我使用 PHP 将给定页面中的事件记录提取为 XML 格式。然后创建另外一个 PHP 页面来接受这种 XML,并通过以 hCalendar 微格式编码的事件构建一个 HTML 页面。在此之前,先简要介绍一下微格式的优点和缺点。

优点和缺点

坦白地说,任何经验丰富的工程师在看过上面的 hCalendar 示例后都会问 “这种微格式 中的格式在哪里?” 的确,微格式算不上一种独立的格式,倒不如说是已有格式(HTML)之上的一层。不得不承认,这是微格式的缺点之一。观察以下对比代码,您会更清楚地理解我的意思:

BEGIN:vCalendar
VERSION:5.0
PRODID:-//Microsoft Corporation//Works 2000//EN
BEGIN:vEvent
DTSTART: 20060501T000000
DTEND: 20060502T000000
SUMMARY;ENCODING=QUOTED-PRINTABLE: My Conference opening
PRIORITY: 3
END:vEvent
END:vCalendar

这段代码强大、易于阅读,毫无疑问,它也非常容易解析。这段代码看上去似乎超出了 Fortran 77 或早期的 UNIX® 惯例。您可以毫不困难地设想出一种 XML 替代格式,能够对上述格式略加改进。但要想从中获取这种信息格式,您必须编写专用的客户机。这是新标准乃至 XML 标准的一个很大的缺点,却是微格式的显著优点。

使用微格式,您能利用两种极为成功的技术:HTTP(Hypertext Transfer Protocol)和 HTML。这两种标准集成到了世界上的所有桌面中,工程师们一直在努力拓展其用途,使您能够利用它们做更多的事情。

以 Greasemonkey 为例。Greasemonkey 是 Mozilla Firefox 浏览器的一个绝妙扩展,允许您在载入一个页面之后为页面应用 JavaScript 代码。有些人使用此扩展来移除标题广告。有些人用它在不受自己控制的站点上添加额外的信息。另外还有些人编写 Greasemonkey 脚本来查找微格式化的数据。最妙的就是脚本是用 JavaScript 代码编写的,因此您不必担心平台问题。这里完全不需要 Win32 或 Cocoa:只要使用可移植 JavaScript 代码编写您的扩展即可。但对于非 HTML 页面,Greasemonkey 是不起作用的,此时就是微格式大显身手的时机了。

在介绍微格式时,我想采用先抑后扬的方法,我将回顾不久前我在华盛顿西雅图的一次技术研讨会上的经历。当时我在与 Technorati(一个博客搜索引擎)的创建者之一交谈,我认为继 HTML 之后,最成功的基于标记的数据格式应该是 RSS。但在谈话中,那个人指出大多数 RSS 要么形式不佳,要么比较过时,所以 Technorati 不依靠 RSS。他们直接使用 HTML,并在代码中寻求博客风格的模式。

这并不表示 RSS 不好:我热爱 RSS。但我认为在现实中使人们接受 HTML 以外的其他格式非常艰难。所以在 HTML 之上设置格式更有意义。

现在,停止我的长篇大论,让我们来看看代码。先来读取一个页面中的微格式。


读取微格式

要读取一个使用微格式数据编码的页面,您必须获得一个其上具有事件的 Web 页面。我从一个 .html 文件入手,如 清单 2 所示。

清单 2. Hcalendar.html
<html>
  <head>
    <style>
      body { font-family: arial, verdana, sans-serif; }
    </style>
  </head>
  <body>
    <div style="width:600px;">

      <div class="vevent" id="one">
        <a class="url" href="http://myevent.com">
          <abbr class="dtstart" title="20060501">May 1</abbr> - 
          <abbr class="dtend" title="20060502">02, 2006</abbr>
          <span class="summary">My Conference opening</span> - at
          <span class="location">Hollywood, CA</span>
        </a>
        <div class="description">The opening days of the conference</div>
      </div>

      <div class="vevent" id="two">
        <a class="url" href="http://myevent.com">
          <abbr class="dtstart" title="20060503">May 3</abbr> -
          <abbr class="dtend" title="20060504">04, 2006</abbr>
          <span class="summary">My Conference closing</span> - at
          <span class="location">Hollywood, CA</span>
        </a>
        <div class="description">The closing days of the conference</div>
      </div>

    </div>
  </body>
</html>

在 Web 浏览器中查看此文件时,显示效果如 图 2 所示。

图 2. hcalendar.html 页面
hcalendar.html 页面

这看上去并不美观,但我只是想让这个示例尽可能地简单。关键在于您可以使用希望的任何 HTML 样式,使卡片显示为您喜欢的任何外观。只要使用了正确的 CSS 类名,它仍然会被识别为一个微格式化的 hCalendar 条目。

既然已经有了页面,我需要一些 PHP 代码来读取此页面。清单 3 显示了此代码,这是读取脚本的基础。

清单 3. get_page 函数
<?php
require_once 'HTTP/Client.php';

function get_page( $url )
{
  $client = new HTTP_Client();
  $client->get( $url );
  $resp = $client->currentResponse();
  return $resp['body'];
}

此代码使用 HTTP Client PEAR 模块从给定 URL 读取内容。如果您还没有安装该模块,可以使用 PEAR 命令行来安装:

% pear install HTTP_Client

接下来,我将站点返回的 HTML 转换成一个 XML Document Object Model(DOM)。谢天谢地,站点返回的 HTML 是 Extensible HTML(XHTML),因此使一个 XML 阅读器指向它非常简单。我利用 清单 4 中的 get_events() 函数完成这个任务。

清单 4. get_events 函数
function get_events( $page )
{
  $body = get_page( $page );

  $dom = new DomDocument();
  $dom->loadXML( $body );

  $xpath = new DOMXPath( $dom );
  $events = $xpath->query("//div[@class='vevent']");

  $parsed_events = array();
  foreach( $events as $event )
  {
    $e = parse_event( $dom, $event );
    $parsed_events []= $e;
  }
  return $parsed_events;
}

此函数首先调用 get_page() 函数来检索页面内容。随后创建并加载 DomDocument() 函数。得到了 DOM 版本后,我使用 XPath 查询来获得出现 vevent 类的页面上的所有 <div> 标记,并将那些节点传递给 parse_event

如果您不熟悉 XPath,我将为您略加分析。表达式:

//div

将匹配任何级别的 <div> 标记。添加如下约束:

//div[@class='vevent']

这样就仅匹配具有一个名为 class 的属性、且其本身具有与 vevent 相匹配的值的 <div> 标记。如果您使用 XML 或一种基于 XML 的语言(例如 XHTML 或 RSS),您应该对 XPath 比较熟悉。XPath 是迄今为止导航 XML 树并获得您寻找的信息的最简单的方法。

为了获得各事件 <div> 标记中的数据,parse_event 类使用了更多的 XPath 查询来提取这些数据。如 清单 5 所示。

清单 5. parse_event() 函数
function parse_event( $dom, $event )
{
  $data = array();

  $xpath = new DOMXPath( $dom );

  $url = $xpath->query( ".//*[contains(@class,'url')]/@href", $event );
  $data['url'] = $url->length > 0 ? $url->item(0)->nodeValue : '';

  $dtstart = $xpath->query( ".//*[contains(@class,'dtstart')]/@title", $event );
  $data['dtstart'] = $dtstart->length > 0 ? $dtstart->item(0)->nodeValue : '';

  $dtend = $xpath->query( ".//*[contains(@class,'dtend')]/@title", $event );
  $data['dtend'] = $dtend->length > 0 ? $dtend->item(0)->nodeValue : '';

  $summary = $xpath->query( ".//*[contains(@class,'summary')]", $event );
  $data['summary'] = $summary->length > 0 ? $summary->item(0)->nodeValue : '';

  $location = $xpath->query( ".//*[contains(@class,'location')]", $event );
  $data['location'] = $location->length > 0 ? $location->item(0)->nodeValue : '';

  $desc = $xpath->query( ".//*[contains(@class,'description')]", $event );
  $data['desc'] = $desc->length > 0 ? $desc->item(0)->nodeValue : '';

  return $data;
}

代码看上去有点复杂,但实际上这只是一组 XPath 查询,在 XML DOM 树中查找具有特定类名的特定标记。而这种 XPath 编码更为复杂,首先,其中有一个符号:

.//*

该符号表示 “从这里 开始往下的所有标记”,其中的这里 也就是代码当前看到的 <event> 标记。请注意,$xpath->query 语句现指定了一个附加的参数 $event,这是搜索的根。典型情况下,XPath 查询从文档根处开始,但您可指定其他根。为此,我使用了 $event 条目。

现在,我并不想获得所有标记。我想要的只是一个具有某个属性的标记,该属性名为 class,并包含一个特定的值(例如 url)。因此,我添加了如下语法:

.//*[contains(@class,'url')]

这样它就会匹配类名中包含 url 的标记。但我真正想获得的是该标记中的 href 属性,所以我进一步精炼了此查询,方法如下:

.//*[contains(@class,'url')]/@href

精炼过的这个查询将获取任何匹配标记的 href 属性。

事件完成并作为来自 get_events() 函数的数组返回后,我还需要另外一个函数,将这个事件数组导出为 XML。为此,我使用了 dump_events() 函数,如 清单 6 所示。

清单 6. dump_events() 函数
function dump_events( $events )
{
  $dom = new DomDocument();
  $dom->formatOutput = true;
  $root = $dom->createElement( 'events' );
  $dom->appendChild( $root );

  foreach( $events as $event )
  {
    $elEvent = $dom->createElement( 'event' );
    $root->appendChild( $elEvent );

    $elUrl = $dom->createElement( 'url' );
    $elUrl->appendChild( $dom->createTextNode( $event['url'] ) );
    $elEvent->appendChild( $elUrl );

    $elStart = $dom->createElement( 'start' );
    $elStart->appendChild( $dom->createTextNode( $event['dtstart'] ) );
    $elEvent->appendChild( $elStart );

    $elEnd = $dom->createElement( 'end' );
    $elEnd->appendChild( $dom->createTextNode( $event['dtend'] ) );
    $elEvent->appendChild( $elEnd );

    $elSummary = $dom->createElement( 'summary' );
    $elSummary->appendChild( $dom->createTextNode( $event['summary'] ) );
    $elEvent->appendChild( $elSummary );

    $elLocation = $dom->createElement( 'location' );
    $elLocation->appendChild( $dom->createTextNode( $event['location'] ) );
    $elEvent->appendChild( $elLocation );

    $elDesc = $dom->createElement( 'description' );
    $elDesc->appendChild( $dom->createTextNode( $event['desc'] ) );
    $elEvent->appendChild( $elDesc );
  }

  print( $dom->saveXML() );
}

此函数与其他函数背道而驰。这段代码不是围绕某些 XML 进行查询,而是使用 createElementappendElement 创建一个树,从而创建了一个 DOM。随后,我使用 saveXML 命令将数据导出到标准输出。

在命令行中使用 hcalendar.html 页面的 URL 运行这个 PHP 脚本时,我得到了 清单 7 所示的输出结果。

清单 7. PHP 脚本的输出结果
% php get_calendar.php http://localhost/micro/hcalendar.html
<?xml version="1.0"?>
<events>
  <event>
    <url>http://myevent.com</url>
    <start>20060501</start>
    <end>20060502</end>
    <summary>My Conference opening</summary>
    <location>Hollywood, CA</location>
    <description>The opening days of the conference</description>
  </event>
  <event>
    <url>http://myevent.com</url>
    <start>20060503</start>
    <end>20060504</end>
    <summary>My Conference closing</summary>
    <location>Hollywood, CA</location>
    <description>The closing days of the conference</description>
  </event>
</events>
%

现在我有了一个脚本,可以指向任何 Web 页面,并将任何 hCalendar 格式的条目提取为 XML。


通过 XML 创建 hCalendar 条目

既然已经有了从 Web 页面中提取出来的 XML,那么就可以创建一个 PHP 页面,将此 XML 格式化为 HTML 内的 hCalendar 条目。清单 8 显示了此页面。

清单 8. Index.php
<?php
$dom = new DomDocument();
$dom->load( "calendar.xml" );

$xpath = new DomXPath($dom);
$events = $xpath->query( '//event' );
?>
<html>
  <head>
    <title>My Calendar</title>
    <style>
      body { font-family: arial, verdana, sans-serif; }
      td { border-bottom: 1px solid black; border-top: 1px solid black; }
      abbr { border-bottom: none; }
    </style>
  </head>
  <body>
    <table>
      <?php
        foreach( $events as $event )
        {
          $desc = $xpath->query( 'description', $event )->item(0)->nodeValue;
          $start= $xpath->query( 'start', $event )->item(0)->nodeValue;
          $end = $xpath->query( 'end', $event )->item(0)->nodeValue;
          $location = $xpath->query( 'location', $event )->item(0)->nodeValue;
          $summary = $xpath->query( 'summary', $event )->item(0)->nodeValue;
          $url = $xpath->query( 'url', $event )->item(0)->nodeValue;
      ?>
      <tr>
        <td>
          <div class="vevent">
            <a class="url" href="<?php echo( $url ); ?>">
              <span class="summary"><?php echo($summary ); ?></span></a><br/>
              Start: <abbr class="dtstart" title="<?php echo($start ); ?>">
                <?php echo($start ); ?></abbr><br/>
              End: <abbr class="dtend" title="<?php echo($end ); ?>">
                <?php echo($end ); ?></abbr><br/>
                Location: <span class="location"><?php echo($location ); ?></span><br/>
              <div class="description"><?php echo($desc ); ?></div>
          </div>
        </td>
      </tr>
      <?php
      }
      ?>
    </table>
  </body>
</html>

这段代码似乎有些复杂,但实际上非常简单。加载前面使用 get_calendar.php 脚本创建的 calendar.xml 文件来启动页面。页面随后启动 HTML,直至 <table> 标记。在此标记中,我循环遍历了各个 <event> 标记,并将它们导出为 HTML 中的行。这样,就完成了 Web 页面。图 3 显示了最终结果。

图 3. index.php 页面
Index.php 页面

为了查看这段代码是否确实编码了 hCalendar 条目,我将 get_calendar.php 脚本指向它。清单 9 显示了结果。

清单 9. 事件 XML 片段
% php get_calendar.php http://localhost/micro/index.php
<?xml version="1.0"?>
<events>
  <event>
    <url>http://myevent.com</url>
    <start>20060501</start>
    <end>20060502</end>
    <summary>My Conference opening</summary>
    <location>Hollywood, CA</location>
    <description>The opening days of the conference</description>
  </event>
...
%

这有多好呢?我有了一个脚本,能读取带有日历条目的页面并将这些条目导出为 XML。然后我又有了另外一个页面,能将 XML 转换回日历条目。随后原始脚本可以读取该页面并获得相同的数据。这最终变成了一个无休无止的循环。

好吧,或许这不怎么样。也不怎么美观。如果我想让显示效果更美观一点,会怎么样呢?我是否必须转储微格式?完全不需要。在 清单 10 中,我改进了日历条目的格式。

清单 10. Index2.php
...
<?php
foreach( $events as $event )
{
  $desc = $xpath->query( 'description', $event )->item(0)->nodeValue;
  $start= $xpath->query( 'start', $event )->item(0)->nodeValue;
  $end = $xpath->query( 'end', $event )->item(0)->nodeValue;
  $location = $xpath->query( 'location', $event )->item(0)->nodeValue;
  $summary = $xpath->query( 'summary', $event )->item(0)->nodeValue;
  $url = $xpath->query( 'url', $event )->item(0)->nodeValue;
?>
<tr>
  <td class="event">
    <div class="vevent">
      <table width="100%" cellspacing="0" cellpadding="0">
        <tr>
          <td colspan="2">
            <a class="url" href="<?php echo( $url ); ?>">
            <span class="summary"><?php echo($summary ); ?></span></a>
          </td>
        </tr>
        <tr>
          <td>Start</td>
          <td><abbr class="dtstart" title="<?php echo($start ); ?>">
            <?php echo($start ); ?></abbr></td>
        </tr>
        <tr>
          <td>End</td>
          <td><abbr class="dtend" title="<?php echo($end ); ?>">
            <?php echo($end ); ?></abbr></td>
        </tr>
        <tr>
          <td>Location</td>
          <td><span class="location"><?php echo($location ); ?></span></td>
        </tr>
        <tr>
          <td colspan="2">
            <div class="description"><?php echo($desc ); ?></div>
          </td>
        </tr>
      </table>
    </div>
  </td>
</tr>
<?php
}
?>
...

也许您很难从这些标签中发现有什么改变,但从显示效果中能清楚地看出差别,如 图 4 所示。

图 4. index2.php 页面
Index2.php 页面

现在,startendlocation 列排列得非常美观。但它是否仍然解析为 hCalendar 条目?是的,原因在于 get_calendar.php 脚本中的 XPath 代码极为灵活。

清单 11 展示了我为证明上述问题而运行的测试。

清单 11. 对 index2.php 的测试
% php get_calendar.php http://localhost/micro/index2.php
<?xml version="1.0"?>
<events>
  <event>
    <url>http://myevent.com</url>
    <start>20060501</start>
    <end>20060502</end>
...

我真的非常喜欢这两个读取和写入脚本之间的对称。


结束语

微格式是一种注重实效的方法,解决了 Web 上结构化数据的问题。从架构上来看,它是否纯粹是通过 XSLT 样式表这样的机制与格式相分离的 XML 编码数据?并非如此。但我认为这种方法是一种实际的中间步骤,能够帮助您构建起更为智能化的 Web,更易于使用,并且能够提供更好的搜索和数据集成。

参考资料

学习

获得产品和技术

  • 使用 IBM 试用软件 构建您的下一个开发项目,这些软件可直接从 developerWorks 下载得到。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Open source, Web development
ArticleID=155374
ArticleTitle=使用 microformats 分离数据与格式
publish-date=08212006