我理解的正規表示式

junsircoding發表於2022-01-25

一、前言

當我們想以特定的規則從字串中匹配出想要的子串時,正規表示式非常有用。而且,大部分程式語言都整合了正規表示式,我們學會它的語法規則後,可以直接在各種程式語言中運用。

正規表示式大致有以下應用:

  • 資料驗證(例如:檢查時間字串是否格式正確)
  • 資料抓取(例如:網頁抓取,以特定順序查詢包含特定單詞集的所有頁面)
  • 資料整理(例如:將原始資料轉換為另一種格式)
  • 字串解析(例如:捕獲所有 URL GET 引數,捕獲一組括號內的文字)
  • 字串替換、語法高亮、檔案重新命名、資料包嗅探

在實際場景中,被匹配的字串內容雜亂無章,但稍加分析,會發現其實質要麼是 ASCII 碼,要麼就是實現了 unicode 的各種編碼。unicode 相容 ASCII,而這些編碼有個特點,在特定的二進位制範圍中表示的字元有著相同的特性。比如 ASCII 中的 0-31 位表示一些控制字元,32-127 位則是一些可列印出來的字元,不一而足。

由此不難想見,我們可以針對不同類別字元的特性,制定一些規則。而正規表示式正是使用一些特殊的元字元構造出了自己的一套匹配規則,從而匹配出任意形式的資料。這些元字元本來沒有什麼特殊,但經過正規表示式引擎的編譯,便具有了特殊的功能,看似不知所云,但實際記錄了很多的資訊。

正則通常用 // 包裹,後面跟上若干個匹配標誌,比如:/a[bc]+/gm

一個正規表示式,大致遵循這樣的編寫思路:什麼位置,什麼樣的關鍵詞,有幾個,怎麼做,那麼上面的這個正則可以這樣解讀:「以全域性、多行的模式匹配出任意位置的這樣一個字串:a 後面跟著一個或多個 bc」。

下面按照這個思路分層次講解,其中給出的用例可以使用這個工具測試。

二、編寫思路

1. 什麼位置

1.1 關鍵詞位於行開頭(^)或行結尾($

^ 習慣上讀作 Cat$ 讀作 Dollar

正規表示式 含義說明
ok 匹配字串 ok
^ok 匹配字串 ok,而且 ok 前面是行開頭。
ok$ 匹配字串 ok,而且 ok 後面是行結尾。
^ok$ 匹配字串 ok,而且 ok 前面是行開頭,ok 後面是行結尾。

1.2 指定關鍵詞前後的字元內容 (?=)(?<=)

正規表示式 含義說明
(?<=r)ok ok 前面 r,結果不捕獲 r
ok(?=r) ok 後面 r,結果不捕獲 r

這裡可以將 = 替換為 !,表示否定。

正規表示式 含義說明
ok(?!r) ok 後面不是 r,但結果不捕獲 r
(?<!r)ok ok 前面不是 r,但結果不捕獲 r

2. 什麼樣的關鍵詞

2.1 單字元關鍵詞

2.1.1 限定單字元關鍵詞的備選值:用 [] 包裹

正規表示式 含義說明
[abc] 包含 abc 中的一個,等價於 a|b|c
[a-c] 同上。
[a-fA-F0-9] 單個十六進位制數字的字串,且不區分大小寫。
[0-9]% % 前面是一個數字。
[^a-zA-Z] 不在 a~Z 的範圍內。在這個例子中,^ 表示否定。

2.1.2 元字元轉義

看到這裡,我們知道 ^$<>()[]{}|/\.+*?:=! 這些元字元在正則中可能有著特殊的作用。那麼這裡會有個問題,正規表示式是用字串來描述匹配規則,進而去匹配字串。如果我們需要匹配這些元字元本身該怎麼辦呢?

這時可以在這些字元前新增轉義符號 \,使其還原為字元本身,不再具備限定含義。

正規表示式 含義說明
\$\d $ 後面跟著一個數字字元。

2.2 多字元關鍵詞

2.2.1 構造關鍵詞元組:用 () 包裹

之前的例子中,只能對單字元關鍵詞進行限定,如果要對多字元關鍵詞進行限定,可以用 () 括起來,構造成一個關鍵詞元組,看下面的例子。

正規表示式 含義說明
a(bc) a 後面跟著一個 bc
a(?:bc)* a 後面不能跟著一個 bc
a(?<foo>bc) a 後面跟著一個 bc,並將 bc 這個元組命名為 foo

當我們自己的程式語言從字串或文字資料中匹配資訊時,像這樣構造關鍵詞元組非常有用。這樣的匹配結果會返回一個列表,從而方便我們根據下標去取值。

而如果使用的是上表的第三種,給關鍵詞元組命名的方式,返回的結果則是字典。鍵的名稱是組名 foo,值是匹配結果列表。

2.2.2 引用關鍵詞元組:\1

正規表示式 含義說明
([abc])\1 引用第一個關鍵詞元組 ([abc]) 匹配的相同文字。
([abc])([de])\2\1 \2 引用第二個關鍵詞元組 ([de]) 匹配的相同文字。
(?<foo>[abc])\k<foo> \k<foo> 引用前面名為 foo 的關鍵詞元組 (?<foo>[abc]) 匹配的相同文字。結果與 ([abc])\1 相同。

2.3 特定型別關鍵詞

正規表示式 含義說明
. 匹配任意字元,當匹配標誌不是 s 時,不匹配 \n
\d 匹配數字
\D 匹配非數字
\b 表示它的一邊是單詞字元,一邊不是單詞字元。如 \bok\bok 前面是 \n空格 這樣的非單詞字元,後面也是非單詞字元。
\B 匹配 \b 所有不匹配的內容。如 \Bok\Bok 前面是單詞字元,後面也是單詞字元。
\w 匹配字母數字下劃線
\W 匹配非字母非數字非下劃線
\s 匹配空白字元,包括空格( 水平製表符(\t換行符(\n換頁符(\f回車符(\r垂直製表符(\v空字元(\0
\S 匹配非空白字元

類似 \d 這樣的限定方法,當 \ 後面的字母為大寫時,表示的限定規則恰好與小寫相反。

3. 有幾個

3.1 指定關鍵詞的數量:*+?{}

符號 含義 數學區間
* 匹配零個或多個 x ∈ Zx = 1x ∈ [0, +∞)
+ 匹配一個或多個 x ∈ Zx = 1x ∈ [1, +∞)
? 匹配零個或一個 x ∈ Zx = 0x = 1
{a,b} 匹配 a~b 個。 x ∈ Zx ∈ [a, b]

限定數量時,限定條件須寫在關鍵詞的後面,看下面的例子:

正規表示式 含義說明
abc* ab 後面跟著零個或多個 c
abc+ ab 後面跟著一個或多個 c
abc? ab 後面跟著零個或一個 c
abc{2} ab 後面跟著兩個 c
abc{2,} ab 後面跟著兩個或兩個以上c
abc{2,5} ab 後面跟著兩個到五個 c
a(bc)* ab 後面跟著零個或多個 bc
a(bc){2,5} ab 後面跟著兩個到五個 bc

這裡還可以加入操作符 |[],更靈活地指定數量。

正規表示式 含義說明
a(b|c) 匹配 a。要求 a 後面跟著 bc
a[bc] 匹配 abac。要求 a 後面跟著 bc

這兩者都要求 a 後面是備選詞 bc,區別在於,前者不捕獲備選詞,後者捕獲。

4. 怎麼做

4.1 一些匹配標誌碼

在執行匹配時,可以通過設定標誌碼來改變匹配的模式:

標誌碼 模式含義 備註
g 全域性模式 查詢所有的匹配項。
m 多行模式 使邊界字元 ^$ 匹配每一行的開頭和結尾,記住是多行,而不是整個字串的開頭和結尾。
i 忽略大小寫 將匹配設定為不區分大小寫,搜尋時不區分大小寫: Aa 沒有區別。
s 單行模式 在其他模式中,. 不匹配類似 \n 的控制字元,在這種模式下匹配
U 非貪婪模式 使用非貪婪模式匹配。

當未指定非貪婪模式時,上述的數量限定符號* + {},匹配時都是貪婪模式,即它們會盡可能地在提供的文字中匹配目標值。

例如,在 This is a <div> simple div </div> test 這個例子中,<.+> 匹配到 <div>simple div</div>

.+ 意為:一個或多個任意字元,這裡它匹配了 div>simple div</div,但我們的目的明顯是匹配出 div</div>

為了只捕獲 div,可以在 .+ 之後新增 ?,讓它用非貪婪模式執行:

正規表示式 含義說明
<.+?> 匹配一次或多次 <> 中的內容,根據需要擴充套件。

在上述例子中,會匹配出 <div<> 這樣的結果,為了更加嚴謹,應該儘量減少 . 的使用,可以優化為:

正規表示式 含義說明
<[^<>]+> 匹配一次或多次這樣的字元:<> 中不含 <>

各種程式語言中的正則使用方法

這裡列舉 Python、Java、JavaScript、Golang 的正則使用方式,使用時替換正規表示式即可。

Python

import re
pattern = re.compile(ur'^[0-9]*$')
str = u'12345'
print(pattern.search(str))

Java

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexMatches {
	
	public static void main(String args[]) {
		String str = "";
		String pattern = "^[0-9]*$";

		Pattern r = Pattern.compile(pattern);
		Matcher m = r.matcher(str);
		System.out.println(m.matches());
	}

}

JavaScript

var pattern = /^[0-9]*$/g,
	str = '12345';
console.log(pattern.test(str));``

Golang

package main

import (
	"fmt"
	"regexp"
)

func main() {
	str := ""
	matched, err := regexp.MatchString("^[0-9]*$", str)
	fmt.Println(matched, err)
}

參考文章

相關文章