上個月,Java 技術講師 Sam Pullara 向我演示了他最新的支持 Java 的電 話 Nokia 6630。這個手機使用了全面的技術 —— 嵌入式 JVM、GPRS 和藍牙, 但是它也遭遇了所有智能手機都苦惱的問題 —— 有限的屏幕實際使用區。有些 Web 站點支持基於手機的浏覽器,而且嵌入式浏覽器也試圖在小小的屏幕上有效 地渲染頁面,但是,在電話屏幕上查看典型的 Web 頁面,就像要把一頭大象強 行塞進車後座一樣(其中的每個參與者都會感到失望,包括您、車和大象)。 Sam 構建了一個簡單的、優雅的解決方案,從他喜歡的 Web 站點上對數據進行 屏幕搜集,然後把數據重新格式化,在小屏幕上顯示。
新方法
從 HTML 文檔提取數據的方法有許多種,但是我真的很喜歡 Sam 采用的方法 :既把 XQuery 當作屏幕搜集工具(從頁面中提取相當的數據),又把它當作樣 式表工具(重新格式化數據,以便數據適應頁面,不需要進行頁面滾動)。只要 少量基礎設施和一些非常簡單的 XQuery 表達式,就可以從大量數據源提取出相 關數據 —— 例如交通、天氣和財務報價等,並在電話上完好地顯示數據。
我過去經常處於這種情況:對 HTML 頁面進行屏幕搜集對某些特定問題來說 似乎是可行的方案,但是幾乎沒有用於屏幕搜集的 Java 工具包。有許多 HTML 解析工具,但它們通常缺少足夠的抽象能力(把屏幕搜集代碼弄得亂七八糟), 大量不符合 HTML 規范的應用限制了它們,它們也無法處理那些結構可能隨時間 發生變化的、動態生成的頁面。
為了彌補質量低下的 HTML 和豐富的 XML 處理工具之間的空白,首先要把 HTML 轉換成 XML。許多工具有助於完成這項工作;JTidy 工具包做得很好,可 以使這項工作變得輕松一些。JTidy 的設計目標是讀入典型質量(即很糟)的 HTML 並輸出更整潔的結果(有選項可供選擇),它還提供了一個 DOM 接口,用 來遍歷能夠發送給 XML 解析器的 HTML 文檔。清單 1 中的代碼將從 InputStream 中讀取 HTML 文檔,並生成文檔的 DOM 表示:
清單 1. 用 JTidy 把 HTML 轉換成 XML 兼容的 DOM
Tidy tidy = new Tidy();
tidy.setQuiet(true);
tidy.setShowWarnings(false);
Document tidyDOM = tidy.parseDOM(inputStream, null);
用這個簡單的轉換,就差不多能把每個 Web 頁面都當作 XML 文檔進行處理 ,還能用自己喜歡的任何 XML 工具(比如 SAX、XSL、XPath,等等)提取數據 。雖然 XSL 可能是很明智的選擇(因為其設計目標就是為了從 XML 文檔中提取 信息並轉換這些信息,以便顯示它們),但是如果不了解 XSL 的話,它的學習 曲線就很難掌握,即使是最簡單的 XSL 轉換也復雜得讓人心煩。XPath 是處理 信息提取的一個好選擇 —— XSL 和 XQuery 都用它進行內容選擇,可以很容易 地使用 XPath 把需要的數據提取出來,然後對 HTML 進行格式化,但是 XQuery 會讓這項工具更加容易。
XQuery:簡介
XQuery 的設計目標是從可能非常大的 XML 數據集中提取數據。輸入的數據 集不必是 XML 文檔,雖然它可能是 XML 文檔,但是也可能是已經編入索引並保 存在 XML 數據庫中的文檔集合,甚至是一組關系數據庫中的表。像 SQL 一樣, XQuery 包含從多個數據集中提取數據、匯總數據、聚合數據和連接數據的函數 。
就像 JSP、ASP 或 Velocity 這樣的表示性模板語言一樣,XQuery 把兩個域 (表示域和計算域)中的元素組合成一種組合語法。結果,所有 XML 文檔都自 動成為有效的 XQuery 表達式,並對自身進行評估。XQuery 還包含一些語言語 句(language statement),例如“for”和“let”,它們可以與 XML 元素混 合使用。
清單 2 顯示了一個示例 XML 文檔 bib.xml,它表示一個書目。然後我們將 介紹一些快速的 XQuery 表達式,讓您對 XQuery 能夠做什麼形成一種認識,最 後我們將再轉到屏幕搜集的示例上。要全面介紹 XQuery 的語法和使用情況可能 要用幾百頁的篇幅,有關更詳細的參考材料和示例,請參閱 參考資料 小節。
清單 2. 示例 XML 書目
<bib>
<book year="1994">
<title>TCP/IP Illustrated</title>
<author><last>Stevens</last><first>W.</firs t></author>
<publisher>Addison-Wesley</publisher>
<price> 65.95</price>
</book>
. . . more books . . .
</bib>
清單 3 顯示了一個 XQuery 表達式,它選擇 Addison-Wesley 在 1991 年以 後出版的所有書籍,提取它們的標題,並把標題格式化成前面有項目符號的 (<ul>)列表。大括號表示從“表示模式”(數據直接傳遞到輸出 ,例 如 <ul> 和 <li> 標簽)到“代碼模式”的切換;然後在 return 子句之後立即進行從“代碼模式”到“表示模式”的隱式切換。
清單 3. 根據查詢參數選擇圖書標題的 XQuery 表達式
<ul>
{
for $b in doc("bib.xml")/bib/book
where $b/publisher = "Addison-Wesley" and $b/@year > 1991
return
<li>{ data($b/title) }</li>
}
</ul>
查詢語法引入了“for”,通常稱之為“Flower 表達式”(來自 FLWOR,是 for-let-where-order-return 的縮寫),該語法從文檔中選擇一系列 XML 節點 ,在該例中,用 XPath 選取了來自 bib.xml 文檔的 <book> 節點集,然 後進一步過濾出與指定查詢參數(出版商是 Addison-Wesley,出版日期是 1991 年之後)匹配的節點。對於選出的每個節點,將在 return 子句中計算表達式, 在這裡是標記(<li> 標簽)與代碼(提取出每個 <book> 節點的 <title> 元素的內容)的混合。
這個簡單的 XQuery 示例描述了 XQuery 的幾個方面 —— 某一文檔中表示 與代碼的混合、XPath 的運用、子條件的運用($b 引用)、非凡的查詢表達式 、XQuery 函數(data()),還有一個事實:輸出文檔的結構不必與輸入文檔的 結構匹配。就在這個相當緊湊的、讀起來不是很難的查詢中,孕育著強大的處理 能力。
清單 4 顯示了一個更簡單的 XQuery 表達式,它把書目中不同出版商的數量 ,在一個 <count> 元素中輸出。像前一個示例一樣,它用 XPath 表達式 選擇一組節點,然後用 XQuery 函數選擇惟一值,並計算節點的數量。它通過運 算獲得一個數字 —— bib.xml,即文檔中不同出版商的數量。
清單 4. 計算不同出版商數量的 XQuery 表達式
<count>
{
let $d := distinct-values(doc("bib.xml")/book/publisher)
return count($d)
}
</count>
這些示例只是 XQuery 能夠執行的各種查詢類型的很少一部分,提供這些例 子僅僅是為了讓您對使用 XQuery 能夠做的事情有些感覺,以及提示您如何才能 用 XQuery 把 XML 文檔轉換成自己選擇的格式。雖然 XQuery 的大部分功能主 要用於查詢大型文檔或者其他數據源,但是也可以使用 XQuery 非常簡單的子集 來對 HTML 文檔進行屏幕搜集,為各種應用程序提取出需要的數據,例如在屏幕 大小有限的設備(例如蜂窩電話)上顯示有關的數據,或者創建一個 DIY 的門 戶網站,聚集並顯示來自多個站點的數據。
用 XQuery 進行屏幕搜集
對 Web 頁面做屏幕搜集的許多挑戰之一是:它們通常沒有可以自我標識的結 構,而且它們的結構可能隨著站點內容的編輯而變化,甚至有可能根據不同的請 求,在頁面中插入不同的動態內容(例如廣告內容)。因此,對於頁面中哪一部 分的內容與要提取的數據相對應,通常不得不進行猜測。
股票價格
現在,讓我們從提取 Yahoo! 財經頁面中 IBM 股票的當前價格開始 (http://finance.yahoo.com/q?s=IBM)。這個頁面上有許多材料 —— 新聞標 題、廣告、財經數據,等等,但是我想要的是股票的價格數據,它放在一個表格 單元格中,靠近包含“Last Trade”的單元格。清單 5 中的查詢語句將選擇所 有文本內容中包含“Last Trade”的 <td> 節點,然後為每個節點(希望 只有一個)輸出一個包含後續 <td> 節點內容的表格行。內容是用 return 子句中的 data() 函數提取的;否則,不僅僅會得到 <td> 節點 中的文本,還會得到所有的標記。(在這個查詢中,惟一包含技巧的部分是 text()[1] 這個部分;在這裡,text() 函數匹配的是 <td> 元素中的所 有元素 —— 在這個例子只有一個元素,但 XQuery 並不知道這一點 —— 所以 必須進一步告訴它在進行文本匹配之前,必須選擇第一個文本節點)。只要頁面 包含一個表格單元格的文本是“Last Trade”,而且後續的單元格包含的是股票 價格,那麼,即使頁面的結構隨意變化,也不會造成查詢失敗。
清單 5. 從 Yahoo! 財經提取股票報價的 XQuery 表達式
<table>
{
for $d in //td
where contains($d/text()[1], "Last Trade")
return <tr><td> { data($d/following-sibling::td) } </td></tr>
}
</table>
天氣
現在來試一下另外一個頁面。Yahoo! 天氣頁面包含許多 portlet 面板,我 想提取上面所列城市的名稱、溫度和圖標。(如果登錄 Yahoo! 天氣頁面 http://weather.yahoo.com,則屏幕上會顯示出在“我的 Yahoo!”中所選城市 的天氣,否則會顯示一些主要大城市的天氣情況。)清單 6 顯示了一個查詢, 它查找包含文本“New York, NY”的子面板,然後導航到封閉表格(enclosing table),並選中所有行:
清單 6. 從 Yahoo! 天氣提取天氣信息的 XQuery 表達式
<table>
{
for $d in //td[contains(a/small/text(), "New York, NY")]
for $row in $d/parent::tr/parent::table/tr
where contains($d/a/small/text()[1], "New York")
return <tr><td>{data($row/td[1])}</td>
<td>{data($row/td[2])}</td>
<td>{$row/td[3]//img}</td> </tr>
}
</table>
然後,對於每一行,XQuery 會提取出三個相關的數據列 —— 城市名稱、溫 度和圖標 —— 並輸出一個相對簡單的表,表中只包含這三項信息。結果就是比 較緊湊地顯示了所關心城市的信息,適合在小屏幕上顯示。結果如下所示:
Chicago, IL 49...63 F London, UK 32...41 F New York, NY 36...44 F San Francisco, CA 52...67 F這個查詢不像 清單 5 中的查詢那麼功能強大。它假設文本“New York, NY ”將在 small 元素中(這就是下次 Yahoo! 重新設計他們的頁面時可以輕松更 改的那類標記)。而且,也很容易將“New York, NY”多次顯示在天氣頁面上。 但是,可以多花些精力來開發查詢,從而減輕這些風險元素;正如許多開發選擇 一樣,在查詢的復雜性與查詢的穩定性之間會有一個權衡。
清單 5 和 清單 6 中所示的查詢不是制作這些查詢的惟一方式。使用更復雜 的 XPath 語法,清單 6 中的兩個 for 子句可以合並到一個 XPath 表達式中, 那麼整個 清單 5 就能變成一個 XPath 表達式,而不是使用 FLWOR 的語法。如 果是 XPath 高手,那麼可能會發現,使用更加面向 XPath 的方式會更容易一些 ,而有更多 SQL 經驗的人則會發現 FLWOR 的語法更有吸引力。
工具
針對 HTML 頁面執行 XQuery 表達式所需要的代碼非常少。可以用 JTidy 庫 來清理 HTML 文檔,然後把它表示成 DOM 對象(請參閱 清單 1)。Saxon XQuery 引擎被用來編譯和執行針對文檔 DOM 對象的查詢。編譯和執行一個針對 文檔 DOM 表示的 XQuery 查詢只需要 6 行代碼,如清單 7 所示:
清單 7. 用 Saxon 編譯和執行 XQuery 表達式的代碼
Configuration c = new Configuration();
StaticQueryContext qp = new StaticQueryContext(c);
XQueryExpression xe = qp.compileQuery(query);
DynamicQueryContext dqc = new DynamicQueryContext(c);
dqc.setContextNode(new DocumentWrapper(tidyDOM, url, c));
List result = xe.evaluate(dqc);
查詢計算的結果是 DOM Element 的 List,您可以用自己喜歡的 DOM 操縱技 術(或者最不喜歡的 DOM 操作技術)把查詢結果轉變成文檔。
結束語
雖然 XQuery 是為了查詢大型文檔而設計的,但是對於轉換簡單的文檔,它 也是一個不錯的工具。不管是把復雜的頁面簡化成在小屏幕上顯示的頁面,還是 從多個頁面提取元素,把它們聚合在自己的門戶上,或者僅僅是因為無法通過其他編程方式獲得數據才從 Web 頁面上提取數據,XQuery 都提供了從 HTML 頁面 搜集所需數據的相對簡單的方法。