PHP 5 引入了新的類 XMLReader,用於讀取可擴展標記語言(Extensible Markup Language,XML)。與 SimpleXML 或文檔對象模型(Document Object Model,DOM)不同,XMLReader 以流模式進行操作。即它從頭到尾讀取文檔。在文檔後面的內容編譯完成之前,可以先處理已編譯好的文檔前面的內容,從而實現非常快速、非常高效、非常節省地使用內存。需要處理的文檔越大,這個特點就越發重要。
這裡所說的 XMLReader API 位於 Gnome Project 中用於 C 和 C++ 的 libXML 庫之上。實際上 XMLReader 只是在 libXML 的 XMLTextReader API 之上的很薄的 PHP 層。XMLTextReader 本身是模仿 .Net 的 XMLTextReader 類和 XMLReader 類,盡管並不具有與這些類相似的代碼。
與 Simple API for XML (SAX) 不同,XMLReader 是推解析器,而不是拉解析器。這意味著程序是可以控制的。您將告訴解析器何時獲取下一個文檔片段,而不是在解析器看到文檔後告訴您所看到的內容。您將請求內容,而不是對內容進行反應。從另一個角度來考慮這個問題:XMLReader 是 Iterator 設計模式的實現,而不是 Observer 設計模式的實現。
示例問題
先從簡單例子開始討論。假定正在編寫 PHP 腳本,用來接收 XML-RPC 請求並生成響應。更具體一些,假定請求如清單 1 所示。文檔的根元素是 methodCall,它包含 methodName 元素和 params 元素。方法的名稱是 sqrt。params 元素包含一個 param 元素,param 元素包含 double,double 的平方根是希望得到的值。沒有使用名稱空間。
<?XML version="1.0"?>
<methodCall>
<methodName>sqrt</methodName>
<params>
<param>
<value><double>36.0</double></value>
</param>
</params>
</methodCall>
下面是 PHP 腳本需要完成的工作:
sqrt(它是該腳本懂得如何處理的惟一方法),則生成錯誤響應。
<?XML version="1.0"?>
<methodResponse>
<params>
<param>
<value><double>6.0</double></value>
</param>
</params>
</methodResponse>
下面我們逐步展開說明。
回頁首
初始化解析器並載入文檔
第一步是創建新的解析器對象。創建操作很簡單:
$reader = new XMLReader();
接著,需要為它提供一些用於解析的數據。對於 XML-RPC,這是超文本傳輸協議(Hypertext Transfer Protocol,HTTP)請求的原始主體。然後可以將該字符串傳遞到讀取器的 XML() 函數:
如果發現 $HTTP_RAW_POST_DATA 是空的,則將以下代碼行添加到 PHP.ini 文件:
always_populate_raw_post_data = On
$request = $HTTP_RAW_POST_DATA; $reader->XML($request);
可以解析任何字符串,無論它是從何處獲取的。例如,可以是程序中的一串文字或從本地文件讀取。還可以使用 open() 函數從外部 URL 載入數據。例如,下面的語句准備解析其中一個 Atom 提要:
$reader->XML('http://www.cafeaulait.org/today.atom');無論是從何處獲取原始數據,現在已建立了閱讀器並為解析做好准備。
回頁首
讀取文檔
read() 函數使解析器前進到下一個標記。最簡單的方法是在 while 循環中遍歷整個文檔:
while ($reader->read()) {
// processing code goes here...
}完成遍歷後,關閉解析器以釋放它所持有的任何資源,並且重置解析器以便用於下一個文檔:
$reader->close();
在循環內部,將解析器放置在特殊節點上:元素的起點、元素的終點、文本節點、注釋等等。通過檢查以下屬性,可以發現解析器正在查看的內容:
localName 是本地的、未帶前綴的節點名。name 是可能的節點前綴名。對於像注釋這種沒有名稱的節點,包括 #comment、#text、#document 等等,與 DOM 中的一樣。namespaceURI 是節點名稱空間的統一資源標識符(Uniform Resource IdentifIEr,URI)。nodeType 是代表節點類型的整數 —— 例如,2 代表屬性節點,7 代表處理指令。prefix 是節點的名稱空間前綴。value 是節點的下一個文本內容。hasValue 值為 true;否則,值為 false。當然,並非所有節點類型都具有所有這些屬性。例如,文本節點、CDATA 部件、注釋、處理指令、屬性、空格、文檔類型和 XML 聲明具有值。而其它節點類型(最重要的是元素和文檔)則沒有值。通常,程序將使用 nodeType 屬性來斷定它所查找的內容,然後做出適當的響應。清單 3 展示了簡單的 while 循環,該循環使用這些函數來打印它所查看的內容。清單 4 展示了將清單 1 輸入程序後的輸出。
while ($reader->read()) {
echo $reader->name;
if ($reader->hasValue) {
echo ": " . $reader->value;
}
echo "\n";
}methodCall
#text:
methodName
#text: sqrt
methodName
#text:
params
#text:
param
#text:
value
double
#text: 10
double
value
#text:
param
#text:
params
#text:
methodCall大多數程序並非這麼簡單。它們接受特定格式的輸入,並以某種方式來處理輸入。在 XML-RPC 例子中,僅需要讀取輸入中的一個元素:double 元素,該元素應該只有一個。為此,查找名稱為 double 的元素的起點:
if ($reader->name == "double"
&& $reader->nodeType == XMLReader::ELEMENT) {
// ...
}該元素可能有單個文本子節點,可以通過將解析器前進到下一個節點來進行讀取,如下所示:
if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {
$reader->read();
respond($reader->value);
}在這裡 respond() 函數構建了 XML-RPC 響應並將它發送到客戶機。但是,在展示上述操作前,還有一些事情需要處理。不能絕對保證請求文檔中的 double 元素僅包含一個文本節點。可能包含多個文本節點,以及注釋和處理指令。例如,可能看起來像以下代碼:
<value><double> <!--value follows-->6.<!--fractional part next-->0 </double></value>
該模式存在一個潛在的缺陷。嵌套的 double 元素(例如 <double>6<double>1.2</double></double>)將違背該算法。然而它將成為無效的 XML-RPC;並且不久您將看到如何使用 RELAX NG 驗證來拒絕所有此類文檔。在諸如可擴展超文本標記語言(Extensible Hypertext Markup Language,XHtml)之類的文檔類型中,允許相同元素互相包含(例如 table 元素包含在另一個 table 元素中),因此您還需要知道元素的深度,從而確保結束標記與開始標記之間進行正確匹配。
一個健壯的解決方案需要獲得 double 元素的所有文本子節點,將它們連接起來,並且僅將結果轉換為 double。必須小心避免任何注釋或可能出現的其它非文本節點。這有一點復雜,但並不是十分復雜,如清單 5 所示。
while ($reader->read()) {
if ($reader->nodeType == XMLReader::TEXT
|| $reader->nodeType == XMLReader::CDATA
|| $reader->nodeType == XMLReader::WHITESPACE
|| $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
$input .= $reader->value;
}
else if ($reader->nodeType == XMLReader::END_ELEMENT
&& $reader->name == "double") {
break;
}
}您可以暫時忽略文檔中的其它任何內容。(稍後我將添加更多的錯誤處理。)
回頁首
構建響應
正如它的名稱所暗示的,XMLReader 僅僅用於讀取。相應的 XMLWriter 類正在開發中,但還不能投入到生產。幸運的是,寫入 XML 比讀取 XML 要容易得多。首先,應使用 header() 函數來設置響應的媒體類型。對於 XML-RPC 來說,媒體類型是 application/XML。例如:
header('Content-type: application/XML');通常直接將內容顯示在頁面上,如清單 6 中的 respond() 函數所示。
function respond($input) {
echo "<?XML version='1.0'?>
<methodResponse>
<params>
<param>
<value><double>" .
sqrt($input)
. "</double></value>
</param>
</params>
</methodResponse>";
}
甚至可以將響應的文字部分直接嵌入 PHP 頁面中,就像使用 Html 時一樣。清單 7 展示了該技術。
function respond($input) {
?><?XML version='1.0'?>
<methodResponse>
<params>
<param>
<value><double>"<?php
echo sqrt($input);
?>
</double></value>
</param>
</params>
</methodResponse>
<?PHP
}回頁首
錯誤處理
到現在為止,一直隱含假定輸入文檔是格式規范的文檔。但是不能保證情況都是如此。像任何 XML 解析器一樣,只要發現一個規范格式錯誤,XMLReader 就必須停止處理。如果是這樣的話,read() 函數將返回 false。
從理論上講,解析器將報告數據直到發現第一個錯誤。但是在對小型文檔進行試驗時,幾乎是立刻顯示錯誤信息。底層解析器將預解析大塊文檔,對它進行緩存,然後每次分發出一小塊文檔。因此往往會過早地檢查錯誤。出於安全考慮,不要假定在發現第一個規范格式錯誤之前能夠解析內容。此外,也不要假設解析錯誤出現之前看不到任何內容。如果希望只接受完整的、格式規范的文檔,那麼請確保在看到文檔終點之前腳本不能進行任何不可逆操作。
如果解析器檢測到規范格式錯誤,那麼 read() 函數將顯示如下錯誤消息(如果啟用了詳細錯誤報告,且位於開發服務器上時):
<br /> <b>Warning</b>: XMLReader::read() [<a href='function.read'>function.read</a>]: < value><double>10</double></value> in <b>/var/www/root.PHP</b> on line <b>35</b><br />
您可能不希望將它復制到用戶所看到的 Html 頁面中。更好的方法是在 $PHP_errormsg 環境變量中捕獲錯誤消息。為此,需要啟用 PHP.ini 文件中的 track_errors 配置選項:
track_errors = On
默認情況下,track_errors 選項是關閉的;這在 php.ini 中是顯式指定的,因此請確保更改了該行代碼。如果提早在 PHP.ini 中添加了上述一行代碼(正如最初我所進行的操作),則後面的 track_errors = Off 代碼將重寫先前的代碼。
該程序僅將響應發送到完整的、格式良好的輸入。(也是有效的,不過將實現這點。)因此您需要等待,直到完成了文檔的解析(已經跳出 while 循環)。這時,檢查是否設置了 $PHP_errormsg 變量。如果沒有進行設置,則文檔是格式良好的文檔,然後發送 XML-RPC 響應消息。如果設置了該變量,則文檔不是格式良好的文檔,並發送 XML-RPC 錯誤響應。如果有人請求負數的平方根,也將發送錯誤響應。清單 8 展示以上操作。
// set up the request
$request = $HTTP_RAW_POST_DATA;
error_reporting(E_ERROR | E_WARNING | E_PARSE);
if (isset($php_errormsg)) unset(($php_errormsg);
// create the reader
$reader = new XMLReader();
// $reader->setRelaxNGSchema("request.rng");
$reader->XML($request);
$input = "";
while ($reader->read()) {
if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {
while ($reader->read()) {
if ($reader->nodeType == XMLReader::TEXT
|| $reader->nodeType == XMLReader::CDATA
|| $reader->nodeType == XMLReader::WHITESPACE
|| $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
$input .= $reader->value;
}
else if ($reader->nodeType == XMLReader::END_ELEMENT
&& $reader->name == "double") {
break;
}
}
break;
}
}
// make sure the input was well-formed
if (isset($php_errormsg) ) fault(21, $PHP_errormsg);
else if ($input < 0) fault(20, "Cannot take square root of negative number");
else respond($input);
這是 XML 流處理中簡單的常見模式。解析器將填寫一個數據結構,當完成文檔時該數據結構將起作用。通常數據結構要比文檔本身簡單。這裡所使用的數據結構尤其簡單:一個字符串。
回頁首
驗證
在 libXML 的早期版本中,RELAX NG 有一些嚴重錯誤,XMLReader 取決於 libXML 庫。請確保所使用的版本至少是 2.06.26 版。很多系統(包括 Mac OS X Tiger)捆綁了較早的、有錯誤的 libXML 版本。
到目前為止,對於驗證數據是否位於所預期的地方,並沒有給予關注。實現該驗證的最簡單的方法是檢查文檔的模式。XMLReader 支持 RELAX NG 模式語言;清單 9 展示了簡單的 RELAX NG 模式,用於這個特定的 XML-RPC 請求表單。
<element name="methodCall" xmlns="http://relaxng.org/ns/structure/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<element name="methodName">
<value>sqrt</value>
</element>
<element name="params">
<element name="param">
<element name="value">
<element name="double">
<data type="double"/>
</element>
</element>
</element>
</element>
</element>可以使用 setRelaxNGSchemaSource() 將模式作為一串文字直接嵌入 PHP 腳本,或者使用 setRelaxNGSchema() 從外部文件或 URL 讀取模式。例如,假定清單 9 位於 sqrt.rng 文件中,下面將展示如何載入模式:
reader->setRelaxNGSchema("sqrt.rng")在開始解析文檔 之前,執行上述操作。解析器在進行讀取時將檢查文檔的模式。若要檢查文檔是否有效,則調用 isValid(),如果文檔是有效的(目前為止),則返回 true,否則,返回 false。清單 10 展示了完整的程序,包括所有錯誤處理。這樣將接受任何合法輸入,然後返回正確的值,而且將拒絕所有不正確的請求。我還添加了 fault() 方法,當發生故障時將發送 XML-RPC 錯誤響應。
<?php
header('Content-type: application/xml');
// try grammar
$schema = "<element name='methodCall'
xmlns='http://relaxng.org/ns/structure/1.0'
datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes'>
<element name='methodName'>
<value>sqrt</value>
</element>
<element name='params'>
<element name='param'>
<element name='value'>
<element name='double'>
<data type='double'/>
</element>
</element>
</element>
</element>
</element>";
if (!isset($HTTP_RAW_POST_DATA)) {
fault(22, "Please make sure always_populate_raw_post_data = On in php.ini");
}
else {
// set up the request
$request = $HTTP_RAW_POST_DATA;
error_reporting(E_ERROR | E_WARNING | E_PARSE);
// create the reader
$reader = new XMLReader();
$reader->setRelaxNGSchema("request.rng");
$reader->XML($request);
$input = "";
while ($reader->read()) {
if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {
while ($reader->read()) {
if ($reader->nodeType == XMLReader::TEXT
|| $reader->nodeType == XMLReader::CDATA
|| $reader->nodeType == XMLReader::WHITESPACE
|| $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
$input .= $reader->value;
}
else if ($reader->nodeType == XMLReader::END_ELEMENT
&& $reader->name == "double") {
break;
}
}
break;
}
}
if (isset($php_errormsg) ) fault(21, $php_errormsg);
else if (! $reader->isValid()) fault(19, "Invalid request");
else if ($input < 0) fault(20, "Cannot take square root of negative number");
else respond($input);
$reader->close();
}
function respond($input)
{
?>
<methodResponse>
<params>
<param>
<value><double><?php
echo sqrt($input);
?></double></value>
</param>
</params>
</methodResponse>
<?PHP
}
function fault($code, $message)
{
echo "<?XML version='1.0'?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>" . $code . "</int></value>
</member>
<member>
<name>faultString</name>
<value>
<string>" . $message . "</string>
</value>
</member>
</struct>
</value>
</fault>
</methodResponse>";
}回頁首
屬性
在正常的推解析期間不會看到屬性。若要讀取屬性,請停止在元素的起點處,通過名稱或編號來請求特定屬性。
將需要的屬性名稱傳遞到 getAttribute(),以便在當前元素上查找該屬性的值。例如,下面的語句請求當前元素的 id 屬性:
$id = $reader->getAttribute("id");如果屬性位於名稱空間中 —— 例如,xlink:href —— 則調用 getAttributeNS(),將本地名稱和名稱空間 URI 分別作為第一個和第二個參數進行傳遞。(前綴是無關緊要的。)例如,下面的語句將請求 http://www.w3.org/1999/xlink/ 名稱空間中 xlink:href 屬性的值:
$href = $reader->getAttributeNS("href", "http://www.w3.org/1999/xlink/");如果屬性不存在,那麼這兩種方法都將返回空字符串。(這是不正確的。它們應該返回 null。當前設計很難區分值為空字符串的屬性和值根本不存在的屬性。)
在 XML 文檔中,屬性次序並不重要,並且不受解析器的保護。這裡用於屬性索引的編號僅僅是為了方便起見。不能保證開始標記中的第一個屬性就是屬性 1,第二個就是屬性 2 等等。不要編寫依賴於屬性次序的代碼。
如果僅希望了解元素上的所有屬性,並且事先並不知道屬性名,那麼當讀取器位於元素上時,調用 moveToNextAttribute()。一旦解析器位於屬性節點上,就可以讀取屬性的名稱、名稱空間以及元素所使用的相同屬性的值。例如,以下代碼片段將打印當前元素的所有屬性:
if ($reader->hasAttributes and $reader->nodeType == XMLReader::ELEMENT) {
while ($reader->moveToNextAttribute()) {
echo $reader->name . "='" . $reader->value . "'\n";
}
echo "\n";
}
對於 XML API 來說非常難得的是,XMLReader 允許從元素的起點 或終點 讀取屬性。為了避免重復計算,確認代碼類型是 XMLReader::ELEMENT 而不是 XMLReader::END_ELEMENT 是很重要的,後者也可能擁有屬性。
回頁首
結束語
XMLReader 是添加到 PHP 程序員工具箱中的一個很有用的工具。與 SimpleXML 不同,它是處理所有文檔(而不是部分文檔)的完整 XML 解析器。與 DOM 不同,它可以處理大於可用內存的文檔。與 SAX 不同,它將程序置於控制之下。如果 PHP 程序需要接受 XML 輸入,則 XMLReader 是很值得考慮的一個工具。