天天看點

《高階Perl》——1.7 HTML

我曾承諾遞歸對操作階層化定義的資料結構有用,我還用了檔案系統作為一個例子。但它是個稍微特殊的資料結構的例子,因為通常認為資料結構是在記憶體裡,而不是在磁盤上。

檔案系統的樹型結構讓人聯想到目錄,每個目錄都含有一列其他檔案。任何擁有一些包含其他條目清單的領域都将含有樹型結構。一個極好的例子是html資料。

html資料是一系列元素和普通文本。每個元素含有一些内容,它們又是一系列更多的元素和更多的普通文本。這是個遞歸的描述,類似檔案系統的描述,html檔案的結構也類似于檔案系統的結構。

元素有一個開始标簽(start tag),如下:

<code>&lt;font&gt;</code>

與相應的結束标簽(end tag),如下:

<code>&lt;/font&gt;</code>

開始标簽可以有一組 屬性-值對(attribute杤alue pair),如下:

<code>&lt;font size=3 color="red"&gt;</code>

結束标簽總是一樣的。它沒有屬性-值對。

在開始标簽和結束标簽之間可以存在任何html文本序列,包括其他元素,也包括普通文本。這裡有一個簡單的html檔案例子:

這個檔案的結構如圖1-3所示:

《高階Perl》——1.7 HTML

檔案主要有三個組成部分:<code>&lt;h1&gt;</code>元素與它的内容;<code>&lt;p&gt;</code>元素與它的内容;以及它們之間的空行。<code>&lt;p&gt;</code>元素依次有三個組成部分:在<code>&lt;font&gt;</code>元素之前沒有标記的文本;<code>&lt;font&gt;</code>元素與它的内容;以及<code>&lt;font&gt;</code>元素之後未标記的文本。<code>&lt;h1&gt;</code>元素有一個組成部分,就是未标記的文本what junior said next。

第8章将介紹如何建立一個針對類似html語言的解析器。此刻将考察一個半标準的子產品html::treebuilder,它把一個html檔案轉換成一個樹型結構。

假設html資料已經在一個變量裡了,如$html。下面的代碼使用html::treebuilder把文本轉換成一個清晰的樹型結構:

方法ignore_ignorable_whitespace()告訴html::treebuilder不允許丢棄某些空白符,如<code>&lt;h1&gt;</code>元素後的換行符,正常情況下這是可忽略的。

現在$tree表示樹型結構。它是散列樹,每個散列是樹的一個節點并表示一個元素。每個散列有一個鍵_tag,它的值是它的标簽名;還有一個鍵_content,它的值是元素内容依次排列的一個清單;_content清單中的每個條目或者是一個字元串,表示沒有标簽的文本,或者是另一個散列,表示另一個元素。如果标簽還有屬性-值對,那它們都直接存放在散列中,屬性作為散列的鍵,相應的值作為散列的值。

例如,與例子中的<code>&lt;font&gt;</code>元素對應的樹節點如下:

與<code>&lt;p&gt;</code>元素對應的樹節點包含<code>&lt;font&gt;</code>節點,如下:

建立一個函數周遊這些html樹之一并為所有文本“去标簽”,即剝離标簽,并不困難。對于_content清單中的每個條目,都可以通過ref()函數把它識别成一個元素,前者對元素(即散列引用)産生真,對普通字元串則是假:

函數檢查傳入的html條目是否是一個普通的字元串,如果是,函數立即傳回它。如果它不是一個普通的字元串,函數假設它是一個樹節點,如前所述,并疊代它的内容,遞歸地把每個條目轉換成普通文本,累積成結果字元串并傳回它。對于這個例子,就是:

sean burke,html::treebuilder的作者,告訴我如此獲得html::treebuilder對象的内部資訊是不規範的,因為他可能在未來改變它們。健壯的程式應該使用子產品提供的通路器方法。在這些例子中,将繼續直接擷取内部資訊。

可以向dir_walk()學習,通過把這個函數分成兩部分而使它更有用:一部分處理html樹,另一部分處理累積普通文本的專門任務:

這個函數的結構和dir_walk()的完全一樣。它以兩個輔助的函數為參數:$textfunc計算一個普通文本字元串的某個有意思的值,$elementfunc接受元素與其間條目的值計算與一個元素相對應的值。$textfunc類似dir_walk()中的$filefunc,$elementfunc類似$dirfunc。

現在可以把剝離器寫成如下這樣:

參數$textfunc是一個函數,它原封不動地傳回它的參數。參數$elementfunc是一個函數,它丢棄元素本身,然後連接配接為它的内容計算得到的文本,并傳回連接配接的文本。輸出和untag_html()的一樣。

假設我們想要一個檔案摘要,輸出在<code>&lt;h1&gt;</code>标簽内的文本,而丢棄其他東西:

這本質上和untag_html()一樣,除了當元素函數看到它正在處理一個<code>&lt;h1&gt;</code>元素時,它就輸出未标記的文本。

如果期望函數傳回(return)頭部文本而不是輸出它,那麼必須用點小技巧。考慮一個這樣的例子:

最好丢棄文本is a naughty boy,這樣它就不出現在結果中了。但是對于walk_html(),它隻不過是另一個普通文本條目,和junior看起來完全一樣,而後者是不想丢棄的。也許應當簡單地丢棄出現在非頭部标簽中的所有東西,但是這樣行不通:

不能僅由于junior出現在<code>&lt;b&gt;</code>标簽裡而丢棄它,因為<code>&lt;b&gt;</code>标簽本身也在<code>&lt;h1&gt;</code>标簽裡,但是想保留它。

可以這樣解決這個問題:從每個walk_html()的執行體把有關目前标簽的上下文的資訊傳遞到下一個執行體,但它以其他方式傳回資訊更簡單。檔案中的文本或者是“該留的”,因為知道它在一個<code>&lt;h1&gt;</code>元素内,或者是“也許該留的”,因為我們不知道。每當處理一個<code>&lt;h1&gt;</code>元素時,就将把它包含的所有“也許該留的”文本提升為“該留的”文本。最後,将輸出“該留的”文本并丢棄“也許該留的”文本:

從walk_htm()傳回的值将是一列帶标記的文本條目。每個文本條目是一個匿名數組,它的第一個元素是maybe或者keeper,而第二個條目是一個字元串。普通文本函數簡單地标記它的參數為maybe。對于字元串junior,它傳回帶标記的條目['maybe', 'junior'];對于字元串is a naughty boy.,它傳回['maybe', 'is a naughty boy.']。

元素函數更有趣。它得到一個元素和一列帶标記的文本條目。如果元素表現為一個<code>&lt;h1&gt;</code>标簽,那麼函數從它的其他參數中抽取所有的文本,合并到一起,并把結果标記為keeper。如果元素是其他種類,函數原封不動地傳回它的标簽文本。這些文本将插入帶标記文本的清單,然後傳遞給元素函數調用,作為上一層元素,比較這個與1.5節中最後的dir_walk()例子,後者以類似方式傳回一列檔案名。

因為最後從walk_html()傳回的值是标記文本的清單,是以需要過濾它們并丢棄仍然标記為maybe的那些。這最後一步是不能省略的。由于函數差別對待頂層的和嵌入在<code>&lt;h1&gt;</code>标簽内的不帶标簽的文本條目,是以就必須有某部分過程能知曉在頂層的東西。walk_html()無法做到,因為它在每層做同樣的事情。是以必須建立一個最終的函數處理頂層:

或者可以寫得更緊湊:

《高階Perl》——1.7 HTML

剛才看到了如何從html檔案中抽取所有<code>&lt;h1&gt;</code>标簽的文本。主要的過程是promote_if_h1tag()。但是下次也許會想要抽取更詳細的摘要,包括來自<code>&lt;h1&gt;</code>、<code>&lt;h2&gt;</code>、<code>&lt;h3&gt;</code>以及其他存在的标簽的所有文本。為做到這個,需要對promote_if_h1tag()做個小改動,把它變成一個新的函數:

但是如果<code>promote_if_h1tag</code>能比最初意識到的更普遍适用,那将是個提取普遍有用的部分的好方法。可以把變化的部分參數化以達到此目的:

現在不必寫個專門的函數promote_if_h1tag()了,可以把同樣的行為表現成promote_if()的一個特殊情況。不用寫成:

可以用這個:

第7章将介紹完成此任務的更整齊的方式。