文本分析基礎-正規表達式(RegExp)

“你是誰?/你叫什麼?/你在幹嘛?/你做什麼工作?”

“我是小Q,來自台灣、最喜歡吃章魚,每天的工作是在這邊陪你打屁聊天。”

以上是常見基本的人工智慧對話──上述問題都能讓機器在偵測後、對應到同一種回復。這是怎麼做到的呢?

答案是:正規表達式(Regular Expression)

若說編程本身就是一種邏輯訓練,那麼正規表達式就是將邏輯的強大力量發揮到極致的一種數學邏輯公式。

其應用之廣泛,從文本資料分析查詢到人工智慧的腳本編寫,都能看到正規表達式的使用。

為求尚未接觸過相關知識的讀者閱讀方便,本篇在講述正規表達式時,將使用最為簡單易解的語言介紹什麼是正規表達式,並附上參考研究資料供您後續深入探索。

本篇介紹有另外製作成好讀簡報版,歡迎參考。

 

 

什麼是正規表達式?

Regular Expression,台譯作正規表達式,又稱常規表達式正則表達式(陸譯, 適用於查找知乎之類的中國網站)。

別說你沒用過正規表達式,Office軟體中的”尋找/取代”就是一種正規表達式的應用。

搜尋文字檔、尋找相符的字串(Matching)是正規表達式最基本也最常使用的功能,故常看到正規表達式相關文本出現「匹配」、「配對」等字詞。

密密麻麻的程式碼中,該如何查詢所需要的字串呢?

在查詢文本資料、或使用程式編輯器寫了長長一大串的程式碼後,我們常常遇到下列問題:

  • “灰色”的英文到底是拼作”gray”還是”grey”? 該怎麼把相關的字詞都找出來呢?
  • “gray”, “grey”, “Grey”, “Gray”, “GReY”, “gRAy”, “grey123″…等大小寫不一、或包含數字的資字串,該如何全部都要找出來? 如果我只想要找不含數字的字串呢?

就算是用 ctrl-F 的”尋找”功能來查,面對這麼多的變化、也得一個個查到天翻地覆吧。

機器最大的功用,就是取代繁複的人工、讓日常作業變得更加簡單高效,因此我們需要像正規表達式這樣的小幫手來協助我們查詢。

事實上正規表達式並不是程式語言,只是一款「表達字串形式」的「邏輯式」,1956 年由數學家 Stephen Kleene 所提出,包括 JavaScript, Perl, Python, Java, C/C++ 在內的多種程式語言都能支援正規表達式。

這不但表示我們把正規表達式子貼到這些語言中都能跑得動,也意味著只要學會正規表達式、就能跨平台使用且十年以上不退流行 (想1956年至今60年),妙處多多。

正規表達式乃身為一個程式工程師所具備的基本常識。

比如上述所提:「想要查找”灰色”的英文,但忘記到底是拼作”grey”還是”gray”?」我們可將這樣的搜尋要求轉換成一行式子──

"gr[ae]y",表示gray或grey皆得查詢。 

若想要同時涵蓋大小寫開頭,可寫成"[Gg]r[ae]y"。

 

同理:

當我們想同時查詢 google, goooogle 和 gooooooogle 時,可以寫成 goo+gle

當我們想同時查詢  color 或colour 
(美國人和英國人為什麼總愛用有些差異的拼法呢),可寫成 colou?r

除了這些功用以外,正規表達式還能做些甚麼呢?

程式語言本身就是一種邏輯訓練的過程;寫過程式的人都知道,常見的 “if/else” 條件式對機器下命令的方式是:

“If(當)發生A事件的時候, 做出A動作; 若否, else(則) 做出 B 動作”

但當我們看看註冊帳號時、常見的密碼設定條件:

密碼必須是包含大小寫字母和數字的組合,且長度在8-12之間。

若請你用“if/else”條件式試著達成這個要求?相信我,至少得寫個數十行以上的程式碼。但若我們使用正規表達式,短短一行就能解決:

(?=.*\u)(?=.*\l)(?=.*\d).{8,12}$

 

基本上,在密碼 (符合特定字數與符號要求)、身分證字號 (英文開頭, 第二個數字必定為1或2, 後面接8個數字)、手機號碼 (09開頭,中間”-”符號可有可無, 如09**-***-***)、網址 (http:// 開頭) 等字串校驗時,都是採用正規表達式來編寫。

或者當面對到廣告 Email 灌爆信箱的問題時,只要注意廣告信件上的「折扣!/獨家優惠」等固定的標題或內容,只要在每次來信時都先使用正規表達式、將來信的標題與內容的進行比對,發現有不良信件就立刻刪除,大幅降低人工比對字串的麻煩。

既然這麼方便好用,為什麼程式語言不通通都用正規表達式來寫呢?有道是:「常人有三種東西看不懂──醫生的處方,道士的鬼畫符,工程師的正規表達式。」

(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))

看似如天書一般的正規表達式,在理解上比用文字寫成的程式碼更加困難… 不但

Debug難(你可能寫完都忘記自己是用甚麼邏輯寫成的了)

Review難 (除了你自己之外其他人很難看懂啊! 下一個接手你專案的工程師會恨死你)

複用更難 (正規表達式的數字式難以用在多個不同情境上) 。

因此,並不是所有問題都要使用正規表達式。大量、重複、簡單的文字處理,是最常使用正規表達式的時機。

不過正規表達式能做的不僅僅是這些。

作為一款靈活高效的文字處理工具,如要深入使用、它可以用來描述、分析、新增、移除、隔離甚至是各種翻轉惡搞文字。

歷來在人工智慧 (AI)、機器人 (Robot) 與自然語言處理 (NLP) 的領域上也被廣泛的使用,可以說是機器人對話的雛型。

比如說,當提到:「我WIFI炸了!/可惡wifi掛了/WifI一直連不上真的很煩/wiFI壞掉啦」

機器人都能回答:「 啊哈哈哈你看看你連不上網~」

注意到了嗎?

關鍵在於── WIFI/wifi/WiFI/wIFi 等文字不限於大小寫。

且在使用到「」、「」、「」、「連不上」等字眼的同時,機器會吐出相同的回答。

故而正規表達式加上資料庫、資料探勘、網頁爬蟲、人工智慧,將成為無比強大的工具。接下來,就讓我們來初步瞭解正規表達式該如何寫成。

 

 

正規表達式基本語法

小時候在學數學時,花了很多的時間精力背誦九九乘法表。正規表達式的學習也是一樣──熟背語法、多做練習題作為活絡腦袋的邏輯訓練。

真不行時… Stack Overflow(工程師的知識+)是你的好夥伴。

由於正規表達式可以被安插在多種程式語言之中,初學者可以使用 Regular Expression 101 網站進行測試:

首先選用左方的 PHP、JavaScript 或 Python 三種語言環境作為正規表達式測試器。

依照不同語言、採用 // 雙斜線 (PHP和JavaScript) 或是””雙引號 (Python)

將正規表達式子包起來。右下角提供了常用的語法查詢。

如果讀者試著在//裡面輸入比對的規則、在Text String中填入需要比對的文本,會發現:「為什麼只能找出一個字串呢?」

為什麼只能找出一個cat?內文中明明還有其他cat?

正規表達式的組成為── 「/pattern/flag」,意思是「/比對規則/比對方式

舉例而言:/cat/: 以//雙引號將cat這個正規表達式框起來,cat是規則(pattern)。沒設定比對方式的話,它只會比對一筆資料。

因此我們必須在/規則/外面加上比對方式(flag),比如在/cat/外加上g、變成/cat/g,表示我們想要比對全部範圍的文本。

 

flag 根據不同的程式語言也有所不同,可在網站右下角查詢flag所代表的意思。至於最為常用的flag主要有兩個:

/pattern/g: 表示查詢全部文本 (global match)。

/pattern/i: 表示不限大小寫 (ignore case)。

若同時打上gi,表示要查「文本中全部的單字」且「不限大小寫」。

講完了正規表達式的結構,來談談式子中的規則吧!

正規表達式可分成一般字元 (Characters )與特殊字元 (Operators)。

一般字元指的是普通的文字,特殊字元則有特定的字元查詢功能。下列是正規表達式中的特殊字元:

[] : 代表集合中的任一字元, 比如說[1,3,5,7]代表1,3,5或7任何一個字。逗點可省略,[1,3,5,7]=[1357]。

– : 代表從…到的範圍,比如說[1-4]等於[1,2,3,4]。

{} : 代表字元出現次數, 比如a{2,7}表示a字元出現了2次到7次。

() : 將比對符合的字元暫時存入一個變數, 供系統後續使用。比如(x) and (y)可以由 ‘xx and yyy’字串比對出’xx’和’yyy’, 並將這兩個比對得到的字串暫存入RegExp.$1 和 RegExp.$2中, 讓系統後續在使用RegExp.$1 時即代表’xx’。

| : 代表(or),比如ant|bug|worm代表ant或bug或worm其中之一皆可。

. : 代表任意字元, 比如 .at可以符合cat, bat, rat等任意字元的開頭, 後面要接at。

^ : 代表字元在開頭位置, 比如^cat代表cat, catch與cathay皆符合。

$ : 代表字元在結束位置, 比如ind$代表kind, blind, wind皆符合。

\<\>: 代表查詢單字本身。^AT代表僅鎖死開頭, ATM和ATT4FUN都符合; 而AT$代表鎖死結尾, PAT和BAT都符合。但如果要鎖死開頭和結尾, 只查「AT」這個單字, 可以寫成^AT$, 或是\<AT\>。比如\<mon\>只能查mon,而在lemon、monster和admonish都不符合。

[^]: 在括號內代表否定, 比如[^abcde]代表配對abcde以外的字元。

! : 代表條件相反的反向比對。比如a[!bc]代表只要不是ab或是ac, 其他都符合; ad即符合a[!ab]。

小練習:(可使用Regular Expression 101進行測試) 

1. /[pf]our/代表什麼意思呢? 

答: pour和four都符合配對。/[pf]our/同於/[p|f]our/或/[p,f]our/。 


2. (a)7331 (b)73331 (c)733331 (d)7333331; 哪些選項符合/73{2,4}1/? 

答: (a)(b)(c)符合。


3. (a)8ap9We (b)mU3 (c)Zom0 (d)KeD5; 哪些選項符合/[k-q].[^4-7]/呢? 

答: (a)p9W,(b)mU3,(c)om0。故(a)(b)(c)皆符合。

 

 

? : 代表?前的字元可出現0次或1次, 比如bin?d的n可出現0次或1次, 同時符合bid和bind。也等於n{0,1}。

+ : 代表+前面的字元可出現1次或多次 , 比如Re+ally, 符合Really, Reealy, Reeeeealy ; e至少出現一次。也等於e{1,}。

* : 代表*前面的字元可出現零次或多次, 比如go* 符合g 和gooooo。也等於g{0,}。

小練習:(可使用Regular Expression 101進行測試) 

1. 請表達任意整數。如0, 17, 25189。 

答: /^(0|[1-9][0-9]*)$/。


2. 請表達任意小數。如0.388, 73592.16。 

答: /^(0|[1-9][0-9]*)\.[0-9]*[1-9])$/。

 

到這邊,讀者可能會有個問題:如果我想查”50+”怎麼辦?

意即將”+”當作一般字元而非特殊字元來查找,單純查”50+”、避免查成”50”或”500000000”。此時我們便需要:

\ : 稱為跳脫字元, 可去除特殊字元的功用、讓特殊字元回歸成普通字元。比如想查”50+”, 得寫成”50\+”避免上述情形發生。同理亦適用於\. \* \^ \$ \?。

記完了特殊字元後,讓我們來看看特殊字元的衍生──字元族(Character Class)

我們已經知道了 [A-Z] 代表大寫字母 A 到 Z;[a-z] 代表小寫字母 a 到 z;[0-9] 代表阿拉伯數字 0 到 9 。然而每次寫 [a-z] 相當麻煩又不直覺,因此正規表達式又內定了一些字元族

\d: 所有數字,等同[0-9]。d是數字(digit)的意思。

\D: 數字以外的字元, 等同[^0-9]或[^\d]。比如ABC, abc,*()#%都符合。

\l: 所有小寫字母, 等同[a-z]或abcdefghijklmnopqrstuvwxyz。l是小寫(lower)的意思。

\L: 小寫字母以外的字元, 等同[^a-z]或[^\l]。比如ABC, 123, *()#%都符合。

\u:所有大寫字母, 等同[A-Z]或ABCDEFGHIJKLMNOPQRSTUVWXYZ。u是大寫(upper)的意思。

\U:大寫字母以外的字元, 等同[^A-Z]或[^\u]。比如abc, 123, *()#%都符合。

\w: 所有大小寫字母、數字與底線_ , 等同[A-Za-z0-9_]。比如”$5.28″可比對出”5”。

\W: 大小寫字母、數字與底線_以外的字元, 等同[^A-Za-z0-9_]。比如”30%”可比對出”%”。

\a: 所有字母與數字, 不含底線。等同[A-Za-z0-9]。

\c: 所有字母。等同[A-Za-z] 。

\s: 所有空格。由於空格大小不一, \s同時包括了空白鍵、tab、換頁、換行。若要更精確── \\代表空白鍵、\t代表tab、\f代表換頁(form-feed)、\n代表換行。

\S: 非空格字元, 等同[^\s]。

 

小練習:(可使用Regular Expression 101進行測試) 

1.只能輸入3位數字。 

答: /^\d{3}$/。 開頭必須是數字(^\d)且有三次({3}),後面不能接其他字元($)。 


2. 驗證手機號碼。 

答: /^09[0-9]{8}$/。 
開頭必須是(^09),加上8個任意數字([0-9]{8}),後面不能接其他字元($)。 


3. 驗證身分證。 

答: /^\c[12]\d{8}$/。 
開頭必須是1個任意英文字母(^\c),第二個字元必須是1或2([12]),
加上8個任意數字(\d{8}),後面不能接其他字元($)。

 

最後關頭!如何寫出文章一開始所說的密碼驗證條件呢?驗證要求──「密碼必須是包含大小寫字母和數字的組合,且長度在8-12之間。」這邊需要幾個進階的正規表達式的符號來達成:

.{8,12}: 表示字串長度須介於8~12之間。

?=.* : 字串的篩選條件。 比如(?=.*[\d])表示字串中必須要有數字出現。(?=.*[\U])則代表字串中不能有大寫英文字母出現。

答案在此:

(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,12}$

或是 (?=.*\u)(?=.*\l)(?=.*\d).{8,12}$

呼!經歷千辛萬苦,我們終於對正規表達式有一些基本概念了。

事實上,正規表達式的字元族還遠遠不止上述這些;然而族繁不及備載,建議讀者能將最常用的幾個語法背熟、餘下的待有需要時再查找即可。

正規表達式的標準格式

 

最後,來試著解決看看一些生活中的難題吧!

Apple公司懷疑員工小賈將公司機密文件以複本順便寄給了競爭公司Samsung和HTC。該如何以正規表達式阻止機密外洩呢?

RegExp比對字串:/@samsung\.com\.*\c*\c*|@htc\.com\.*\c*\c*/

 

小智和皮卡丘爭吵後、盛怒之下寫了一封充滿髒話的黑函寄給皮卡丘。皮卡丘該怎麼過濾小智的惡意來信呢?只知道小智很愛罵: fuck you, 和智障、白癡與笨蛋。因此信件可能會寫成 “Fuck U 智障”、”fuck you白癡”、”fUCK YoU 笨蛋”、”FuckU白癡”等形式。

RegExp比對字串:/fuck\s*(yo)?u\s*智障|白癡|笨蛋/i

 

。:.゚ヽ(*´∀`)ノ゚.:。

正規表達式入門容易,只要有國中英文的程度加上數小時的學習即可簡單上手;然而從學會到精通卻是一段陡峭的學習曲線。身為正規表達初心學習者,還有很多待發掘的知識尚未瞭解,也歡迎讀者日後一同討論精進、感受RegExp的魅力吧。