首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 理論與實踐:善用 XQuery 進行網頁擷取與重構

     編輯:Java 相關知識

上個月,Java 技術講師 Sam Pullara 向我展示了他新入手的 Nokia 6630 智慧型手機。這款裝置雖然內建 JVM、支援 GPRS 與藍牙,卻同樣面臨所有智慧型手機的共同痛點:螢幕顯示區域極為有限。雖然部分網站提供行動版瀏覽,嵌入式瀏覽器也嘗試在小螢幕上渲染網頁,但在手機上檢視典型網頁,無異於試圖將大象塞進汽車後座——對所有參與者來說都是場災難。Sam 想出一個簡潔優雅的解決方案:從他常瀏覽的網站擷取資料,重新排版後在小螢幕上呈現。

全新的擷取思路

從 HTML 文件提取資料的方法不勝枚舉,但我特別欣賞 Sam 的做法:他同時將 XQuery 當作「擷取工具」(從頁面抽取所需資料)與「樣式表工具」(將資料重新排版以適應螢幕,免除捲動)。只要少量基礎建設與幾個簡單的 XQuery 表達式,即可從海量資料源(如交通、天氣、財經報價)提取關鍵資訊,完美呈現在手機上。

過去我常遇到這種情境:對 HTML 頁面進行擷取似乎是解決特定問題的可行方案,但 Java 生態系中幾乎缺乏專門的擷取工具包。雖然 HTML 解析器眾多,但它們通常抽象層級不足(導致擷取代碼雜亂無章),大量不符合規範的 HTML 限制了它們的效用,且無法應對結構隨時間變動的動態生成頁面。

填補劣質 HTML 與豐富 XML 工具的鴻溝

解決之道在於先將 HTML 轉換為 XML。多種工具可完成此任務;JTidy 表現優異,能輕鬆處理典型的「劣質」HTML 並輸出整潔結果(提供多種選項),同時提供 DOM 介面供遍歷,方便送入 XML 解析器。以下代碼示範從 InputStream 讀取 HTML 並生成 DOM 表示:

清單 1. 使用 JTidy 將 HTML 轉換為 XML 相容的 DOM

Tidy tidy = new Tidy();
tidy.setQuiet(true);
tidy.setShowWarnings(false);
Document tidyDOM = tidy.parseDOM(inputStream, null);

有了這個簡單轉換,幾乎任何網頁都能視為 XML 文件處理,並可使用偏好的 XML 工具(SAX、XSL、XPath 等)提取資料。雖然 XSL 專為從 XML 提取並轉換資訊而設計,是合乎邏輯的選擇,但其學習曲線陡峭,即使最簡單的轉換也令人頭大。XPath 是資訊提取的不錯選擇——XSL 與 XQuery 皆以它為選擇基礎,輕鬆提取所需資料再格式化;但 XQuery 讓這項工作更加容易。

XQuery 簡介

XQuery 設計目標是從可能龐大的 XML 資料集中提取資料。輸入資料集不限於 XML 文件,也可以是已編入索引並儲存在 XML 資料庫的文件集合,甚至是關聯式資料庫的表集合。類似 SQL,XQuery 包含從多資料集提取、彙總、聚合與連接資料的函數。

如同 JSP、ASP 或 Velocity 這類表現層模板語言,XQuery 將「表現域」與「計算域」的元素融合成一種組合語法。結果是:所有 XML 文件自動成為有效的 XQuery 表達式,並對自身求值。XQuery 還包含 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.</first></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 多面向:表現與代碼混合、XPath 運用、子條件引用($b)、強大的查詢表達式、XQuery 函數(data()),以及輸出結構無需匹配輸入結構的事實。就在此緊湊、可讀的查詢中,蘊藏著強大處理能力。

清單 4 顯示更簡單的表達式,將書目中不同出版社數量輸出於 <count> 元素。同樣用 XPath 選擇節點集,再用 XQuery 函數取唯一值並計數,運算出一個數字——即 bib.xml 中不同出版社數量。

清單 4. 計算不同出版社數量的 XQuery 表達式

<count>
{
let $d := distinct-values(doc("bib.xml")/book/publisher)
return count($d)
}
</count>

這些示例僅是 XQuery 能執行查詢類型的冰山一角,旨在讓您對其能力有所體感,並提示如何用 XQuery 將 XML 文件轉換為所需格式。雖然 XQuery 主要用於查詢大型文件或其他資料源,但其簡單子集極適合對 HTML 文件進行擷取,為各類應用提取所需資料——例如在螢幕尺寸受限裝置(如行動電話)上顯示資訊,或建立 DIY 入口網站聚合多站點資料。

實戰:用 XQuery 進行網頁擷取

網頁擷取的主要挑戰之一:頁面通常缺乏自我識別結構,且結構可能隨內容編輯而變,甚至依請求不同插入動態內容(如廣告)。因此,往往只能猜測頁面哪部分對應目標資料。

股票報價

先從提取 Yahoo! 財經頁面中 IBM 股票當前價格開始(https://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 面板,我想提取所列城市的名稱、溫度與圖示。(登入 https://weather.yahoo.com 會顯示「我的 Yahoo!」中選定城市的天氣,否則顯示主要大城市天氣)。清單 6 的查詢尋找包含文本「New York, NY」的子面板,導航至其外層表格,選取所有列:

清單 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 語法。

工具鏈

對網頁執行 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 操作技術將結果轉為文件。

結語

雖然 XQuery 設計初衷是查詢大型文件,但對轉換簡單文件同樣是得力工具。無論是將複雜頁面簡化為小螢幕適用版本,從多頁面提取元素聚合於自建入口,或是因無法以程式方式取得資料而從網頁擷取,XQuery 都提供了從 HTML 頁面搜集所需資料的相對簡單途徑。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved