Menu

使用php的SimpleXML快速處理XML文件

瞭解和 PHP 版本 5 捆綁到一起的 SimpleXML 擴展,它使 PHP 頁面能夠以 PHP 友好的語法來查詢、搜索、修改和重新發佈 XML。

PHP 版本 5 引入了 SimpleXML,一種用於讀寫 XML 的新的應用程序編程接口(API)。在 SimpleXML 中,下面的這樣的表達式:

$doc->rss->channel->item->title

從文檔中選擇元素。只要熟悉文檔的結構,很容易編寫這種表達式。但是,如果不很清楚需要的元素出現在何處(比如 Docbook、HTML 和類似的敘述性文檔中),SimpleXML 可以使用 XPath 表達式尋找這些元素。

開始使用 SimpleXML

假設需要一個 PHP 頁面將 RSS 提要(feed)轉化成 HTML。RSS 是一種簡單的 XML 格式用於發佈連鎖內容。文檔的根元素是 rss,它包括一個 channel 元素。channel 元素包含關於提要的元數據,如標題、語言和 URL。它還包含各種封裝在 item 元素中的報導。每個 item 都有一個 link 元素,包括一個 URL,還有 titledescription(通常兩者都有),包含普通文本。不使用名稱空間。RSS 的內容當然不止這些,不過對本文來說知道這些就足夠了。清單 1 顯示了一個典型的例子,它包含兩個新聞項。


清單 1. RSS 提要
 
<?xml version="1.0" encoding="UTF-8"?>
<rss version="0.92">
<channel>
<title>Mokka mit Schlag</title>
<link>http://www.elharo.com/blog</link>
<language>en</language>
<item>
<title>Penn Station: Gone but not Forgotten</title>
<description>
The old Penn Station in New York was torn down before I was born.
Looking at these pictures, that feels like a mistake. The current site is
functional, but no more; really just some office towers and underground
corridors of no particular interest or beauty. The new Madison Square...
</description>
<link>http://www.elharo.com/blog/new-york/2006/07/31/penn-station</link>
</item>
<item>
<title>Personal for Elliotte Harold</title>
<description>Some people use very obnoxious spam filters that require you
to type some random string in your subject such as E37T to get through.
Needless to say neither I nor most other people bother to communicate with
these paranoids. They are grossly overreacting to the spam problem.
Personally I won't ...</description>

<link>http://www.elharo.com/blog/tech/2006/07/28/personal-for-elliotte-harold/</link>
</item>
</channel>
</rss>

我們來開發一個 PHP 頁面將 RSS 提要格式化為 HTML。清單 2 顯示了這個頁面的基本結構。


清單 2. PHP 代碼的靜態結構
 

<html xml:lang="en" lang="en">
<head>
<title><?php // The title will be read from the RSS ?></title>
</head>
<body>

<h1><?php // The title will be read from the RSS again ?></h1>

<?php
// Here we'll put a loop to include each item's title and description
?>

</body>
</html>

解析 XML 文檔

第一步是解析 XML 文檔並保存到變量中。只需要一行代碼,向 simplexml_load_file() 函數傳遞一個 URL 即可:

$rss =  simplexml_load_file('http://partners.userland.com/nytRss/nytHomepage.xml');

警告

這 裡選擇的方案絕不是最佳方案。實際上不應該每次單擊頁面時都加載和解析 RSS 提要。對於該頁面的讀者來說這樣做太慢,而且可能造成所加載 RSS 提要的拒絕服務,多數 RSS 都規定了適當的每小時最大的刷新次數。真正的解決方案應該緩衝生成的 HTML 頁面、RSS 提要或兩者。但是,我們重點是使用 SimpleXML 庫,因此這裡沒有過多考慮。

對於這個例子,我已經從 Userland 的 New York Times 提要(在 http://partners.userland.com/nytRss/nytHomepage.xml)填充了頁面。當然,也可使用其他 RSS 提要的任何 URL。

要注意,雖然名稱為 simplexml_load_file(),該函數實際上解析遠程 HTTP URL 上的 XML 文檔。但這並不是該函數唯一令人感到奇怪的地方。返回值(這裡存儲在 $rss 變量中)並沒有指向整個文檔,如果使用過其他 API 如文檔對象模型(DOM)您可能會這樣期望。相反,它指向文檔的根元素。從 SimpleXML 不能訪問文檔序言和結語部分的內容。

尋找提要標題

整個提要的標題(不是提要中各報導的標題)位於 rss 根元素 channeltitle 孩子中。很容易找到這個標題,就彷彿 XML 文檔是類 rss 的一個對象的序列化形式,它的 channel 字段本身帶有一個 title 字段。使用常規 PHP 對象引用語法,尋找標題的語句如下:

$title =  $rss->channel->title;

找到之後可以將其添加到輸出 HTML 中。這樣做很簡單,只要回顯 $title 變量即可:

<title><?php echo $title; ?></title>

這一行輸出元素的字符串值而不是整個元素。就是說寫入文本內容但不包括標籤。

甚至可以完全跳過中間變量 $title

<title><?php echo $rss->channel->title; ?></title>

因為該頁面在多處重用這個值,我發現用一個含義明確的變量來存儲會更方便。

迭代新聞項

然後必須發現提要中的項。完成這項任務的表達式很簡單:

$rss->channel->item

但是,提要通常包含多個新聞項。但也可能一個也沒有。因此,該語句返回一個數組,可以通過 for-each 循環來遍歷它:

foreach ($rss->channel->item as $item) {
echo "<h2>" . $item->title . "</h2>";
echo "<p>" . $item->description . "</p>";
}

通過從 RSS 提要中讀取 link 元素值添加鏈接也很容易。只要在 PHP 中輸出一個 a 元素,並使用 $item->link 檢索 URL 即可。清單 3 增加了該元素並填充到 清單 1 的框架中。


清單 3. 簡單而完整的 PHP RSS 閱讀器
 
$rss = simplexml_load_file('http://partners.userland.com/nytRss/nytHomepage.xml');
$title = $rss->channel->title;
?>
<html xml:lang="en" lang="en">
<head>
<title>



</h1>

<?php
// Here we'll put a loop to include each item's title and description
foreach ($rss->channel->item as $item) {
echo "<h2><a href='" . $item->link . "'>" . $item->title . "</a></h2>";
echo "<p>" . $item->description . "</p>";
}
?>

</body>
</html>


這樣就用 PHP 完成了一個簡單的 RSS 閱讀器:只需要幾行 HTML 和幾行 PHP。不算空白的話一共只有 20 行。當然,這個實現的功能還不夠豐富,也不夠優化或者健壯。我們來看看還能做什麼。







錯誤處理

並非所有 RSS 提要都如期望的那樣結構良好。XML 規範要求處理程序在發現結構良好性錯誤時停止處理文檔,SimpleXML 是符合標準的 XML 處理程序。但是在發現錯誤時它沒有提供多少幫助。一般來說,它在 php-errors 文件中記錄錯誤(但是不包括詳細的錯誤消息),simplexml-load-file() 函數返回 FALSE。如果不能確保解析的文件是結構良好的,在使用文件數據之前要檢查錯誤,如清單 4 所示。


清單 4. 避免結構錯誤的輸入
 
<?php
$rss = simplexml_load_file('http://www.cafeaulait.org/today.rss');
if ($rss) {
foreach ($rss->xpath('//title') as $title) {
echo "<h2>" . $title . "</h2>";
}
}
else {
echo "Oops! The input is malformed!";
}
?>

其他常見的錯誤是文檔實際上是結構良好的,但是沒有在期望的地方包含期望的元素。如果項沒有標題(比如在 top-100 這樣的 RSS 提要中),$doc->rss->channel->item->title 這樣的表達式會怎麼樣呢?最簡單的辦法是將返回值永遠看作一個數組並循環遍歷該數組。這樣就可以判斷元素比預期的多還是少。但是,如果確定只需要文檔中的第一個元素 —— 即使有多個,可以按索引訪問,索引號從零開始。比如,如果要請求一個項的標題,可以用如下代碼:

$doc->rss->channel->item[0]->title[0]

如果沒有第一項,或者第一項沒有標題,該項就按照常規 PHP 數組索引越界處理。即結果是空,在將其插入輸出 HTML 時,它會被轉化成空白字符串。

識別和拒絕不打算處理的非預期格式通常屬於 XML 驗證解析器的範疇。然而,SimpleXML 不能針對文檔類型定義(DTD)或模式進行驗證。它只檢查結構良好性。




處理名稱空間

很多站點現在從 RSS 轉向了 Atom。清單 5 顯示了一個 Atom 文檔的例子。該文檔大部分和 RSS 的例子類似。但是增加了一些元數據,而且根元素變成了 feed 而不是 rssfeed 元素包含 entry 而不是項(item)。content 元素代替了 description。最重要的是,Atom 文檔使用了名稱空間,但 RSS 文檔沒有。這樣,Atom 文檔就可以在內容中內嵌真正的、沒有轉義的可擴展 HTML(XHTML)。


清單 5. Atom 文檔
 
<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"
xml:base="http://www.cafeconleche.org/today.atom">
<updated>2006-08-04T16:00:04-04:00</updated>
<id>http://www.cafeconleche.org/</id>
<title>Cafe con Leche XML News and Resources</title>
<link rel="self" type="application/atom+xml" href="/today.atom"/>
<rights>Copyright 2006 Elliotte Rusty Harold</rights>
<entry>
<title>Steve Palmer has posted a beta of Vienna 2.1, an open source
RSS/Atom client for Mac OS X.
</title>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml"
id="August_1_2006_25279" class="2006-08-01T07:01:19Z">

<p>
Steve Palmer has posted a beta of <a shape="rect"
href="http://www.opencommunity.co.uk/vienna21.php">Vienna
2.1</a>, an open source RSS/Atom client for Mac OS X. Vienna
is the first reader I've found acceptable for daily use; not
great but good enough. (Of course my standards for "good
enough" are pretty high.) 2.1 focuses on improving the user
interface with a unified layout that lets you scroll through
several articles, article filtering (e.g. read all articles
since the last refresh), manual folder reordering, a new get
info window, and an improved condensed layout.
</p>

</div>
</content>
<link href="/#August_1_2006_25279"/>
<id>http://www.cafeconleche.org/#August_1_2006_25279</id>
<updated>2006-08-01T07:01:19Z</updated>
</entry>
<entry>
<title>Matt Mullenweg has released Wordpress 2.0.4,
a blog engine based on PHP and MySQL.
</title>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml"
id="August_1_2006_21750" class="2006-08-01T06:02:30Z">

<p>
Matt Mullenweg has released <a shape="rect"
href="http://wordpress.org/development/2006/07/wordpress-204
/">Wordpress 2.0.4</a>, a blog engine based on PHP and
MySQL. 2.0.4 plugs various security holes, mostly involving
plugins.
</p>
</div>
</content>
<link href="/#August_1_2006_21750"/>
<id>http://www.cafeconleche.org/#August_1_2006_21750</id>
<updated>2006-08-01T06:02:30Z</updated>
</entry>

</feed>

雖然元素名稱變了,但用 SimpleXML 處理 Atom 文檔的基本方法和 RSS 相同。一個區別是現在請求被命名的元素和本地名稱時必須指定名稱空間統一資源標識符(URI)。這需要兩個步驟:首先通過向 children() 函數傳遞名稱空間 URI 請求給定名稱空間中的孩子元素。然後用那個名稱空間中適當的本地名稱請求元素。假設第一次把 Atom 提要加載到變量 $feed 中,如下所示:

$feed = simplexml_load_file('http://www.cafeconleche.org/today.atom');

下面的兩行尋找 title 元素:

$children =  $feed->children('http://www.w3.org/2005/Atom');
$title = $children->title;

如果願意可以將這些代碼壓縮成一行,雖然行會變得有點長。名稱空間中的所有其他元素也必須類似處理。清單 6 給出了一個完整的 PHP 頁面,其中顯示帶名稱空間的 Atom 提要中的標題。


清單 6. 簡單的 PHP Atom 標題閱讀器
 
$children = $feed->children('http://www.w3.org/2005/Atom');
$title = $children->title;
?>
<html xml:lang="en" lang="en">
<head>
<title>



</h1>

<?php

$entries = $children->entry;
foreach ($entries as $entry) {

$details = $entry->children('http://www.w3.org/2005/Atom');
echo "<h2>" . $details->title . "</h2>";
}
?>

</body>
</html>





混合的內容

為什麼這個例子中只顯示標題行呢?因為在 Atom 中,記錄的內容可以包含報導的全部文本:不僅僅是普通文本,還包括標記。這是一種敘述性結構:行中的詞句是供人閱讀的。和多數的此類數據相似,也有大量的混合內容。於是 XML 就不那麼簡單了,SimpleXML 方法也開始顯示出了一些不足之處。由於不能合理地處理混合內容,這一不足使其在很多應用中被排除了。

可以做到一點,但這不是一個完整的解決方案,只能用於 content 元素包含真正的 XHTML 的情況。可以使用 asXML() 函數將這些 XHTML 作為非解析源代碼直接複製到輸出中,比如:

echo "<p>" . $details->content->asXML() . "</p>";

生成的結果如清單 7 所示。


清單 7. asXML 輸出
 
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml"
id="August_7_2006_31098" class="2006-08-07T09:38:18Z">
<p>
Nikolai Grigoriev has released <a shape="rect"
href="http://www.grigoriev.ru/svgmath">SVGMath 0.3</a>, a
presentation MathML formatter that produces SVG written in
pure Python and published under an MIT license. According to
Grigoriev, "The new version can work with multiple-namespace
documents (e.g. replace all MathML subtrees with SVG in an
XSL-FO or XHTML document); configuration is made more
flexible, and several bugs are fixed. There is also a
stylesheet to adjust the vertical position of the resulting
SVG image in XSL-FO."
</p>
</div>
</content>

這不是純粹的 XHTML。content 元素悄悄從 Atom 文檔中溜了進來,您真的不願這樣。更糟的是,它的名稱空間不對,因此不能被識別。幸運的是,這個多出來的元素實際上沒有多大害處,因為 Web 瀏覽器會忽略不認識的任何標籤。完成的文檔是無效的,但是關係不大。如果還是覺得彆扭,可以通過字符串操作將其去掉,如下所示:

  $description = $details->content->asXML();
$tags = array('<content type="xhtml"'>", "</content>");
$notags = array("", "");
$description = str_replace($tags, $notags, $description);

為了使代碼更加健壯,可以使用正則表達式而不是假定起始標籤和前面相同。具體來說,可以考慮各種可能的屬性:

  // end-tag is fixed in form so it's easy to replace
$description = str_replace("</content>", "", $description);
// remove start-tag, possibly including attributes and white space
$description = ereg_replace("<content[^>]*>", "", $description);

即使這樣改進之後,代碼還是會在註釋、處理指令和 CDATA 節上出錯。無論怎麼分解,恐怕都不會簡單了。混合內容實際上超出了 SimpleXML 所能處理的範圍。




XPath

只要知道文檔有什麼元素以及在什麼位置,$rss->channel->item->title 這樣的表達式很方便。但是,不一定會知道得這麼清楚。比方說,在 XHTML 中,標題元素(h1h2h3 等等)可以是 bodydivtable 或其他幾種元素的孩子。此外,divtableblockquote 及其他元素又可以互相嵌套多次。在很多不那麼明確的場合中,使用 //h1//h1[contains('Ben')] 這樣的 XPath 表達式更方便。SimpleXML 通過 xpath() 函數支持這種功能。

清單 8 顯示的 PHP 頁面列出了 RSS 文檔中的所有標題,包括提要本身以及每個項的標題。


清單 8. 使用 XPath 查找 title 元素
 
<html xml:lang="en" lang="en">
<head>
<title>XPath Example</title>
</head>
<body>

<?php
$rss = simplexml_load_file('http://partners.userland.com/nytRss/nytHomepage.xml');
foreach ($rss->xpath('//title') as $title) {
echo "<h2>" . $title . "</h2>";
}
?>

</body>
</html>

SimpleXML 僅支持 XPath 位置路徑及位置路徑的組合。不支持那些不返回節點集的 XPath 表達式,如 count(//para)contains(title)

從 PHP 5.1 版開始,SimpleXML 可以直接對帶名稱空間的文檔使用 XPath 查詢。和通常一樣,XPath 位置路徑必須使用名稱空間前綴,即使搜索的文檔使用默認名稱空間也仍然如此。registerXPathNamespace() 函數把前綴和後續查詢中使用的名稱空間 URL 聯繫在一起。比方說,如果要查詢 Atom 文檔中的所有 title 元素,應使用清單 9 中所示的代碼。


清單 9. 使用 XPath 和名稱空間
 
$atom = simplexml_load_file('http://www.cafeconleche.org/today.atom');
$atom->registerXPathNamespace('atm', 'http://www.w3.org/2005/Atom');
$titles = $atom->xpath('//atm:title');
foreach ($titles as $title) {
echo "<h2>" . $title . "</h2>";
}

最後一點忠告:PHP 中的 XPath 速度非常慢。當改為 XPath 表達式之後頁面加載延遲從難以覺察變成了幾秒鐘,即使是在負荷不高的本地服務器上。如果採用這些技術,必須使用某種緩存技術來獲得適當的性能。不可能動態生成每個頁面。







結束語

如 果不需要處理混合內容,SimpleXML 對於 PHP 程序員的工具箱來說是個不錯新玩意。其適用的情況很多。具體而言,它能夠很好地處理簡單的、類似記錄的數據。只要文檔層次不深、不很複雜,而且沒有混合內 容,SimpleXML 要比使用 DOM 簡單得多。如果事先知道文檔結構該工具將更有用,雖然通過 XPath 可以滿足這種要求。雖然不支持驗證和混合內容有點不方便,但不是絕對的。很多簡單格式沒有混合內容,而且很多應用只涉及到可預知的數據格式。如果符合您的 需要,可以自己嘗試一下 SimpleXML。只要對錯誤處理稍加注意,並且通過緩存來解決性能問題,SimpleXML 可以成為 PHP 中一種可靠、健壯的 XML 處理方法。



參考資料

學習
  • 您可以參閱本文在 developerWorks 全球網站上的 英文原文
  • SimpleXML 的官方文檔:仔細閱讀 PHP 版本 5 用戶手冊,瞭解這個將 XML 轉換成對象以便用一般屬性選擇器和數組迭代器處理的工具集。
  • XML in a Nutshell(Elliotte Rusty Harold 和 W. Scott Means,O'Reilly,2005):該書集中介紹了 XML,請深入閱讀關於 XML 和 XPath 的內容。
  • Ajax RSS reader」(Jack D. Herrington,developerWorks,2006 年 5 月):討論了在 PHP 中使用數據庫、JavaScript 代碼以及 DOM 編寫 RSS 的一種高級方法。
  • An overview of the Atom version 1.0 Syndication Format」(James Snell,developerWorks,2005 年 8 月):討論了 Atom 與其他聯合格式相比在技術上的優勢,並通過幾個典型的例子加以說明。
  • IBM XML 認證:看看如何才能成為一名 IBM 認證的 XML 及相關技術的開發人員。
  • XML 技術庫:developerWorks XML 專區提供了大量技術文章和技巧、教程、標準以及 IBM 紅皮書。
  • 隨時關注developerWorks 技術事件和網絡廣播

獲得產品和技術
  • 使用IBM 試用軟件:構建您的下一個項目,可直接從 developerWorks 下載。

討論


關於作者


Elliotte Rusty Harold 來自新奧爾良, 現在他還定期回老家喝一碗美味的秋葵湯。不過目前,他和妻子 Beth 定居在紐約臨近布魯克林的 Prospect Heights,同住的還有他的貓咪 Charm(取自夸克)和 Marjorie(取自他岳母的名字)。他是 Polytechnic 大學計算機科學的副教授,他在該校講授 Java 和面向對象編程。他的 Web 站點 Cafe au Lait 已經成為 Internet 上最流行的獨立 Java 站點之一,它的姊妹站點 Cafe con Leche 已經成為最流行的 XML 站點之一。他的書包括 Effective XML Processing XML with Java Java Network Programming The XML 1.1 Bible。他目前在從事處理 XML 的 XOM API、Jaxen XPath 引擎和 Jester 測試覆蓋率工具的開發工作。