寫爬蟲,不會(huì)正則怎么行?

導(dǎo)讀:正則在各語言中的使用是有差異的,本文以 Python 3 為基礎(chǔ)。本文主要講述的是正則的語法,對(duì)于 re 模塊不做過多描述,只會(huì)對(duì)一些特殊地方做提示。

很多人覺得正則很難,在我看來,這些人一定是沒有用心。其實(shí)正則很簡(jiǎn)單,根據(jù)二八原則,我們只需要懂 20% 的內(nèi)容就可以解決 80% 的問題了。我曾經(jīng)有幾年幾乎每天都跟正則打交道,剛接手項(xiàng)目的時(shí)候我對(duì)正則也是一無所知,花半小時(shí)百度了一下,然后寫了幾個(gè) demo,就開始正式接手了。三年多時(shí)間,我用到的正則鮮有超出我最初半小時(shí)百度到的知識(shí)的。

1、正則基礎(chǔ)

1.1、基礎(chǔ)語法

(1)常用元字符

語法 描述
\b 匹配單詞的開始或結(jié)束
\d 匹配數(shù)字
\s 匹配任意不可見字符(空格、換行符、制表符等),等價(jià)于[ \f\n\r\t\v]。
\w 匹配任意 Unicode 字符集,包括字母、數(shù)字、下劃線、漢字等
. 匹配除換行符(\n)以外的任意字符
^ 或 \A 匹配字符串或行的起始位置
$ 或 \Z 匹配字符串或行的結(jié)束位置

(2)限定詞(又叫量詞)

語法 描述
* 重復(fù)零次或更多次
+ 重復(fù)一次或更多次
? 重復(fù)零次或一次
{n} 重復(fù) n 次
{n,} 重復(fù) n 次或更多次
{n,m} 重復(fù) n 到 m 次

(3)常用反義詞

語法 描述
\B 匹配非單詞的開始或結(jié)束
\D 匹配非數(shù)字
\S 匹配任意可見字符, [^ \f\n\r\t\v]
\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](減去)

以上便是正則的基礎(chǔ)內(nèi)容,下面來寫兩個(gè)例子看下:

s = '123abc你好'
re.search('\d+', s).group()
re.search('\w+', s).group()

結(jié)果:

123
123abc你好

是不是很簡(jiǎn)單?


1.2、修飾符

修飾符在各語言中也是有差異的。

Python 中的修飾符:

修飾符 描述
re.A 匹配 ASCII字符類,影響 \w, \W, \b, \B, \d, \D
re.I 忽略大小寫
re.L 做本地化識(shí)別匹配(這個(gè)極少極少使用)
re.M 多行匹配,影響 ^ 和 $
re.S 使 . 匹配包括換行符(\n)在內(nèi)的所有字符
re.U 匹配 Unicode 字符集。與 re.A 相對(duì),這是默認(rèn)設(shè)置
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()

結(jié)果:

123abc
你好

但是描述中還有 \d\D,數(shù)字不都是 ASCII 字符嗎?這是什么意思?別忘了,還有 全角和半角!

s = '0123456789'    # 全角數(shù)字
re.search('\d+', s, re.U).group()

結(jié)果:

0123456789

(2)re.M
多行匹配的模式其實(shí)也不常用,很少有一行行規(guī)整的數(shù)據(jù)。

s = 'aaa\r\nbbb\r\nccc'

re.findall('^[\s\w]*?$', s)
re.findall('^[\s\w]*?$', s, re.M)

結(jié)果:

['aaa\r\nbbb\r\nccc']        # 單行模式
['aaa\r', 'bbb\r', 'ccc']    # 多行模式

(3)re.S
這個(gè)簡(jiǎn)單,直接看個(gè)例子。

s = 'aaa\r\nbbb\r\nccc'

re.findall('^.*', s)
re.findall('^.*', s, re.S)

結(jié)果:

['aaa\r']
['aaa\r\nbbb\r\nccc']

(4)re.X
用法如下:

rc = re.compile(r"""
\d+ # 匹配數(shù)字
# 和字母
[a-zA-Z]+
""", re.X)
rc.search('123abc').group()

結(jié)果:

123abc

注意,用了 X 修飾符后,正則中的所有空格會(huì)被忽略,包括正則里面的原本有用的空格。如果正則中有需要使用空格,只能用 \s 代替。

(5)(?aiLmsux)
修飾符不僅可以代碼中指定,也可以在正則中指定。(?aiLmsux) 表示了以上所有的修飾符,具體用的時(shí)候需要哪個(gè)就在 ? 后面加上對(duì)應(yīng)的字母,示例如下,(?a)re.A 效果是一樣的:

s = '123abc你好'
re.search('(?a)\w+', s).group()
re.search('\w+', s, re.A).group()

結(jié)果是一樣的:

123abc
123abc

1.3、貪婪與懶惰

當(dāng)正則表達(dá)式中包含能接受重復(fù)的限定符時(shí),通常的行為是(在使整個(gè)表達(dá)式能得到匹配的前提下)匹配盡可能多的字符。

s = 'aabab'
re.search('a.*b', s).group()    # 這就是貪婪
re.search('a.*?b', s).group()   # 這就是懶惰

結(jié)果:

aabab
aab

簡(jiǎn)單來說:

  • 所謂貪婪,就是盡可能 的匹配;
  • 所謂懶惰,就是盡可能 的匹配。
  • *+、{n,} 這些表達(dá)式屬于貪婪;
  • *?、+?、{n,}? 這些表達(dá)式就是懶惰(在貪婪的基礎(chǔ)上加上 ?)。

2、正則進(jìn)階

2.1、捕獲分組

語法 描述
(exp) 匹配exp,并捕獲文本到自動(dòng)命名的組里
(?P<name>exp) 匹配exp,并捕獲文本到名稱為 name 的組里
(?:exp) 匹配exp,不捕獲匹配的文本,也不給此分組分配組號(hào)
(?P=name) 匹配之前由名為 name 的組匹配的文本

注意:在其他語言或者網(wǎng)上的一些正則工具中,分組命名的語法是 (?<name>exp)(?'name'exp) ,但在 Python 里,這樣寫會(huì)報(bào)錯(cuò):This named group syntax is not supported in this regex dialect。Python 中正確的寫法是:(?P<name>exp)

示例一:

分組可以讓我們用一條正則提取出多個(gè)信息,例如:

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}')

結(jié)果:

name:張三, phone:13812345678

示例二:

(?P<name>exp) 有時(shí)還是會(huì)用到的, (?P=name) 則很少情況下會(huì)用到。我想了一個(gè) (?P=name) 的使用示例,給大家看下效果:

s = '''
<name>張三</name>
<age>30</age>
<phone>138123456789</phone>
'''

pattern = r'<(?P<name>.*?)>(.*?)</(?P=name)>'
It = re.findall(pattern, s)

結(jié)果:

[('name', '張三'), ('age', '30'), ('phone', '138123456789')]

2.2、零寬斷言

語法 描述
(?=exp) 匹配exp前面的位置
(?<=exp) 匹配exp后面的位置
(?!exp) 匹配后面跟的不是exp的位置
(?<!exp) 匹配前面不是exp的位置

注意:正則中常用的前項(xiàng)界定 (?<=exp) 和前項(xiàng)否定界定 (?<!exp) 在 Python 中可能會(huì)報(bào)錯(cuò):look-behind requires fixed-width pattern,原因是 python 中 前項(xiàng)界定的表達(dá)式必須是定長的,看如下示例:

(?<=aaa)        # 正確
(?<=aaa|bbb)    # 正確
(?<=aaa|bb)     # 錯(cuò)誤
(?<=\d+)        # 錯(cuò)誤
(?<=\d{3})      # 正確

2.3、條件匹配

這大概是最復(fù)雜的正則表達(dá)式了。語法如下:

語法 描述
(?(id/name)yes|no) 如果指定分組存在,則匹配 yes 模式,否則匹配 no 模式

此語法極少用到,印象中只用過一次。

以下示例的要求是:如果以 _ 開頭,則以字母結(jié)尾,否則以數(shù)字結(jié)尾。

s1 = '_abcd'
s2 = 'abc1'

pattern = '(_)?[a-zA-Z]+(?(1)[a-zA-Z]|\d)'

re.search(pattern, s1).group()
re.search(pattern, s2).group()

結(jié)果:

_abcd
abc1

2.4、findall

Python 中的 re.findall 是個(gè)比較特別的方法(之所以說它特別,是跟我常用的 C# 做比較,在沒看注釋之前我想當(dāng)然的掉坑里去了)。我們看這個(gè)方法的官方注釋:

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.

簡(jiǎn)單來說,就是

  • 如果沒有分組,則返回整條正則匹配結(jié)果的列表;
  • 如果有 1 個(gè)分組,則返回分組匹配到的結(jié)果的列表;
  • 如果有多個(gè)分組,則返回分組匹配到的結(jié)果的元組的列表。

看下面的例子:

s = 'aaa123bbb456ccc'

re.findall('[a-z]+\d+', s)          # 不包含分組
re.findall('[a-z]+(\d+)', s)        # 包含一個(gè)分組
re.findall('([a-z]+(\d+))', s)      # 包含多個(gè)分組
re.findall('(?:[a-z]+(\d+))', s)    # ?: 不捕獲分組匹配結(jié)果

結(jié)果:

['aaa123', 'bbb456']
['123', '456']
[('aaa123', '123'), ('bbb456', '456')]
['123', '456']

零寬斷言中講到 Python 中前項(xiàng)界定必須是定長的,這很不方便,但是配合 findall 有分組時(shí)只取分組結(jié)果的特性,就可以模擬出非定長前項(xiàng)界定的效果了。

結(jié)語

其實(shí)正則就像是一個(gè)數(shù)學(xué)公式,會(huì)背公式不一定會(huì)做題。但其實(shí)這公式一點(diǎn)也不難,至少比學(xué)校里學(xué)的數(shù)學(xué)簡(jiǎn)單多了,多練習(xí)幾次也就會(huì)了。


掃碼關(guān)注我的個(gè)人公眾號(hào):大齡碼農(nóng)的Python之路

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容