Python3正則表達式(Python3 Regular Expression)

正則表達式是一種通用的工具,並不只屬於Python語言,基本大部分語言都封裝好了這個工具。

引子

正則表達式(Regular Expression)是一種用於做字符串匹配的工具,它能夠非常方便地從一段文本中找到/匹配出符合一定要求/規律的目標字符串。

但是我們什麼情況下要做字符串匹配呢?而且爲什麼要用正則表達式做呢,直接用一對一的去對不行嗎?舉一個簡單的例子來回答上面的問題。

比如說,我們有如下一段文本,假設這是你寫的一段日記:

我昨天認識了一個女孩A,她給了我她的郵箱[email protected],和她道別後,又遇到另一個女孩B,她也給了我她的郵箱[email protected],沒想到,我之後又遇見了第三個女孩C......

那麼,現在,你肯定想做的事情就是給這些遇見的女孩子們羣發約會的邀請信息,但是想偷懶的你不想手動一個個地去從上述文本里肉眼查找,然後複製粘貼每個女孩的郵箱(數量少時,你手動做肯定沒問題)。於是,你想能不能用一個工具自動提取出所有日記裏面的郵箱地址出來。

然後你也想到,首先每個人郵箱的名字都不一樣,可能是各種數字與字母的組合,其次郵箱所屬的機構名(163, sina, qq, gmail, outlook)也可能不一樣,域名(.com, .net, .cn)也可能不一樣,那麼這可怎麼匹配?

但是,聰明的你總結出了一條規律:郵箱不過就是【若干個字符(郵箱名)+@+若干個字符(機構名)+.+若干個字符(域名)】,如果程序能夠懂這個模式就能挑選出字符串了!

現在告訴你,正則表達式就可以做到,它就可以按照【若干個字符(郵箱名)+@符號+若干個字符(機構名)+.+若干個字符(域名)】的模式去從文本里把所有符合這個模式的字符串全部找出來。

用python3具體做法如下:

import re
pattern=re.compile(r'[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}')
text='''我昨天認識了一個女孩A,她給了我她的郵箱[email protected],
和她道別後,又遇到另一個女孩B,她也給了我她的郵箱[email protected],
沒想到,我之後又遇見了第三個女孩C......'''
match = pattern.findall(text)
for email in match:
    print(email)
# [email protected]
# [email protected]

搞定!

目前你看不懂上面的代碼沒關係,下面我們來一一講解。

語法

要想用好正則表達式,首先要學習正則表達式的使用語法/使用規則。

圖來自Python正則表達式指南

我們按照圖裏面的順序來分別講解不同正則表達式不同部分的語法:

字符

  • 一般字符

如”a”,”b”,”g”,”4”,”,”等這種比較常用的字符,在正則表達式中都是一對一地匹配和自身相同的字符,沒什麼特別。

import re
text='abcdefg'
match=re.search('cd',text)
if match:
  print(match.group())
# cd 匹配到cd
  • .(點)

.(一個點)用於匹配任意除了換行符”\n”以外的字符。

import re
text='ab3defg'
match=re.search('b..',text)
if match:
  print(match.group())
# b3d 匹配到b及其後緊跟的兩個任意字符
  • \(反斜槓)

反斜槓表示轉義字符,它將使得緊跟在它後面的字符轉變成特殊的含義,或者消除特殊字符本身的特殊含義。(注意,後面的\d 和\w等正則本身包含的元字符中的反斜槓不屬於此類轉義作用,而是正則中規定好的組合,元字符裏的反斜槓就是普通的反斜槓字符。這一點目前看不懂沒關係,結合後面的注意事項部分理解)

比如當\與.(點)結合在一起,點就不再是匹配任意單個字符,而是確實地匹配一個點(消除特殊含義)。

  • […]

中括號中放上任意多的字符,則這些字符會構成一個字符集合,這個模式將會在遇到集合中的任意一個字符時都認定爲匹配。

import re
text='abc345def'
match=re.search('[345]',text)
if match:
  print(match.group())
# bc3 由於匹配到了(3,4,5)中的3,不再繼續向後匹配。

match=re.search('[b5aef]',text)
if match:
  print(match.group())
# a  由於匹配到了(b,5,a,e,f)中的a,不再繼續向後匹配

預定義字符集

  • \d \D 數字字符

d是decimal(十進位的)首字母,代表數字。w

\d用來匹配任意單個數字(0到9),而\D則是匹配任意的非數字(大寫就是反過來,比較好記)

import re
text='abc345def'
match=re.search('\d\d',text)
if match:
  print(match.group())
# 34 匹配到連續的兩個數字

match=re.search('\D\D',text)
if match:
  print(match.group())
# ab 匹配到連續的兩個非數字字符
  • \s \S 空白字符

s代表space(空格,空白),可以匹配空格,\t製表符,\r,\n換行符,\f,\v這幾個不會在屏幕上輸出可見字符的字符。

import re
text='abc3 45def'
match=re.search('\d\s\d',text)
if match:
  print(match.group())
# 3 4 匹配到數字+空格+數字的三個連續字符的組合
  • \w \W 單詞字符

w代表word(單詞),能構成word的字符包括大小寫的字母(a-z和A-Z),也包括數字(0-9),還包括下劃線_。

import re
text='_a3%sdef'
match=re.search('\w\w\w\w',text)
if match:
  print(match.group())
# sdef 匹配到連續的四個單詞字符,但不能匹配"_a3%",因爲%是非單詞字符

數量詞

首先明白,數量詞不能單獨使用,是配合前面的字符使用,用以描述對前面的字符進行什麼數量的匹配。

  • *(星號)

星號*表示對前面的字符串進行0次或多次匹配(多次即大於等於1次)。

import re
text='_a34534%1sdef'
match=re.search('a\d*',text)
if match:
  print(match.group())
# a34534 匹配到a與其後的多個數字,直到遇到第一個非數字的%號停止匹配。

match=re.search('a\s*\d*',text)
if match:
  print(match.group())
# a34534 依然是匹配到a與其後的多個數字。雖然在字符a與數字之間沒有任何空白字符,但是由於星號允許匹配0個(即沒有),所以也算匹配。

簡而言之,星號是有則匹配,沒有也不要緊,繼續往後走。

    • (加號)

加號相對星號就“真的很嚴格~~”,因爲它要求前面的字符至少匹配一次,多次也ok。

承接上面星號中的例子,只是把”\s*”改成了”\s+”:

import re
text='_a34534%1sdef'
match=re.search('a\s+\d*',text)
if match:
  print(match.group())

輸出爲空白,因爲字符a和後面的數字之間根本沒有空白字符,於是匹配不成功,返回空。

import re
text='_a34534%1sdef'
match=re.search('a\d+',text)
if match:
  print(match.group())
# a34534 這樣就還是能匹配到字符a及後面的數字。
  • ? (問號)

問號就是確認一下有木有,有或沒有都行,但是你有的話,就只能有一個,不準多了,多了它不負責匹配。

import re
text='_a34534%1sdef'
match=re.search('a\d?',text)
if match:
  print(match.group())
# a3 匹配到a及後面緊接的一個數字,不再繼續向後匹配。

match=re.search('a\d*!?',text)
if match:
  print(match.group())
# a34534 匹配到a及後面的數字,又嘗試匹配數字後的感嘆號,發現沒有(實際是百分號),於是停止匹配。
  • {m}

這個就比較簡單,就是匹配前一個字符m次。

import re
text='_a34534%1sdef'
match=re.search('\d{3}',text)
if match:
  print(match.group())
# 345 匹配數字字符三次。
  • {m,n}

這個也很簡單,就是匹配n到m次,即大於等於n,小於等於m的任一次數都可以。不過會盡量往多的次數匹配。

import re
text='_a34534%1sdef'
match=re.search('\d{1,4}',text)
if match:
  print(match.group())
# 3453 匹配數字1-4次,因爲儘量往多的匹配,匹配了4個數字

match=re.search('\d{1,3}s',text)
if match:
  print(match.group())
# 1s 匹配數字1-3次及後面的s字符
  • *? +? ?? {m,n}?

注意,這裏的問號不是前面講的“匹配字符0或1次”的那個問號的作用,而是用於聲明使用懶惰模式(或者叫非貪婪模式)來進行匹配。

什麼叫貪婪模式?即默認情況下,凡是涉及到數量詞的匹配,正則表達式的匹配工具都是儘可能地往次數多的去匹配的(例子見上面講{m,n}時的第一個示例代碼),這就是貪婪模式(儘可能多就是很“貪婪”嘛~)。而非貪婪模式/懶惰模式就是相反地做儘可能少的匹配。

兩種情況下,星號、加號、問號、{m,n}實際上就分別成了以下作用:

符號 貪婪模式 非貪婪模式
* 多次(無限次) 0次
+ 多次(無限次) 1次
1次 0次
{m,n} n次 m次

舉個簡單的例子:

import re
text='_a34534%1sdef'
match=re.search('\d{2,3}',text)
if match:
  print(match.group())
# 345 匹配到連續的三個數字(貪婪模式)

match=re.search('\d{2,3}?',text)
if match:
  print(match.group())
# 34 匹配到連續的兩個數字(非貪婪模式)

邊界匹配

  • ^ $

^代表匹配字符串的開頭(在多行模式中匹配每一行的開頭)。$代表匹配字符串的末尾(在多行模式中匹配每一行的末尾)。

這兩個符號主要是針對只想匹配文本的開頭或結尾部分的情況使用的。而當一個正則表達式同時使用兩個符號”^XXXXXX$”時,意思就是對字符串進行從頭到尾的整體匹配。

比如我們在任意網站註冊賬號時要求填入郵箱信息,那麼網站的後臺一般會對郵箱做合法性檢驗,那麼就會對我們輸入的郵箱地址做從頭到尾的正則匹配,看是否符合要求。

import re
text='%a1234'
match=re.search('a\d{4}',text)
if match:
  print(match.group())
# a1234 匹配到a和連續的4個數字

match=re.search('^a\d{4}',text)
if match:
  print(match.group())
# 輸出空白,因爲從頭開始匹配,而開頭是百分號,不是a,匹配失敗。
  • \A \Z

\A和^的作用相同,\Z和$的作用相同。

邏輯、分組

  • (…)分組

圓括號會將包含在裏面的所有內容作爲一個分組的整體,我們可以在分組加數量詞來匹配分組若干次。

import re
text='_a34a534b123%1sdef'
match=re.search('(a\d{2,3})+',text)
if match:
  print(match.group())
# a34a534 匹配分組[字符a+2-3個數字]一次或多次
  • |(豎線)

豎線本身會將正則表達式切分成左右兩個部分,而豎線的作用就是:優先匹配左邊的部分,如果匹配到了,就完成匹配;如果未匹配到,再匹配右邊的部分,如果匹配到了,就完成匹配,否則,匹配失敗。

import re
text='_a34a524b1234%1sdef'
match=re.search('[a-z]\d{5}|[a-z]\d{4}',text)
if match:
  print(match.group())
# b1234 豎線左邊小寫字母和5個數字的匹配未成功,轉而匹配右邊小寫字母和4個數字的匹配成功。

常用的正則語法也就是上面這些,其它的大家可以再需要用到時再回來看和理解,這裏就先不詳細講解了。

注意事項

正則表達式前的r是什麼

r表示raw(原生的,未加工的),放在字符串前,表示這個字符串爲原生字符串,而原生字符串指的是區別於正則表達式的一般字符串。

強調原生字符串這種概念的主要原因是針對反斜槓(\)的問題。因爲反斜槓在一般編程語言的字符串裏的含義是轉義符,轉義符的作用在前面也有提到過,就是轉換或消除後面緊跟的一個字符的特殊含義。即單個的反斜槓出現時,就會被認爲是在對緊跟在它後面的一個字符進行轉義操作。

但是問題是,有時候我們可能想匹配的文本就是一個包含有反斜槓字符的字符串,那麼我們在正則裏面就需要把反斜槓的轉義的作用給消除掉,而表達出我們就是要匹配一個反斜槓字符的意思。

首先,我們要明白的是,如果我們想在一個普通字符串裏面表達一個反斜槓字符,而非轉義符時,應該這麼做:

string='abc\ndef'
print(string)
# abc
# def
# 會分兩行輸出abc和def,因爲\n中的反斜槓被認爲成轉義符,和n結合時就成了換行符。


# 第一種方法:反斜槓+反斜槓
string='abc\\ndef'
print(string)
# abc\ndef
# 只輸出一行,因爲\\n中的第一個反斜槓作爲轉義符,將後面的第二個反斜槓給轉義了,使其成爲了普通的反斜槓字符(消除特殊含義)。


# 第二種方法:聲明原生字符串
string=r'abc\ndef'
print(string)
# abc\ndef
# 只輸出一行,因爲被聲明成原生字符串的字符串,其裏面所有的字符都將被消除特殊含義,於是單個的反斜槓也不是轉義符了,而成了普通的反斜槓字符。

好,回到我們的正則表達式,正則表達式裏如果我們想寫一個能匹配反斜槓的表達式,怎麼寫呢?

import re
text=r'%abc\ndef'
match=re.search('\\\\',text)
if match:
  print(match.group())
# \

對,沒有錯,你得寫四個反斜槓才行!!!原因是,一般的字符串裏你想表達一個反斜槓字符得用兩個反斜槓(\),但是在正則表達式裏規定了:表達一個普通字符串裏的反斜槓字符需要用兩個反斜槓字符(很繞很奇怪我知道- -!),於是這代表着,你需要組合兩個反斜槓字符(\),即四個反斜槓一起,才能在正則表達式裏面表達出一個反斜槓字符的含義

於是,爲了簡化這個問題,防止在正則表達式裏面寫反斜槓寫到暈厥,我們可以用原生字符串的方法來減少麻煩:

import re
text=r'%abc\ndef'
match=re.search(r'\\',text) # 不是轉義
if match:
  print(match.group())
# \

即,當你把正則表達式聲明爲原生字符串時,裏面所有的反斜槓也不再具備轉義含義,而是純粹的反斜槓字符,於是只用寫兩個就可以表示普通字符串裏面的一個反斜槓字符了。(所以追根溯源,這個坑就是因爲正則裏面規定了表達式裏的兩個反斜槓字符才能算普通字符串裏的一個反斜槓字符)。

總結:一般情況下,我們也提倡在正則表達式的前面加個r聲明這是原生字符串,因爲可以減少匹配反斜槓字符時候的麻煩(寫兩個反斜槓就夠了),也不影響其它正則元字符的正常使用(\d,\s,\w等),該咋寫咋寫。

這個部分比較繞,不過實際情況中也並沒有很多需要做反斜槓字符匹配的地方,所以不太理解此部分問題也不大,能夠順暢使用正則表達式裏的上述其它語法也已經可以應對絕大多數匹配問題了。

另外,字符串前面有時還有帶一個u字母的,意思是表示該字符串是一個unicode編碼的字符串。詳情參見python字符串前綴 u和r的區別

Python3正則工具

發佈了81 篇原創文章 · 獲贊 193 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章