寫爬蟲,怎麼可以不會正則呢?
- 2019 年 10 月 8 日
- 筆記
本文轉自公眾號『大齡碼農的Python之路』
很多人覺得正則很難,在我看來,這些人一定是沒有用心。其實正則很簡單,根據二八原則,我們只需要懂 20% 的內容就可以解決 80% 的問題了。我曾經有幾年幾乎每天都跟正則打交道,剛接手項目的時候我對正則也是一無所知,花半小時百度了一下,然後寫了幾個 demo,就開始正式接手了。三年多時間,我用到的正則鮮有超出我最初半小時百度到的知識的。
1、正則基礎
1.1、基礎語法
(1)常用元字元
語法 |
描述 |
---|---|
b |
匹配單詞的開始或結束 |
d |
匹配數字 |
s |
匹配任意不可見字元(空格、換行符、製表符等),等價於[ fnrtv]。 |
w |
匹配任意 Unicode 字符集,包括字母、數字、下劃線、漢字等 |
. |
匹配除換行符(n)以外的任意字元 |
^ 或 A |
匹配字元串或行的起始位置 |
$ 或 Z |
匹配字元串或行的結束位置 |
(2)限定詞(又叫量詞)
語法 |
描述 |
---|---|
* |
重複零次或更多次 |
+ |
重複一次或更多次 |
? |
重複零次或一次 |
{n} |
重複 n 次 |
{n,} |
重複 n 次或更多次 |
{n,m} |
重複 n 到 m 次 |
(3)常用反義詞
語法 |
描述 |
---|---|
B |
匹配非單詞的開始或結束 |
D |
匹配非數字 |
S |
匹配任意可見字元, [^ fnrtv] |
W |
匹配任意非 Unicode 字符集 |
[^abc] |
除 a、b、c 以外的任意字元 |
(4)字元族
語法 |
描述 |
---|---|
[abc] |
a、b 或 c |
[^abc] |
除 a、b、c 以外的任意字元 |
[a-zA-Z] |
a 到 z 或 A 到 Z |
[a-d[m-p]] |
a 到 d 或 m 到 p,即 [a-dm-p](並集) |
[a-z&&[def]] |
d、e 或 f(交集) |
[a-z&&[^bc]] |
a 到 z,除了 b 和 c:[ad-z](減去) |
[a-z&&[^m-p]] |
a 到 z,減去 m 到 p:[a-lq-z](減去) |
以上便是正則的基礎內容,下面來寫兩個例子看下:
s = '123abc你好' re.search('d+', s).group() re.search('w+', s).group()
結果:
123 123abc你好
是不是很簡單?

1.2、修飾符
修飾符在各語言中也是有差異的。
Python 中的修飾符:
修飾符 |
描述 |
---|---|
re.A |
匹配 ASCII字元類,影響 w, W, b, B, d, D |
re.I |
忽略大小寫 |
re.L |
做本地化識別匹配(這個極少極少使用) |
re.M |
多行匹配,影響 ^ 和 $ |
re.S |
使 . 匹配包括換行符(n)在內的所有字元 |
re.U |
匹配 Unicode 字符集。與 re.A 相對,這是默認設置 |
re.X |
忽略空格和 # 後面的注釋以獲得看起來更易懂的正則。 |
(1)re.A
修飾符 A
使 w
只匹配 ASCII 字元,W
匹配非 ASCII 字元。
s = '123abc你好' re.search('w+', s, re.A).group() re.search('W+', s, re.A).group()
結果:
123abc 你好
但是描述中還有 d
和 D
,數字不都是 ASCII 字元嗎?這是什麼意思?別忘了,還有 全形和半形!
s = '0123456789' # 全形數字 re.search('d+', s, re.U).group()
結果:
0123456789
(2)re.M 多行匹配的模式其實也不常用,很少有一行行規整的數據。
s = 'aaarnbbbrnccc' re.findall('^[sw]*?$', s) re.findall('^[sw]*?$', s, re.M)
結果:
['aaarnbbbrnccc'] # 單行模式 ['aaar', 'bbbr', 'ccc'] # 多行模式
(3)re.S 這個簡單,直接看個例子。
s = 'aaarnbbbrnccc' re.findall('^.*', s) re.findall('^.*', s, re.S)
結果:
['aaar'] ['aaarnbbbrnccc']
(4)re.X 用法如下:
rc = re.compile(r""" d+ # 匹配數字 # 和字母 [a-zA-Z]+ """, re.X) rc.search('123abc').group()
結果:
123abc
注意,用了
X
修飾符後,正則中的所有空格會被忽略,包括正則裡面的原本有用的空格。如果正則中有需要使用空格,只能用s
代替。
(5)(?aiLmsux) 修飾符不僅可以程式碼中指定,也可以在正則中指定。(?aiLmsux)
表示了以上所有的修飾符,具體用的時候需要哪個就在 ? 後面加上對應的字母,示例如下,(?a)
和 re.A
效果是一樣的:
s = '123abc你好' re.search('(?a)w+', s).group() re.search('w+', s, re.A).group()
結果是一樣的:
123abc 123abc
1.3、貪婪與懶惰
當正則表達式中包含能接受重複的限定符時,通常的行為是(在使整個表達式能得到匹配的前提下)匹配儘可能多的字元。
s = 'aabab' re.search('a.*b', s).group() # 這就是貪婪 re.search('a.*?b', s).group() # 這就是懶惰
結果:
aabab aab
簡單來說:
- 所謂貪婪,就是儘可能 多 的匹配;
- 所謂懶惰,就是儘可能 少 的匹配。
*
、+
、{n,}
這些表達式屬於貪婪;*?
、+?
、{n,}?
這些表達式就是懶惰(在貪婪的基礎上加上?
)。
2、正則進階
2.1、捕獲分組
語法 |
描述 |
---|---|
(exp) |
匹配exp,並捕獲文本到自動命名的組裡 |
(?Pexp) |
匹配exp,並捕獲文本到名稱為 name 的組裡 |
(?:exp) |
匹配exp,不捕獲匹配的文本,也不給此分組分配組號 |
(?P=name) |
匹配之前由名為 name 的組匹配的文本 |
注意:在其他語言或者網上的一些正則工具中,分組命名的語法是
(?<name>exp)
或(?'name'exp)
,但在 Python 里,這樣寫會報錯:This named group syntax is not supported in this regex dialect。Python 中正確的寫法是:(?P<name>exp)
示例一:
分組可以讓我們用一條正則提取出多個資訊,例如:
s = '姓名:張三;性別:男;電話:138123456789' m = re.search('姓名[::](w+).*?電話[::](d{11})', s) if m: name = m.group(1) phone = m.group(2) print(f'name:{name}, phone:{phone}')
結果:
name:張三, phone:13812345678
示例二:
(?P<name>exp)
有時還是會用到的, (?P=name)
則很少情況下會用到。我想了一個 (?P=name)
的使用示例,給大家看下效果:
s = ''' <name>張三</name> <age>30</age> <phone>138123456789</phone> ''' pattern = r'<(?P<name>.*?)>(.*?)</(?P=name)>' It = re.findall(pattern, s)
結果:
[('name', '張三'), ('age', '30'), ('phone', '138123456789')]
2.2、零寬斷言
語法 |
描述 |
---|---|
(?=exp) |
匹配exp前面的位置 |
(?<=exp) |
匹配exp後面的位置 |
(?!exp) |
匹配後面跟的不是exp的位置 |
(?<!exp) |
匹配前面不是exp的位置 |
注意:正則中常用的前項界定
(?<=exp)
和前項否定界定(?<!exp)
在 Python 中可能會報錯:look-behind requires fixed-width pattern,原因是 python 中 前項界定的表達式必須是定長的,看如下示例:
(?<=aaa) # 正確 (?<=aaa|bbb) # 正確 (?<=aaa|bb) # 錯誤 (?<=d+) # 錯誤 (?<=d{3}) # 正確
2.3、條件匹配
這大概是最複雜的正則表達式了。語法如下:
語法 |
描述 |
---|---|
(?(id/name)yes|no) |
如果指定分組存在,則匹配 yes 模式,否則匹配 no 模式 |
此語法極少用到,印象中只用過一次。
以下示例的要求是:如果以 _ 開頭,則以字母結尾,否則以數字結尾。
s1 = '_abcd' s2 = 'abc1' pattern = '(_)?[a-zA-Z]+(?(1)[a-zA-Z]|d)' re.search(pattern, s1).group() re.search(pattern, s2).group()
結果:
_abcd abc1
2.4、findall
Python 中的 re.findall
是個比較特別的方法(之所以說它特別,是跟我常用的 C# 做比較,在沒看注釋之前我想當然的掉坑裡去了)。我們看這個方法的官方注釋:
Return a list of all non-overlapping matches in the string. If one or more capturing groups are present in the pattern, return a list of groups; this will be a list of tuples if the pattern has more than one group. Empty matches are included in the result.
簡單來說,就是
- 如果沒有分組,則返回整條正則匹配結果的列表;
- 如果有 1 個分組,則返回分組匹配到的結果的列表;
- 如果有多個分組,則返回分組匹配到的結果的元組的列表。
看下面的例子:
s = 'aaa123bbb456ccc' re.findall('[a-z]+d+', s) # 不包含分組 re.findall('[a-z]+(d+)', s) # 包含一個分組 re.findall('([a-z]+(d+))', s) # 包含多個分組 re.findall('(?:[a-z]+(d+))', s) # ?: 不捕獲分組匹配結果
結果:
['aaa123', 'bbb456'] ['123', '456'] [('aaa123', '123'), ('bbb456', '456')] ['123', '456']
零寬斷言中講到 Python 中前項界定必須是定長的,這很不方便,但是配合 findall 有分組時只取分組結果的特性,就可以模擬出非定長前項界定的效果了。
結語
其實正則就像是一個數學公式,會背公式不一定會做題。但其實這公式一點也不難,至少比學校里學的數學簡單多了,多練習幾次也就會了。