正規表示式(regular expression),用於字串匹配、替換,是字串的重要處理方式。其中正則是有規則,有規律的意思。正規表示式有三要素,本文主要介紹 Python 中的這三要素:

  • 要處理的字元(串),需要注意轉義序列
  • 用於處理的程式,即正則引擎:Python 自帶re模組
  • 處理規則:正規表示式語法

1 Python 字串

1.1 轉義序列

Python 字串可以包含兩部分:

  • 普通字元組成的字串
  • 轉義序列組成的字串

轉義序列(Escape Squence)跳脫字元(Escape Character)和後續字元組成,跳脫字元放在字元序列前面時,它將對它後續的幾個字元進行替代並解釋。跳脫字元是元字元(Meta Character)的一種特殊情況。Python 中跳脫字元是反斜槓(\)。Python 中所有轉義序列如下表:

轉義序列意義
\ (行尾)續行符
\aalert
\bbackspace
\nnew line
\rcarriage return
\ttable
\vvertical table
\'single qoute
\"double qoute
\\back slash
\xyyhex
\0zzoct, start with number 0
\000null

通常也把轉移序列成為跳脫字元。跳脫字元擁有兩個含義:

  • 轉義序列標誌,如 Python 中的反斜槓
  • 轉義序列,由轉義序列標誌符和後續字元組成,整體的有特殊意義

後面的描述中,跳脫字元特指轉義序列。

處理引號也可以用跳脫字元:Python 並不明確區分單雙引號,只要要匹配使用就行;如要表示一個字面意義上的引號,需用另外一種引號來巢狀,或者跳脫字元:

"'"  # use another kind of qoute
"\""  # use escape character

輸入這些符號時需要多次敲鍵盤,但 Python 都把它們當成長度為1的字元來處理:

In [1]: len("\017")
Out[1]: 1

In [2]: len("\n")
Out[2]: 1

In [3]: len("\\")
Out[3]: 1

1.2 Python 中轉義序列標誌符

反斜槓是 Python 中轉義序列標誌符,它有如下特性需要注意:

  1. 表示一個字面意義的\需要轉義序列標誌符\
  2. Python 中顯示一個字面意義上\則是\\
In [1]: len("")
Out[1]: 0

In [2]: len("\")
  File "<ipython-input-2-553b6950c0a1>", line 1
    len("\")
            ^
SyntaxError: EOL while scanning string literal

In [3]: len("\\")
Out[3]: 1

In [4]: "\\"[0]
Out[4]: "\\"

轉義序列標誌只有在特定的字元組合前才有效果,如果後面的字元組合不能組合為轉義,則只會被當成簡單的字元。 比如\n是特殊含義,而\m沒有,\m只會被看成是一個字面上的\m組成的長度為 2 的字串。

1.3 原始字串

在某些特定情況下希望字元是所見即所得。一個\n組合在一起就是表示兩個字元,而不是換行符。此時可以用 Python 的原始字串(Raw String),在要處理的字元前面加上r

In [1]: len(r"\n")
Out[1]: 2

In [2]: len(r"\")
  File "<ipython-input-6-aa225c032aa6>", line 1
    len(r"\")
             ^
SyntaxError: EOL while scanning string literal

In [3]: len(r"\\")
Out[3]: 2

原始字串並非萬能:單獨的反斜槓\沒法用原始字串表示,2 中把最後的引號給轉義掉,3 是一個原始字串,但表示兩個反斜槓。無法用原始字串表是轉義序列

2 Python 正則引擎

用於匹配的程式叫正規表示式引擎,Python 自帶re的模組。當然還有些第三方庫正則引擎,但並不是主要介紹的物件。需要提前匯入re模組:

In [1]: import re

同時,關於匹配的詳細細節也不是本文介紹的重點。本文主要介紹正則引擎常用函式的使用,常見函式有兩種用法:

re.func(REG, string, flag)  # call the func with re directly
re.compile(REG, flag).func(string)  # call the compiled object

通常認為先 compile(compilere模組的函式),後運算可以提升效率。但實際上並不特別關注效率問題:

  1. 某些高效的正規表示式可能難以理解
  2. Python 本身執行就想對慢一些
  3. 本文主要介紹使用,並不過分關注原理

函式參數意義如下:

  1. REG(regular expression)就是正規表示式,第三部分會詳細介紹。
  2. string 是要匹配的字串,Python 字串。
  3. flag 是正則運算時候的參數,會在介紹具體函式時候介紹。

常見的func是:

  • match 匹配字串的開始,返回一個re.Match(匹配上)或None(沒匹配上)
  • search 找到一個匹配就返回,即使有多個也只返回第一個,返回一個re.Match(匹配上)或者None(沒匹配上)
  • findall 找到所有匹配,返回一個列表(第四部分詳細介紹這個列表)
  • finditer 找到所有匹配,返回一個迭代器
  • split 對字串按照一定規則切分,返回一個列表
  • sub 對字串進行替換,返回替換後的字串
  • subn 對字串進行替換,返回 tuple,前面是替換後的字元,後面是總共替換次數

所謂re.Match,有如下方法:

re.match.span()  # span, tuple
re.match.start()  # start index
re.match.end()  # end index
re.match.group()  # match group
re.match.group_dict()  # match group

其中group表示匹配的組, 會在後面捕獲組一節中詳細介紹。這裡可以通過一個例子就re.Match進行簡單說明。由於使用例子要用到正規表示式,這裡先給出正規表示式的一條規則:大部分字元都匹配自身

In [1]:x = re.search("abc", 'zyxabcdre')

In [2]: x
Out[2]: <re.Match object; span=(3, 6), match='abc'>

In [3]: x.start()
Out[3]: 3

In [4]: x.end()
Out[4]: 6

In [5]: x.span()
Out[5]: (3, 6)

In [6]: x.group()
Out[6]: 'abc'

In [7]: x.group(0)
Out[7]: 'abc'

In [8]: x.groupdict()
Out[8]: {}

In [9]: re.match("abc", 'zyxabcdre')

match匹配的是開始,如果開始沒有匹配到,就返回None;而search可以匹配字串的中間部分,匹配到一個就返回。

flag可以給匹配新增更多選項,常見的flag如下:

flag作用
re.IGNORECASE忽略大小寫
re.DOTALL點可以匹配換行符
re.MULTILINE多行模式

3 正規表示式語法

這也是通常說的正規表示式,正規表示式用 Python 字串實現,它們屬於 Python 字串,正則引擎處理正規表示式時候有額外的語法

從第二部分知道,大部分字元都匹配它們自己,少數字符可以匹配其它字串,它們需要被重點關注:

.
[]
^
$
-
*
+
?
{}
\
()
|

3.1 匹配單個字元

3.1.1 大部分字元匹配自己

匹配大小寫敏感,可新增re.IGNORECASE通配大小寫

In [1]: re.search('a', 'ABCD')

In [2]: re.search('a', 'ABCD', re.IGNORECASE)
Out[2]: <re.Match object; span=(0, 1),  match='A'>

轉義序列也包含其中,但不包含反斜槓

In [1]: re.search('\n', 'ab\ncd')
Out[1]: <re.Match object; span=(2, 3),  match='\n'>

對反斜槓的特殊處理

確實存在匹配單個字面意義上的反斜槓的情況,如匹配如下LaTeXLaTeX

\section{back slash}

若按普通轉義序列一樣處理反斜槓,即用\\作為正規表示式,則會報錯:

In [1]: re.search("\\", "\\x")
---------------------------------------------------------------------------
error                                     Traceback (most recent call last)
<ipython-input-2-ace9affb99aa> in <module>
----> 1 re.search("\\", "\\x")

/usr/local/lib/python3.10/re.py in search(pattern, string, flags)
    199     """Scan through string looking for a match to the pattern, returning
    200     a Match object, or None if no match was found."""
--> 201     return _compile(pattern, flags).search(string)
    202
    203 def sub(pattern, repl, string, count=0, flags=0):

/usr/local/lib/python3.10/re.py in _compile(pattern, flags)
    302     if not sre_compile.isstring(pattern):
    303         raise TypeError("first argument must be string or compiled pattern")
--> 304     p = sre_compile.compile(pattern, flags)
    305     if not (flags & DEBUG):
    306         if len(_cache) >= _MAXCACHE:

/usr/local/lib/python3.10/sre_compile.py in compile(p, flags)
    762     if isstring(p):
    763         pattern = p
--> 764         p = sre_parse.parse(p, flags)
    765     else:
    766         pattern = None

/usr/local/lib/python3.10/sre_parse.py in parse(str, flags, state)
    940     # parse 're' pattern into list of (opcode, argument) tuples
    941
--> 942     source = Tokenizer(str)
    943
    944     if state is None:

/usr/local/lib/python3.10/sre_parse.py in __init__(self, string)
    230         self.index = 0
    231         self.next = None
--> 232         self.__next()
    233     def __next(self):
    234         index = self.index

/usr/local/lib/python3.10/sre_parse.py in __next(self)
    243                 char += self.decoded_string[index]
    244             except IndexError:
--> 245                 raise error("bad escape (end of pattern)",
    246                             self.string, len(self.string) - 1) from None
    247         self.index = index + 1

error: bad escape (end of pattern) at position 0

原因如下:正則引擎首先對正規表示式處理,把\\(兩個連續反斜槓)翻譯為\(一個反斜槓),此時正規表示式變成\"(一個反斜槓結合後面的引號),正規表示式缺少字串結束標誌符—引號",報錯。(正則引擎對正規表示式還會有其它處理,後面會介紹。)正確姿勢是用四個反斜槓

In [1]: re.search("\\\\", "\\subgraph")
Out[1]: <re.Match object; span=(0, 1), match='\\'>

好傢伙!為匹配一個字面意義上的反斜槓,需要在正規表示式中用四個連續反斜槓。這也是所謂的反斜槓災難。為了避免複雜的寫法,正規表示式中也有原始字串(Raw String),作用是:正則引擎處理正規表示式時候,不把連續兩個反斜槓\\翻譯為一個反斜槓\

In [1]: re.search(r"\\", "\\x")
Out[1]: <re.Match object; span=(0, 1), match='\\'>

為了讓正規表示式和 Python 字串更加接近,一條推薦的規則是:不管多麼簡單的規則,都採用原始字串的方式進行匹配。

3.1.2 點.匹配所有字元

.匹配所有字元,這個和萬用字元中星號*表示所有字元一樣。通常點並不能匹配換行符,加入re.DOTALL就可以了。

需要注意的是這裡說的是單個字元。

3.1.3 []表示集合

中括號([])表示範圍,匹配中括號中任意一個元素。中間是或的關係,元素之間不需要隔開。

In [1]: re.search(r'[1234]','843')
Out[1]: <re.Match object; span=(1, 2), match='4'>

中括號中的元素可以重複,但是並沒有特殊意義:

In [1]: re.search(r'[ddxs]','d')
Out[1]: <re.Match object; span=(0, 1), match='d'>

中括號中大部分字元(包含轉移序列)都只表示原本意思,除了特殊字元:點(.)可以匹配任意字元,但在中括號([])中卻只匹配字面上的點(.):

In [1]: re.search(r'.', ',')
Out[1]: <re.Match object; span=(0, 1), match=','>

In [2]: re.search(r'[.]', ',')

In [3]: re.search(r'[\n]', 'x\n')
Out[3]: <re.Match object; span=(1, 2), match='\n'>

對某些需要轉義表示的字元,也可以放到中括號中來避免使用轉義符號,比如反斜槓:

In [1]: re.search(r"[\\]", '\\x')
Out[1]: <re.Match object; span=(0, 1), match='\\'>

需要特殊關注的字元^-[,規則如下:

  1. ^符號表示取反,但是^一定要新增在最開始
In [1]: re.search(r'[^1234]','1843')
Out[1]: <re.Match object; span=(1, 2), match='8'>

如果^在中間,則只會被當成是一個普通字元

In [1]: re.search(r'[7^1234]','1843')
Out[1]: <re.Match object; span=(2, 3), match='1'>
  1. 中括號中使用-表示範圍,實現對錶達式的精簡
r"[0-9]" # equal  r"[0123456789]" in regular expression
r"[a-z]" # equal r"[abcdefghijklmnopqrstuvwxyz]"in regular expression

需注意若-前後不是範圍,則只會匹配普通的-符號:

In [1]: re.search("[-a]", "b-")
Out[1]: <re.Match object; span=(1, 2), match='-'>
  1. 中括號開始標誌[ 如果非要表示字面意義上左中括號([),則要使用\[
In [1]: re.search(r"[]]", ']')
Out[1]: <re.Match object; span=(0, 1), match=']'>

In [2]: re.search(r"[[]", ']')
<ipython-input-44-863737df3ef5>:1: FutureWarning: Possible nested set at position 1
  re.search(r"[[]", ']')

當要處理(字面意義)中括號時候,最好通通用轉義序列

3.1.4 正規表示式轉義序列

它們雖然是反斜槓(\)和某個字元的組合,但不是Python 轉義序列:只有正則引擎會對它們特殊處理,而在其它情況下只會被看成是普通字串。它們也可看成對中括號([])的擴充。如下正規表示式含義比較清楚:

r1 = "[0-9]"
r2 = "[a-zA-Z_]"

r1表示一位十進位制數,r2表示任意字母數字或下劃線。可以用正規表示式轉義序列進一步簡寫:

r1 = "\d"
r2 = "\w"

更多的正規表示式轉義序列如下:

簡寫方式意義
\ddigital,一位十進位制數字符
\D一位非十進位制數字符
\wword,大小寫都是可以的
\W非字母
\sspace,空格
\S非空格
  • 一個規律是大寫為小寫取反

正規表示式中的轉義序列並不會被 Python 字元特殊對待。此時反斜槓只會被看成字面意義上的反斜槓。

In [1]: len("\d")
Out[1]: 2

前面介紹正規表示式原始字串時候提到,正則引擎會對首先正規表示式分析,所謂的正規表示式跳脫字元就是被正則引擎進行處理。

3.2 字串匹配

實際中更有用的是字串的匹配。如同字串對字元的擴充方式,正規表示式並列排布,則匹配並列排布的字元和字串:

  • 若正規表示式regAregB分別匹配字元chaAchaB,則正規表示式regAregB匹配字串chaAchaB
  • 若正規表示式regAregB分別匹配字串strAstrB,則正規表示式regAregB匹配字串strAstrB
reg1 = r"\d"
reg2 = r"\d\d"
reg3 = r"\d\w"
re.search(reg1, "56")  # 5
re.search(reg2, "56")  # 55
re.search(reg3, "5x")  # 5x

3.2.1 小括號界定正規表示式範圍

並列排布的運算並不都是從左往右計算,而是依據運算的優先順序從高到低進行運算。為了說明優先順序會影響匹配的結果,這裡引入一個優先順序極低的運算子號|,它表示或的關係。

In [1]: reg1, reg2 = r'a', r'b|c'

In [2]: re.match(r'ab|c', 'ac')

In [3]: re.match(r'ab|c', 'c')
Out[3]: <re.Match object; span=(0, 1), match='c'>

In [4]: re.match(r'ab|c', 'ab')
Out[4]: <re.Match object; span=(0, 2), match='ab'>

直接把reg1reg2組合到一起,並不能匹配ac,只能匹配ab或者c了。實際上匹配的優先順序被修改。字元組合優先順序高於或(|)運算。此時如果要實現匹配ac,可以新增小括號(實現:

In [1]: re.match(r'a(b|c)', 'ac')
Out[1]: <re.Match object; span=(0, 2), match='ac'>

小括號界定正規表示式的範圍,改變了匹配優先順序。新增非字面意思的小括號並不是匹配小括號的意思。

In [1]: re.search(r'x|y|z', "z")
Out[1]: <re.Match object; span=(0, 1), match='z'>

In [2]: re.search(r'(x)|(y)|(z)', "z")
Out[2]: <re.Match object; span=(0, 1), match='z'>

同時,小括號可以把一個正規表示式界定為一個組(group)。後續可以對這個組進行操作。下一節中將對組進行說明。

3.2.2 重複匹配

根據前面的規則,下面的正規表示式用於匹配多位十進位制數:

\d
\d\d
\d\d\d
\d\d\d\d

如果要匹配位數更多的數,則需要加長正規表示式。顯然這樣並不方便,於是就有了如下簡寫:

(REG){m,n} # 至少m次,至多n次
(REG){,n} # 至少0次,至多n次
(REG){m,} # 至少m次
(REG){m} # 只能是m次

特殊情況下可繼續簡寫:

(REG){0,1} -> (REG)?
(REG){1,} -> (REG)+
(REG){0,} -> (REG)*

上面檢索都是貪婪的:會盡可能長地匹配。在正規表示式後面加上?實現非貪婪搜尋:

In [1]: re.search(r'm{2,3}', "mmm")
Out[1]: <re.Match object; span=(0, 3), match='mmm'>

In [2]: re.search(r'm{2,3}?', "mmm")
Out[2]: <re.Match object; span=(0, 2), match='mm'>

除了對單個字元這樣操作,也可以把用括號,把多個正規表示式作為一組進行操作:

In [1]: re.search(r'(ab)+', 'abababxxx')
Out[1]: <re.Match object; span=(0, 6), match='ababab'>

In [2]: re.search(r'ab+', 'abababxxx')
Out[2]: <re.Match object; span=(0, 2), match='ab'>

1 把ab看成一個整體,進行重複匹配;2 是匹配a和多個b。這裡括號讓一系列正規表示式組成一個組,然後對整個組進行操作。

3.3 捕獲組、非捕獲組與命名組

3.3.1 小括號與捕獲組

總結一下小括號作用:

  1. 界定正規表示式範圍
  2. 改變匹配的優先順序
  3. 把一系列正規表示式匹配的結果對應到組(group)中

REG是一個合法的正規表示式:

(REG)

此時REG匹配到的結果就會存到一個 group 中,可以通過group函式進行訪問。

In [1]: x = re.search(r'((\d)\w(\d))', "a1b3x")

In [2]: x.group(0)
Out[2]: '1b3'

In [3]: x.group(1)
Out[3]: '1b3'

In [4]: x.group(2)
Out[5]: '1'

In [5]: x.group(3)
Out[5]: '3'

In [6]: x.group(4)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-29-b8b09168b4e1> in <module>
----> 1 x.group(4)

IndexError: no such group

In [7]: x.group(1, 3)
Out[7]: ('1b3', '3')

以正規表示式最左邊為1,從左往右開始計數,(非轉義)左括號(()排列的順序就是 group 函式訪問時候的參數,此時返回當前左括號和對應右括號中的正規表示式匹配的結果。group()group(0)等價,都表示整個正則表達是匹配的結果。

In [1]: x = re.search(r'\d', '5')

In [2]: x.group(0)
Out[2]: '5'

In [3]: x.group()
Out[3]: '5'

這種用小括號包圍正規表示式得到的組就是捕獲組(Capture Group)。捕獲組的結果會儲存到記憶體中,可以通過 group 的 index 進行訪問。

3.3.2 非捕獲組

對於組而言,儲存匹配結果並且訪問並非一定必要:有時僅僅想知道有這麼個匹配(並不想知道具體匹配結果),或者說儲存結果這個過程佔用空間、影響速度而變得不可以。此時就引入非捕獲組(Non Capture Group),只表示匹配關係,不儲存具體結果

(?:REG)

非捕獲組也使用了小括號()來界定範圍,單非捕獲組正規表示式前面會有問號(?)和冒號(:)。

問號(?)來自 Perl(大部分語言的正規表示式實現都受 Perl 影響)。Perl 從 4 升級到 5 時,為了保持相容性引入了(?的寫法:對一個非轉義的左括號(()用問號(?)進行重複匹配沒有意義,換言之(?並不會引起歧義; 從另一個角度來看,不會有一個合法的正規表示式以問號(?)開頭。

冒號(:)表示普通非捕獲組,表示匹配當前位置的字元(串)。當然,還有其它非捕獲組,如後向斷言(lookbehind)和前向斷言(lookahead)。

非捕獲組不會計入捕獲組的 index 中,無論是否同時存在捕獲組:

In [1]:  x = re.search(r'((?:\d)\w(\d))', "a1b3x")

In [2]: x.group(0,1,2)
Out[2]: ('1b3', '1b3', '3')

In [3]: x.group(3)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-9-6cd92edb32bb> in <module>
----> 1 x.group(3)

IndexError: no such group

group(2)匹配最後一個3,沒有group(3)。非捕獲組不計入 group 結果只針對非捕獲組所在的括號,但對非捕獲組外面或者內部的捕獲組並沒有影響

In [1]: x = re.search(r'([a-c])(?:[d-f](<[g-h]>))','adg')

In [2]: x.group(0,1,2)
Out[2]: ('adg', 'a', 'g')

3.3.3 命名組

有時除了用 index 來訪問匹配內容,還希望賦予匹配一個有意義的名字。此時可以用命名組(Named Group),它屬於捕獲組,主要解決組命名的問題。

(?P<name>REG)

P表示這是Python 正規表示式的語法(而非來自 Perl)。命名組是一種捕獲組,之前捕獲組使用 index 來訪問 group 的方式依舊有效。命名組在此之外新增了用名字訪問的方式,同時會獲得一個非空的groupdict

In [1]: x = re.search(r'(?P<hh>[a-c])(?:[d-f](<[g-h]>))','adg')

In [2]: x
Out[2]: <re.Match object; span=(0, 3), match='adg'>

In [3]: x.group(0,1,2)
Out[3]: ('adg', 'a', 'g')

In [4]: x.group('hh')
Out[4]: 'a'

In [5]: x.groupdict()
Out[5]: {'hh': 'a'}

3.3.4 組的應用—匹配重複的字元(串)

如果要匹配連續重複的字串,則可以用\{num},其中{num}是數字,一定要是一個存在的 index,且不能是0,這也要求要重複的物件一定要在小括號中。

In [1]: re.search(r'(\d)\1{2}', '21112121211')
Out[1]: <re.Match object; span=(1, 4), match='111'>

上面的\1中的1就是 group 的 index。

In [1]: re.search(r'(\d)\0{2}', '21112121211')

In [2]: re.search(r'\d\0{2}', '21112121211')

In [3]: re.search(r'(\d)\2{2}', '21112121211')
---------------------------------------------------------------------------
error                                     Traceback (most recent call last)
<ipython-input-32-86e83c2fdb65> in <module>
----> 1 re.search(r'(\d)\2{2}', '21112121211')

/usr/lib64/python3.9/re.py in search(pattern, string, flags)
    199     """Scan through string looking for a match to the pattern, returning
    200     a Match object, or None if no match was found."""
--> 201     return _compile(pattern, flags).search(string)
    202
    203 def sub(pattern, repl, string, count=0, flags=0):

/usr/lib64/python3.9/re.py in _compile(pattern, flags)
    302     if not sre_compile.isstring(pattern):
    303         raise TypeError("first argument must be string or compiled pattern")
--> 304     p = sre_compile.compile(pattern, flags)
    305     if not (flags & DEBUG):
    306         if len(_cache) >= _MAXCACHE:

/usr/lib64/python3.9/sre_compile.py in compile(p, flags)
    762     if isstring(p):
    763         pattern = p
--> 764         p = sre_parse.parse(p, flags)
    765     else:
    766         pattern = None

/usr/lib64/python3.9/sre_parse.py in parse(str, flags, state)
    946
    947     try:
--> 948         p = _parse_sub(source, state, flags & SRE_FLAG_VERBOSE, 0)
    949     except Verbose:
    950         # the VERBOSE flag was switched on inside the pattern.  to be

/usr/lib64/python3.9/sre_parse.py in _parse_sub(source, state, verbose, nested)
    441     start = source.tell()
    442     while True:
--> 443         itemsappend(_parse(source, state, verbose, nested + 1,
    444                            not nested and not items))
    445         if not sourcematch("|"):

/usr/lib64/python3.9/sre_parse.py in _parse(source, state, verbose, nested, first)
    523
    524         if this[0] == "\\":
--> 525             code = _escape(source, this, state)
    526             subpatternappend(code)
    527

/usr/lib64/python3.9/sre_parse.py in _escape(source, escape, state)
    421                 state.checklookbehindgroup(group, source)
    422                 return GROUPREF, group
--> 423             raise source.error("invalid group reference %d" % group, len(escape) - 1)
    424         if len(escape) == 2:
    425             if c in ASCIILETTERS:

error: invalid group reference 2 at position 5

3.4 關係匹配

有時候希望匹配的字串前面或者後面滿足一些條件,但是對條件本身的內容並不關心。此時就要用到關係匹配。

關係匹配也可以認為是零寬度匹配,返回寬度為零的字串。

3.4.1 開頭結尾匹配

^$分別匹配開頭和結尾:

In [1]: re.search(r"^c\w", 'cacb')
Out[1]: <re.Match object; span=(0, 2), match='ca'>

In [2]: re.search(r"\w$", 'cacb')
Out[2]: <re.Match object; span=(3, 4), match='b'>

In [3]: re.search(r"https|http","https")
Out[3]: <re.Match object; span=(0, 4), match='http'>

In [4]: re.search(r"(http)|(https)", "https")
Out[4]: <re.Match object; span=(0, 4), match='http'>

更寬泛的條件是邊界匹配,可用\b來表示正規表示式。

3.4.2 後向斷言(lookbehind)

實際上要求字串前面滿足一些條件。 這翻譯容易讓人迷惑,估計這裡是說要著重觀察後面的結果並且返回。

(:<=REG)  # positive
(:<!REG)  # negative
In [1]: re.search(r'(?<=\d)abc', 'abc1abcdabc\n')
Out[1]: <re.Match object; span=(4, 7), match='abc'>

In [2]: re.search(r'(?<!\d)abc', 'abc1abcdabc\n')
Out[2]: <re.Match object; span=(0, 3), match='abc'>

In [3]: re.search(r'(?<=c)(?<=\d)abc', 'abc1abcdabc\n')

1 要求前面一定要有一個 \d;而 2 正好相反,只返回 4~7 處的abc

後向斷言是一種零寬度匹配,它匹配長度為零的字元(串)。按照前面正規表示式的排列規則,多個正規表示式並列排布表示一起匹配。由於零寬度字元相加還是零款度字元,多個後向斷言並列排布,還是匹配一個結果。此時就起到了一個與運算的效果。

In [1]: re.search(r'(?<=c\d)abc', 'abc1abcdabc\n')
Out[1]: <re.Match object; span=(4, 7), match='abc'>

3.4.2 前向斷言(lookahead)

後向斷言對稱操作,表示式後面要滿足一定條件。

(?=REG)  # positive
(?!REG)  # negative

這兩種斷言也可以聯合使用:

In [1]: re.search(r'(?<!\d)abc(?=\d)', 'abc1abcdabc\n')
Out[1]: <re.Match object; span=(0, 3), match='abc'>

3.4.3 用斷言來拼湊正規表示式取反

如何用正規表示式來確認一個字串中沒有另外的字串?

3.5 正規表示式優先順序

如同程式語言的運算都有運算優先順序一樣,正規表示式也有優先順序,當正規表示式遇到一起時,就需要考慮其優先順序。 下表列出了優先順序,從上到下遞減,從左到右遞減:

  • \ 跳脫字元
  • (), (?:), (?=), (?!),[]
  • \*, +, ?, {n}, {n,}, {n,m}
  • ^, $, \任何元字符、任何字元 定位點和序列(即:位置和順序)
  • | “或”操作字元

4 其它函式

4.1 re.findall找到所有匹配

re.findall返回所有匹配結果組成的列表:

In [1]: re.findall(r'[a-z][A-Z]', 'aAxxxxxxbBxxxxxx')
Out[1]: ['aA', 'bB']

In [2]: re.findall(r'([a-z])([A-Z])', 'aAxxxxxxbBxxxxxx')
Out[2]: [('a', 'A'), ('b', 'B')]

若正則表達中沒有捕獲組,返回列標的元素是整個匹配(group()group(0)),如上面的 1;如果有捕獲組,則列表元素是所有 group 組成的 tuple,如上面的 2。

4.2 re.split分割字串

re.split用於對字串進行切片。比 Python 自帶的str.split功能更加強大。

re.split(reg, string, maxsplit=0)

maxsplit的意思是最多切分次數,預設值0表示所有都切分。

In [1]: re.split(r"\d", "20python21")
Out[1]: ['', '', 'python', '', '']

In [2]: re.split(r"\d", "20python21", 1)
Out[2]: ['', '0python21']

split的結果包含空字串。

4.3 re.sub替換字串

re.sub 用於匹配物件替換,sub是 substring 的意思。

re.sub(reg, repl, string)

其中repl是替換方式,可以是固定字串、函式或者lambda表示式,對於後面兩種情況,操作的物件是match group

In [1]: re.sub(r'\d', 'NUM', "hello world 5")
Out[1]: 'hello world NUM'
In [1]: re.sub(r'\d', lambda x: str(int(x.group()) + 3), "hello world 5")
Out[1]: 'hello world 8'

字串替換函式replace替換物件只能是固定的字串,而正規表示式擴充了替換的物件。多次呼叫replace的結果並不一定和re.sub等價。

sub相似的函式是subn,返回一個 tuple,兩個元素依次是替換後的字串和替換次數:

In [1]: re.subn(r'\d', lambda x: str(int(x.group()) + 3), "hello world 5")
Out[1]: ('hello world 8', 1)

5 正規表示式實戰

  1. 自動給 markdown 標題編號
  2. 自動給程式碼中的 IPython 輸入輸出編號

6 參考文獻