爬蟲入門系列(六):正則表示式完全指南(下)
爬蟲入門系列目錄:
正則表示式是一種更為強大的字串匹配、字串查詢、字串替換等操作工具。上篇講解了正則表示式的基本概念和語法以及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][email protected][\w]+\.[\w]+$" >>> re.match(rex, "[email protected]") # 匹配 <_sre.SRE_Match object at 0x102f05bf8> >>> re.match(rex, "the email is [email protected]") # 不匹配 >>>
儘管第二個字串中包含有郵件地址,但字串整體不能當作一個郵件地址來使用,在網頁上填郵件地址時,顯然第二種寫法是無效的。
通常,search 方法可用於判斷字串中是否包含有與正則表示式相匹配的子字串,還可以從中提出匹配的子字串,例如:
>>> rex = r"[\w][email protected][\w]+\.[\w]+" >>> m = re.search(rex, "the email is [email protected] .") >>> m is None False >>> m.group() '[email protected]' >>>
細心的你可能已經發現了,上面例子與前面例子的正則表示式寫法有細微區別,前者多一個元字元 $
,它的目的是用於完全匹配字串。因為不加 $,那麼下面這種情況用match方法也匹配,顯示這在表單驗證時是無法滿足要求的。
>>> rex = r"[\w][email protected][\w]+\.[\w]+" >>> re.match(rex, "[email protected] is my email") <_sre.SRE_Match object at 0x10cadebf8> >>>
那麼有沒有可能不加$,就可以判斷是否完全匹配字串呢?在 Python3 中,re.fullmatch
就可以滿足這樣的需求。
>>> rex = r"[\w][email protected][\w]+\.[\w]+" >>> re.fullmatch(rex, "[email protected] is my email") # 不匹配 >>> re.fullmatch(rex, "[email protected]") # 匹配 <_sre.SRE_Match object; span=(0, 10), match='[email protected]'>
雖然二者都可以通過 group() 提取出匹配的子字串,但是,如果字串中有多個匹配的子字串時,兩個方法都不行,因為它們都是在一旦匹配了第一個子字串,就不再往後匹配了。
>>> m = re.search(rex, "email is [email protected], anthor email is [email protected] !") >>> m.group() '[email protected]'
那麼如何把文字中的所有匹配的郵件地址提取出來呢?re 模組為我們準備了 re.findall() 和 re.finditer() 這兩個方法,它們會返回文字中所有與正則表示式相匹配的內容。前者返回的是一個列表(list)物件,後者返回的是一個迭代器(iterator)。
re.findall(pattern, string)
>>> emails = re.findall(rex, "email is [email protected], anthor email is [email protected]") >>> emails ['[email protected]', '[email protected]']
findall 返回的物件是由匹配的子字串組成的列表,它返回了所有匹配的郵件地址。
re.finditer(pattern, string)
>>> emails = re.finditer(rex, "email is [email protected], anthor email is [email protected]") >>> 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方法來處理。此外,正則表示式中如果有分組括號,那麼返回結果又不一致,這個可以留給大家查閱文件,某些場景用得著。
把所有郵箱地址替換成 [email protected]
>>> rex = r"[\w][email protected][\w]+\.[\w]+" # 郵件地址正則 >>> re.sub(rex, "[email protected]", "[email protected], [email protected] ") '[email protected], [email protected] ' >>>
另外一個例子,就是上次講過的將 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')
那麼,後者有什麼好處呢?為了提高正則匹配的速度,它可以重複利用正則物件,如果一個正則表示式需要匹配多個字串,那麼就推薦後者,先編譯在去匹配。更多使用方式可以參考文件 https://docs.python.org/3/library/re.html
關注公眾號「Python之禪」(id:vttalk)獲取最新文章