1. 程式人生 > >刨根究底正則表達式之零——前言

刨根究底正則表達式之零——前言

modifier rcu latex 多人 www 不同 空間 收藏 link

前言

技術分享

一、緣起

1.

前面在字符編碼系列文章的前言中曾說過,類似於字符編碼這樣基礎、重要、應用廣泛而又特別容易讓人困惑的主題還有字節序(即大小端表示)、正則表達式以及浮點數實現、日期時間處理等等。其中,字節序、正則表達式跟字符編碼的關系非常密切。字符編碼以及字節序的問題已經在字符編碼系列文章中介紹過了,這個系列再來討論正則表達式。

不同於字符編碼,正則表達式目前市面上並不缺乏專業著作,比如那本被譽為正則表達式學習聖經的《精通正則表達式》就很值得一讀,另外該書的譯者余晟先生所寫的《正則指引》也不錯;如果僅用於入門,則《正則表達式必知必會》肯定不能錯過,還有網上流傳極廣的《正則表達式30分鐘入門教程》也是不錯的入門資料。

但是,結合我自身痛苦的正則表達式學習經歷和運用體會,僅有這些是遠遠不夠的。記得被大家稱之為“輪子哥”的大神級程序員vczh在知乎上說過,當初被正則表達式虐得一氣之下,幹脆自己寫了一個正則引擎(源碼托管在Github上),才算真正徹底搞懂正則表達式(於是被戲稱為“一言不合”就造輪子)。當然不是每個程序員都能如此生猛,但即便都有這麽生猛,似乎也沒必要都像“輪子哥”這樣真的自己都去再造一遍正則引擎的“輪子”。

那到底應該怎樣才能最高性價比地掌握正則表達式這個神器呢?這正是我寫這個系列文章的目的。

2.

正則表達式,一聽就是個非常專業的術語,對於大多數人而言完全不知道這跟自己有啥關系。但事實上,只要是平時用Word寫點東西的人,都可能用得上。

沒錯,Word就支持正則表達式,雖然所支持的功能遠遠不如常規的正則表達式強大,語法上也有很大的不同,因而只能算得上是準正則表達式或類正則表達式;但對我而言,只要是用Word寫文章,就已經無法想象沒有正則表達式的情形了。

當然,在Word中使用正則表達式,只能算是小試牛刀。正則表達式更大的用武之地在於各大常用高級編程語言、編輯器以及grep、sed和awk等命令行文本處理工具中,用以文本的查找、提取、替換、切分等操作。

3.

正則表達式是典型的那種沒用過的話,不覺得對自己有什麽影響,可是一旦用過了,就再也回不去了的神器。當然,我這裏所說的“用過”,不是指簡單用用一些基本功能,而是指能夠熟練運用其基本功能和高級功能。用得越熟練,就會越驚嘆於其強大與神奇。

看到這裏,我相信某些學過正則表達式、會使用一些基本功能的童鞋,心裏或許在犯嘀咕了:神器是神器,可這玩意兒看起來就像天書一樣,也太難學、太難懂了,要達到熟練運用的程度,談何容易!短短的一個正則表達式,或許不到10個字符,其中的每個字符都認識,但連在一起,卻越看越迷惑,越想越迷糊……

是的,正則表達式既然被捧上了神器級別的高度,自然是有著相當強大的功能,這當然就意味著其有非常深厚的內涵,也就意味著有很多需要註意的細節。

4.

註意,我這裏沒有說正則表達式是因為復雜而難以理解,因為深厚的內涵不等於復雜,細節很多不意味著難以理解。看到這裏,或許有人有意見了,正則表達式還不算復雜?還不夠難理解?你秀智商呢還是秀優越感吶?哦,相信我,其實這兩者我都不太沾邊。智商我也只是中等而已,否則早就不在這裏碼字了;而優越感則更提不上——既不高也不富更不帥,何來優越感?!

其實,我真正想說的是,繁復或許是真的,雜亂倒未必。因此,簡單地說正則表達式復雜,似乎不夠準確而客觀。正如跟一個牛叉而又性格獨特(廢話,真正牛叉的人基本上都有獨特的性格)的人打交道,關鍵不在於糾結其性格的獨特、脾氣的古怪,而是重在充分了解並理解其獨特的性格、古怪的脾氣,然後在此基礎上與他/她進行良好的溝通,以便能好好發揮其牛叉之處。

5.

學習並熟練掌握正則表達式的過程也是如此——關鍵在於先要摸透其“性格”到底獨特在哪裏,其“脾氣”又究竟古怪在何方。一旦摸清楚了其“性格”,其“脾氣”,學習起來就事半功倍了。

因此,我下面準備從我自己的角度,先嘗試著來分析一下正則表達式那獨特的“性格”與古怪的“脾氣”,看看究竟為什麽正則表達式給那麽多人的感覺都是那麽難以“親近”。

技術分享

二、正則表達式為什麽難學?

1.

對於正則表達式的分析和解讀,目前大多數文章和書籍多集中在正則表達式自身,比如對正則表達式的各個元字符、元轉義序列以及匹配原理的分析和解讀上。

當然,這些自然也是很有必要的,而且是學習的主要內容,是理解正則表達式所必需的。然而,很多人在看了大量這類文章和書籍之後,仍然覺得正則表達式很難看懂,不好理解,經常有一種智商被碾壓的即視感。

2.

難道真的是正則表達式的學習者智商不夠嗎?其實,理解一個事物,都應該有兩個維度,或者說兩個層面:一是,深入到該事物本身裏面去理解;二是,跳出到該事物外面,站在更高的一個維度或層面來理解。

正如蘇軾那首著名的哲理詩《題西林壁》所說的,“不識廬山真面目,只緣身在此山中”。很多時候往往是這樣,當你只從該事物本身來看的話,就如在雲裏霧裏,是遠遠不夠的;而一旦跳出到該事物之外,站在更高的一個角度來看,則又正如王安石的《登飛來峰》中所說:“不畏浮雲遮望眼,只緣身在最高層”。

3.

對正則表達式而言,前者正是目前大多數文章和書籍在做的;而後者,卻很少有文章和書籍能夠跳出正則表達式,站在更高的維度或層面來分析和解讀正則表達式。這裏就包括了被譽為正則表達式學習聖經的《精通正則表達式》,以及該書的譯者余晟先生所著的《正則指引》兩書。

這裏需要特別強調一下的是,我絕沒有貶低上述這兩本專業著作及其作者和/或譯者之意,而且恰恰相反,這兩本專著正是本系列文章的重要參考書。尤其無論是作為《精通正則表達式》的譯者,還是作為《正則指引》的著者,余晟先生都絕對稱得上是既專業又嚴謹。

4.

那麽,前面所謂“更高的維度或層面”,到底指的是什麽呢?那就是,從編程語言發展史以及編程範式的角度來看正則表達式。什麽?正則表達式竟然也算得上是一門正式的編程語言嗎?別急,請繼續往下看。

正則表達式有一個非常明顯的特點:高度簡潔、高度抽象。正則表達式中短短的幾個字符,或許就代表了一段復雜的處理邏輯和匹配算法。

5.

我們知道,程序代碼是對現實事務處理邏輯的抽象,而正則表達式則是對復雜的字符匹配程序代碼的進一步抽象;也就是說,高度簡潔的正則表達式,可以認為其背後所對應的是字符匹配程序代碼,而字符匹配程序代碼,背後對應的是字符匹配處理邏輯。

因此可以這麽認為,字符匹配處理邏輯,抽象為字符匹配程序代碼;字符匹配程序代碼,再進一步抽象為高度簡潔的正則表達式。所以說,高度簡潔的正則表達式也是高度抽象的。

6.

事實上,從編程語言發展的角度來看,正則表達式也是一種編程語言,而且是屬於第4代語言(4GL)——面向問題語言(第1代語言為機器語言——由0和1組成的位串,第2代語言為匯編語言——用接近於英語單詞的助記碼mnemonic code來代替由0和1組成的位串,第3代語言為高級語言——用接近於自然語言的語法元素編寫程序,如C/C++、Java、C#、Perl、Python、PHP、JavaScript等語言,第4代語言為面向問題語言——用針對問題領域專門設計的語法元素編寫程序或表達式,如SQL、SAS、SPSS、LaTeX、Regex(即正則表達式)等,第5代語言為人工智能語言——Prolog、Mercury、OPS5等;不過,從第4代語言到第5代語言的演化還不是很清晰,目前學術界爭議較大,這裏不作討論)。

註:這裏強烈推薦鄭暉先生所著的《冒號課堂——編程範式與OOP思想》一書,該書既以宏觀視角,縱向審視了編程語言發展簡史,橫向比較了各類編程語言的特點;又以微觀實踐,娓娓道來各編程範式的優劣得失,更是深入探討了面向對象編程的方方面面,是不可多得的一本中文原創計算機專著。這裏是我在豆瓣寫的該書書評,供參考。】

技術分享

第4代語言相對於第3代語言,更專註於其所應用或者說其所適用的某個特定的業務邏輯和問題領域。程序員主要負責分析問題,以及使用第4代語言來描述問題,而無需花大量時間去考慮具體的處理邏輯和算法實現(事實上,最初之所以提出第4代語言的概念,就是希望非專業程序員也能做應用開發)。

7.

從編程範式(programming paradigm)的角度上來講,第4代語言屬於聲明式編程範式,聲明式編程重在目標而非過程、重在描述而非實現,以聲明式語句直接描述問題,專註於問題的分析和表達,而非專註於處理邏輯和算法實現過程,其具體的處理邏輯和算法實現是由語言解析引擎(編譯器或解釋器)來負責的。

當然,這樣一來,這些由語言解析引擎實現的處理邏輯和具體算法其通用性就會較差,只能適用於某些特定業務或特定領域。也正是這個原因,第4代語言基本都是局限於某些特定領域的,多被認為是領域特定語言DSL(Domain Specific Language)。

區別於算法實現可由程序員自由靈活設計的通用編程語言GPPL(General-Purpose Programming Language,作為第3代語言的高級語言基本上都屬於通用編程語言),領域特定語言DSL的算法基本上由語言解析引擎自動實現,程序員靈活設計、自由發揮的空間很小,因此DSL幾乎沒有通用性(而且DSL大都是非圖靈完備的語言),只能專用於解決特定業務方向和業務領域的問題。

比如,SQL是專用於數據庫操作的語言、SAS和SPSS是專用於統計分析的語言、LaTeX是專用於排版的語言,而正則表達式Regex(regular expression)則是專用於處理字符匹配的語言

8.

理解了這一點,就比較容易理解正則表達式是字符匹配處理邏輯的抽象;更進一步地來說,正則表達式中的某些元字符與特殊結構,可理解為某種具體的程序邏輯和算法的體現。

比如,正則表達式中的量詞*這一元字符,就是高級語言的處理邏輯“循環結構”的體現(具體來說量詞*代表的是不定次數循環),而前後多個量詞的嵌套就是多層循環的嵌套;或運算符|這一元字符,就是高級語言的處理邏輯“選擇結構”的體現。

而當或運算符|出現在由量詞*所限定的圓括號中時,其實就是“循環結構”中嵌套了“選擇結構”;而如果進一步地,“循環結構”所嵌套的“選擇結構”中的某個分支,又被某個量詞*所限定,那麽則相當於“循環結構”所嵌套的“選擇結構”又嵌套了“循環結構”。

理解這一點非常重要,是快速、深入理解正則表達式的一把鑰匙、一條捷徑。站在編程語言發展史和編程範式的高度,再結合對正則表達式本身原理的深入理解,裏外結合,高下相較,既登高望遠、一覽眾山小,又洞幽燭微、復觀千水深,正則表達式的奧義,就能盡在掌握之中了。

9.

當然,正則表達式之所以難學、難理解,除了由於正則表達式作為一個字符匹配領域的DSL,具有高度簡潔、高度抽象的特點之外,大致上應該還有以下幾個原因:

0) 學習者不求甚解,不了解正則引擎內部的基本原理

作為正則表達式的使用者,不需要深入了解正則引擎內部原理的技術實現細節,那是正則引擎開發者更應該了解的;但若完全不了解其基本工作原理和運行機制,也是不足取的。

1) 有多個多義元字符,特別容易使人混淆、迷亂

比如-、+、?、^,尤其是元字符?,既可以作為量詞表示其所限定的子表達式為可選(即匹配0次或1次),也可以置於量詞之後表示懶惰匹配,而且還有很多特殊分組結構中用到它,比如(?<name>sub-regex)、(?:sub-regex)、(?>sub-regex)、(?=sub-regex)、(?!sub-regex)、(?<=sub-regex)、(?<!sub-regex)、(?|sub-regex)、(?modifier-modifier)、(?(condition)|)、(?R)、(?num)、(?#comment)等;還記得我自己當初剛開始學習的時候,一看到正則表達式中的問號?,我就有一種獨自在風中淩亂的感覺。

2) 轉義也是難點

什麽情況下需要轉義,什麽情況下不需要轉義,貌似復雜得令人抓狂;當然,其實是有一定的規律的,掌握了這些規律,再遇到轉義問題,不至於心潮澎湃了。

3) 學習期望與學習方法不對

不應該期望一次性記住、學會並熟練運用,正確的學習姿勢應該是:先簡單入門,對一些基本的規則與元字符大致了解一遍,有個印象就好,在需要時再回過頭來看,不用刻意去強行記憶;然後接下來就應該多練、多實踐、多運用,邊學、邊深入、邊熟練。

4) 有用於入門的好教程、備忘單,也有用於深入的大部頭專著,但卻缺乏好的速查手冊

由於需要邊學、邊深入、邊熟練,因此,平時手頭邊更需要的不是簡單的入門教程、備忘單(Cheat Sheet),也不僅僅是知識點分散於各處的大部頭專著,而是一本按語法元素將知識點綜合在一起進行編排的、在需要回過頭來看時能夠隨時快速翻查的速查手冊。這樣,在實踐運用中遇到問題就可方便隨時快速翻查,而這一點恰恰對於正則表達式這種不可能短期內快速掌握並熟練運用的專業工具的學習與使用非常重要。

5) 沒有使用好的學習工具

你知道regex101.com、RegexBuddy、regexper.com等正則表達式的專業網站和專業工具嗎?這些堪稱學習正則表達式的神器,可令學習事半功倍,但很多人不知道,或知道但很少使用。

技術分享

三、關於本系列文章的編排設計

1.

本系列有關正則表達式的文章,出自於我自己在學習正則表達式的過程中所經歷過的真切體會和真實痛點。因此,正如前面所述,采取的編排風格類似於速查手冊。

但是要特別註意,這僅僅是出於邊用邊快速翻查的目的而作出的編排設計,不等於是通常大家所理解的那種簡單解釋一下概念,然後羅列一下功能,再加幾個示例的雞肋般“食之無味,棄之可惜”的字典式簡易手冊(這種簡易手冊僅供入門使用);更不是將元字符、元轉義序列、特殊結構的簡單解釋編排在一張A4大小紙張上的備忘單(當然,這種備忘單也並非沒有意義,至少通過一張A4大小的紙張就可快速了解正則表達式所支持的語法元素包括哪些,因此本系列文章也會提供幾份我收藏的備忘單供大家參考,但顯然也僅供入門使用)。

這也就是文章名稱中之所以特別強調“刨根究底”,而不是直接名之為速查手冊、快速參考之類的重要原因。

2.

因此,本系列文章與相關專著一樣,也同樣會涉及到正則引擎內部的相關匹配原理與匹配機制的解釋(而且還獨創性地總結為了幾大原則,便於“以簡馭繁”、“提綱挈領”地快速掌握要領以便於記憶和理解),只是與其他專著用專門章節進行介紹不同,而是各自糅合於對相關語法元素的解釋之中了。

這種為了便於快速翻查而沒有將匹配原理與匹配機制予以專章介紹的特殊編排,自然也有其缺點(比如,你可能會在不同的語法元素中發現類似的雷同解釋,這或許有重復啰嗦之嫌,但畢竟這符合我們的編排目的),但問題在於市面上進行專章介紹的專著已經有很多了,再重復它們意義不大;而專門針對前述的正則表達式學習和運用痛點的文章和專著則基本沒有,而這正是本系列文章的意義和目的所在。

3.

也因此,出於更偏向於實踐運用的目的,本系列文章不會花費過多的筆墨在DFA、NFA等過於深入的正則表達式幕後技術細節的講解上。

事實上,我認為只要大致了解它們的基本原理與工作機制以及兩者之間在功能特性上的差異,就完全可以熟練掌握並運用正則表達式了,除非你是想自己開發一個正則引擎,實在沒必要過於陷入DFA、NFA等狀態機(自動機)的實現細節上。

雖然前面曾提到過,“輪子哥”vczh為了徹底搞懂正則表達式,硬生生自己重新造了一個“輪子”。然而,要是為了學個正則表達式,都非要這樣重造輪子,既無可能,也實在沒有必要。

那麽,真的在不重造輪子、不陷入DFA、NFA等技術細節的基礎上,也能搞懂正則表達式?

我自己的體會是,能!本系列文章就是我自己學習心得體會的總結,文章中除滲透了前文所述的“裏外結合,高下相較”這一相對“務虛”的基本思路之外,當然也有相對“務實”的“幹貨”——總結出來的八大原則(包括六大基本原則:最左原則、先到先得原則、最長原則、逐位置依次嘗試匹配原則、整體匹配優先原則、占有匹配優先原則;以及兩大衍生原則:最左先到先得原則、最左最長原則)多角度立體的文字講解,以及大量圖示

因此,我相信通過反復閱讀本系列文章,再多加練習、勤於實踐,然後在實際運用時再不斷回過頭來隨時翻看,應該完全可以熟練掌握這個像毒品一樣會讓人用上癮的神器。

好了,牛皮吹過了,到底是騾子是馬,後面會拉出來遛遛……

技術分享

四、下面是正則表達式系列文章將會涉及到的內容:

一)什麽是正則表達式

二)為什麽使用正則表達式

三)正則表達式簡史(含正則表達式流派簡介)

五)正則表達式基礎

六)八大原則簡介,包括:

六大基本原則:最左原則、先到先得原則、最長原則、逐位置依次嘗試匹配原則、整體匹配優先原則、占有匹配優先原則;

兩大衍生原則:最左先到先得原則、最左最長原則

七)元字符逐個詳解,包括:\、(、)、[、{、.、-、*、+、?、|、^、$,其中-、+、?、^為多義元字符

八)元轉義序列逐個詳解,包括:

固定字符:\a、\b(字符組內部)、\e、\f、\n、\r、\t、\v(非Perl系)

字符組簡記:\d、\D、\h、\H、\N{}、\p{}與\pP、\P{}與\PP、\s、\S、\v(僅Perl系)、\V、\w、\W

進制轉義字符:\octal-num(Perl系中也可寫作\o{octal-num})、\xhex-num(Perl系中也可寫作\x{hex-num})、\uhex-num(非Perl系,Ruby1.9+等個別語言中還可寫作\u{hex-num})

控制字符:\cX系列

錨點:\A、\z、\Z、\b(字符組外部)、\b{}、\B、\B{}、\G

引用:\num、\g{num}、\gnum、\k{name}、\k<name>、\k‘name‘

修飾:\E、\F、\l、\L、\Q、\u(僅Perl,不是僅Perl系)、\U

其他:\C、\K、\N、\R、\X、\<、\>

九)特殊構造(特殊結構)逐個詳解,包括:

字符組、多選分支結構、捕獲分組、命名捕獲分組、非捕獲分組、預查分組(即環視分組)、固化分組(即原子分組)、嵌入條件分組、內聯選項與取消內聯選項分組、註釋分組、分支復位分組、表達式引用分組、平衡分組等

十)匹配模式逐個詳解,包括:i、s、m、x、g等常用匹配模式

十一)POSIX字符組方括號表達式、排除型POSIX字符組方括號表達式

十二)字符組運算:字符組減法運算、字符組邏輯與運算

十三)正則表達式各語法元素優先級

(未完待續)

刨根究底正則表達式之零——前言