程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 構建用於正則表達式的抽象Java API

構建用於正則表達式的抽象Java API

編輯:關於JAVA

當您在 Java 中使用正則表達式時,依賴某一具體 regexp 庫通常不是個好 主意。如果使用抽象層,您可以在不同 regexp 庫之間切換,降低您的代碼與特 定庫之間的耦合,並能選擇哪個庫最適合您的需要。如果您正在考慮在下一個項 目中使用 Java regexp 庫,軟件開發人員 Jose San Leandro Armendariz 將向 您演示如何使代碼獨立於所選擇的具體庫。並讓您進一步了解 regexp 及其工作 原理,隨後還提供了一些練習。

簡介

盡管您可能認為編寫需要分析文本的 Java 應用程序是一項簡單任務,但象 許多事情一樣,它會很快變得復雜起來。那的確是我在編寫代碼以解析 HTML 頁 面時的經驗。開始的時候,我偶爾會使用 Perl5 正則表達式(regexp)。但是 ,由於某些原因(稍後說明),我後來常常使用它們。

背景知識

在我的經驗中,大多數 Java 開發人員都需要解析某種文本。通常,這意味 著他們最初要花一些時間使用象 indexOf 或 substring 那樣的與 Java 字符串 相關的函數或方法,並且希望輸入格式永遠不變。但是,如果輸入格式改變,那 麼用於讀取新格式的代碼維護起來就會變得更復雜、更困難。最後,代碼可能需 要支持自動換行(word wrapping)、區分大小寫等。

由於邏輯變得更加復雜,所以維護也變得很困難。因為任何更改都可能產生 副作用並使文本解析器的其它部分停止工作,所以開發人員需要時間修正這些小 錯誤。

有一定 Perl 經驗的開發人員可能也有過使用正則表達式的經驗。如果夠幸 運(或優秀)的話,這位開發人員能夠說服團隊其余的人(或至少是團隊領導) 使用這項技術。新的方法將取消編寫用來調用 String 方法的多行代碼,它意味 著將解析器邏輯的核心委托出去,並替換為 regexp 庫。

接受了有 Perl5 經驗的開發人員的建議後,團隊必須選擇哪個 regex 實現 最適合他們的項目。然後他們需要學習如何使用它。

在簡要地研究了從因特網上找到的眾多可選方案後,假設團隊決定從人們更 熟悉的庫中選擇一個使用,如屬於 Jakarta項目的 Oro。接下來,對解析器進行 較大程度地重構或幾乎重新編寫,並且解析器最終使用了 Oro 的類,如 Perl5Compiler 、 Perl5Matcher 等。

這一決定的後果很明顯:

代碼與 Jakarta Oro 的類緊密地耦合在一起。

團隊承擔了風險,因為不知道非功能性需求(如性能或線程模型)是否將得 到滿足。

團隊已花費時間和財力來學習並重新編寫代碼,以使它使用 regexp 庫。如 果他們的決定是錯誤的並且選擇了新的庫,則這一工作在成本上將不會有很大區 別,因為將需要再次重新編寫代碼。

即使庫工作正常,如果他們決定應該遷移到全新的庫(例如,包括在 JDK 1.4 中的庫),怎麼辦?

去耦的好處

有沒有辦法使團隊知道哪個實現最適合他們的需要呢(不僅現在能將來也能 )?讓我們試著尋找答案。

避免依賴任何特定的實現

前面的情形在軟件工程中十分常見。在有些情況中,這樣的情形會導致較大 的投資和較長的延期。當不了解所有後果就作出決定而且決策制定人不太走運或 缺乏必需的經驗時,就常常會發生這種情況。

可將該情形概括如下:

您需要某種提供者

您沒有選擇最佳提供者的客觀標准

您希望能用最低的成本來評估所有的待選項

所作的決定不應將您束縛在所選的提供者上

這一問題的解決方法是使代碼更加獨立於提供者。這引入了新的層 ― 同時 去除客戶機和提供者的耦合的層。

在服務器端開發中,很容易找到使用該方法的模式或體系結構。下面引用一 些示例:

對於 J2EE,您主要關注如何構建應用程序而不是應用程序服務器的細節。

數據訪問對象(Data Access Object,DAO)模式隱藏了如何訪問數據庫(或 LDAP 服務器、XML 文件等)的細節和復雜性,因為它提供了訪問抽象持久存儲 層的方法,而您則不需要在客戶機代碼中處理數據庫問題(數據實際存儲在哪裡 )。這不是 四人組(Gang of Four,GoF)模式,而是 Sun 的 J2EE 最佳實踐 的一部分。

在假想的開發團隊示例中,他們正在尋找這樣的層:

抽象所有正則表達式實現背後的概念。團隊就可以著重學習和理解這些概念 。他們所學的可以應用到任何實現或版本。

支持新的庫且沒有副作用。基於插件體系結構,動態選擇執行 regexp 模式 的實際庫,並且適配器不會被耦合。新庫僅會引入對新適配器的需要。

提供比較不同可選方案的方法。一個簡單的基准實用程序就可以顯示有趣的 性能測量結果。如果對每個實現都執行這樣的實用程序,團隊就會獲得有價值的 信息並能選擇最好的可選方案。

聽起來不錯,但……

任何去耦方法都至少有一個缺點:如果客戶機代碼僅需要一個實現所提供的 特定功能,怎麼辦?您不能使用任何其它實現,因此您最終將代碼與該實現耦合 。也許將來會在這方面有所改善,但您現在卻束手無策。

這樣的示例並不象您想的那樣少。在 regexp 領域中,一些編譯器選項僅被 某些實現支持。如果您的客戶機代碼需要這種特定的功能,那麼這個一般層是不 夠的 ― 至少從迄今對它描述來看是不夠的。

附加層是否應支持每個實現的所有 非公共功能,並且如果選擇了不支持該實 現的附加層則拋出異常?那可以是一種解決方案,但它並不支持僅定義 公共抽 象概念這一最初目標。

有一個 GoF 模式非常適合這種情形: 職責鏈(Chain of Responsibility) 。它在設計中引入了另一種間接方法。用這種方法,客戶機代碼向能處理其所發 消息的實體列表發送消息或命令。列表項被組織成鏈,因此消息可按順序被處理 並且在到達鏈尾之前被用掉。

在這種情況中,可以通過特殊類型的消息對僅被某些實現支持的特定功能建 模。由鏈中的每一項根據其是否了解這些功能來決定是否將該消息傳給下一項。

定義一個公共 API

這裡講述的 API 名為 RegexpPlugin 。已將它設計成遵循剛剛討論的方法, 並且它在 regexp 庫和使用該庫的代碼之間支持去耦。

RegexpPlugin

在以下示例中,我將總結一下使用具體實現(Jakarta Oro)和使用 RegexpPlugin API 之間的差別。

我從一個非常簡單的 regexp 開始:假定您必須要解析的文本只是人名。您接收的格式是象 John A. Smith這樣的內容,而您只想獲取名字( John)。但您不知道單詞由什麼分隔,是空格、換行符、制表符還是這些字符的組合。能處理這樣的輸入格式的 regexp 只是 .*\s*(.*?)\s+.* 。我將一步一步地說明如何使用該 regexp 來抽取信息。

第一部分是點號和星號字符 .* ,它們在這裡表示 任意數量的空格和 (.*?) 組之前的任何字符 。第二部分比較引人注意(因為它被圓括號括起來)。問號 表示 取第一個符合條件的項。

接下來的符號表示任意數量的空格、換行或制表符( \s ),但至少要有一個 ( + )。最後的點號和星號 .* 僅代表文本的余下部分(對它沒有興趣)。

因此,該 regexp 相當於: 取空格前的第一段文本。讓我們來編寫 Java 代 碼。

上機實踐

要在 Java 代碼中使用正則表達式,通常需要完成以下七個步驟:

第 1 步:創建編譯器實例。如果使用 Jakarta Oro,則必須實例化 Perl5Compiler :

org.apache.oro.text.regex.Perl5Compiler compiler =
  new org.apache.oro.text.regex.Perl5Compiler();

使用 RegexpPlugin 時的等同代碼是相似的:

org.acmsl.regexpplugin.Compiler compiler =
  org.acmsl.regexpplugin.RegexpManager.createCompiler();

但存在差異。正如前面提到的,該 API 對實際使用哪個具體實現加以隱藏。 您可以選擇一個具體實現或保留缺省的 Jakarta Oro。如果所選的庫在運行時不 可用,則 RegexpPlugin API 會嘗試用它的類名創建一個編譯器。如果該操作失 敗,它會將異常發回 API 的客戶機。

假定您一直在使用 JDK 1.4 的內置 regexp 類。那樣的話,包含始終不會使 用的額外 jar 文件毫無意義。那就是為什麼僅僅調用 createCompiler() 方法 還不夠的原因。您需要管理這樣的異常:每當所選的庫不存在時就會拋出該異常 。因而必須更新示例:

try
{
  org.acmsl.regexpplugin.Compiler compiler =
    org.acmsl.regexpplugin.RegexpManager.createCompiler();
}
catch (org.acmsl.regexpplugin.RegexpEngineNorFoundException exception)
{
  [..]
}

第 2 步:編譯 regexp 模式。將正則表達式本身編譯到 Pattern 對象中。

org.apache.oro.text.regex.Pattern pattern =
compiler.compile(".*\\s*(.*?)\\s+.*", Perl5Compiler.MULTILINE_MASK);

注:您必須轉義反斜槓(\)字符。

該模式對象代表以文本格式定義的正則表達式。請盡可能多地重用模式實例 。然後,如果 regexp 是固定的(缺少任何可變部分,如 “(.*?) Tom.*”),則模式應是類中的靜態成員。

compile 方法適合用標志(如 EXTENDED_MASK )來配置(請參閱 參考資料 以獲得更詳細的 regexp 教程)。但是,RegexpPlugin 並不允許隨意的標志。 受支持的標志只有 case sensitivity 和 multiline ,因為所有受支持的庫都 可以處理它們。

編譯器實例有特定的特性來定義這些標志:

compiler.setMultiline(true);
org.acmsl.regexpplugin.Pattern pattern =
  compiler.compile(".*\\s*(.*?)\\s+.*");

第 3 步:創建 Matcher 對象。在 Jakarta Oro中,這一步非常簡單:

org.apache.oro.text.regex.Perl5Matcher matcher =
  new org.apache.oro.text.regex.Perl5Matcher();

它之所以如此簡單是因為它不需要構造任何信息。在後來的 regexp 中,它 將變得具體。基本上,RegexpPlugin 中的步驟差不多相似。您不必親自創建 matcher ,而是可以將其代理給 RegexpManager 類:

org.acmsl.regexpplugin.Matcher matcher =
  org.acmsl.regexpplugin.RegexpManager.createMatcher();

區別和前面一樣,您需要處理 RegexpEngineNotFoundException 。實際上, RegexpManager 需要為您所選的庫或缺省庫創建 matcher 適配器。如果這樣的 類在運行時不可用,它會拋出該異常。

第 4 步:評估正則表達式。 matcher 對象需要解釋正則表達式並抽取所需 的信息。這在一行代碼中完成:

if (matcher.contains("John A. Smith", pattern))
{

如果輸入文本與正則表達式匹配,則該方法返回 true。隱含的副作用是,執 行該行代碼之後, matcher 對象包含在輸入文本中找到的第一個匹配項。接下 來的一步演示如何實際獲取感興趣的信息。

通過使用 RegexpPlugin API,在此時根本沒有任何不同。

第 5 步:檢索找到的第一個匹配項。 這一簡單的步驟僅用一行完成:

org.apache.oro.text.regex.MatchResult matchResult = matcher.getMatch();

您可以聲明一個局部變量來存儲這樣的對象,該對象含有與 regexp 匹配的 一段文本。在這兩種情況下,該步驟是相同的,除了變量聲明(因為一個是另一 個的適配器):

org.acmsl.regexpplugin.MatchResult matchResult =
    matcher.getMatch();

第 6 步:獲取感興趣的 group 。 您可以使用兩種方法:

具體庫

RegexpPlugin API

因為您的 regexp 是 .*\s*(.*?)\s+.* ,所以您只有一個組: (.*?)

MatchResult 對象包含已排序列表中的所有組。您只需要知道要獲取的組的 位置。因為該示例只有一個組,所以毫無疑問:

String name = matchResult.group(1);
  [..]
}

變量 name 現在包含文本 John,那正是您需要的。

第 7 步:如果需要,則重復該過程。 如果您需要的信息可多次出現,而您 想分析所有出現的信息而不只是第一個,那麼您只需循環執行第 5步到第 7步, 直到不滿足 第 4 步中描述的條件為止:

while (matcher.contains("John A. Smith", pattern))
{

映射

除了編寫公共抽象 API,主要的工作實際上是實現 Java 環境中某些已存在 的 regexp 引擎的適配器。

以下各表提供了對如何從一個庫遷移至另一個庫的詳細描述。有些情況中, 概念明顯不同。也有些情況中,卻不是那麼明顯。

Regexp 概念 GNU Regexp 1.2 編譯器 gnu.regexp.RE 模式 gnu.regexp.RE 匹配程序 gnu.regexp.REMatchEnumeration

gnu.regexp.RE

匹配結果 gnu.regexp.REMatch 畸形模式異常 gnu.regexp.REException

Regexp 概念 Jakarta Oro 2.0.6 編譯器 org.apache.oro.text.regex.Perl5Compiler 模式 org.apache.oro.text.regex.Pattern 匹配程序 org.apache.oro.text.regex.Perl5Matcher 匹配結果 org.apache.oro.text.regex.MatchResult 畸形模式異常 org.[..].regex.MalformedPatternException

Regexp 概念 Jakarta Regexp 1.3 編譯器 org.apache.regexp.RE

org.apache.regexp.RECompiler

org.apache.regexp.REProgram 模式 org.apache.regexp.REProgram

org.apache.regexp.RE 匹配程序 org.apache.regexp.RE

org.apache.regexp.REProgram 匹配結果 org.apache.regexp.RE 畸形模式異常 org.apache.regexp.RESyntaxException

Regexp 概念 JDK 1.4 regex 包 編譯器 java.util.regex.Pattern 模式 java.util.regex.Pattern 匹配程序 java.util.regex.Matcher 匹配結果 java.util.regex.Matcher 畸形模式異常 java.util.regex.PatternSyntaxException

基准

該 API 較顯著的用法之一是用來比較實現、測量性能、對 Perl5 語法的兼 容性或其它標准之間的差異。

為這些測試開發的基准實用程序使用 HTML 解析器來處理 Web 內容,更新有 關鏈接、表單和表等元素的信息。但是,重要的是解析邏輯用正則表達式來表示 ,因此會通過 RegexpPlugin API 實現。

基准測試包括對非常簡單的 HTML 頁面解析 10000 次。結果在下表中顯示。

Regexp 庫 Benchmark 結果(秒) Jakarta Oro 2.0.6 130,71 Jakarta Regexp 1.2 23,261 GNU Regexp 1.1.4 1,966.939 JDK1.4 33,222

您可以用多種方法在實際應用程序中改進性能。最重要的是,當您使用 regexp 庫時,不需要每次都編譯模式,而是編譯它們並重用各自的實例。但是 ,如果 regexp 本身不固定,則不能忽略編譯過程。

因為基准需要在實現之間切換以比較性能,所以必須始終廢棄已編譯模式以 避免庫之間的交互。但是,正如您所見,大多數已評估的庫有相似的響應時間, 盡管更詳細的基准能讓我們更好的理解每個庫在不同環境下的行為。

結束語

正則表達式解析器有強大的功能。一旦團隊適應了它,解析邏輯就會改進, 這有助於降低維護。但是,開發人員需要了解 regexp 語法以理解這些代碼是如 何工作的。本文已經用一個非常簡單的示例說明了如何使用這些庫中的一個。除 此之外,本文還描述了使用附加層去除客戶機代碼與 regexp 引擎本身之間的耦 合的好處。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved