綜述
XML代表Extensible Markup Language(eXtensible Markup Language的縮寫,意為可擴充的標記語言)。XML是一套定義語義標記的規則,這些標記將文檔分成許多組件並對這些組件加以標識。它也是元標記語言,即定義了用於定義其他與特定領域有關的、語義的、結構化的標記語言的句法語言。XML是當今最熱門的技術。而PHP也具有分析XML文檔的功能,下面我們將共同探討一下PHP中的XML應用的情況。
XML概貌
談起XML(eXtended Markup Language:可延伸標記語言 (XML)),我們不妨先看一段HTML的代碼:
<html>
<title>XML</title>
<body>
<p><center><font color="red">TEXT</font></center></p>
<a href="www.domain.com"><img src="http://edu.cnzz.cn/NewsInfo/logo.jpg"/></a>
</body>
</html>
上面這段代碼從結構上就符合XML的規則,XML可以理解是包含資料的樹形的結構類型:
1、引用同一個元素的時候,使用一致的大小寫,如<center></Center>就是不符合規定的
2、任何屬性值(如 href="????")要用""引起來,如<a href=www.yahoo.com>就是不正確的
3、所有元素必須由開啟<和關閉>標註組成,元素應該形如<body></body>或空元素<img ... />,如果結尾的 "/>" 少了"/"就是錯誤的代碼
4、所有元素必須彼此嵌套,就像寫程式的迴圈一樣,而且,所有的元素必須嵌套於根項目之中,比如上面的代碼所有的內容都嵌套於<html></html>之中。
5、元素名稱(即上面的body a p img等)應為字母開頭。
怎樣應用PHP的XML解析器Expat?
Expat是PHP指令碼語言的XML解析器(同樣稱為XML處理器),可以使程式訪問XML文檔的結構和內容。它是一種基於事件的解析器。XML解析器有兩種基本類型:
基於樹型的解析器:將XML文檔轉換成樹型結構。這類解析器分析整篇文章,同時提供一個API來訪問所產生樹的每個元素。其通用的標準為DOM(文檔對象模式)。
基於事件的解析器:將XML文檔視為一系列的事件。當一個特殊事件發生時,解析器將調用開發人員提供的函數來處理。基於事件的解析器有一個XML文檔的資料集中視圖,也就是說它集中在XML文檔的資料部分,而不是其結構。這些解析器從頭到尾處理文檔,並將類似於-元素的開始、元素的結尾、特徵資料的開始等等-事件通過回調(callback)函數報告給應用程式。
以下是一個"Hello-World"的XML文檔範例:
<greeting>
Hello World
</greeting>
基於事件的解析器將報告為三個事件:
開始元素:greeting
CDATA項的開始,值為:Hello World
結束元素:greeting
基於事件的解析器不產生描述文檔的結構,當然如果使用Expat,必要時它一樣可以在PHP中產生完全的原生樹結構。在CDATA項中,基於事件的解析器不會得到父元素greeting的資訊。然而,它提供一個更底層的訪問,這就使得可以更好地利用資源和更快地訪問。通過這種方式,就沒有必要將整個文檔放入記憶體;而事實上,整個文檔甚至可以大於實際記憶體值。
上面Hello-World的範例雖然包括完整的XML格式,但它是無效的,因為既沒有DTD(文件類型定義)與其聯絡,也沒有內嵌DTD。但是Expat是一個不檢查有效性的解析器,因此忽略任何與文檔聯絡的DTD。應注意的是文檔仍然需要完整的格式,否則Expat(和其他符合XML標準的解析器一樣)將會隨著出錯資訊而停止。
編譯Expat
Expat可以編譯進PHP3.0.6版本(或以上)中。從Apache1.3.22開始,Expat已經作為Apache的一部分。在Unix系統中,可以通過-with-xml選項配置PHP將其編譯入PHP。
如果將PHP編譯為Apache的模組,而Expat將預設作為Apache的一部分。在Windows中,則必須要載入XML動態串連庫。
XML範例:XMLstats
我們所要討論的範例是使用Expat來收集XML文檔的統計資料。
對於文檔中每個元素,以下資訊都將被輸出:
該元素在文檔中使用的次數
該元素中字元資料的數量
元素的父元素
元素的子項目
注意:為了示範,我們利用PHP來產生一個結構來儲存元素的父元素和子項目
用於產生XML解析器執行個體的函數有哪些?
用於產生XML解析器執行個體的函數為xml_parser_create()。該執行個體將用於以後的所有函數。這個思路非常類似於PHP中MySQL函數的串連標記。在解析文檔前,基於事件的解析器通常要求註冊回呼函數-用於特定的事件發生時調用。Expat沒有例外事件,它定義了如下七個可能事件:
對象 XML解析函數 描述
元素 xml_set_element_handler() 元素的開始和結束
字元資料 xml_set_character_data_handler() 字元資料的開始
外部實體 xml_set_external_entity_ref_handler() 外部實體出現
未解析外部實體 xml_set_unparsed_entity_decl_handler() 未解析的外部實體出現
處理指示 xml_set_processing_instruction_handler() 處理指示的出現
記法聲明 xml_set_notation_decl_handler() 記法聲明的出現
預設 xml_set_default_handler() 其它沒有指定處理函數的事件
所有的回呼函數必須將解析器的執行個體作為其第一個參數(此外還有其它參數)。
對於本文最後的範例指令碼,需要注意的是它既用到了元素處理函數又用到了字元資料處理函數。元素的回調處理函數通過xml_set_element_handler()來註冊。
這個函數需要三個參數:
解析器的執行個體
處理開始元素的回呼函數的名稱
處理結束元素的回呼函數的名稱
當開始解析XML文檔時,回呼函數必須存在。它們必須定義為與PHP手冊中所描述的原型一致。
例如,Expat將三個參數傳遞給開始元素的處理函數。在指令碼範例中,其定義如下:
function start_element($parser, $name, $attrs)
$parser是解析器標誌,$name是開始元素的名稱,$attrs為包含元素所有屬性和值的數組。
一旦開始解析XML文檔,Expat在遇到開始元素是都將調用start_element()函數並將參數傳遞過去。
XML的Case Folding選項
用xml_parser_set_option()函數將Case folding選項關閉。這個選項預設是開啟的,使得傳遞給處理函數的元素名自動轉換為大寫。但XML對大小寫是敏感的(所以大小寫對統計XML文檔是非常重要的)。對於我們的範例,case folding選項必須關閉。
如何對文檔進行解析?
在完成所有的準備工作後,現在指令碼終於可以解析XML文檔:
Xml_parse_from_file(),一個自訂的函數,開啟參數中指定的檔案,並以4kb的大小進行解析
xml_parse(),和xml_parse_from_file()一樣,當發生錯誤時,即XML文檔的格式不完全時,將會返回false。
我們可以使用xml_get_error_code()函數來得到最後一個錯誤的數字代碼。將此數字代碼傳遞給xml_error_string()函數即可得到錯誤的文本資訊。輸出XML當前的行數,使得調試更容易。
當解析文檔時,對於Expat需要強調問題的是:如何保持文檔結構的基本描述?
如前所述,基於事件的解析器本身並不產生任何結構資訊。不過標籤(tag)結構是XML的重要特性。例如,元素序列<book><title>表示的意思不同於<figure><title>。書名和圖名是沒有關係的,雖然它們都用到"title"這個術語。因此,為了更有效地使用基於事件的解析器處理XML,必須使用自己的棧(stacks)或列表(lists)來維護文檔的結構資訊。
為了產生文檔結構的鏡像,指令碼至少需要知道目前元素的父元素。用Exapt的API是無法實現的,它只報告目前元素的事件,而沒有任何前後關係的資訊。因此,需要建立自己的棧結構。
指令碼範例使用先進後出(FILO)的棧結構。通過一個數組,棧將儲存全部的開始元素。對於開始元素處理函數,目前的元素將被array_push()函數推到棧的頂部。相應的,結束元素處理函數通過array_pop()將最頂的元素移走。
對於序列<book><title></title></book>,棧的填充如下:
開始元素book:將"book"賦給棧的第一個元素($stack[0])。
開始元素title:將"title"賦給棧的頂部($stack[1])。
結束元素title:從棧中將最頂部的元素移去($stack[1])。
結束元素title:從棧中將最頂部的元素移去($stack[0])。
PHP3.0通過一個$depth變數手動控制元素的嵌套來實現範例,這就使指令碼看起來比較複雜。PHP4.0通過array_pop()和array_push()兩個函數來使指令碼看起來更簡潔。
如何收集XML文檔中的元素資訊?
為了收集每個元素的資訊,指令碼需要記住每個元素的事件。通過使用一個全域的陣列變數$elements來儲存文檔中所有不同的元素。數組的項目是元素類的執行個體,有4個屬性(類的變數)
$count -該元素在文檔中被發現的次數
$chars -元素中字元事件的位元組數
$parents -父元素
$childs - 子項目
注意:PHP的一個特性是你可以通過while(list() = each())loop遍曆整個類結構,如同你遍曆整個相應的數組一樣。所有的類變數(當你用PHP3.0時還有方法名)都以字串的方式輸出。
當發現一個元素時,我們需要增加其相應的記數器來跟蹤它在文檔中出現多少次。在相應的$elements項中的記數元素也要加一。
我們同樣要讓父元素知道目前的元素是它的子項目。因此,目前元素的名稱將會加入到父元素的$childs數組的項目中。最後,目前元素應該記住誰是它的父元素。所以,父元素被加入到目前元素$parents數組的項目中。
顯示統計資訊
剩下的代碼在$elements數組和其子數組中迴圈顯示其統計結果。這就是最簡單的嵌套迴圈,儘管輸出正確的結果,但代碼既不簡潔又沒有任何特別的技巧,它僅僅是一個你可能每天用他來完成工作的迴圈。
指令碼範例被設計為通過PHP的CGI方式的命令列來調用。因此,統計結果輸出的格式為文字格式設定。如果你要將指令碼運用到互連網上,那麼你需要修改輸出函數來產生HTML格式。
如何用PHP&XML編製一個迷你搜尋引擎執行個體?
讓我們首先來熟悉一下我們程式中用到的那個XML(儲存為xyz.xml)。
<?xml version="1.0" encoding="gb2312" ?>
<links>採用PHP和XML技術構建的搜尋引擎
<web memo="memo1" url="">name1</web>
<sub>電腦網路
<web memo="nemo2">name2</web>
<sub>程式設計
<web memo="memo3">name3</web>
<sub>PHP
<web url="http://www.phpbuilder.com/" memo="[英文]PHP開發資源。">
www.phpbuilder.com</web>
<web url="http://www.fokus.gmd.de" memo="[英文]PHP開發手冊。 ">
PHP Manual</web>
</sub>
</sub>
</sub>
</links>
它的結構相當簡單,根項目就是links,sub代表著一個類別,web就是一個網站的資訊,其中包含著屬性,url代表網站的聯結,memo為備忘資訊,<web>??</web> 、<sub>??</sub>中包含的為元素的資料在這裡就是類別和網站的名稱,這是符合上面的規定的。
現在我們來回答上面提出的問題:為什麼要用XML來編製搜尋引擎?
第一個原因就是有時候由於各種原因我們可能不能用到資料庫(MySQL或者其他);
其次,對於小資料量的搜尋引擎來說,它的資料量很小,如果用資料庫來做,效率未必有多高;
最重要的一點是,這個搜尋引擎維護起來相當的簡單,並且不用編寫繁瑣的資料庫的維護的程式。例如,我們要添加一個類別或者網頁,只要編輯文本的檔案,加上一福紈eb>???</web>或是<sub>????</sub>就可以了,而且,如果想把一個類別移動到另一個地方的話,我們只要將這一部分的sub複製過去就行了。
下面一個最簡單的用PHP顯示XML的範例。
下面的程式是將解析XML並按照樹形結構輸出至瀏覽器,並顯示每層的元素總數。
<?php
$file = "demo.xml";// XML檔案
function xml_parse_from_file($parser, $file) {// 解析XML檔案的函數 }
function start_element($parser, $name, $attrs) {//遇到了開元素標記如<a href="link">就執行這一段}
function stop_element($parser, $name) {//遇到了開元素標記如</body>就執行這一段}
function data($parser, $data) {……}
function showcount(){ //顯示每一層的元素總數}
global $level,$levelcount,$maxlevel;
$level = -1;
$parser = xml_parser_create();// 產生解析器的執行個體
xml_set_element_handler($parser, "start_element", "stop_element"); // 設定處理函數
xml_set_character_data_handler($parser, "data");
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
$ret = xml_parse_from_file($parser, $file); // 解析檔案
if(!$ret) {
die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser)));
}
xml_parser_free($parser); // 釋放解析器
showcount();
?>
在上面的程式的基礎上,可以顯示一段子樹,我們依照元素的層數和他在該層的第幾號來對他進行定位
例如:
links (0,1)
+----web (1,1)
+----sub (1,2)
| +----web (2,1)
| +----sub (2,2)
| | +----web (3,1)
| | +----sub (3,2)
:
:
:
下面的代碼是我們的搜尋引擎的基礎。因為,要顯示出一個子類別(如程式設計->PHP->)的資訊就要用到他。
<?php
……
function start_element($parser, $name, $attrs) {
global $level,$levelcount,$maxlevel,$hide,$lev,$num,$PHP_SELF;
$level += 1;
if($level>$maxlevel)
$maxlevel=$level;
$levelcount[$level]+=1;
if($hide){ //判斷是否在子樹的範圍內,$hide==FALSE 為在
if($level==$lev&&$levelcount[$level]==$num)
$hide=FALSE;
}else{
if($level<=$lev)$hide=TRUE;
}
if(!$hide){
……//輸出
}
}
function data($parser, $data) {
global $level,$hide;
if(!$hide){
if(trim($data)!=""){ echo trim($data); }
}
}
……
global $hide,$lev,$num,$PHP_SELF;
$level = -1;
$hide = TRUE;
echo "<p><a href=$PHP_SELF>Root</a></p>";
if($lev==""){
$lev=0;$num=1;
}
……
?>
mini的搜尋引擎到底如何做呢?
作了若干的鋪墊,下面我們就來看一下我們的搜尋引擎的主要檔案。
第一段為仿sina,yahoo的按照類別查詢
第二段為搜尋查詢部分(把整個樹遍曆一遍)顯示符合的內容。
xml3.php
關鍵字匹配採用eregi函數,我們假設輸入的文字都是不會導致錯誤的。