Regular Expression常縮寫為Regexp、Regex或RE,中文則有正則表達式、正規表達式、常規表示式等譯名,Regex強大有目共睹,不局限於資訊領域應用,其他領域也能夠從Regex獲益良多。

然而,正如Regex全名本身不易理解而產生各種譯名,Regex語法中繁多符號的排列組合,產生出高資訊密度的Regex語句,也經常令人費解而望之卻步。

數小時到數秒鐘的魔術元素

你有一堆HTML檔案,每個HTML中都有一堆<img>標籤被<a></a>標籤包裹,每個<img>與包在外頭<a>中有一個以上的HTML屬性設定,你得把那些包在<img>外的<a></a>去除,手工檢查這堆HTML也許要花上個把個小時的時間,使用文字編輯器的「尋找」功能也是費力的一件事,如果手邊有個支援Regex的工具,可指定比對<a.+>(<img.+>)</a>並以$1取代,那麼只要幾秒就可以完成任務,不必是程式設計師也能辦到。

就像魔術,只要你懂其中的元素就會恍然大悟,<a.+>(<img.+>)</a>中可以先拿掉.+()這幾個符號,剩下的就是<a><img></a>,很簡單的意涵,找出被<a></a>中包裹<img>的文字,Regex的一部份就是由這類按照字面意義比對的字面字元(Literal)組成,那麼實現魔術的部份就是.+()這幾個符號了,這類符號是不照字面比對的詮譯字元(metacharacter),在不同情境中可能有不同的意涵。

以這邊的Regex來說,「.」表示任意字元,「+」表示可以出現一次以上,「.+」就表示「有一個以上的任意字元」,<a.+>或<img.+>就表示<a>或<img>中可以有任意個字元,也就作為HTML屬性設定的那些字元,(<img.+>)表示被<img.+>比對到的文字納為一組,使用$1就可以取得被比對到的這組文字,將<a.+?>(<img.+?>)</a>比對到的文字,以<img.+>比對到的文字取代,結果就是去除了<img>外的<a></a>。

按照字面搜尋的功能,基本的文字編輯器就做得到,顯然地,讓魔術得以實現的是詮譯字元,而且,網路上找到數不清的Regex文件,多是在告訴你詮譯字元代表哪些規則。使用Regex的時機提示就是,你在處理文字檔時有了重複操作,把你的重複操作步驟寫下來,找出比對規則,因為電腦看不懂人類的文字,因此,你要使用電腦看得懂的詮譯字元告訴它字面文字外的比對規則,只要有了規則,電腦絕對比人類擅長處理重複的事。

如果你沒有學過Regex,不瞭解各個詮譯字元在Regex中代表的意義,單看<img.+>只覺得其中有毫無意義的符號,實際上,詮譯字元真正的意義,在於統一了描述規則的方式。

舉例來說,對方才的HTML案例,「<img>中有一種以上的HTML屬性設定」、「<img>中有一個以上的字元」都可以是描述的方式,也許你寫下的需求描述也與我不同,然而,詮譯字元給了我們共同的描述方式,只要我們都學過基本的Regex,就都能理解<img.+>代表什麼意義。

詮譯字元是文字處理中的模式,文字處理中會有「X或Y」的比對,Regex可以寫為X|Y,文字處理中會有多個「或」的需求,因此有字元類(Character class)結構,文字處理會有「出現幾次」的描述,因此會有量詞(Quantifier),你也需要描述定位,因此有錨點(Anchor),對於某組具有相同模式的文字,你可能會很感興趣,因此才會有分組(Group),類似設計模式提供了開發者共同的設計語言,詮譯字元也為文字處理統一描述方式。

對於簡單地搜尋比對需求,使用現代程式語言,也許只要短短幾行就辦得到,只要語言的語法或API命名具有意義,也容易閱讀出比對描述的意義,而Regex也有類似作用,使用符號來代表規則描述,當一堆符號組合在一起時,就會包括極為大量的資訊,試著用程式語言來實作出<a.+>(<img.+>)</a>應有的效果,就會發現資訊的龐大程度。

另一方面,完整的Regex其實是逐步加入小規則建構起來的,如果當初沒有將建構順序記錄下來,未來看到完整的Regex時,就會因為難以理解當初建構的順序,而不懂Regex甚至弄錯真正想要比對的對象。

雖然詮譯字元給了共同的描述方式,然而,從人類文字轉譯為詮譯字元的過程中,容易有失去準頭的問題,這就類似程式語言或人類語言在轉譯時也不是一對一的情況,特別是在對另一門語言不瞭解的情況下,轉譯就更容易出錯。

例如,「<img>中有一個以上的字元」這個描述轉譯為Regex時,寫為<img+>看似正確,因為+代表一個以上的字元,實際是錯的,因為這只會比對到<img>、<imgg>、<imggg>這類情況,因為+是配合g,<img+>是指「<img>中的g可以有一或多個」。

理解文字內容並善用工具

就像樂高一樣,每個零件都很簡單,然而可以用各種方式組合出複雜成品,完整的Regex實際上也是由許多小零件組合而成,只是在運用這些小零件之前,你得逐步理解要處理的文件內容,先用一般文字寫下你的需求,然而從最簡單的字面文字開始建構Regex,測試一下尋找到的對象,然後加入新的規則再測試,在這個反覆過程中,你往往會發現,自己一開始對文件中要比對的目標內容理解度並不是那麼精確,你得重新調整對目標內容的認識,然後用更精確的規則來描述它,並加入Regex的建構之中。

舉例來說,「<img>中有一種以上的HTML屬性設定」這個需求是籠統的,你用<img>這個Regex作為開頭,實際比對不出HTML中任何東西,用<img開始測試才能有些結果,然後使用<img.測試,接著是<img.+測試時,你發現找出的東西除了<img>標籤外,後頭的東西也找出來了,於是,再使用<img.+></a>加上限制,然後,再往前依><img.+></a>、.+><img.+></a>、a.+><img.+></a>與<a.+><img.+></a>的順序,得到完整的Regex。

根據你對文件內容的理解方式,也會影響建構Regex的順序,以這邊的需求來說,你也可以從<a開始。完整的Regex建構,其實也像程式設計,都是從問題子集開始個別擊破,對於每個比對子集可以試著加入()加以分組,對於Regex日後解讀會有幫助。

在逐步建構Regex的過程中,一個方便的工具是必要的,像是老牌的Regex建構工具Expresso,可以方便你一邊建構Regex一邊即時地測試,建構Regex時的設計選單,可以讓你不用查閱詮譯字元的意義或誤打詮譯字元,Expresso本身也內建了一些常用的Regex。

如果想在產生Regex的過程中順便記錄下建構過程,Github上有個有趣的VerbalExpressions專案,它提供了Java、Python、JavaScript、Ruby等語言的實作,可以讓你用流暢API風格來建構Regex,例如若使用Java實作版本,可以撰寫new VerbalExpression.Builder().find("<a").something().find("><img").something().find("></a>").build(),產生的Regex就有<a.+>(<img.+>)</a>的比對效果,既表現Regex的順序,也不用記憶詮譯字元的意義。

將Regex視為語言

雖然Regex一般人會將它看成是一種比對規則的表示方式,實際上它更像是門語言,VerbalExpressions專案只是突顯了這個事實,進一步賦予詮譯字元的符號有意義的名稱,如果你看看VerbalExpressions的Ruby實作版本,就更能瞭解這個事實,現在多數程式語言都會內建對Regex的直接或間接支援,這讓Regex就像是個外部特定領域語言(Domain-specific language),專門用來處理文字比對相關事務,而不是使用既有語言的語法,寫一大堆繁複的程式碼,來解決相同的事。

就如同各門程式語言的學習,你該學的並不只是語法,而是背後的文化、思考與典範,既然Regex可視為一門語言,一門專用於文字比對的特定領域語言,就代表著你要理解該門語言的思考框架,像是理解文件內容、尋找目標規則、個別小任務的擊破與組合等,而不僅止於跟那些奇妙的符號搏鬥。

作者簡介


Advertisement

更多 iThome相關內容