iThome 網站首載:Regex的強大與哀愁
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
取代,那麼只要幾秒時間就可以完成這項任務,只要懂regex的魔術,不必是程式設計師也能辦到這種事。就像魔術,只要你懂其中的元素就會恍然大悟,
<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可視為一門語言,一門專用於文字比對的特定領域語言,就代表著你要理解該門語言的思考框架,像是理解文件內容、尋找目標規則、個別小任務的擊破與組合等,而不只是僅止於跟那些奇妙的符號搏鬥。