32.企業級開發進階4:正規表示式

大牧莫邪發表於2017-06-05

本節內容,要講解的和我們的資訊檢索有關係,這一方面也是Python在目前非常流行的一個應用方向:爬蟲。

本節內容

  • 什麼是正規表示式
  • 正規表示式入門程式
  • python中的正規表示式模組介紹
  • 正規表示式元字元匹配
  • 正規表示式量詞匹配
  • 正規表示式範圍匹配
  • 正規表示式分組匹配
  • 正規表示式的貪婪模式和懶惰模式
  • 正規表示式特殊匹配

1. 什麼是正規表示式

正規表示式:也成為規則表示式,英文名稱Regular Expression,我們在程式中經常會縮寫為regex或者regexp,專門用於進行文字檢索、匹配、替換等操作的一種技術。 注意:正規表示式是一種獨立的技術,並不是某程式語言獨有的

關於正規表示式的來歷 long long logn years ago,美國新澤西州的兩個人類神經系統工作者,不用幹正事也能正常領工資的情況下,有段時間閒的發慌,於是他們開始研究一個課題~怎麼使用數學方式來描述人類的神經網路。

這一研究,還真是搞事!另一個數學家Stephen Kleene根據他們的研究基礎,通過數學演算法處理,釋出了《神經網事件表示法》,利用的就是正則集合的數學符號描述這個模型,正規表示式的概念進入了人們的視線。

又來了一個搞事的人~某個傢伙上學學完正常的課程之後(這事在中國貌似發生不了),開始搗鼓計算機作業系統,並且搞出了現在在軟體行業非常出名的系統:Unix,它就是Unix之父Ken Thompson,這個傢伙也看到了那個數學家釋出的論文,於是將正規表示式經過優化處理之後,引入到了Unix作業系統中專門用於文字的高效檢索。

一發不可收拾,正規表示式,開始陸陸續續在各個程式語言中出現,並以它優雅的表現形式和高效的工作態度,而著名於各個語言和行業方向。

正規表示式,是一種特殊的符號,這樣的符號是需要解釋才能使用的,也就是需要正規表示式引擎來進行解釋,目前正規表示式的引擎主要分三種:DFA,NFA、POSIX NFA,有興趣了正規表示式引擎的童鞋,可以自己檢視資料

2. 正規表示式語法結構

接下來,我們開始瞭解這樣一個神祕的可以類似人類神經網路一樣思考問題的技術的語法結構。 注意:我們通過python程式進行測試,但是正規表示式的語法結構在各種語言環境中都是通用的。

2.1. 入門案例:瞭解正規表示式

我們通過一個簡單的案例入手:通常情況下,我們會驗證使用者輸入的手機號碼是否合法,是否156/186/188開頭的手機號碼,如果按照常規驗證手段,就需要對字串進行拆分處理,然後逐步匹配

重要提示:python中提供了re模組,包含了正規表示式的所有功能,專門用於進行正規表示式的處理;

我們首先看一下,常規的手機號碼驗證過程 ```

userphone = input("請輸入手機號碼:")

# 驗證使用者手機號碼是否合法的函式
def validatePhone(phone):
    msg = "提示資訊:請輸入手機號碼"
    # 判斷輸入的字元的長度是否合法
    if len(phone) == 11:
        # 判斷是否156/186/188開頭
        if phone.startswith("156") or phone.startswith("186") or phone.startswith("188"):
            # 判斷每一個字元都是數字
            for num in phone:
                # isdigit()函式用於判斷呼叫者是否數字
                if not num.isdigit():
                    msg = "不能包含非法字元"
                    return msg
            msg = "手機號碼合法"
        else:
            msg = "開頭數字不合法"
    else:
        msg = "長度不合法"
    return msg

# 開始測試
print(validatePhone(userphone))

``` 執行上面的程式碼,分別輸入不同的手機號碼,結果如下

請輸入手機號碼:188 長度不合法

請輸入手機號碼:15568686868 開頭數字不合法

請輸入手機號碼:1566868686a 不能包含非法字元

請輸入手機號碼:15688888888 手機號碼合法

我們再次使用正規表示式來改造這段程式 注意:如果下面的程式中出現了一些語法不是很明白,沒關係,後面會詳細講解 ```

import re

# 接收使用者輸入
userphone = input("請輸入手機號碼")

# 定義驗證手機號碼的函式
def validatePhone(phone):
    # 定義正規表示式,Python中的正規表示式還是一個字串,是以r開頭的字串
    regexp = r"^(156|186|188)\d{8}$"
    # 開始驗證
    if re.match(regexp, phone):
        return "手機號碼合法"
    else:
        return "手機號碼只能156/186/188開頭,並且每一個字元都是數字,請檢查"

# 開始驗證
print(validatePhone(userphone))

``` 執行上面的程式碼,我們得到正常驗證的結果,大家可以自己試一試。 我們從這兩套程式碼中,可以看出來,使用了正規表示式之後的程式變得非常簡潔了,那保持好你的衝動和熱情,讓正規表示式來搞事吧

2.3. python中的正規表示式模組re

python提供的正規表示式處理模組re,提供了各種正規表示式的處理函式

2.3.1 字串查詢匹配的函式:

  • |函式|描述|
  • |--|:--|
  • |re.match(reg, info)|用於在開始位置匹配目標字串info中符合正規表示式reg的字元,匹配成功會返回一個match物件,匹配不成功返回None|
  • |re.search(reg, info)|掃描整個字串info,使用正規表示式reg進行匹配,匹配成功返回匹配的第一個match物件,匹配不成功返回None|
  • |re.findall(reg, info)|掃描整個字串info,將符合正規表示式reg的字元全部提取出來存放在列表中返回|
  • |re.fullmatch(reg, info)|掃描整個字串,如果整個字串都包含在正規表示式表示的範圍中,返回整個字串,否則返回None|
  • |re.finditer(reg, info)|掃描整個字串,將匹配到的字元儲存在一個可以遍歷的列表中|

參考官方re.py原始碼如下: ```

def match(pattern, string, flags=0):
    """Try to apply the pattern at the start of the string, returning
    a match object, or None if no match was found."""
    return _compile(pattern, flags).match(string)

def fullmatch(pattern, string, flags=0):
    """Try to apply the pattern to all of the string, returning
    a match object, or None if no match was found."""
    return _compile(pattern, flags).fullmatch(string)

def search(pattern, string, flags=0):
    """Scan through string looking for a match to the pattern, returning
    a match object, or None if no match was found."""
    return _compile(pattern, flags).search(string)

def findall(pattern, string, flags=0):
    """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."""
    return _compile(pattern, flags).findall(string)

def finditer(pattern, string, flags=0):
    """Return an iterator over all non-overlapping matches in the
    string.  For each match, the iterator returns a match object.

    Empty matches are included in the result."""
    return _compile(pattern, flags).finditer(string)

```

2.3.2 字串拆分替換的函式:

|函式|描述| * |--|:--| * |re.split(reg, string)|使用指定的正規表示式reg匹配的字元,將字串string拆分成一個字串列表,如:re.split(r"\s+", info),表示使用一個或者多個空白字元對字串info進行拆分,並返回一個拆分後的字串列表| * |re.sub(reg, repl, string)|使用指定的字串repl替換目標字串string匹配正規表示式reg的字元|

參考官方原始碼如下: ```

def split(pattern, string, maxsplit=0, flags=0):
    """Split the source string by the occurrences of the pattern,
    returning a list containing the resulting substrings.  If
    capturing parentheses are used in pattern, then the text of all
    groups in the pattern are also returned as part of the resulting
    list.  If maxsplit is nonzero, at most maxsplit splits occur,
    and the remainder of the string is returned as the final element
    of the list."""
    return _compile(pattern, flags).split(string, maxsplit)

def sub(pattern, repl, string, count=0, flags=0):
    """Return the string obtained by replacing the leftmost
    non-overlapping occurrences of the pattern in string by the
    replacement repl.  repl can be either a string or a callable;
    if a string, backslash escapes in it are processed.  If it is
    a callable, it's passed the match object and must return
    a replacement string to be used."""
    return _compile(pattern, flags).sub(repl, string, count)

```

接下來,我們進入正規表示式乾貨部分

2.4. 正規表示式中的元字元

在使用正規表示式的過程中,一些包含特殊含義的字元,用於表示字串中一些特殊的位置,非常重要,我們先簡單瞭解一下一些常用的元字元

  • |元字元|描述|
  • |--|:--|
  • |^|表示匹配字串的開頭位置的字元|
  • |$|表示匹配字串的結束位置的字元|
  • |.|表示匹配任意一個字元|
  • |\d|匹配一個數字字元|
  • |\D|匹配一個非數字字元|
  • |\s|匹配一個空白字元|
  • |\S|匹配一個非空白字元
  • |\w|匹配一個數字/字母/下劃線中任意一個字元|
  • |\W|匹配一個非數字字母下劃線的任意一個字元|
  • |\b|匹配一個單詞的邊界|
  • |\B|匹配不是單詞的開頭或者結束位置|

上乾貨:程式碼案例 ```

# 匯入正規表示式模組
import re

# 定義測試文字字串,我們後續在這段文字中查詢資料
msg1 = """Python is an easy to learn, powerful programming language.
It has efficient high-level data structures and a simple but effective approach to object-oriented programming.
Python’s elegant syntax and dynamic typing, together with its interpreted nature, 
make it an ideal language for scripting and rapid application development in many areas on most platforms.
"""
msg2 = "hello"
msg3 = "hello%"

# 定義正規表示式,匹配字串開頭是否為python
regStart = r"efficient"

# 從字串開始位置匹配,是否包含符合正規表示式的內容,返回匹配到的字串的Match物件
print(re.match(regStart, msg1))
# 掃描整個字串,是否包含符合正規表示式的內容,返回匹配到的第一個字串的Match物件
print(re.search(regStart, msg1))
# 掃描整個字串,是否包含符合正規表示式的內容,返回匹配到的所有字串列表
print(re.findall(regStart, msg1))
# 掃描整個字串,是否包含符合正規表示式的內容,返回匹配到的字串的迭代物件
for r in re.finditer(regStart, msg1):
    print("->"+ r.group())
# 掃描整個字串,是否包含在正規表示式匹配的內容中,是則返回整個字串,否則返回None
print(re.fullmatch(r"\w*", msg2))
print(re.fullmatch(r"\w*", msg3))

``` 上述程式碼執行結果如下:

~ None ~ ~['efficient'] ~->efficient ~ ~None

2.5. 正規表示式中的量詞

正規表示式中的量詞,是用於限定數量的特殊字元

|量詞|描述| * |--|:--| * |x*|用於匹配符號*前面的字元出現0次或者多次| * |x+|用於匹配符號+前面的字元出現1次或者多次| * |x?|用於匹配符號?前面的字元出現0次或者1次| * |x{n}|用於匹配符號{n}前面的字元出現n次| * |x{m,n}|用於匹配符號{m,n}前面的字元出現至少m次,最多n次| * |x{n, }|用於匹配符號{n, }前面的字元出現至少n次|

接上程式碼乾貨: ```

# 匯入正規表示式模組
import re

# 定義測試文字字串,我們後續在這段文字中查詢資料
msg1 = """goodgoodstudy!,dooodooooup"""

# 匹配一段字串中出現單詞o字元0次或者多次的情況
print(re.findall(r"o*", msg1))
# 匹配一段字串中出現單詞o字元1次或者多次的情況
print(re.findall(r"o+", msg1))
# 匹配一段字串中出現單詞o字元0次或者1次的情況
print(re.findall(r"o?", msg1))
# 匹配字串中連續出現2次字元o的情況
print(re.findall(r"o{2}", msg1))
# 匹配字串中連續出現2次以上字元o的情況
print(re.findall(r"o{2,}", msg1))
# 匹配字串中連續出現2次以上3次以內字元o的情況
print(re.findall(r"o{2,3}", msg1))

``` 上述程式碼大家可以自行嘗試並分析結果。執行結果如下:

['', 'oo', '', '', 'oo', '', '', '', '', '', '', '', '', '', 'ooo', '', 'oooo', '', '', ''] ['oo', 'oo', 'ooo', 'oooo'] ['', 'o', 'o', '', '', 'o', 'o', '', '', '', '', '', '', '', '', '', 'o', 'o', 'o', '', 'o', 'o', 'o', 'o', '', '', ''] ['oo', 'oo', 'oo', 'oo', 'oo'] ['oo', 'oo', 'ooo', 'oooo'] ['oo', 'oo', 'ooo', 'ooo']

2.6. 正規表示式中的範圍匹配

在正規表示式中,針對字元的匹配,除了快捷的元字元的匹配,還有另一種使用方括號進行的範圍匹配方式,具體如下:

  • |範圍|描述|
  • |--|:--|
  • |[0-9]|用於匹配一個0~9之間的數字,等價於\d|
  • |[^0-9]|用於匹配一個非數字字元,等價於\D|
  • |[3-6]|用於匹配一個3~6之間的數字|
  • |[a-z]|用於匹配一個a~z之間的字母|
  • |[A-Z]|用於匹配一個A~Z之間的字母|
  • |[a-f]|用於匹配一個a~f之間的字母|
  • |[a-zA-Z]|用於匹配一個a~z或者A-Z之間的字母,匹配任意一個字母|
  • |[a-zA-Z0-9]|用於匹配一個字母或者數字|
  • |[a-zA-Z0-9_]|用於匹配一個字母或者數字或者下劃線,等價於\w|
  • |[^a-zA-Z0-9_]|用於匹配一個非字母或者數字或者下劃線,等價於\W| 注意:不要使用[0-120]來表示0~120之間的數字,這是錯誤的

整理測試程式碼如下: ```

# 引入正規表示式模組
import re

msg = "Hello, The count of Today is 800"
# 匹配字串msg中所有的數字
print(re.findall(r"[0-9]+", msg))
# 匹配字串msg中所有的小寫字母
print(re.findall(r"[a-z]+", msg))
# 匹配字串msg中所有的大寫字母
print(re.findall(r"[A-Z]+", msg))
# 匹配字串msg中所有的字母
print(re.findall(r"[A-Za-z]+", msg))

``` 上述程式碼執行結果如下:

['800'] ['ello', 'he', 'count', 'of', 'oday', 'is'] ['H', 'T', 'T'] ['Hello', 'The', 'count', 'of', 'Today', 'is']

2.7. 正規表示式中的分組

正規表示式主要是用於進行字串檢索匹配操作的利器 在一次完整的匹配過程中,可以將匹配到的結果進行分組,這樣就更加的細化了我們對匹配結果的操作 正規表示式通過圓括號()進行分組,以提取匹配結果的部分結果

常用的兩種分組:

  • |分組|描述|
  • |--|:--|
  • |(expression)|使用圓括號直接分組;正規表示式本身匹配的結果就是一個組,可以通過group()或者group(0)獲取;然後正規表示式中包含的圓括號就是按照順序從1開始編號的小組|
  • |(?Pexpression)|使用圓括號分組,然後給當前的圓括號表示的小組命名為name,可以通過group(name)進行資料的獲取|

廢話少說,上乾貨: ```

# 引入正規表示式模組
import re

# 使用者輸入座機號碼,如"010-6688465"
phone = input("請輸入座機號碼:")
# 1.進行正則匹配,得到Match物件,物件中就包含了分組資訊
res1 = re.search(r"^(\d{3,4})-(\d{4,8})$", phone)
# 檢視匹配結果
print(res1)
# 匹配結果為預設的組,可以通過group()或者group(0)獲取
print(res1.group())
# 獲取結果中第一個括號對應的組資料:處理區號
print(res1.group(1))
# 獲取結果中第二個括號對應的組資料:處理號碼
print(res1.group(2))

# 2.進行正則匹配,得到Match物件,物件中就包含了命名分組資訊
res2 = re.search(r"^(?P<nstart>\d{3,4})-(?P<nend>\d{4,8})$", phone)
# 檢視匹配結果
print(res2)
# 匹配結果為預設的組,可以通過group()或者group(0)獲取
print(res2.group(0))
# 通過名稱獲取指定的分組資訊:處理區號
print(res2.group("nstart"))
# 通過名稱獲取指定分組的資訊:處理號碼
print(res2.group("nend"))

``` 上述程式碼就是從原始字串中,通過正規表示式匹配得到一個結果,但是使用了分組之後,就可以將結果資料通過分組進行細化處理,執行結果如下:

請輸入座機號碼:021-6565789 <_sre.SRE_Match object; span=(0, 11), match='021-6565789'> 021-6565789 021 6565789 <_sre.SRE_Match object; span=(0, 11), match='021-6565789'> 021-6565789 021 6565789

2.8. 正規表示式中的特殊用法

使用分組的同時,會有一些特殊的使用方式如下:

|表示式|描述| * |--|:--| * |(?:expression)|作為正規表示式的一部分,但是匹配結果丟棄| * |(?=expression)|匹配expression表示式前面的字元,如 "How are you doing" ,正則"(?.+(?=ing))" 這裡取ing前所有的字元,並定義了一個捕獲分組名字為 "txt" 而"txt"這個組裡的值為"How are you do"| * |(?<=expression)|匹配expression表示式後面的字元,如 "How are you doing" 正則"(?(?<=How).+)" 這裡取"How"之後所有的字元,並定義了一個捕獲分組名字為 "txt" 而"txt"這個組裡的值為" are you doing";| * |(?!expression)|匹配字串後面不是expression表示式字元,如 "123abc" 正則 "\d{3}(?!\d)"匹配3位數字後非數字的結果| * |(?

2.9 正規表示式的貪婪模式和懶惰模式

在某些情況下,我們匹配的字串出現一些特殊的規律時,就會出現匹配結果不盡如人意的意外情況 如:在下面的字串中,將div標籤中的所有內容獲取出來 ```

<div>內容1</div><p>這本來是不需要的內容</p><div>內容2</div>

此時,我們想到的是,使用<div>作為關鍵資訊進行正規表示式的定義,如下

regexp = r"<div>.*</div>"

本意是使用上述程式碼來完成div開始標籤和結束標籤之間的內容匹配,但是,匹配的結果如下

<div> [內容1</div><p>這本來是不需要的內容</p><div>內容2] </div>

我們可以看到,上面匹配的結果,是將字串開頭的<div>標籤和字串結束的</div>當成了匹配元素,對包含在中間的內容直接進行了匹配,也就得到了我們期望之外的結果:

內容1</div><p>這本來是不需要的內容</p><div>內容2

上述就是我們要說的正規表示式的第一種模式:貪婪模式 **貪婪模式**:正規表示式匹配的一種模式,速度快,但是匹配的內容會從字串兩頭向中間搜尋匹配(比較貪婪~),一旦匹配選中,就不繼續向字串中間搜尋了,過程如下:

開始:<div>內容1</div><p>這本來是不需要的內容</p><div>內容2</div>

第一次匹配:【<div>內容1</div><p>這本來是不需要的內容</p><div>內容2</div>】

第二次匹配<div>【內容1</div><p>這本來是不需要的內容</p><div>內容2】</div>

匹配到正則中需要的結果,不再繼續匹配,直接返回匹配結果如下:
內容1</div><p>這本來是不需要的內容</p><div>內容2

```

明顯貪婪模式某些情況下,不是我們想要的,所以出現了另一種模式:懶惰模式 懶惰模式:正規表示式匹配的另一種模式,會首先搜尋匹配正規表示式開始位置的字元,然後逐步向字串的結束位置查詢,一旦找到匹配的就返回,然後接著查詢 ```

regexp = r"<div>.*?</div>"

開始:<div>內容1</div><p>這本來是不需要的內容</p><div>內容2</div>

第一次匹配:【<div>】內容1</div><p>這本來是不需要的內容</p><div>內容2</div>

第二次匹配【<div>內容1</div>】<p>這本來是不需要的內容</p><div>內容2</div>

匹配到正則中需要的結果:內容1

繼續向後查詢

第三次匹配<div>內容1</div>【<p>這本來是不需要的內容</p>】<div>內容2</div>

第四次匹配<div>內容1</div><p>這本來是不需要的內容</p>【<div>內容2</div>】

匹配到正則中需要的結果:內容2

查詢字串結束!

```

正規表示式匹配的兩種模式:貪婪模式、懶惰模式 貪婪模式:從目標字串的兩頭開始搜尋,一次儘可能多的匹配符合條件的字串,但是有可能會匹配到不需要的內容,正規表示式中的元字元、量詞、範圍等都模式是貪婪匹配模式,使用的時候一定要注意分析結果,如:<div>.*</div>就是一個貪婪模式,用於匹配

之間所有的字元 懶惰模式:從目標字串按照順序從頭到位進行檢索匹配,儘可能的檢索到最小範圍的匹配結果,語法結構是在貪婪模式的表示式後面加上一個符號?即可,如<div>.*?</div>就是一個懶惰模式的正則,用於僅僅匹配最小範圍的
之間的內容

不論貪婪模式還是懶惰模式,都有適合自己使用的地方,大家一定要根據實際需求進行解決方案的確定


大牧莫邪.png

相關文章