爬蟲入門系列(六):正規表示式完全指南(下)

劉志軍發表於2017-05-30

正規表示式是一種更為強大的字串匹配、字串查詢、字串替換等操作工具。上篇講解了正規表示式的基本概念和語法以及re模組的基本使用方式,這節來詳細說說 re 模組作為 Python 正規表示式引擎提供了哪些便利性操作。

 >>> import re複製程式碼

正規表示式的所有操作都是圍繞著匹配物件(Match)進行的,只有表示式與字串匹配才有可能進行後續操作。判斷匹配與否有兩個方法,分別是 re.match()re.search(),兩者有什麼區別呢?

re.match(pattern, string)

match 方法從字串的起始位置開始檢查,如果剛好有一個子字串與正規表示式相匹配,則返回一個Match物件,只要起始位置不匹配則退出,不再往後檢查了,返回 None

>>> re.match(r"b.r", "foobar")   # 不匹配
>>> re.match(r"b.r", "barfoo")   # 匹配
<_sre.SRE_Match object at 0x102f05b28>
>>>複製程式碼

re.search(pattern, string)

search 方法雖然也是從起始位置開始檢查,但是它在起始位置不匹配的時候會一直嘗試往後檢查,直到匹配為止,如果到字串的末尾還沒有匹配,則返回 None

>>> re.search(r"b.r", "foobar") # 匹配
<_sre.SRE_Match object at 0x000000000254D578>
>>> re.match(r"b.r", "foobr")  # 不匹配複製程式碼

兩者接收引數都是一樣的,第一個引數是正規表示式,第二個是預匹配的字串。另外,不管是 search 還是 match,一旦找到了匹配的子字串,就立刻停止往後找,哪怕字串中有多個可匹配的子字串,例如

>>> re.search(r"f.o", "foobarfeobar").group()
'foo'複製程式碼

兩者的差異使得他們在應用場景上也不一樣,如果是檢查文字是否匹配某種模式,比如,檢查字串是不是有效的郵箱地址,則可以使用 match 來判斷:

>>> rex = r"[\w]+@[\w]+\.[\w]+$"
>>> re.match(rex, "123@qq.com")  # 匹配
<_sre.SRE_Match object at 0x102f05bf8> 
>>> re.match(rex, "the email is 123@qq.com") # 不匹配
>>>複製程式碼

儘管第二個字串中包含有郵件地址,但字串整體不能當作一個郵件地址來使用,在網頁上填郵件地址時,顯然第二種寫法是無效的。

通常,search 方法可用於判斷字串中是否包含有與正規表示式相匹配的子字串,還可以從中提出匹配的子字串,例如:

>>> rex = r"[\w]+@[\w]+\.[\w]+"
>>> m = re.search(rex, "the email is 123@qq.com .")
>>> m is None
False
>>> m.group()
'123@qq.com'
>>>複製程式碼

細心的你可能已經發現了,上面例子與前面例子的正規表示式寫法有細微區別,前者多一個元字元 $,它的目的是用於完全匹配字串。因為不加 $,那麼下面這種情況用match方法也匹配,顯示這在表單驗證時是無法滿足要求的。

>>> rex = r"[\w]+@[\w]+\.[\w]+"
>>> re.match(rex, "123@qq.com is my email")
<_sre.SRE_Match object at 0x10cadebf8>
>>>複製程式碼

那麼有沒有可能不加$,就可以判斷是否完全匹配字串呢?在 Python3 中,re.fullmatch 就可以滿足這樣的需求。

>>> rex = r"[\w]+@[\w]+\.[\w]+"
>>> re.fullmatch(rex, "123@qq.com is my email") # 不匹配
>>> re.fullmatch(rex, "123@qq.com") # 匹配
<_sre.SRE_Match object; span=(0, 10), match='123@qq.com'>複製程式碼

雖然二者都可以通過 group() 提取出匹配的子字串,但是,如果字串中有多個匹配的子字串時,兩個方法都不行,因為它們都是在一旦匹配了第一個子字串,就不再往後匹配了。

>>> m = re.search(rex, "email is 123@qq.com, anthor email is abc@gmail.com !")
>>> m.group()
'123@qq.com'複製程式碼

那麼如何把文字中的所有匹配的郵件地址提取出來呢?re 模組為我們準備了 re.findall() 和 re.finditer() 這兩個方法,它們會返回文字中所有與正規表示式相匹配的內容。前者返回的是一個列表(list)物件,後者返回的是一個迭代器(iterator)。

re.findall(pattern, string)

>>> emails = re.findall(rex, "email is 123@qq.com, anthor email is abc@gmail.com")
>>> emails
['123@qq.com', 'abc@gmail.com']複製程式碼

findall 返回的物件是由匹配的子字串組成的列表,它返回了所有匹配的郵件地址。

re.finditer(pattern, string)

>>> emails = re.finditer(rex, "email is 123@qq.com, anthor email is abc@gmail.com")
>>> emails
<callable-iterator object at 0x0000000002592390>
>>> for e in emails:
...     print(e.group())
...
123@qq.com
abc@gmail.com複製程式碼

finditer 返回的物件是由 Match 物件組成的迭代器,因為裡面的元素是Match物件,所以要獲取裡面的郵件地址還需要呼叫group方法來提取。關於列表和迭代器的區別,此文不做介紹,可以檢視公眾號“Python之禪”的歷史文章。

re.split

我們都知道字串有一個split方法,可根據某個子串分隔字串,如:

>>> "this is a string.".split(" ")
['this', 'is', 'a', 'string.']複製程式碼

但該方法有一個缺陷,比如上面的字串,根據空格分隔字串時,字串後面多一個點,如果用 re.split 就可以避免這種情況。

>>> words = re.split(r"\W+", "this is a string.")
>>> words
['this', 'is', 'a', 'string', '']
>>> list(filter(lambda x: x, words))
['this', 'is', 'a', 'string']
>>>複製程式碼

re.split是一種更為高階的字串分隔操作的方法。在這裡,split根據非字母正則來分隔字串,但凡是 string.split 沒法處理的問題,可以考慮使用re模組下的split方法來處理。此外,正規表示式中如果有分組括號,那麼返回結果又不一致,這個可以留給大家查閱文件,某些場景用得著。

re.sub(pattern, repl, string)

re.split是一種更為高階的字串分隔操作的方法。在這裡,split根據非字母正則來分隔字串,但凡是 string.split 沒法處理的問題,可以考慮使用re模組下的split方法來處理。此外,正規表示式中如果有分組括號,那麼返回結果又不一致,這個可以留給大家查閱文件,某些場景用得著。

把所有郵箱地址替換成 admin@qq.com

>>> rex = r"[\w]+@[\w]+\.[\w]+" # 郵件地址正則
>>> re.sub(rex, "admin@qq.com", "234@qq.com, 456@qq.com ")
'admin@qq.com, admin@qq.com '
>>>複製程式碼

另外一個例子,就是上次講過的將 img 標籤的 src 路徑替換成絕對完整的URL地址

html = """
        ...
        <img src="/images/category.png">
        this is anthor words
        <img src="http://foofish.net/images/js_framework.png">
       """複製程式碼

如果用字串的replace方法是沒法實現了,這時需要用到正規表示式的 re.sub,正規表示式應用了非貪婪模式,使用了一個分組,用於提取 src 的路徑。

rex = r'.*?<img src="(.*?)".*?>'複製程式碼

這裡我們要把替換目標 repl 作為函式來處理。


def fun(m):
    img_tag = m.group()
    src = m.group(1)
    if not src.startswith("http:"):
        full_src = "http://foofish.net" + src
    else:
        full_src = src
    new_img_tag = img_tag.replace(src, full_src)
    return new_img_tag複製程式碼

引擎會自動把所有匹配的結果應用到該函式中,函式的引數就是每一個匹配的Match物件,通過 group(1) 提取分組後判斷是否為一個完整的URL路徑,只有是不完整的我們才替換,否則還是按照原來的方式返回。

new_html = re.compile(rex).sub(fun, html)
print(new_html)
# 輸出
...
<img src="http://foofish.net/images/category.png">
this is anthor words
<img src="http://foofish.net/images/js_framework.png">複製程式碼

如果還想知道替換次數是多少,那麼可以使用 re.subn方法,這個方法具體使用可以參考文件,留著讀者自己思考。

此外,以上方法都有一個預設的 flag 引數,該引數用於改變匹配的行為,常用的可選值有:

  • re.I(IGNORECASE): 忽略大小寫(括號內的單詞為完整寫法,兩種方式都支援)
  • re.M(MULTILINE): 多行模式,改變'^'和'$'的行為
  • re.S(DOTALL): 改變'.'的行為,預設 . 只能匹配除換行之外的字元,加上它就可以匹配換行了
    例如:
>>> re.match(r"foo", "FoObar", re.I)
<_sre.SRE_Match object; span=(0, 3), match='FoO'>
>>>複製程式碼

以上介紹的都是 re 模組下面的方法,其實,這些只不過是一些簡便方法,例如 re.match 方法

re.match(r'foo', 'foo bar')複製程式碼

等價於

pattern = re.compile(r'foo')
pattern.match('foo bar')複製程式碼

那麼,後者有什麼好處呢?為了提高正則匹配的速度,它可以重複利用正則物件,如果一個正規表示式需要匹配多個字串,那麼就推薦後者,先編譯在去匹配。更多使用方式可以參考文件 docs.python.org/3/library/r…

相關文章