第十六章、正則表達式


有什么用?

大家看一個例子。

一個文本文件里面存儲了 一些市場職位信息,格式如下所示

Python3 高級開發工程師 上海互教教育科技有限公司上海-浦東新區2萬/月02-18滿員
測試開發工程師(C++/python) 上海墨鵾數碼科技有限公司上海-浦東新區2.5萬/每月02-18未滿員
Python3 開發工程師 上海德拓信息技術股份有限公司上海-徐匯區1.3萬/每月02-18剩余11人
測試開發工程師(Python) 赫里普(上海)信息科技有限公司上海-浦東新區1.1萬/每月02-18剩余5人
Python高級開發工程師 上海行動教育科技股份有限公司上海-閔行區2.8萬/月02-18剩余255人
python開發工程師 上海優似騰軟件開發有限公司上海-浦東新區2.5萬/每月02-18滿員

現在,我們需要寫一個程序,從這些文本里面抓取 所有職位的薪資。

就是要獲取這樣的結果

2
2.5
1.3
1.1
2.8
2.5

怎么做?

大家先自己思考一下。

這是典型的字符串處理。

分析這里面的規律,可以發現,薪資的數字 后面 都有關鍵字 萬/月 或者 萬/每月

根據我們學過的知識,我們不難寫出下面的代碼

content = ''' Python3 高級開發工程師 上海互教教育科技有限公司上海-浦東新區2萬/月02-18滿員 測試開發工程師(C++/python) 上海墨鵾數碼科技有限公司上海-浦東新區2.5萬/每月02-18未滿員 Python3 開發工程師 上海德拓信息技術股份有限公司上海-徐匯區1.3萬/每月02-18剩余11人 測試開發工程師(Python) 赫里普(上海)信息科技有限公司上海-浦東新區1.1萬/每月02-18剩余5人 Python高級開發工程師 上海行動教育科技股份有限公司上海-閔行區2.8萬/月02-18剩余255人 python開發工程師 上海優似騰軟件開發有限公司上海-浦東新區2.5萬/每月02-18滿員 '''

# 將文本內容按行放入列表
lines = content.splitlines() for line in lines: # 查找'萬/月' 在 字符串中什么地方
    pos2 = line.find('萬/月') if pos2 < 0: # 查找'萬/每月' 在 字符串中什么地方
        pos2 = line.find('萬/每月') # 都找不到
        if pos2 < 0: continue

    # 執行到這里,說明可以找到薪資關鍵字
    # 接下來分析 薪資 數字的起始位置
    # 方法是 找到 pos2 前面薪資數字開始的位置
    idx = pos2-1

    # 只要是數字或者小數點,就繼續往前面找
    while line[idx].isdigit() or line[idx]=='.': idx -= 1

    # 現在 idx 指向 薪資數字前面的那個字,
    # 所以薪資開始的 索引 就是 idx+1
    pos1 = idx + 1

    print(line[pos1:pos2])

 

運行一下,發現完全可以。

在你高興完之后,我們再看看寫的代碼。

怎么樣?

太麻煩了,是不是。

為了從每行獲取薪資對應的數字,我們 可是 寫了不少行代碼。

這種 從字符串中搜索出某種特征的子串 有沒有更簡單的方法呢?

解決方案就是我們今天要介紹的 正則表達式 。


如果我們使用正則表達式,代碼可以這樣

content = ''' Python3 高級開發工程師 上海互教教育科技有限公司上海-浦東新區2萬/月02-18滿員 測試開發工程師(C++/python) 上海墨鵾數碼科技有限公司上海-浦東新區2.5萬/每月02-18未滿員 Python3 開發工程師 上海德拓信息技術股份有限公司上海-徐匯區1.3萬/每月02-18剩余11人 測試開發工程師(Python) 赫里普(上海)信息科技有限公司上海-浦東新區1.1萬/每月02-18剩余5人 Python高級開發工程師 上海行動教育科技股份有限公司上海-閔行區2.8萬/月02-18剩余255人 python開發工程師 上海優似騰軟件開發有限公司上海-浦東新區2.5萬/每月02-18滿員 '''

import re p = re.compile(r'([\d.]+)萬/每{0,1}月') for one in p.findall(content): print(one)

運行一下看看,結果是一樣的。

但是代碼卻簡單多了。

正則表達式,是一種語法,用來描述你想搜索的字符串的特征。

下面這行代碼指定了一個正則表達式

p = re.compile(r'([\d.]+)萬/每{0,1}月')

compile 函數的參數,就是正則表達式字符串。

上面的例子里面指定了 搜索子串的特征 是 ([\d.]+)萬/每{0,1}月

為什么這么寫? 我們后面再介紹。

這個函數返回一個compile對象。

compile對象的 findall 方法返回所有匹配的子串,放在一個列表中。

 

使用正則表達式關鍵的地方在於, 如何寫出正確的表達式語法 。

正則表達式非常強大,語法非常復雜,點擊這里,參考Python官方文檔里面的描述 。具體的使用細節包括語法都在里面。

在線驗證

怎么驗證你寫的表達式 是否能正確匹配到要搜索的字符串呢?

大家可以訪問這個網址: https://regex101.com/

按照下面的示意圖片輸入 搜索文本 和 表達式,查看你的表達式是否能正確匹配到字符串。

image

 

常見語法

寫在正則表達式里面的普通字符都是表示: 直接匹配它們。

比如 你下面的文本中,如果你要找所有的 test, 正則表達式就非常簡單,直接輸入 test 即可。

如下所示:

image

漢字也是一樣,要尋找漢字,直接寫在正則表達式里面就可以了。


但是有些特殊的字符,術語叫 metacharacters(元字符)。

它們出現在正則表達式字符串中,不是表示直接匹配他們, 而是表達一些特別的含義。

這些特殊的元字符包括下面這些:

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

 

我們分別介紹一下它們的含義:

點-匹配所有字符

. 表示要匹配除了 換行符 之外的任何 單個 字符。

比如,你要從下面的文本中,選擇出所有的顏色。

蘋果是綠色的
橙子是橙色的
香蕉是黃色的
烏鴉是黑色的

也就是要找到所有 以  結尾,並且包括前面的一個字符的 詞語。

就可以這樣寫正則表達式 .色 。

其中 點 代表了任意的一個字符, 注意是一個字符。

.色 合起來就表示 要找 任意一個字符 后面是 色 這個字, 合起來兩個字的 字符串

驗證一下,如下圖所示

image

 

只要表達式正確,就可以寫在Python代碼中,如下所示

content = '''蘋果是綠色的 橙子是橙色的 香蕉是黃色的 烏鴉是黑色的'''

import re p = re.compile(r'.色') for one in p.findall(content): print(one)

運行結果如下

綠色
橙色
黃色
黑色

星號-重復匹配任意次

* 表示匹配前面的子表達式任意次,包括0次。

比如,你要從下面的文本中,選擇每行逗號后面的字符串內容,包括逗號本身。注意,這里的逗號是中文的逗號。

蘋果,是綠色的
橙子,是橙色的
香蕉,是黃色的
烏鴉,是黑色的
猴子,

就可以這樣寫正則表達式 ,.* 。

  • 緊跟在 . 后面, 表示 任意字符可以出現任意次, 所以整個表達式的意思就是在逗號后面的 所有字符,包括逗號

驗證一下,如下圖所示

image

特別是最后一行,猴子逗號后面沒有其它字符了,但是*表示可以匹配0次, 所以表達式也是成立的。

 

只要表達式正確,就可以寫在Python代碼中,如下所示

content = '''蘋果,是綠色的 橙子,是橙色的 香蕉,是黃色的 烏鴉,是黑色的 猴子,'''

import re p = re.compile(r',.*') for one in p.findall(content): print(one)

運行結果如下

,是綠色的
,是橙色的
,是黃色的
,是黑色的
,

注意, .* 在正則表達式中非常常見,表示匹配任意字符任意次數。

當然這個 * 前面不是非得是 點 ,也可以是其它字符,比如

image

 

加號-重復匹配多次

+ 表示匹配前面的子表達式一次或多次,不包括0次。

比如,還是上面的例子,你要從文本中,選擇每行逗號后面的字符串內容,包括逗號本身。

但是 添加一個條件, 如果逗號后面 沒有內容,就不要選擇了。

比如,下面的文本中,最后一行逗號后面 沒有內容,就不要選擇了。

蘋果,是綠色的
橙子,是橙色的
香蕉,是黃色的
烏鴉,是黑色的
猴子,

就可以這樣寫正則表達式 ,.+ 。

驗證一下,如下圖所示

image

最后一行,逗號后面沒有其它字符了,+表示至少匹配1次, 所以最后一行沒有子串選中。

問號-匹配0-1次

? 表示匹配前面的子表達式0次或1次。

比如,還是上面的例子,你要從文本中,選擇每行逗號后面的1個字符,也包括逗號本身。

蘋果,綠色的
橙子,橙色的
香蕉,黃色的
烏鴉,黑色的
猴子,

就可以這樣寫正則表達式 ,.? 。

驗證一下,如下圖所示

image

最后一行,逗號后面沒有其它字符了,但是?表示匹配1次或0次, 所以最后一行也選中了一個逗號字符。

花括號-匹配指定次數

花括號表示 前面的字符匹配 指定的次數 。

比如 ,下面的文本

紅彤彤,綠油油,黑乎乎乎乎,綠油油油油

表達式 油{3} 就表示匹配 連續的 油 字 3次

表達式 油{3,4} 就表示匹配 連續的 油 字 至少3次,至多 4 次

就只能匹配 后面的,如下所示:

image

貪婪模式和非貪婪模式

我們要把下面的字符串中的所有html標簽都提取出來,

source = '<html><head><title>Title</title>'

得到這樣的一個列表

['<html>', '<head>', '<title>', '</title>']

很容易想到使用正則表達式 <.*>

寫出如下代碼

source = '<html><head><title>Title</title>'

import re p = re.compile(r'<.*>') print(p.findall(source))

但是運行結果,卻是

['<html><head><title>Title</title>']

怎么回事? 原來 在正則表達式中, ‘*’, ‘+’, ‘?’ 都是貪婪地,使用他們時,會盡可能多的匹配內容,

所以, <.*> 中的 星號(表示任意次數的重復),一直匹配到了 字符串最后的 </title> 里面的e。

解決這個問題,就需要使用非貪婪模式,也就是在星號后面加上 ? ,變成這樣 <.*?>

代碼改為

source = '<html><head><title>Title</title>'

import re # 注意多出的問號
p = re.compile(r'<.*?>') print(p.findall(source))

對元字符的轉義

反斜杠 \ 在正則表達式中有多種用途。

比如,我們要在下面的文本中搜索 所有點前面的字符串,也包含點本身

蘋果.是綠色的
橙子.是橙色的
香蕉.是黃色的

如果,我們這樣寫正則表達式 .*. , 聰明的你肯定發現不對勁。

因為 點 是一個 元字符, 直接出現在正則表達式中,表示匹配任意的單個字符, 不能表示 . 這個字符本身的意思了。

怎么辦呢?

如果我們要搜索的內容本身就包含元字符,就可以使用 反斜杠進行轉義。

這里我們就應用使用這樣的表達式: .*\.

示例,Python程序如下

content = '''蘋果.是綠色的 橙子.是橙色的 香蕉.是黃色的'''

import re p = re.compile(r'.*\.') for one in p.findall(content): print(one) 運行結果如下 蘋果. 橙子. 香蕉.

匹配某種字符類型

反斜杠后面接一些字符,表示匹配 某種類型 的一個字符。

比如

\d 匹配0-9之間任意一個數字字符,等價於表達式 [0-9]

\D 匹配任意一個不是0-9之間的數字字符,等價於表達式 [^0-9]

\s 匹配任意一個空白字符,包括 空格、tab、換行符等,等價於表達式 [\t\n\r\f\v]

\S 匹配任意一個非空白字符,等價於表達式 [^ \t\n\r\f\v]

\w 匹配任意一個文字字符,包括大小寫字母、數字、下划線,等價於表達式 [a-zA-Z0-9_]

缺省情況也包括 Unicode文字字符,如果指定 ASCII 碼標記,則只包括ASCII字母

\W 匹配任意一個非文字字符,等價於表達式 [^a-zA-Z0-9_]

 

反斜杠也可以用在方括號里面,比如 [\s,.] 表示匹配 : 任何空白字符, 或者逗號,或者點

方括號-匹配幾個字符之一

方括號表示要匹配 指定的幾個字符之一 。

比如

[abc] 可以匹配 a, b, 或者 c 里面的任意一個字符。等價於 [a-c] 。

[a-c] 中間的 - 表示一個范圍從a 到 c。

如果你想匹配所有的小寫字母,可以使用 [a-z]

 

一些 元字符 在 方括號內 失去了魔法, 變得和普通字符一樣了。

比如

[akm.] 匹配 a k m . 里面任意一個字符

這里 . 在括號里面不在表示 匹配任意字符了,而就是表示匹配 . 這個 字符

如果在方括號中使用 ^ , 表示  方括號里面的字符集合。

比如

content = 'a1b2c3d4e5'

import re p = re.compile(r'[^\d]' ) for one in p.findall(content): print(one)

[^\d] 表示,選擇非數字的字符

輸出結果為:

a
b
c
d
e

起始、結尾位置 和 單行、多行模式

^ 表示匹配文本的 開頭 位置。

正則表達式可以設定 單行模式 和 多行模式

如果是 單行模式 ,表示匹配 整個文本 的開頭位置。

如果是 多行模式 ,表示匹配 文本每行 的開頭位置。

比如,下面的文本中,每行最前面的數字表示水果的編號,最后的數字表示價格

001-蘋果價格-60002-橙子價格-70, 003-香蕉價格-80,

如果我們要提取所有的水果編號,用這樣的正則表達式 ^\d+

 

上面的正則表達式,使用在Python程序里面,如下所示

content = '''001-蘋果價格-60 002-橙子價格-70 003-香蕉價格-80'''

import re p = re.compile(r'^\d+', re.M) for one in p.findall(content): print(one)

注意,compile 的第二個參數 re.M ,指明了使用多行模式,

運行結果如下

001
002
003

如果,去掉 compile 的第二個參數 re.M, 運行結果如下

001

就只有第一行了。

因為單行模式下,^ 只會匹配整個文本的開頭位置。

$ 表示匹配文本的 結尾 位置。

如果是 單行模式 ,表示匹配 整個文本 的結尾位置。

如果是 多行模式 ,表示匹配 文本每行 的結尾位置。

比如,下面的文本中,每行最前面的數字表示水果的編號,最后的數字表示價格

001-蘋果價格-60002-橙子價格-70, 003-香蕉價格-80,

如果我們要提取所有的水果編號,用這樣的正則表達式 \d+$

對應代碼

content = '''001-蘋果價格-60 002-橙子價格-70 003-香蕉價格-80'''

import re p = re.compile(r'\d+$', re.MULTILINE) for one in p.findall(content): print(one)

注意,compile 的第二個參數 re.MULTILINE ,指明了使用多行模式,

運行結果如下

60
70
80
如果,去掉 compile 的第二個參數 re.MULTILINE, 運行結果如下
80

就只有最后一行了。

因為單行模式下,$ 只會匹配整個文本的結束位置。

豎線-匹配兩者之一

豎線表示 匹配 前者 或 后者 。

比如 ,

image

特別要注意的是, 豎線在正則表達式的優先級是最低的, 這就意味着,豎線隔開的部分是一個整體

比如 綠色|橙 表示 要匹配是 綠色 或者  ,

而不是 綠色 或者 綠橙

括號-組選擇

括號稱之為 正則表達式的 組選擇。 是從正則表達式 匹配的內容 里面 扣取出 其中的某些部分

前面,我們有個例子,從下面的文本中,選擇每行逗號前面的字符串,也 包括逗號本身 。

蘋果,蘋果是綠色的
橙子,橙子是橙色的
香蕉,香蕉是黃色的

就可以這樣寫正則表達式 ^.*, 。

但是,如果我們要求 不要包括逗號 呢?

當然不能直接 這樣寫 ^.*

因為最后的逗號 是 特征 所在, 如果去掉它,就沒法找 逗號前面的了。

但是把逗號放在正則表達式中,又會包含逗號。

解決問題的方法就是使用 組選擇符 : 括號。

我們這樣寫^(.*), ,結果如下

image

大家可以發現,我們把要從整個表達式中提取的部分放在括號中,這樣 水果 的名字 就被單獨的放在 組 group 中了。

對應的Python代碼如下

content = '''蘋果,蘋果是綠色的 橙子,橙子是橙色的 香蕉,香蕉是黃色的'''

import re p = re.compile(r'^(.*),', re.MULTILINE) for one in p.findall(content): print(one)

 

分組,還可以多次使用。

比如,我們要從下面的文本中,提取出每個人的 名字 和對應的 手機號

張三,手機號碼15945678901
李四,手機號碼13945677701
王二,手機號碼13845666901

可以使用這樣的正則表達式 ^(.+),.+(\d{11})

可以寫出如下的代碼

content = '''張三,手機號碼15945678901 李四,手機號碼13945677701 王二,手機號碼13845666901'''

import re p = re.compile(r'^(.+),.+(\d{11})', re.MULTILINE) for one in p.findall(content): print(one)

回到開頭的例子

有了上面的知識,我們再來看 本文開始的例子

從下面的文本里面抓取 所有職位的薪資。

Python3 高級開發工程師 上海互教教育科技有限公司上海-浦東新區2萬/月02-18滿員 測試開發工程師(C++/python) 上海墨鵾數碼科技有限公司上海-浦東新區2.5萬/每月02-18未滿員 Python3 開發工程師 上海德拓信息技術股份有限公司上海-徐匯區1.3萬/每月02-18剩余11人 測試開發工程師(Python) 赫里普(上海)信息科技有限公司上海-浦東新區1.1萬/每月02-18剩余5人 

我們使用的表達式是 ([\d.]+)萬/每{0,1}月

為什么這么寫呢?

[\d.]+ 表示 匹配 數字或者點的多次出現 這就可以匹配像: 3 33 33.33 這樣的 數字

萬/每{0,1}月 是后面緊接着的,如果沒有這個,就會匹配到別的數字, 比如 Python3 里面的3。

其中 每{0,1}月 這部分表示匹配 每月 每 這個字可以出現 0次或者1次。

聰明的你能想到,還可以用什么來表示這個 每{0,1}月 嗎?

對啦,還可以用 每?月 因為問號表示 前面的字符匹配0次或者1次

使用正則表達式切割字符串

字符串 對象的 split() 方法只適應於非常簡單的字符串分割情形。當你需要更加靈活的切割字符串的時候,就不好用了。

比如,我們需要從下面字符串中提取武將的名字。

我們發現這些名字之間, 有的是分號隔開 ,有的是逗號隔開,有的是空格隔開, 而且分割符號周圍還有不定數量的空格

names = '關羽; 張飛, 趙雲,馬超, 黃忠 諸葛亮'

 

這時,最好使用正則表達式里面的 split 方法:

import re names = '關羽; 張飛, 趙雲, 馬超, 黃忠 諸葛亮' namelist = re.split(r'[;,\s]\s*', names) print(namelist)

正則表達式 [;,\s]\s* 指定了,分割符為 分號、逗號、空格 里面的任意一種均可,並且 該符號周圍可以有不定數量的空格。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM