DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> XML學習教程 >> XML詳解 >> PHP 中的 XML 拉模式解析
PHP 中的 XML 拉模式解析
編輯:XML詳解     

PHP 5 引入了新的類 XMLReader,用於讀取可擴展標記語言(Extensible Markup Language,XML)。與 SimpleXML 或文檔對象模型(Document Object Model,DOM)不同,XMLReader 以流模式進行操作。即它從頭到尾讀取文檔。在文檔後面的內容編譯完成之前,可以先處理已編譯好的文檔前面的內容,從而實現非常快速、非常高效、非常節省地使用內存。需要處理的文檔越大,這個特點就越發重要。

libXML

這裡所說的 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 元素。方法的名稱是 sqrtparams 元素包含一個 param 元素,param 元素包含 doubledouble 的平方根是希望得到的值。沒有使用名稱空間。


清單 1. XML-RPC 請求
 
<?XML version="1.0"?>
<methodCall>
  <methodName>sqrt</methodName>
  <params>
    <param>
      <value><double>36.0</double></value>
    </param>
  </params>
</methodCall>

下面是 PHP 腳本需要完成的工作:

  1. 檢查方法名,如果不是 sqrt(它是該腳本懂得如何處理的惟一方法),則生成錯誤響應。
  2. 找到參數,如果參數不存在或參數類型錯誤,則生成錯誤響應。
  3. 另外,計算平方根。
  4. 在表單中返回結果,如清單 2 所示。

清單 2. XML-RPC 響應
 
<?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 輸入程序後的輸出。


清單 3. 解析器所查看的內容
     while ($reader->read()) {
      echo $reader->name;
      if ($reader->hasValue) {
        echo ": " . $reader->value;
      }
      echo "\n";
    }


清單 4. 清單 3 的輸出
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 所示。


清單 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() 函數所示。


清單 6. Echo XML
  function respond($input) {

  echo "<?XML version='1.0'?>
<methodResponse>
  <params>
    <param>
      <value><double>" .
       sqrt($input)
  . "</double></value>
    </param>
  </params>
</methodResponse>";
  
}

甚至可以將響應的文字部分直接嵌入 PHP 頁面中,就像使用 Html 時一樣。清單 7 展示了該技術。


清單 7. 文字表示的 XML
  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 展示以上操作。


清單 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 版本

libXML 的早期版本中,RELAX NG 有一些嚴重錯誤,XMLReader 取決於 libXML 庫。請確保所使用的版本至少是 2.06.26 版。很多系統(包括 Mac OS X Tiger)捆綁了較早的、有錯誤的 libXML 版本。

到目前為止,對於驗證數據是否位於所預期的地方,並沒有給予關注。實現該驗證的最簡單的方法是檢查文檔的模式。XMLReader 支持 RELAX NG 模式語言;清單 9 展示了簡單的 RELAX NG 模式,用於這個特定的 XML-RPC 請求表單。


清單 9. 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 錯誤響應。


清單 10. 完整的 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 是很值得考慮的一個工具。


XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved