正規表示式快速入門(歸納版)

paraller發表於2017-10-23

轉載請註明出處 來源:paraller`s blog
原文排版連線: 點選跳轉

想系統的學習正規表示式,在網上找了很多教程,其中《55分鐘學會正規表示式》這個翻譯自外網的教程講的最系統詳細,學完整個正規表示式的耗時大概在兩個多小時的樣子。
為什麼寫這篇文章,因為我覺得如果不是這篇文章太生硬,廢話比較多,排版亂,有些地方存在錯誤的話,我覺得55分鐘就學完了。所以重新整理排版了一次。

學習的工具是 cat log | grep -E `pattern`

正規表示式的基礎語法

核心知識點
  • 正規表示式由只代表自身的字面值和代表特定含義的元字元組成。
  • 字面值(Literals): 意味著它們查詢的是自身
  • 元字元:代表一些模式匹配
句點 .
  • 一個.表示匹配任何單個字元。下面這個正規表示式c.t代表: 先找到c,接著找到任何單個字元,再找到t。
  • 將會找到cat,cot ,甚至字面值為c.t的字串,但是不包括ct或者coot。
反斜槓
  • 任何元字元如果用一個反斜杆進行轉義就會變成字面值。所以上述的正規表示式c.t ,能夠匹配文字 c.t
  • 文字ah,可以通過 a\h找到
字元類( Character classes )
  • 定義: 字元在方括號中的集合。表示:找到其中任意的字元
  • 正規表示式c[aeiou]t表示“找到c後跟一個母音字母,再找到t”。將會匹配到cat
  • 正規表示式[0123456789]表示找到一個數字
  • 正規表示式[a]a意義相同:“找到a”

一些轉義的例子:

  • [a] 表示文字 [a]
  • [[]ab] 表示匹配一個 [ 或者 ] 或者 a 或者 b
  • [\[]] 表示“匹配一個 或者 [ 或者 ]

note:

  • 一些字元在字元類內部扮演著元字元的角色,但在字元類外部則充當字面值。比如:連字元 -
  • 一些字元做著相反的事。比如: .表示“匹配任意字元”,但是[.]表示“匹配句點”
  • 一些字元在兩種情形都為元字元,但在各自情形裡代表不同的含義。
字元類區間(ranges)

你可以在字元類中使用連字元來表示一個字母或數字的區間_:

  • [b-f][bcdef] 一個意思
  • [A-Z][ABCDEFGHIJKLMNOPQRSTUVWXYZ] 都表示“匹配大寫字母”。
  • [1-9][123456789] 都表示“匹配一個非零數字”。

區間和單獨的字元,可能會共存於同一個字元類:

  • [0-9.,]表示“匹配 一個數字 或者 一個. 或者 一個,”。
  • [0-9a-fA-F]表示“匹配一個十六進位制數”。
  • [a-zA-Z0-9-]表示“匹配 一個字母數字字元 或 -”。

Note:

  • 你可以嘗試在區間內以非字母數字字元結束(比如abc[!-/]),但這在其它實現中的語法不一定對。即使語法正確,但在這個區間內很難看出包含了哪個字元。請不要這麼幹
  • 同樣的,區間端點的範圍應該一致。即使像[A-z]這種表示式在你選擇的實現中合法,但它做的可能會與你想法用出入。
  • 注意。 區間是字元的區間,不是數字的區間。正規表示式[1-31]表示 找到一個1或一個 2或一個3,不是找到一個從1到31的整數
字元類的否定(negation)

你可以通過在最開始的位置使^來排除一個字元類。

  • [^a]表示“匹配除了a的任意字元”。
  • [^a-zA-Z0-9]表示“找到一個非字母數字字元”。
  • [^abc]表示“找到一個^ 或者 a 或者 b 或者 c”。
  • [^^]表示“找到除了^的任意字元”
字元類補充
  • d 含義與[0-9]一致:“匹配一個數字”。
  • w 的含義與[0-9A-Za-z_]一致:“匹配一個單詞字元,( 字母或數字或下劃線或漢字)”。
  • s 表示“匹配任意空白字元(空格,tab)”。
  • s+ 可以表示回車和換行
  • D[^0-9]:“匹配任意非數字的字元”。
  • W[^0-9A-Za-z_]:“匹配任意非單詞字元(譯者注:匹配任意不是字母,數字,下劃線,漢字的字元)”。
  • S 表示“匹配任意不是空白符的字元”。
  • 表示製表符
  • [u4e00-u9fa5] 表示中文和英文
  • [^x00-xff] 匹配雙位元組字元,中文也是雙位元組的字元,不包括英文
乘法器(Multipliers)

可以在字面值或者字元類後跟著一個大括號來使用乘法器。

  • 正規表示式a{1}同a,表示“匹配一個a”。
  • d{3} 表示3個相連的數字。
  • a{0} 表示“匹配空字元”。
  • a{2} 代表文字 a{2}
  • 在字元類中大括號沒有特別的含義。[{}]代表“匹配一個{ 或者 }
  • 乘法器沒有記憶。[abc]{2}表示“匹配a或者b或者c,接著匹配a或者b或者c。這跟匹配aabbcc含義不同
乘法器區間
  • x{4,4} 跟x{4}一樣。
  • colou{0,1}r 表示“匹配colour或color。
  • a{3,5} 表示“匹配aaaaa或aaaa或aaa”。
  • a{1,} 表示“在一列中找到一個或多個a”。然而你的乘法器將會是貪婪的。在找到第一個a後,它將會盡可能匹配到更多的a。
  • .{0,} 表示“匹配任何情形”。不管你的輸入文字是什麼——甚至為空

注意:

  • 優先選擇更長的匹配,因為乘法器是貪婪的。如果你輸入的文字是I had an aaaaawful day,該正規表示式就會在aaaaawful中匹配到aaaaa。不會在第三個a後就停止匹配。
  • 乘法器在找到第一個文字的時候就會停止,如果你的輸入文字為I had an aaawful daaaaay,之後這個正規表示式會在第一次的匹配中於aaawful找到aaa。只有在你說“給我找到另一個匹配”的時候,它才會繼續搜尋然後在daaaaay中找到aaaaa
乘法器補充
  • ?代表的含義與{0,1}相同。比如說,colou?r表示“匹配colour或color”。
  • *等於{0,}。比如說,*表示“匹配一切”,跟上面提到的一樣。
  • +等於{1,}。比如說,w+表示至少匹配一個或以上的單詞。 [0-9]+代表至少匹配一個或以上數字
  • ?*+表示?*+
  • [?*+]表示找到一個?或者一個*或者一個+
惰性(Non-greed)

前面說到乘法器是貪婪的,可以通過新增?來消除貪婪特性

  • d{4,5}? 就會等於 d{4} ,在找到合適的文字之後就停下來
  • ".*?"表示“匹配一個雙引號,跟著一個儘可能少的字元,再跟著一個雙引號”。這實際上很有用
分支(Alternation)

你可以使用管道符號來實現匹配多種選擇:

  • cat|dog表示“匹配cat或dog”。
  • red|blue|red||blue以及|red|blue 都是同樣的意思,“匹配red或blue或空字串”。
  • a|b|c[abc]一樣。
  • cat|dog||表示“匹配cat或dog或管道符號”。
  • [cat|dog]表示“找到a或c或d或d或g或o或t或一個管道符號”。
組合(Grouping)
  • 在一週中找到一天,使用(Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day
  • (w*)ility等同於w*ility。都表示“找到以ility結尾的單詞”。為什麼第一種形式更有用,後面會看到…
  • ()表示“匹配一個(後,再匹配一個)”。
  • [()]表示“匹配一個( 或 一個)”。
單詞邊界(Word boundaries)

單詞邊界是一個單詞字元和非單詞字元之間的位置。記住,一個單詞字元是w,它是[0-9A-Za-z_],一個非單詞字元是W,也就是1

文字的開頭和結尾總是當作單詞邊界。輸入的文字it`s a cat有八個單詞邊界。如果我們在cat後追加一個空格,這裡就會有九個單詞邊界。

  • 正規表示式b表示“匹配一個單詞邊界”。
  • www表示“匹配一個三個字母的單詞”。
  • aa表示“找到a,跟著一個單詞邊界,接著找到a”。不管輸入文字是什麼,這個正規表示式永遠都不會成功找到一個匹配。
  • 單詞邊界不是字元。它們寬度為零.下面的正規表示式表示相同的含義:
行邊界(Line boundaries)

每一塊文字會分解成一個或多個行,用換行符分隔,像這樣:

  • 正規表示式^表示“匹配開始行”。
  • 正規表示式$表示“匹配結束行”。
  • ^$表示“匹配空行”。
  • ^.*$將會匹配整個文字,因為換行符是一個字元,所以.會匹配它。為了匹配單行,要使用惰性乘法器,^.?$ , ^.*?$
  • ^$表示“匹配尖符號後跟著一個美元符號”。
  • [$]表示“匹配一個美元符”。然而,[^]是非法單正規表示式。要記住的是尖符號在方括號中時有不同的特殊含義。把尖符號放在字元類中,這麼用[^]
  • 像單詞邊界一樣,行邊界也不是字元。它們寬度為零。

捕獲和替換

這裡就是正規表示式開始變得異常強大的地方。

捕獲組

你已經知道,括號是用來表示組。它們也可以用來捕獲子串。如果正規表示式是一個很小的電腦程式,這個捕獲組就是它的輸出(的一部分)。

正規表示式(w*)ility表示“找到一個以ility結束的單詞”。捕獲組1就是匹配了部分內容的w*

  • 文字包含單詞accessibility,捕獲組1就是accessib
  • 文字只包含ility,捕獲組1就是空字串。

你可以擁有多個捕獲組,它們甚至可以巢狀使用。捕獲組從左到右進行編號。只要計算左圓括號。

假設我們到正規表示式是(w+) had a ((w+) w+)。如果我們的輸入文字是I had a nice day,那麼:
捕獲組1是I
捕獲組2是nice day
捕獲組3是nice

在一些實現中,你可能可以訪問捕獲組0,即完整匹配:I had a nice day

是的,這確實意味著圓括號有些重複。從一個成功返回的匹配中捕獲組數量總是等於原來正規表示式中捕獲組的數量。記住這一點,因為它可以幫助你理解一些令人困惑的情形。
正規表示式((cat)|dog)表示“匹配cat或dog”。這裡總是存在兩組捕獲組。如果我們的輸入文字是dog,那麼捕獲組1是dog,捕獲組2是空字串,因為另一個選擇未被使用

正規表示式a(w)*表示“ 匹配一個以a開頭的單詞”。這裡總是隻有一個捕獲組(譯者注:除去捕獲組0):

  • 如果輸入文字是a,捕獲組1是空字串。
  • 如果輸入文字是ad,捕獲組1是d
  • 如果輸入文字是avocado,捕獲組1是vocado
替換

一旦你用了正規表示式來查詢字串,你可以指定另一個字串來替換它。第二個字串時替換表示式

你可以在你的替換表示式中引用捕獲組。這是你可以在替換表示式唯一能的特殊的事,它意味著你不必完全銷燬你剛剛發現的東西。

比方說,你嘗試去用ISO 8691格式的日期(YYYY-MM-DD)去替換美式日期(MM/DD/YY)。

  • 通過正規表示式(dd)/(dd)/(dd)開始。注意這裡有三個捕獲組:月,日和兩個數字表示的年。
  • 通過使用一個和一個捕獲組號來引用一個捕獲組。所以,你的替換表示式為203-1-2
  • 如果我們的輸入文字是03/04/05(表示 3月4號,2005年),那麼:

    • 捕獲組1是03;
    • 捕獲組2是04;
    • 捕獲組3是05;
  • 你可以在替換表示式中多次引用捕獲組。
  • 在替換表示式中的反斜杆必須進行轉義。舉個例子,你有一些在計算機程式的字面值中使用的文字。那就意味著你需要在普通文字中的每個雙引號或者反斜杆前放置一個反斜杆。
  • 正規表示式(["])中,捕獲組1是"或者
  • 替換表示式\1中,一個字面值反斜杆後跟著一個匹配的雙引號或者反斜杆。
後向引用(Back-references)
  • 你可以在同樣的表示式中引用同一個捕獲組。這稱為後向引用
  • 表示式([abc])1表示匹配aabbcc
結合正規表示式程式設計

過度反斜線綜合徵(Excessive backslash syndrome)

在一些程式語言中,如Java,對於含有正規表示式的字串沒有提供特別的支援。字串有自己的轉義規則,這些規則與正規表示式的轉義規則疊加,通常會導致反斜杆過多(overload)。比如(還是Java):

  • 為了匹配一個數字,正規表示式d在原始碼中變成String re = "d;"
  • 為了匹配一個雙引號字串,"[^"]*"變成String re = ""[^"]*"";。
  • 為了匹配一個反斜杆或者一個左方括號或者一個又方括號,正規表示式[\\[\]]變成String re = "[\\\[\]]";。
  • String re = "s";和String re = "[ ]";是一樣的。注意不同的轉義“優先順序”。

在其它程式語言裡,通過一個特殊標記來標識正規表示式,通常是正斜杆/。這裡有一些JavaScript例子:

  • 為了匹配一個數字,d變成var regExp = /d/;。
  • 匹配一個反斜杆或者一個左方括號或者一個右方括號,var regExp = /[\\[\]]/;。
  • var regExp = /s/;和var regExp = /[ ]/;一樣。
  • 當然,這意味著必須對正斜槓而不是雙引號進行轉義。匹配URL的前面部分:var regExp = /https?:///;。

基於這一點,我希望你明白為什麼我對你反覆提及反斜杆。

練習

題目如下:

  • 將文字中的所有 中文 替換成 中文
  • 編寫一個正規表示式匹配1到31(含)之間的整數。
  • 雙引號內所有不包含 " 的文字
  • 雙引號內所有文字

答案:

## Find:
(_)([^x00-xff]+)+(_)
## Replace
`**2**`
[1-9]|[12][0-9]|3[01]
cat log | grep -E `"[^"]{0,}"`
cat log | grep -E `".{0,}"`

練習草稿

I had a nice day

a,a
a!b
90
"i love you , mary!"
"i love you","hey"
food
z...z
a b     c
d       g
e
iec ieac
c[abc]at
2016-10-22 12:12:12
cat
c.t
c.t  [a]

其他

偏移量(Offsets)

在文字編輯器中,會在你游標所在處開始搜尋。這個編輯器會向前開始搜尋文字,然後停在第一個匹配的地方。下一次搜尋會在第一次完成搜尋的地方的右側開始。

當程式設計的時候,文字的_偏移量_必須的。這個偏移量會在程式碼中有明確的支援,或儲存在包含文字的物件中(如Perl),或包含正規表示式的物件中(如JavaScirpt)。(在Java裡,這是一個由正規表示式和複合物件的字串。)在任何情況下,預設值> 0,表示文字的開始。搜尋後,偏移量會自動更新,或者作為輸出的一部分返回。

無論什麼情況,通常很容易去使用迴圈來解決這個問題。

注意正規表示式匹配空字串是完全可能的。 你可以立馬實現的一個簡單的例子是a{0}在這種情況下,新的偏移量等於舊偏移量,從而導致死迴圈。

一些實現可能保護你避免發生這些情況,但要查下對應的文件。

動態正規表示式

動態地構造一個正規表示式字串時一定要小心。如果你使用的字串不是固定的,那麼它可能包含意想不到的元字元。這會導致語法錯誤。更糟糕的是,它可能產生一個語法正確,但行為不可預期的正規表示式。

有bug的Java程式碼:

String sep = System.getProperty("file.separator");
String[] directories = filePath.split(sep);

這個bug就是:String.split()認為sep是一個正規表示式。但是在Windows下,sep是由犯斜杆組成的字串.這不是一個語法正確的正規表示式。結果是:一個異常PatternSyntaxException。

任何一個優秀的程式語言都提供了一種機制,用以轉義在一個字串中出現的所有元字元。在Java中,你可以這麼做:

String sep = System.getProperty("file.separator");
String[] directories = filePath.split(Pattern.quote(sep));
郵件地址

不要使用正規表示式來驗證郵件地址。

首先,這很難保證正確無誤。電子郵件地址確實符合一個正規表示式,但是這個表示式長又複雜地讓人聯想到世界末日。任何縮略都會可能產生遺漏(false negatives)。(你知道嗎?電子郵件地址可以包含註釋!)

其次,即使所提供的電子郵件地址符合正規表示式,但也並不能證明它的存在。驗證電子郵件地址的唯一方法是傳送電子郵件給它。

標記

在正式的應用中,不要使用正規表示式來解析HTML或XML。解析HTML/XML是

不可能使用簡單的正則
一般來說很難
一個已解決了的問題。
不妨找一個已有的解析庫來為你搞定這些工作。

一些場景

替換
Host    segmentfault.com
Connection  keep-alive
Content-Length  55
Accept  */*
Origin  https://segmentfault.com
X-Requested-With    XMLHttpRequest
User-Agent  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
^(S+)(s+)(.{0,})
"1":"3",
"Host":"segmentfault.com",
"Connection":"keep-alive",
"Content-Length":"55",
"Accept":"*/*",
"Origin":"https://segmentfault.com",
"X-Requested-With":"XMLHttpRequest",
"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
文字塊 換行匹配
---
layout:     post
tags:
    - linux
---


---(
(.{0,}))*---

參考網站

55分鐘學會正規表示式(譯)


  1. 0-9A-Za-z_

相關文章