Python筆記五之正規表示式

發表於2024-02-25

本文首發於公眾號:Hunter後端

原文連結:Python筆記五之正規表示式

這一篇筆記介紹在 Python 裡使用正規表示式。

正規表示式,Regular Expression,可用於在一個目標字串裡對於指定模式的字元進行查詢、替換、分割等操作。

比如,判斷某個字串裡是否都是數字,或者是否包含指定字串,又或者更直接的例子是判斷電話號碼或者郵箱是否合法等。

這一篇筆記裡,我們將先介紹一個正規表示式的函式,並以此來引入正規表示式的各種模式,並輔以各個例子進行介紹。

u1s1,正規表示式,我學了三四遍,學一遍忘一遍,忘一遍學一遍,只要隔一陣子不用就會忘,所以這一篇筆記力求將正規表示式的所有模式和用法都記錄下來,用作之後的查詢筆記。

以下筆記使用 Python 版本是 3.8。

以下是本篇筆記目錄:

  1. re.findall
  2. 表示式語法

    1. 匹配字串型別

      1. \b-匹配空字串,但只在單詞開始或結尾
      2. \B-匹配空字串,不能在開頭或結尾
      3. \d-匹配十進位制數字
      4. \D-匹配非十進位制字元
      5. \s-匹配空白字元
      6. \S-匹配非空白字元
      7. \w-匹配字元
      8. \W-匹配非單詞字元
    2. 匹配字串出現位置
      1) \A-只匹配字串開始
      2) \Z-只匹配字串結尾
      3) ^-匹配字串開頭
      4) $-匹配字串結尾
    3. 匹配字串數量
      1) *-匹配重複 0 到 n 次
      2) +-匹配重複 1 到 n 次
      3) ?-匹配重複 0 到 1 次
      4) {}-指定匹配重複次數

       a. {m}-只匹配重複 m 次
       b. {m,n}-匹配重複 m 到 n 次
    4. 匹配字串集合
      1) 字元可以單獨列出
      2) 可以表示字元範圍
      3) 特殊字元在集合裡只會匹配其原始字元含義
      4) 字元類 \s 或 \w 可以在集合裡使用
      5) 取反操作可以使用 ^
    5. 其他匹配型別

      1. |-表示式的或操作
      2. ()-匹配括號內的任意正規表示式
  3. 常用正則方法

    1. re.search
    2. re.match
    3. re.split
    4. re.findall
    5. re.finditer
    6. re.sub
    7. re.subn
    8. re.compile
    9. 其他引數
      1) re.I
      2) re.M
  4. re.Match 匹配物件

    1. Match.group()
    2. Match.__getitem__(g)
    3. Match.groups()
    4. Match.re
    5. Match.string
    6. Match.start() 和 Match.end()
    7. Match.span()

1、re.findall

使用正規表示式,首先要引入模組:

import re

這裡從 re.findall 開始介紹,findall 方法表示的是找到目標字串裡符合指定模式的全部資料。

比如我們有一個字串 abcdefg,想要從其中找到 de,就可以如下操作:

str_1 = "abcdefg"
target_str = "de"
print(re.findall(target_str, str_1))

返回的就是一個列表,列表元素是我們的目標字串 de

['de']

我們的 target_str 就是一個匹配模式,這裡是一個純字串,我們可以將其替換成其他的模式字串。

接下來我們將分類介紹正規表示式語法。

2、表示式語法

正規表示式有很多種,比如表示匹配數量的 +*?,還有表示匹配字串內容的 \s\w 等。

下面將從這些類別分別開始介紹:匹配字串型別、匹配字串出現位置、匹配字串數量、匹配字串集合等

1. 匹配字串型別

1) \b-匹配空字串,但只在單詞開始或結尾

\b 可以匹配空字串,但是隻在字串開始或者結尾的位置,這裡的空字串涵蓋了標籤符號,比如 , . ,。? 等,也包括換行製表符 \n \t 等,也包含 '',可以理解為字串的邊界部分。

所以 \b 的作用其實就可用於匹配特定字串的字首或者字尾,這裡的字首和字尾並不僅僅是指整個字串的字首和字尾,也包括字串內部被分割的字首和字尾。

比如對於下面這個字串:

i have a apple

我們想找到是否有 ha 開頭的單詞,可以如此操作:

str_1 = "i have a apple"
target_pattern = r"\bha"
print(re.findall(target_pattern, str_1))

字串如果是以下幾種情況,也可以匹配上:

str_1 = "i ,have a apple"
str_1 = "i \thave a apple"
str_1 = "i ha"

我們還可以使用 \b 來匹配特定的單詞,在英文中,單詞的出現是前後都有空格或者標點符號的,那麼我們就可以前後都加上 \b 來限定匹配是否出現過此單詞:

str_1 = "i have an apple"
str_1 = "i have an apple, "
str_1 = "i have an apple how are you"

target_pattern = r"\bapple\b"
print(re.findall(target_pattern, str_1))

2) \B-匹配空字串,不能在開頭或結尾

\B 是 \b 的取非操作,含義是匹配空字串,但是不能出現在開頭或者結尾,也就是說 \B 所在的位置必須有 1 至多個非空字串來替代:

str_1 = "i have an apple"
target_pattern = r"app\B"
print(re.findall(target_pattern, str_1))

3) \d-匹配十進位制數字

\d 用來匹配十進位制數字,也就是 0-9 這些,比如下面的操作:

str_1 = "asdas98123asa978d"
target_pattern = r"\d"
print(re.findall(target_pattern, str_1))
# ['9', '8', '1', '2', '3', '9', '7', '8']

可以看到返回的結果是分隔開的數字,如果想要他們在一起返回,我們可以使用 \d+ 來操作,+ 表示的匹配 1 到 n 次,這個後面再介紹。

4) \D-匹配非十進位制字元

\D 表示的是 \d 相反的操作,非十進位制字元,可以使用上面的示例進行測試。

5) \s-匹配空白字元

\s 匹配的空白字元不包括標點符號,常見的有換行符,製表符,回車符等跳脫字元,\n \t \r \f

str_1 = "asdas9812\v3a\rs,.\ta9\n78\fd"
target_pattern = r"\s"
print(re.findall(target_pattern, str_1))

6) \S-匹配非空白字元

\S 是 \s 取非操作,除了上面的換行符、製表符等字元外,包括標點符號皆可被匹配上

7) \w-匹配字元

\w 不匹配換行符、製表符等跳脫字元,不匹配中英文常見標點符號,比如 , . ; " 等,但是可以匹配中英文字元、數字和下劃線,比如下面的示例:

str_1 = "asd—— _a你‘'好,s9。8?12\v3a\rs,.\ta9\n78\fd"
target_pattern = r"\w"
print(re.findall(target_pattern, str_1))
# ['a', 's', 'd', '_', 'a', '你', '好', 's', '9', '8', '1', '2', '3', 'a', 's', 'a', '9', '7', '8', 'd']

8) \W-匹配非單詞字元

\W 是 \w 的取反操作。

2. 匹配字串出現位置

前面介紹的匹配字串的型別,這裡介紹匹配出現的位置,比如開頭或者結尾。

1) \A-只匹配字串開始

\A 只匹配字串的開始,也就是我們所說的字串字首:

str_1 = "asd—— _a你‘'好,\ns9。8?12\v3a\rs,.\ta9\n78\fd"
target_pattern = r"\Aasd"
print(re.findall(target_pattern, str_1))

與字串的 startswith() 函式在匹配功能上是一樣的。

2) \Z-只匹配字串結尾

\Z 只匹配字串結尾部分,也就是所說的字串字尾:

str_1 = "asd—— _a你‘'好,\ns9。8?12\v3a\rs,.\ta9\n78d"
target_pattern = r"d\Z"
print(re.findall(target_pattern, str_1))

3) ^-匹配字串開頭

^ 也是隻匹配字串的開頭,但是與 \A 不同的是,使用 re.M 模式下,也可以匹配換行符後的開頭,比如下面的示例,就可以返回兩個結果:

str_1 = "asd—— _\na你‘'好,\ns9。8?12\v3a\rs,.\ta9\n78d"
target_pattern = r"^a"
print(re.findall(target_pattern, str_1, re.M))
# ['a', 'a']

如果去掉 re.M,則會退化成 \A 的功能

4) $-匹配字串結尾

$ 也是隻匹配字串的結尾,但是在 re.M 模式下,也可以匹配換行符後新一行的結尾,比如下面的示例,可以返回兩個結果:

str_1 = "asd—— _\na你‘'好,\ns9。8?12\v3a\rs,.\ta9d\n78d"
target_pattern = r"d$"
print(re.findall(target_pattern, str_1, re.M))

同理,如果去掉 re.M,則會退化成 \Z 的功能。

3. 匹配字串數量

除了匹配指定模式的內容,字元位置,我們還可以匹配指定模式的數量,比如想只要滿足一個即可返回,或者儘可能多的將滿足的字元返回等。

接下來一一介紹如何匹配字串數量。

1) *-匹配重複 0 到 n 次

* 表示匹配的數量可以重複從 0 到 n 次,這個 n 為任意次,儘量多的匹配字串。

比如我們想匹配 a 以及 a 後面可以加上任意個 b 字元,比如希望 aababbabbbb 等都可以被匹配上。

那麼就可以使用下面的操作:

str_1 = "axxxxabbxxxxabbbb"
target_pattern = r"ab*"
print(re.findall(target_pattern, str_1))
# ['a', 'abb', 'abbbb']

2) +-匹配重複 1 到 n 次

+ 表示匹配的數量可以重複從 1 到 n 次,提前條件是匹配模式必須出現一個。

還是上面的例子,我們希望可以匹配上 ababbb,以及無限多個 b,但是不可匹配 a,可以如下操作:

str_1 = "axxxxabbxxxxabbbb"
target_pattern = r"ab+"
print(re.findall(target_pattern, str_1))
# ['abb', 'abbbb']

3) ?-匹配重複 0 到 1 次

? 表示匹配的數量只能出現 0 到 1 次。

比如對於一個字串,我們想匹配 apple 這個單詞,但是我們也想使 apple 的複數形式 apples 也能被匹配上,所以我們這裡的 s 希望它出現的次數是 0 到 1 次

str_1 = "i have an apple, he has two apples, she has three apples"
target_pattern = r"apples?"
print(re.findall(target_pattern, str_1))
# ['apple', 'apples', 'apples']

4) {}-指定匹配重複次數

使用花括號可以指定重複的次數,可以是一個固定的值,也可以是一個範圍,下面分別介紹一下。

a. {m}-只匹配重複 m 次

{m} 表示匹配重複次數為 m,比如我們想要匹配 abbb,也就是 a 字元後重復出現三個 b,可以如下操作:

str_1 = "abbxxxxabbbbaxxxabbb"
target_pattern = r"ab{3}"
print(re.findall(target_pattern, str_1))
b. {m,n}-匹配重複 m 到 n 次

{m,n} 表示匹配重複次數為 m 到 n 次,比如我們想要 a 後面跟著 3,4,5個 b 都可以接受,可以如下操作:

str_1 = "abbbxxxxabbbbbbaxxxabbbb"
target_pattern = r"ab{3,5}"
print(re.findall(target_pattern, str_1))
# ['abbb', 'abbbbb', 'abbbb']

4. 匹配字串集合

我們可以使用中括號 [] 來限定字串或者匹配模式的集合,也就是說我們可以將我們想要匹配的字串或者型別都加到 [] 裡,滿足條件的都可以被匹配返回。

1) 字元可以單獨列出

如果我們想匹配某些單個字元,可以單獨列出來操作,比如 a, t, w, q,可以使用 [atwq],以下是示例:

str_1 = "asdqweasdaterq"
target_pattern = r"[atwq]"
print(re.findall(target_pattern, str_1))
# ['a', 'q', 'w', 'a', 'a', 't', 'q']

2) 可以表示字元範圍

我們可以使用 - 來表示字元的範圍進行匹配,比如 a-z,或者數字型別的 0-9,比如下面的操作:

str_1 = "asdqweasdaterq"
target_pattern = r"[a-j]"
print(re.findall(target_pattern, str_1))
# ['a', 'd', 'e', 'a', 'd', 'a', 'e']
str_1 = "asd136q78w9ea95sd6ater"
target_pattern = r"[4-9]"
print(re.findall(target_pattern, str_1))
# ['6', '7', '8', '9', '9', '5', '6']

注意: 在這裡的連線符 - 是表示範圍的,如果只是想匹配 - 字元,需要使用 \ 進行轉義,或者將 - 放在首位或者末位:

str_1 = "asd136q-78w-9e-a95sd6zater"
target_pattern = r"[a\-z]"
print(re.findall(target_pattern, str_1))
# ['a', '-', '-', '-', 'a', 'z', 'a']

上面的這個操作表示的是希望匹配上 - az 三個字元。

3) 特殊字元在集合裡只會匹配其原始字元含義

特殊字元,比如前面表示數量的 * + 等字元在中括號裡就匹配的是對應的星號和加號:

str_1 = "asdas*adas+asds(das)dasd"
target_pattern = r"[*+()]"
print(re.findall(target_pattern, str_1))
# ['*', '+', '(', ')']

4) 字元類 \s 或 \w 可以在集合裡使用

比如下面的操作,可以 \W 和 0-9 之間的字元:

str_1 = "asdas*adas+asds(90123das)dasd"
target_pattern = r"[\W0-9]"
print(re.findall(target_pattern, str_1))
# ['*', '+', '(', '9', '0', '1', '2', '3', ')']

5) 取反操作可以使用 ^

如果要取反,意思是集合裡的匹配模式都不匹配,比如我們想匹配字串裡的非數字,可以如下操作:

str_1 = "asdas*adas+asds(90123das)dasd"
target_pattern = r"[^\d]"
print(re.findall(target_pattern, str_1))
# ['a', 's', 'd', 'a', 's', '*', 'a', 'd', 'a', 's', 'a', 's', 'd', 's', '(', 'd', 'a', 's', ')', 'd', 'a', 's', 'd']

5. 其他匹配型別

1) |-表示式的或操作

比如我們有兩個表示式 A 和 B,只要有一個符合條件即可,即匹配模式的或操作:

re.findall(r"\d+|[a-z]+", "asdas234asd2341")
# ['asdas', '234', 'asd', '2341']

2) ()-匹配括號內的任意正規表示式

小括號表示分組,可以將匹配模式分組放到多個小括號內進行匹配,匹配後的結果也會以元組的形式分組返回。

比如我們想匹配 英文字母+數字 的形式,並且以括號的形式將其分隔開,那麼返回的匹配結果也會以元組的形式將其返回:

str_1 = "asdas9872"
target_pattern = r"([a-z]+)(\d+)"
result = re.findall(target_pattern, str_1)print(result)
# [('asdas', '9872')]

如果是匹配上了多個結果,那麼多個結果會以列表的形式返回,元素也是匹配的結果以元組的形式返回:

str_1 = "asdas9872asdasklqw8267"
target_pattern = r"([a-z]+)(\d+)"
result = re.findall(target_pattern, str_1)print(result)
# [('asdas', '9872'), ('asdasklqw', '8267')]

3、常用正則方法

前面介紹了 re.findall() 方法,返回的是一個列表,元素是所有匹配上的結果,接下來介紹一些其他常用的正則方法。

1. re.search

re.search() 方法的作用是掃描整個目標字串,找到匹配上的第一個位置,然後返回一個匹配物件,如果沒有滿足要求的資料,則返回 None。

這裡要注意三點,一點是掃描整個字串,直到找到匹配的物件,另一點是找到一個符合條件的字串以後就停止掃描,即便後面還有符合條件的,三是返回一個匹配物件,關於匹配物件下面再介紹。

比如下面的示例是匹配 英文字母+數字:

str_1 = "..()-+/?asdas9872asdasklqw8267"
target_pattern = r"[a-z]+\d+"
result = re.search(target_pattern, str_1)
# <re.Match object; span=(8, 17), match='asdas9872'>

可以看到這裡,其實有多個符合匹配模式的資料,如果這裡使用 re.findall() 會返回多個值,但這裡返回的只是字串裡第一個符合條件的資料。

至於如何獲取返回的這個 re.Match 物件詳情資料,見第四節 re.Match 匹配物件,建議先閱讀該章節再往後讀。

2. re.match

re.match() 方法也是用於匹配指定模式,效果上與 re.search() 無異,唯一不同的一點是 match() 方法只能從字串的頭部開始匹配,返回的也是一個 re.Match 物件。

比如下面的操作,第一種形式匹配不到資料,返回 None,第二種就可以返回 re.Match 物件:

str_1 = "..()-+/?asdas9872asdasklqw8267"
target_pattern = r"[a-z]+\d+"
result = re.match(target_pattern, str_1)
# None

str_1 = "asdas9872asdasklqw8267"
target_pattern = r"[a-z]+\d+"
result = re.match(target_pattern, str_1)
# <re.Match object; span=(0, 9), match='asdas9872'>

3. re.split

根據匹配模式將指定字串進行分割,在效果上相當於 string.split() 的增強版。

因為 string.split() 不能使用正則物件來對字串進行切割,而 re.split() 可以實現。

比如我們想要根據 1,2,3 對指定字串進行切割,就可以用到 | 這個操作:

re.split(r"1|2|3", "as2da1s3asdsa")
# ['as', 'da', 's', 'asdsa']

我們也可以使用其他正則物件,比如我們想要根據字串中的標點符號,空格等對字串進行切割:

re.split(r"\W+", "i have an apple, you have two apples!")
# ['i', 'have', 'an', 'apple', 'you', 'have', 'two', 'apples', '']

re.split() 還可以接受 maxsplit 引數,表示最多切割的次數:

re.split(r"\W+", "i have an apple, you have two apples!", maxsplit=2)
# ['i', 'have', 'an apple, you have two apples!']

4. re.findall

re.findall() 前面有過介紹,根據匹配模式獲取所有的匹配字元,結果以列表形式返回,元素為匹配的字串:

re.findall(r"\d+", "asd12asxda45asd456sd23")
# ['12', '45', '456', '23']

5. re.finditer

re.finditer() 函式與 re.findall() 函式效果類似,都是找到目標字串裡全部滿足條件的字串,但是不同的是 finditer() 返回的一個迭代器,迭代器儲存的是匹配物件,也就是 re.Match 物件:

for item in re.finditer(r"\d+", "asd12asxda45asd456sd23"):
    print(item)
# <re.Match object; span=(3, 5), match='12'>
# <re.Match object; span=(10, 12), match='45'>
# <re.Match object; span=(15, 18), match='456'>
# <re.Match object; span=(20, 22), match='23'>

6. re.sub

替換函式,將字串裡的指定模式替換成目標字串,然後返回:

re.sub(r"\d+", "***", "asd12asxda45asd456sd23")
# 'asd***asxda***asd***sd***'

7. re.subn

re.subn() 與 re.sub() 函式作用一致,都是替換目標字串,但是返回的是一個元組,分別是替換後的字串和替換的次數:

re.subn(r"\d+", "***", "asd12asxda45asd456sd23")
# ('asd***asxda***asd***sd***', 4)

8. re.compile

re.compile() 函式將正規表示式編譯為一個正規表示式物件,然後可以呼叫前面介紹過的這些正則函式,比如 re.search(),re.match(),re.findall() 等。

pattern = re.compile(r"\d+")
pattern.match("3432asdas334asd34asd")
# <re.Match object; span=(0, 4), match='3432'>

re.complie() 這個函式的操作可以用於永福匹配模式,使程式更加高效。

9. 其他引數

接下來介紹一下正則模組在匹配情況下的一些其他引數。

1) re.I

忽略大小寫。

對於目標字串,如果存在字母大小寫的情況,我們可以對原始字串統一進行大寫或者小寫的操作,然後進行匹配,以忽略大小寫的影響,也可以使用 re.I 引數:

re.search(r"[a-z]+", "123SDAFSDsa234ASDd34".lower())
# <re.Match object; span=(3, 11), match='sdafsdsa'>

re.search(r"[a-z]+", "123SDAFSDsa234ASDd34", re.I)
# <re.Match object; span=(3, 11), match='SDAFSDsa'>

2) re.M

多行匹配模式。

前面介紹 ^$ 這兩個符號的匹配的時候介紹過,如果加上 re.M 引數,表示的是可以匹配字串內部有換行符的開頭和結尾的資料:

str_1 = "asd—— _\na你‘'好,\ns9。8?12\v3a\rs,.\ta9\n78d"
target_pattern = r"^a"
print(re.findall(target_pattern, str_1, re.M))
# ['a', 'a']

4、re.Match 匹配物件

對於一些函式,比如前面介紹的 re.search(),返回的就是一個 re.Match 物件,還是以上面的示例為例,如何獲取 re.Match 中的資料呢,下面開始介紹。

1. Match.group()

group() 函式直接呼叫,或者新增引數 0,返回的是匹配的字串資料:

print(result.group())
# asdas9872

print(result.group(0))
# asdas9872

而如果我們匹配的模式使用了小括號 () 進行了分組,那麼則可以對 group() 函式新增其他 index 表示匹配的分組的結果,比如下面的操作:

str_1 = "..()-+/?asdas9872asdasklqw8267"
target_pattern = r"([a-z]+)(\d+)"
result1 = re.search(target_pattern, str_1)

print(result1.group(0))
# asdas9872

print(result1.group(1))
# asdas

print(result1.group(2))
# 9872

2. Match.__getitem__(g)

re.Match 物件實現了 getitem 這個魔術方法,所以我們可以透過索引的方式對來訪問匹配的結果,其效果和 group(index) 是一致的:

print(result1[0])
print(result1.group(0))

print(result1[2])
print(result1.group(2))

3. Match.groups()

groups() 函式返回的是進行了分組匹配的結果,以元組的形式返回。

比如這裡分組的 result1 物件:

print(result1.groups())
# ('asdas', '9872')

而 result 這個 Match 物件匹配模式裡並沒有使用小括號進行分組,所以返回的結果是空元組:

print(result.groups())
# ()

4. Match.re

返回的是生成這個 Match 物件的正則物件,也就是我們輸入的 target_pattern:

print(result.re)
# re.compile('[a-z]+\\d+')

5. Match.string

返回的是傳遞到生成這個 Match 物件的原始字串:

print(result.string)
# ..()-+/?asdas9872asdasklqw8267

6. Match.start() 和 Match.end()

這兩個函式表示的是匹配上的字串的開始位置和結束位置:

print(result.start(), result.end())
# 8 17

我們對原始字串進行起始位置的擷取就是 Match.group() 的結果:

print(str_1[result.start(): result.end()])
# asdas9872

print(result.group())
# asdas9872

而如果我們前面對匹配模式進行了分組的操作,也就是使用了小括號,比如返回的 result1,我們可以想 start() 和 end() 函式新增索引引數分別表示這兩個分組結果開始和結束的下標位置:

print(result1.groups())
# ('asdas', '9872')

print(str_1[result1.start(1): result1.end(1)])
# asdas

print(str_1[result1.start(2): result1.end(2)])
# 9872

7. Match.span()

返回的是匹配結果的開始和結束位置,以元組的形式返回,其實就是一次性返回 Match.start() 和 Match.end() 的結果。

span() 函式也可以接受分組的引數返回分組的起始位置。

如果想獲取更多後端相關文章,可掃碼關注閱讀:

相關文章