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
是很值得考慮的一個工具。