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

Java 理論與實踐:無資料庫環境下的資料查詢——以 XQuery 替代 SQL

     編輯:Java 相關知識

我最近詳細審視一個專案,涉及大量網頁快速搜尋。爬蟲程式爬過不同網站時,會建立一個資料庫,包含所爬網站與網頁、各頁面包含的連結、各頁面分析結果等。最終產出一組報告,說明經過哪些網站與頁面、哪些持續連結、哪些連結已斷開、哪些頁面有錯誤、計算出的頁面規格等。專案初期,沒人確切知道需要什麼報告或格式——只知道有內容要報告。這意味著報告開發階段將是迭代的,經歷多次回饋、修改,可能嘗試不同結構。唯一確定的需求:報告應以 XML 形式呈現,可能也要 HTML。因此開發與修改報告的過程必須輕量,因為需求是「動態發現」而非預先指定。

其實不需要資料庫

對此問題「最顯而易見」的解法是將一切放入 SQL 資料庫——頁面、連結、度量標準、HTTP 結果碼、計時結果與其他中繼資料。這問題用關聯表示能很好解決,特別是因為不需儲存已訪問頁面內容,只需儲存其結構與中繼資料。

到目前為止,專案像典型資料庫應用,且不缺持久化策略選擇。但或許能避免資料庫持久化的複雜性——這個爬蟲只訪問數萬個頁面,數量不大,可將整個資料庫放在記憶體中,需要持久化時透過序列化實現(雖然載入與儲存較耗時,但不常執行)。懶惰反而帶來好處——無需處理持久化大幅縮短開發時間,顯著減少工作量。操作記憶體內資料結構比每次增刪查都用資料庫容易得多。無論選何種持久化模型,都會限制任何觸及資料代碼的構造。

記憶體內資料結構是樹狀結構,如清單 1,根為快速搜尋過的各網站首頁,因此 Visitor 模式是搜尋這些首頁或從中提取資料的理想模式(建立基本 Visitor 類防止陷入連結循環——A 連結 B、B 連結 C、C 連結 A——並不難)。

清單 1. 網頁爬蟲的簡化方案

public class Site {
Page homepage;
Collection<Page> pages;
Collection<Link> links;
}
public class Page {
String url;
Site site;
PageMetrics metrics;
}
public class Link {
Page linkFrom;
Page linkTo;
String anchorText;
}

此爬蟲應用程式有十多個 Visitor,做類似選擇頁面進一步分析、選擇無連結頁面、列出「被連結最多」頁面等事。因所有操作簡單,Visitor 模式(如清單 2)運作良好,且資料結構在記憶體中,徹底搜尋代價不大:

清單 2. 用於網頁爬蟲資料庫的 Visitor 模式

public interface Visitor {
public void visitSite(Site site);
public void visitLink(Link link);
}

哎呀,忘了報表

若不跑報表,Visitor 策略在訪問資料方面表現很好。使用資料庫持久化的一大好處是:生成報表時 SQL 能力大放異彩——幾乎能讓資料庫做任何事。甚至用 SQL 生成報表原型也很容易——跑原型報表,結果不對就修改 SQL 查詢或寫新查詢再試。若只改 SQL 查詢,編輯-編譯-運行週期可能很快。若 SQL 不存程式中,甚至可跳過編譯部分,快速生成報表原型。確定所需報表後,構建進應用程式很容易。

因此,雖然記憶體內資料結構對新增結果、尋找特定結果、特殊傳輸表現不錯,但對報表卻成了不利條件。對於自身結構與資料庫結構不同的所有報表,Visitor 都必須建立全新資料結構包含報表資料。故每種報表類型都需專屬的中間資料結構存放結果、填充它的 Visitor、將中間結構轉為最終報表的後處理代碼。似乎工作量不小,尤其大多數原型報表會被丟棄。例如想列出所有從其他網站連結到某給定網站的頁面報表、所有外部頁面列表報表、以及網站上連結該頁面的頁面列表,再依連結數量分類、連結最多的頁面顯示在最前。這基本上將資料結構由內而外翻轉。用 Visitor 實現這資料轉換,需取得從給定網站可達到的外部頁面連結列表,並依被連結頁面分類,如清單 3:

清單 3. Visitor 列出被連結最多的頁面與連結到它們的頁面

public class InvertLinksVisitor {
public Map<Page, Set<Page>> map = ...;
public void visitLink(Link link) {
if (link.linkFrom.site.equals(targetSite)
&& !link.linkTo.site.equals(targetSite)) {
if (!map.containsKey(link.linkTo))
map.put(link.linkTo, new HashSet<Page>());
map.get(link.linkTo).add(link.linkFrom);
}
}
}

清單 3 的 Visitor 生成一個映射,將每個外部頁面與連結它的一組內部頁面關聯。為準備報表,還須依關聯頁面大小分類條目,再建立報表。雖無困難步驟,但每個報表所需的專屬代碼量大,導致快速報表原型成為重要目標(因無報表需求),試驗新報表的成本高於理想。許多報表需多次傳遞資料以便選擇、彙總、分類。

我的資料模型王國

此時,缺乏正式資料模型開始成不利因素,該模型可描述收集的資料,並能更容易表示選擇與聚合查詢。或許懶惰不如最初希望那樣有效。但雖應用程式缺乏正式資料模型,或許可將資料存入記憶體資料庫並憑藉它查詢,藉此借用一個資料模型。兩種可能立現腦海:開源記憶體 SQL 資料庫 HSQLDB 與 XQuery。我不需要資料庫提供的持久化,但確實需要查詢語言。

HSQLDB 是 Java 編寫的可嵌入資料庫引擎,含適用記憶體表的表類型與適用磁碟表的表類型,設計目標將表完全嵌入應用程式,消除多數真實資料庫相關的管理開銷。將資料載入 HSQLDB 只需寫一個 Visitor 遍歷記憶體資料結構,為每個將儲存實體生成對應 INSERT 語句。然後對記憶體資料庫表執行 SQL 查詢生成報表,完成後丟棄這個「資料庫」。

哎呀,忘了關聯式資料庫有多煩人

HSQLDB 方法可行,但很快發現我必須為物件關聯不匹配「兩次」(而非一次)受罰——一次將樹狀結構資料庫轉為關聯資料模型,一次將平面關聯查詢結果轉為結構化 XML 或 HTML 結果集。此外,將 JDBC ResultSet 後處理為 DOM 表示的 XML 或 HTML 文件也非易事,需為每個報表提供定制編碼。因此雖然記憶體 SQL 資料庫確實簡化查詢,但存取資料所需的額外代碼抵消所有節省的代碼。

讓 XQuery 來拯救您

另一個容易獲得的資料查詢方法是 XQuery。XQuery 優勢在於:它設計用於生成 XML 或 HTML 文件作為查詢結果,故無需對查詢結果後處理。這想法很吸引人——每個報表只有一層編碼,而非兩層或更多。因此第一任務是建構一個表示整個資料集的 XML 文件。設計簡單 XML 資料模型、編寫遍歷資料結構並將每個元素附加到 DOM 文件的 Visitor 很簡單(無需寫出此文件,可保持在記憶體供查詢,完成時丟棄;底層資料改變時可重新生成)。之後只需編寫 XQuery 查詢,選擇並聚合報表所需資料,按最終所需格式(XML 或 HTML)格式化。查詢可存儲在單獨文件中以便快速原型製造,支援多種報表格式。使用 Saxon 評估查詢的代碼如清單 4:

清單 4. 執行 XQuery 查詢並將結果序列化為 XML 或 HTML 文件

String query = readFile(queryFile + ".xq");
Configuration c = new Configuration();
StaticQueryContext qp = new StaticQueryContext(c);
XQueryExpression xe = qp.compileQuery(query);
DynamicQueryContext dqc = new DynamicQueryContext(c);
dqc.setContextNode(new DocumentWrapper(document, z.getName(), c));
List result = xe.evaluate(dqc);
FileOutputStream os = new FileOutputStream(fileName);
XMLSerializer serializer = new XMLSerializer(os, format);
serializer.asDOMSerializer();
for(Iterator i = result.iterator(); i.hasNext(); ) {
Object o = i.next();
if (o instanceof Element)
serializer.serialize((Element) o);
else if (o instanceof Attr) {
Element e = document.createElement("scalar");
e.setTextContent(((Attr) o).getNodeValue());
serializer.serialize(e);
}
else {
Element e = document.createElement("scalar");
e.setTextContent(o.toString());
serializer.serialize(e);
}
}
os.close();

表示資料庫的 XML 文件結構與記憶體資料結構略異,每個 <site> 元素有巢狀 <page> 元素,每個 <page> 有巢狀 <link> 元素,每個 <link> 有 <link-to> 與 <link-from> 元素。實證這種表示法對大多數報表很方便。

清單 5 顯示一個範例 XQuery 報表,處理連結的選擇、分類與顯示。它優於 Visitor 方法——不僅代碼少(查詢語言支援選擇、聚合、分類),且所有報表代碼——選擇、聚合、分類、表示——都在一處。

清單 5. 生成連結次數最多頁面完整報表的 XQuery 代碼

<html>
<head><title>被連結最多的頁面 </title></head>
<body>
<ul>
{
let $links := //link[link-to/@siteUrl ne $targetSite
and link-from/@siteUrl eq $targetSite]
for $page in distinct-values($links/link-to/@url)
let $linkingPages := $links[link-to/@url eq $page]/link-from/@url
order by count($linkingPages)
return
<li>Page {$page}, {count($linkingPages)} links
<ul> {
for $p in $linkingPages return <li>Linked from {$p/@url}</li>
}
</ul></li>
}
</ul> </body> </html>

結語

從開發成本角度看,XQuery 方法已證實能節省大量成本。樹狀結構適合構建與搜尋資料,但不適合報表。XML 方法適合報表(可利用 XQuery 能力),但對整個應用程式實現有諸多不便且降低效能。因資料集大小可控——僅幾十 MB,可將資料從一種格式轉換為從開發角度看最方便的另一種格式。更大資料集(如無法完全存入記憶體)會要求整個應用程式圍繞資料庫構建。雖有許多處理資料持久化的好工具,但它們需要的工作都比單純操作記憶體資料結構多得多。若資料集大小合適,便可同時利用兩種方法的長處。

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