草根學Python(十四) 一步一步瞭解正規表示式

兩點水發表於2017-12-25

目錄

草根學Python(十四) 一步一步瞭解正規表示式

初識 Python 正規表示式

正規表示式是一個特殊的字元序列,用於判斷一個字串是否與我們所設定的字元序列是否匹配,也就是說檢查一個字串是否與某種模式匹配。

Python 自 1.5 版本起增加了re 模組,它提供 Perl 風格的正規表示式模式。re 模組使 Python 語言擁有全部的正規表示式功能。

下面通過例項,一步一步來初步認識正規表示式。

比如在一段字串中尋找是否含有某個字元或某些字元,通常我們使用內建函式來實現,如下:

# 設定一個常量
a = '兩點水|twowater|liangdianshui|草根程式設計師|ReadingWithU'

# 判斷是否有 “兩點水” 這個字串,使用 PY 自帶函式

print('是否含有“兩點水”這個字串:{0}'.format(a.index('兩點水') > -1))
print('是否含有“兩點水”這個字串:{0}'.format('兩點水' in a))
複製程式碼

輸出的結果如下:

是否含有“兩點水”這個字串:True
是否含有“兩點水”這個字串:True
複製程式碼

那麼,如果使用正規表示式呢?

剛剛提到過,Python 給我們提供了 re 模組來實現正規表示式的所有功能,那麼我們先使用其中的一個函式:

re.findall(pattern, string[, flags])
複製程式碼

該函式實現了在字串中找到正規表示式所匹配的所有子串,並組成一個列表返回,具體操作如下:


import re

# 設定一個常量
a = '兩點水|twowater|liangdianshui|草根程式設計師|ReadingWithU'

# 正規表示式

findall = re.findall('兩點水', a)
print(findall)

if len(findall) > 0:
    print('a 含有“兩點水”這個字串')
else:
    print('a 不含有“兩點水”這個字串')

複製程式碼

輸出的結果:

['兩點水']
a 含有“兩點水”這個字串
複製程式碼

從輸出結果可以看到,可以實現和內建函式一樣的功能,可是在這裡也要強調一點,上面這個例子只是方便我們理解正規表示式,這個正規表示式的寫法是毫無意義的。為什麼這樣說呢?

因為用 Python 自帶函式就能解決的問題,我們就沒必要使用正規表示式了,這樣做多此一舉。而且上面例子中的正規表示式設定成為了一個常量,並不是一個正規表示式的規則,正規表示式的靈魂在於規則,所以這樣做意義不大。

那麼正規表示式的規則怎麼寫呢?先不急,我們一步一步來,先來一個簡單的,找出字串中的所有小寫字母。首先我們在 findall 函式中第一個引數寫正規表示式的規則,其中 [a-z] 就是匹配任何小寫字母,第二個引數只要填寫要匹配的字串就行了。具體如下:


import re

# 設定一個常量
a = '兩點水|twowater|liangdianshui|草根程式設計師|ReadingWithU'

# 選擇 a 裡面的所有小寫英文字母

re_findall = re.findall('[a-z]', a)

print(re_findall)

複製程式碼

輸出的結果:

['t', 'w', 'o', 'w', 'a', 't', 'e', 'r', 'l', 'i', 'a', 'n', 'g', 'd', 'i', 'a', 'n', 's', 'h', 'u', 'i', 'e', 'a', 'd', 'i', 'n', 'g', 'i', 't', 'h']
複製程式碼

這樣我們就拿到了字串中的所有小寫字母了。

字符集

好了,通過上面的幾個例項我們初步認識了 Python 的正規表示式,可能你就會問,正規表示式還有什麼規則,什麼字母代表什麼意思呢?

其實,這些都不急,在本章後面會給出對應的正規表示式規則列表,而且這些東西在網上隨便都能 Google 到。所以現在,我們還是進一步加深對正規表示式的理解,講一下正規表示式的字符集。

字符集是由一對方括號 “[]” 括起來的字符集合。使用字符集,可以匹配多個字元中的一個。

舉個例子,比如你使用 C[ET]O 匹配到的是 CEO 或 CTO ,也就是說 [ET] 代表的是一個 E 或者一個 T 。像上面提到的 [a-z] ,就是所有小寫字母中的其中一個,這裡使用了連字元 “-” 定義一個連續字元的字元範圍。當然,像這種寫法,裡面可以包含多個字元範圍的,比如:[0-9a-fA-F] ,匹配單個的十六進位制數字,且不分大小寫。注意了,字元和範圍定義的先後順序對匹配的結果是沒有任何影響的。

其實說了那麼多,只是想證明,字符集一對方括號 “[]” 裡面的字元關係是或關係,下面看一個例子:


import re
a = 'uav,ubv,ucv,uwv,uzv,ucv,uov'

# 字符集

# 取 u 和 v 中間是 a 或 b 或 c 的字元
findall = re.findall('u[abc]v', a)
print(findall)
# 如果是連續的字母,數字可以使用 - 來代替
l = re.findall('u[a-c]v', a)
print(l)

# 取 u 和 v 中間不是 a 或 b 或 c 的字元
re_findall = re.findall('u[^abc]v', a)
print(re_findall)

複製程式碼

輸出的結果:

['uav', 'ubv', 'ucv', 'ucv']
['uav', 'ubv', 'ucv', 'ucv']
['uwv', 'uzv', 'uov']
複製程式碼

在例子中,使用了取反字符集,也就是在左方括號 “[” 後面緊跟一個尖括號 “^”,就會對字符集取反。需要記住的一點是,取反字符集必須要匹配一個字元。比如:q[^u] 並不意味著:匹配一個 q,後面沒有 u 跟著。它意味著:匹配一個 q,後面跟著一個不是 u 的字元。具體可以對比上面例子中輸出的結果來理解。

我們都知道,正規表示式本身就定義了一些規則,比如 \d,匹配所有數字字元,其實它是等價於 [0-9],下面也寫了個例子,通過字符集的形式解釋了這些特殊字元。

import re

a = 'uav_ubv_ucv_uwv_uzv_ucv_uov&123-456-789'

# 概括字符集

# \d 相當於 [0-9] ,匹配所有數字字元
# \D 相當於 [^0-9] , 匹配所有非數字字元
findall1 = re.findall('\d', a)
findall2 = re.findall('[0-9]', a)
findall3 = re.findall('\D', a)
findall4 = re.findall('[^0-9]', a)
print(findall1)
print(findall2)
print(findall3)
print(findall4)

# \w 匹配包括下劃線的任何單詞字元,等價於 [A-Za-z0-9_]
findall5 = re.findall('\w', a)
findall6 = re.findall('[A-Za-z0-9_]', a)
print(findall5)
print(findall6)

複製程式碼

輸出結果:

['1', '2', '3', '4', '5', '6', '7', '8', '9']
['1', '2', '3', '4', '5', '6', '7', '8', '9']
['u', 'a', 'v', '_', 'u', 'b', 'v', '_', 'u', 'c', 'v', '_', 'u', 'w', 'v', '_', 'u', 'z', 'v', '_', 'u', 'c', 'v', '_', 'u', 'o', 'v', '&', '-', '-']
['u', 'a', 'v', '_', 'u', 'b', 'v', '_', 'u', 'c', 'v', '_', 'u', 'w', 'v', '_', 'u', 'z', 'v', '_', 'u', 'c', 'v', '_', 'u', 'o', 'v', '&', '-', '-']
['u', 'a', 'v', '_', 'u', 'b', 'v', '_', 'u', 'c', 'v', '_', 'u', 'w', 'v', '_', 'u', 'z', 'v', '_', 'u', 'c', 'v', '_', 'u', 'o', 'v', '1', '2', '3', '4', '5', '6', '7', '8', '9']
['u', 'a', 'v', '_', 'u', 'b', 'v', '_', 'u', 'c', 'v', '_', 'u', 'w', 'v', '_', 'u', 'z', 'v', '_', 'u', 'c', 'v', '_', 'u', 'o', 'v', '1', '2', '3', '4', '5', '6', '7', '8', '9']
複製程式碼

數量詞

來,繼續加深對正規表示式的理解,這部分理解一下數量詞,為什麼要用數量詞,想想都知道,如果你要匹配幾十上百的字元時,難道你要一個一個的寫,所以就出現了數量詞。

數量詞的詞法是:{min,max} 。min 和 max 都是非負整數。如果逗號有而 max 被忽略了,則 max 沒有限制。如果逗號和 max 都被忽略了,則重複 min 次。比如,\b[1-9][0-9]{3}\b,匹配的是 1000 ~ 9999 之間的數字( “\b” 表示單詞邊界),而 \b[1-9][0-9]{2,4}\b,匹配的是一個在 100 ~ 99999 之間的數字。

下面看一個例項,匹配出字串中 4 到 7 個字母的英文

import re

a = 'java*&39android##@@python'

# 數量詞

findall = re.findall('[a-z]{4,7}', a)
print(findall)
複製程式碼

輸出結果:

['java', 'android', 'python']
複製程式碼

注意,這裡有貪婪和非貪婪之分。那麼我們先看下相關的概念:

貪婪模式:它的特性是一次性地讀入整個字串,如果不匹配就吐掉最右邊的一個字元再匹配,直到找到匹配的字串或字串的長度為 0 為止。它的宗旨是讀儘可能多的字元,所以當讀到第一個匹配時就立刻返回。

懶惰模式:它的特性是從字串的左邊開始,試圖不讀入字串中的字元進行匹配,失敗,則多讀一個字元,再匹配,如此迴圈,當找到一個匹配時會返回該匹配的字串,然後再次進行匹配直到字串結束。

上面例子中的就是貪婪的,如果要使用非貪婪,也就是懶惰模式,怎麼呢?

如果要使用非貪婪,則加一個 ? ,上面的例子修改如下:

import re

a = 'java*&39android##@@python'

# 貪婪與非貪婪

re_findall = re.findall('[a-z]{4,7}?', a)
print(re_findall)

複製程式碼

輸出結果如下:

['java', 'andr', 'pyth']
複製程式碼

從輸出的結果可以看出,android 只列印除了 andr ,Python 只列印除了 pyth ,因為這裡使用的是懶惰模式。

當然,還有一些特殊字元也是可以表示數量的,比如:

?:告訴引擎匹配前導字元 0 次或 1 次

+:告訴引擎匹配前導字元 1 次或多次

*:告訴引擎匹配前導字元 0 次或多次

把這部分的知識點總結一下,就是下面這個表了:

貪 婪 惰 性 描 述
?? 零次或一次出現,等價於{0,1}
+ +? 一次或多次出現 ,等價於{1,}
* *? 零次或多次出現 ,等價於{0,}
{n} {n}? 恰好 n 次出現
{n,m} {n,m}? 至少 n 次枝多 m 次出現
{n,} {n,}? 至少 n 次出現

邊界匹配符和組

將上面幾個點,就用了很大的篇幅了,現在介紹一些邊界匹配符和組的概念。

一般的邊界匹配符有以下幾個:

語法 描述
^ 匹配字串開頭(在有多行的情況中匹配每行的開頭)
$ 匹配字串的末尾(在有多行的情況中匹配每行的末尾)
\A 僅匹配字串開頭
\Z 僅匹配字串末尾
\b 匹配 \w 和 \W 之間
\B [^\b]

分組,被括號括起來的表示式就是分組。分組表示式 (...) 其實就是把這部分字元作為一個整體,當然,可以有多分組的情況,每遇到一個分組,編號就會加 1 ,而且分組後面也是可以加數量詞的。

此處本應有例子,考慮到篇幅問題,就不貼了

re.sub

實戰過程中,我們很多時候需要替換字串中的字元,這時候就可以用到 def sub(pattern, repl, string, count=0, flags=0) 函式了,re.sub 共有五個引數。其中三個必選引數:pattern, repl, string ; 兩個可選引數:count, flags .

具體引數意義如下:

引數 描述
pattern 表示正則中的模式字串
repl repl,就是replacement,被替換的字串的意思
string 即表示要被處理,要被替換的那個 string 字串
count 對於pattern中匹配到的結果,count可以控制對前幾個group進行替換
flags 正規表示式修飾符

具體使用可以看下下面的這個例項,註釋都寫的很清楚的了,主要是注意一下,第二個引數是可以傳遞一個函式的,這也是這個方法的強大之處,例如例子裡面的函式 convert ,對傳遞進來要替換的字元進行判斷,替換成不同的字元。

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import re

a = 'Python*Android*Java-888'

# 把字串中的 * 字元替換成 & 字元
sub1 = re.sub('\*', '&', a)
print(sub1)

# 把字串中的第一個 * 字元替換成 & 字元
sub2 = re.sub('\*', '&', a, 1)
print(sub2)


# 把字串中的 * 字元替換成 & 字元,把字元 - 換成 |

# 1、先定義一個函式
def convert(value):
    group = value.group()
    if (group == '*'):
        return '&'
    elif (group == '-'):
        return '|'


# 第二個引數,要替換的字元可以為一個函式
sub3 = re.sub('[\*-]', convert, a)
print(sub3)
複製程式碼

輸出的結果:

Python&Android&Java-888
Python&Android*Java-888
Python&Android&Java|888
複製程式碼

re.match 和 re.search

re.match 函式

語法:

re.match(pattern, string, flags=0)
複製程式碼

re.match 嘗試從字串的起始位置匹配一個模式,如果不是起始位置匹配成功的話,match() 就返回 none。

re.search 函式

語法:

re.search(pattern, string, flags=0)
複製程式碼

re.search 掃描整個字串並返回第一個成功的匹配。

re.match 和 re.search 的引數,基本一致的,具體描述如下:

引數 描述
pattern 匹配的正規表示式
string 要匹配的字串
flags 標誌位,用於控制正規表示式的匹配方式,如:是否區分大小寫

那麼它們之間有什麼區別呢?

re.match 只匹配字串的開始,如果字串開始不符合正規表示式,則匹配失敗,函式返回 None;而 re.search 匹配整個字串,直到找到一個匹配。這就是它們之間的區別了。

re.match 和 re.search 在網上有很多詳細的介紹了,可是再個人的使用中,還是喜歡使用 re.findall

看下下面的例項,可以對比下 re.search 和 re.findall 的區別,還有多分組的使用。具體看下注釋,對比一下輸出的結果:

示例:


#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

# 提取圖片的地址

import re

a = '<img src="https://s-media-cache-ak0.pinimg.com/originals/a8/c4/9e/a8c49ef606e0e1f3ee39a7b219b5c05e.jpg">'

# 使用 re.search
search = re.search('<img src="(.*)">', a)
# group(0) 是一個完整的分組
print(search.group(0))
print(search.group(1))

# 使用 re.findall
findall = re.findall('<img src="(.*)">', a)
print(findall)

# 多個分組的使用(比如我們需要提取 img 欄位和圖片地址欄位)
re_search = re.search('<(.*) src="(.*)">', a)
# 列印 img
print(re_search.group(1))
# 列印圖片地址
print(re_search.group(2))
# 列印 img 和圖片地址,以元祖的形式
print(re_search.group(1, 2))
# 或者使用 groups
print(re_search.groups())

複製程式碼

輸出的結果:

<img src="https://s-media-cache-ak0.pinimg.com/originals/a8/c4/9e/a8c49ef606e0e1f3ee39a7b219b5c05e.jpg">
https://s-media-cache-ak0.pinimg.com/originals/a8/c4/9e/a8c49ef606e0e1f3ee39a7b219b5c05e.jpg
['https://s-media-cache-ak0.pinimg.com/originals/a8/c4/9e/a8c49ef606e0e1f3ee39a7b219b5c05e.jpg']
img
https://s-media-cache-ak0.pinimg.com/originals/a8/c4/9e/a8c49ef606e0e1f3ee39a7b219b5c05e.jpg
('img', 'https://s-media-cache-ak0.pinimg.com/originals/a8/c4/9e/a8c49ef606e0e1f3ee39a7b219b5c05e.jpg')
('img', 'https://s-media-cache-ak0.pinimg.com/originals/a8/c4/9e/a8c49ef606e0e1f3ee39a7b219b5c05e.jpg')
複製程式碼

最後,正規表示式是非常厲害的工具,通常可以用來解決字串內建函式無法解決的問題,而且正規表示式大部分語言都是有的。python 的用途很多,但在爬蟲和資料分析這連個模組中都是離不開正規表示式的。所以正規表示式對於學習 Python 來說,真的很重要。最後,附送一些常用的正規表示式和正規表示式和 Python 支援的正規表示式元字元和語法文件。

github:https://github.com/TwoWater/Python/blob/master/python14/%E5%B8%B8%E7%94%A8%E7%9A%84%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F.md

歡迎大家 start ,https://github.com/TwoWater/Python 一下,這是草根學 Python 系列部落格的庫。也可以關注我的微信公眾號:

https://user-gold-cdn.xitu.io/2017/12/26/1608e8e73726376a?w=1057&h=705&f=gif&s=1285550

相關文章