1. 程式人生 > >python 歷險記(六)— python 對正則表示式的使用(上篇)

python 歷險記(六)— python 對正則表示式的使用(上篇)

目錄

引言

剛接觸正則表示式,我也曾被它們天書似的符號組合給嚇住,但經過一段時間的深入學習,發現它並沒有想象中那麼可怕,只要多實踐,多理解,也是可以輕鬆搞定的。
而且我發現帶著問題去學習,求知慾會驅使著你往前走,不知不覺就懂了。
下面就是我在學習中提出的幾個問題,在後面會依次進行討論。由於正則表示式涉及到的內容確實非常多,分成兩篇來闡述。

  1. 什麼是正則表示式?
  2. 正則表示式可以幹什麼?
  3. 正則表示式的語法以及在 python 中這些語法是如何使用的?
  4. 正則表示式如何處理中文字元?
  5. python 的正則表示式庫中有哪些重要的函式?

什麼是正則表示式?

正則表示式使用單個字串來描述,匹配一系列符合某個句法規則的字串。

— 維基百科

先來劃重點:

  1. 正則表示式的表現形式是 單個字串
  2. 它用來執行匹配的動作
  3. 匹配的物件也是字串

語言總是有些蒼白的,必須要結合例項才能理解的更清楚,先來看一個例子:

>>> import re
>>>re.search(r'wo\w+d', 'hello world!')
<re.Match object; span=(6, 11), match='world'>
>>> 

這裡先概略說明 re.search 方法:引入的 re 模組就是 python 的正則表示式模組,re.search 函式目的就是接受一個正則表示式和一個字串,並以 Match 物件的形式返回匹配的第一個元素。如果沒有匹配到,則會返回 None。(關於 search 函式先了解這些就可以,後面會有詳細講解。)

下面就拿這個示例中 re.search 中的引數來匹配下上面的概念,加深一下理解

  • 'wo\w+d' 就是正則表示式,它還有一個名稱叫做_模式(pattern)_ ,表示wo 字母后有多個字母並一直到d 字母出現為止(現在不明白沒關係,只要知道它就是正則表示式就可以了,後面會詳細講
  • 'wo\w+d' 前面還有一個 r 表示什麼呢?這個 r 表示 raw的意思,就是原始字串。原始字串不會將 \ 解釋成一個轉義字元,而是這樣做對正則表示式好處是大大的,只有這樣 \w 才能起作用。
  • 'hello world!' 就是要匹配的字串。
  • 整個函式就表示從 'hello world!' 字串中搜索出符合_'wo\w+d'_ 模式的字串,並展示出來,於是 world 字串就被篩選了出來。

正則表示式有什麼用?

我們學習正則表示式的目的是什麼?當然是為了有朝一日能使用它解決我們面臨的問題,要不然,學它幹嘛。那我們就來聊聊正則表示式的用途:

  • 字串驗證

    你肯定在網頁上註冊過賬號吧,假如你在註冊 github 網站,它要求你輸入 Email,而你卻胡亂填寫了幾個數字就想註冊,這時就會彈出提示 "Email is invalid",意思就是你的郵箱是無效的,這就是正則表示式的功勞。

  • 替換文字

    假如你正在寫一篇關於 java 的文章,寫著寫著,你覺得換成 python 更好些,你想一下把出現 java , Java 的地方全都替換成 python , 正則表示式可以幫你做到。

  • 從字串中提取出要獲取的字串

    假如你正在爬取一個汽車排行榜頁面,想要獲取每個車型的編號,而車型編號則隱藏在連結中,怎麼獲取呢?用正則表示式可以。

正則表示式的語法及使用例項

對剛接觸的同學來說,正則表示式的語法很晦澀。不用擔心,我們先大致瀏覽一下完整的語法組合,後面再使用前面講過的 re.search 方法一個個詳細介紹,講著講著,我相信你就明白了。

正則表示式語法有哪些?

字元 功能描述
\ 特殊字元轉義
^ 匹配字串的開始位置
$ 匹配字串的結束位置
* 匹配前面的子表示式零次或多次
+ 匹配前面的子表示式一次或多次
? 匹配前面的子表示式零次或一次
{n} n是非負整數,匹配n次
{n,} n 是非負整數,匹配 >=n 次
{n,m} m是非負整數,n<=m, 匹配>= n 並且 <=m 次
? 非貪心量化
. 匹配除“\r”“\n”之外的任何單個字元
(pattern) 匹配pattern並獲取這一匹配的子字串
(?:pattern) 非獲取匹配
(?=pattern) 正向肯定預查
(?!pattern) 正向否定預查
(?<=pattern) 反向(look behind)肯定預查
(?<!pattern) 反向否定預查
x|y 沒有包圍在()裡,其範圍是整個正則表示式
[xyz] 字元集合(character class),匹配所包含的任意一個字元
[^xyz] 排除型字元集合(negated character classes),匹配未列出的任意字元
[a-z] 字元範圍,匹配指定範圍內的任意字元
[^a-z] 排除型的字元範圍,匹配任何不在指定範圍內的任意字元
\b 匹配一個單詞邊界,也就是指單詞和空格間的位置
\B 匹配非單詞邊界
\cx 匹配由x指明的控制字元
\d 匹配一個數字字元。等價於[0-9]
\D 匹配一個非數字字元。等價於[^0-9]
\f 匹配一個換頁符。等價於\x0c和\cL。
\n 匹配一個換行符。等價於\x0a和\cJ。
\r 匹配一個回車符。等價於\x0d和\cM。
\s 匹配任何空白字元。等價於[ \f\n\r\t\v]
\S 匹配任何非空白字元。等價於[^ \f\n\r\t\v]
\t 匹配一個製表符。等價於\x09和\cI。
\v 匹配一個垂直製表符。等價於\x0b和\cK。
\w 匹配包括下劃線的任何單詞字元。等價於“[A-Za-z0-9_]
\W 匹配任何非單詞字元。等價於“[^A-Za-z0-9_]”。
\ck 匹配控制轉義字元。k代表一個字元。等價於“Ctrl-k
\xnn 十六進位制轉義字元序列
\n 標識一個八進位制轉義值或一個向後引用
\un Unicode轉義字元序列

這些正則到底該怎麼用?

瀏覽一遍,感覺怎麼樣,是不是摩拳擦掌,想要立刻實踐一番,嘿嘿。好的我們現在就開幹。

  • ^ 匹配字串的開始位置

    >>> import re
    >>> re.search(r'^h', 'he is a hero!')          
    <re.Match object; span=(0, 1), match='h'>

    這個例子中雖然有兩個 h,因為前面有 ^ 所以只會匹配第一個

  • $ 匹配字串的結束位置

    >>> import re
    
    >>> re.search(r't$','this is an object')
    
    <re.Match object; span=(16, 17), match='t'>

    雖然這個句子前後都有 t,卻是最後的被匹配到了

  • * 匹配前面的子表示式 0 次或多次,例如,"zo*" 能匹配"z","zo","zoo",我們來驗證下

    >>> import re
    
    >>> re.search(r'zo*', 'z')
    
    <re.Match object; span=(0, 1), match='z'>
    >>> re.search(r'zo*', 'zo')
    
    <re.Match object; span=(0, 2), match='zo'>
    >>> re.search(r'zo*', 'zoo')
    
    <re.Match object; span=(0, 3), match='zoo'>

    ​ 這裡 * 還有一種寫法 {0,},兩者等價。其中 {} 叫做重複。來看例子。

    import re
    re.search(r'zo{0,}','z')
    <_sre.SRE_Match object; span=(0, 1), match='z'>
    re.search(r'zo{0,}','zo')
    <_sre.SRE_Match object; span=(0, 2), match='zo'>
    re.search(r'zo{0,}','zoo')
    <_sre.SRE_Match object; span=(0, 3), match='zoo'>
  • + 匹配前面的子表示式一次或多次,以 "zo+" 為例,它能匹配 "zo","zoo",來驗證下

    >>> import re
    >>> re.search(r'zo+', 'zo')
    
    <re.Match object; span=(0, 2), match='zo'>
    >>> re.search(r'zo+', 'zoo')
    
    <re.Match object; span=(0, 3), match='zoo'>

    這裡 + 還有一種寫法 {1,} 兩者是等價的,來看例子。

    >>> import re
    
    >>> re.search(r'zo{1,}','zo')
    
    <re.Match object; span=(0, 2), match='zo'>
    
    >>> re.search(r'zo{1,}','zoo')
    
    <re.Match object; span=(0, 3), match='zoo'>
  • ? 匹配前面的子表示式0次或 1次,以 "ab(cd)?" 為例,可以匹配 "ab","abcd",看下面例子

    import re
    re.search(r'ab(cd)?','ab')
    <_sre.SRE_Match object; span=(0, 2), match='ab'>
    re.search(r'ab(cd)?','abcd')
    <_sre.SRE_Match object; span=(0, 4), match='abcd'>

    這裡 ? 還有一種寫法 {0,1} 兩者等價,看下面

    import re
    re.search(r'ab(cd){0,1}', 'ab')
    <_sre.SRE_Match object; span=(0, 2), match='ab'>
    re.search(r'ab(cd){0,1}', 'abcd')
    <_sre.SRE_Match object; span=(0, 4), match='abcd'>
  • {n} n 必須是非負整數,能匹配確定的 n 次,以 "o{2}" 為例,它能匹配 "good", 卻不能匹配 "god"

    import re
    re.search(r'o{2}', 'god')
    re.search(r'o{2}', 'good')
    <_sre.SRE_Match object; span=(1, 3), match='oo'>
  • {n,} n是一個非負整數。至少能匹配 n次。例如,“o{2,}”不能匹配 “god”中的 “o”,但能匹配“foooood”中的所有o。“o{1,}”等價於“o+”。“o{0,}”則等價於“o*”,這個可看前面示例。

  • {n,m} m和n均為非負整數,其中n<=m。例如,“o{1,2}”將匹配“google”中的兩個o。“o{0,1}”等價於“o?”。注意在逗號和兩個數之間不能有空格

    re.search(r'o{1,2}', 'google')
    <_sre.SRE_Match object; span=(1, 3), match='oo'>
  • ? 這個叫做非貪心量化(Non-greedy quantifiers),這個字元和前面的 ? 有什麼區別?應用場合是什麼呢?

    當該字元緊跟在任何一個其他重複修飾符(*,+,?,{n},{n,},{n,m})後面時,匹配模式是非貪婪的。非貪婪模式儘可能少的匹配所搜尋的字串,而預設的貪婪模式則儘可能多的匹配所搜尋的字串。舉個例子,"o+" 預設會匹配 o 一次或多次,如果在後面加上 "?",則匹配一次。來看程式碼。

    re.search(r'o+?', 'google')
    <_sre.SRE_Match object; span=(1, 2), match='o'>
    re.search(r'o+', 'google')
    <_sre.SRE_Match object; span=(1, 3), match='oo'>
  • . 匹配除了 \r ,\n 之外的任何單個字元,要匹配包括“\r”“\n”在內的任何字元,請使用像“(.|\r|\n)”的模式

    import re
    re.search(r'a.', 'ab')
    <_sre.SRE_Match object; span=(0, 2), match='ab'>
  • (pattern) 匹配 pattern 並獲取這一匹配的子字串,並用於向後引用。使用圓括號可以指定分組。當使用分組時,除了獲取到整個匹配的完整字串,也可以從匹配中選擇每個單獨的分組。

    下面給出一個本地電話號碼的示例,其中每個括號內匹配的數字都是一個分組。

    >>> import re
    >>> match = re.search(r'([\d]{3,4})-([\d]{7,8})', '010-12345678')
    >>> match
    <re.Match object; span=(0, 12), match='010-12345678'>
    >>> match.group(1)
    '010'
    >>> match.group(2)
    '12345678'
    >>> match.group()
    '010-12345678'
    >>> match.groups()
    ('010', '12345678')

    前面我們只是簡單介紹了 match 物件,為了深入的理解分組,這裡還要簡單介紹下該物件的幾個方法以及如何對應分組資訊的:

    • groups() 用於返回一個對應每一個單個分組的元組。

      >>> match.groups()
      ('010', '12345678')
    • group() 方法(不含引數)則返回完整的匹配字串

      >>> match.group()
      '010-12345678'
    • group(num) num 是分組編號,按照分組順序,從 1 開始取值,能獲取具體的分組資料。

      >>> match.group(1)
      '010'
      >>> match.group(2)
      '12345678'
  • (?:pattern) 匹配 pattern 但不獲取匹配的子字串(shy groups),也就是說這是一個非獲取匹配,不儲存匹配的子字串用於向後引用。這種格式的圓括號不會作為分組資訊,只用於匹配,即在python 呼叫search 方法而得到的 match 物件不會將圓括號作為分組儲存起來。

    來看下面例子,只獲取電話號,而不獲取地區區號。

    >>> match = re.search(r'(?:[\d]{3,4})-([\d]{7,8})', '010-12345678')
    >>> match.groups()
    ('12345678',)
    >>> match.group()
    '010-12345678'

    這種形式對使用 字元`(|)”來組合一個模式的各個部分是很有用的,來看一個例子,想要同時匹配 city 和 cities (複數形式),就可以這樣幹

    >>> match = re.search(r'cit(?:y|ies)','cities')
    >>> match
    <re.Match object; span=(0, 6), match='cities'>
    >>> match = re.search(r'cit(?:y|ies)','city')
    >>> match
    <re.Match object; span=(0, 4), match='city'>
  • (?=pattern)正向肯定預查(look ahead positive assert),在任何匹配 pattern 的字串開始處匹配查詢字串。這是一個非獲取匹配,也就是說,該匹配不需要獲取供以後使用。

    舉個例子,假設我要獲取從不同 python 版本中只獲取 "python" 字串,就可以這樣寫:

    >>> match = re.search(r'python(?=2.7|3.5|3.6|3.7)', 'python3.7')
    >>> match
    <re.Match object; span=(0, 6), match='python'>

    預查不消耗字元,也就是說,在一個匹配發生後,在最後一次匹配之後立即開始下一次匹配的搜尋,而不是從包含預查的字元之後開始。那麼在 python 版本後再加上其他資訊,整體就無法匹配了。

    看下面例子,得到的結果只能是 null。

    >>> match = re.search(r'python(?=2.7|3.5|3.6|3.7) is hacking!', 'python3.7 is hacking!')
    >>> match
  • (?!pattern) 正向否定預查(negative assert),看名字也知道是 正向肯定預查的反面。在任何不匹配 pattern 的字串開始處匹配查詢字串。是一個非獲取匹配,而且預查不消耗字元。

    看下面例子,和正向肯定預查一對比就明白了。

    >>> match = re.search(r'python(?!2.7|3.5|3.6|3.7)', 'python3.7')
    >>> match
    >>> match = re.search(r'python(?!2.7|3.5|3.6|3.7)', 'python3.1')
    >>> match
    <re.Match object; span=(0, 6), match='python'>
    >>> match = re.search(r'python(?!2.7|3.5|3.6|3.7) is hacking!', 'python3.1 is hacking!')
    >>> match
  • (?<=pattern) 反向(look behind)肯定預查,與正向肯定預查類似,只是方向相反。

  • (?<!pattern) 反向否定預查,與正向否定預查類似,只是方向相反。

  • x|y 或,分兩種情況:沒有沒括號包圍,範圍則是整個表示式;被括號包圍,返回是括號內。

    >>> match = re.search(r'today is sunday|tommory is monday','tommory is monday')
    >>> match
    <re.Match object; span=(0, 17), match='tommory is monday'>
  • [xyz] 字元集合,匹配所包含的任意一個字元。分為下面情況

    • 普通字元

      >>> match = re.search(r'[Pp]ython','python')
      >>> match
      <_sre.SRE_Match object; span=(0, 6), match='python'>
    • 特殊字元僅有反斜線 \保持特殊含義,用於轉義字元

    • 其它特殊字元如星號、加號、各種括號等均作為普通字元

      >>> match = re.search(r'[*?+()]python','*python')
      >>> match
      <_sre.SRE_Match object; span=(0, 7), match='*python'>
      >>> match = re.search(r'[*?+()]python','+python')
      >>> match
      <_sre.SRE_Match object; span=(0, 7), match='+python'>
      >>> match = re.search(r'[*?+()]python','(python')
      >>> match
      <_sre.SRE_Match object; span=(0, 7), match='(python'>
    • ^ 出現在字串中間和末尾僅作為普通字元,出現在最前面後面會講。

      >>> match = re.search(r'[*^{]python','^python')
      >>> match
      <_sre.SRE_Match object; span=(0, 7), match='^python'>
      >>> match = re.search(r'[*^]python','^python')
      >>> match
      <_sre.SRE_Match object; span=(0, 7), match='^python'>
    • - 出現在字元集合首位和末尾,僅作為普通字元,出現在中間是字元範圍描述,後面會講。

      >>> match = re.search(r'[-^]python','-python')
      >>> match
      <_sre.SRE_Match object; span=(0, 7), match='-python'>
  • [^xyz] 排除型字元集合(negated character classes)。匹配未列出的任意字元

    >>> re.search(r'[^abc]def','edef')
    
    <re.Match object; span=(0, 4), match='edef'>
  • [a-z] 字元範圍。匹配指定範圍內的任意字元。

    >>> re.search(r'[a-g]bcd','ebcd')
    
    <re.Match object; span=(0, 4), match='ebcd'>
    >>> re.search(r'[a-g]bcd','hbcd')
  • [^a-z] 排除型的字元範圍。匹配任何不在指定範圍內的任意字元。

    >>> re.search(r'[^a-g]bcd','hbcd')
    
    <re.Match object; span=(0, 4), match='hbcd'>
  • \b 匹配一個單詞邊界,也就是指單詞和空格間的位置。

    >>> re.search(r'an\b apple','an apple')
    
    <re.Match object; span=(0, 8), match='an apple'>
  • \B 匹配非單詞邊界

    >>> re.search(r'er\B','verb')
    
    <re.Match object; span=(1, 3), match='er'>
  • \d 匹配一個數字字元。等價於[0-9]

    >>> re.search(r'\d apples', '3 apples')
    
    <re.Match object; span=(0, 8), match='3 apples'>
  • \D 匹配一個非數字字元。等價於[^0-9]

    >>> re.search(r'\D dog', 'a dog')
    
    <re.Match object; span=(0, 5), match='a dog'>
  • \s 匹配任何空白字元,包括空格、製表符、換頁符等等。等價於[ \f\n\r\t\v]。

    >>> re.search(r'a\sdog', 'a dog')
    
    <re.Match object; span=(0, 5), match='a dog'>
  • \S 匹配任何非空白字元。等價於[^ \f\n\r\t\v]

    >>> re.search(r'\S dog', 'a dog')
    
    <re.Match object; span=(0, 5), match='a dog'>
  • \w 匹配包括下劃線的任何單詞字元。等價於“[A-Za-z0-9_]

    >>> re.search(r'\w', 'h')
    
    <re.Match object; span=(0, 1), match='h'>
  • \W 匹配任何非單詞字元。等價於“[^A-Za-z0-9_]

    >>> re.search(r'\W', '@')
    
    <re.Match object; span=(0, 1), match='@'>
  • \un Unicode轉義字元序列。其中n是一個用四個十六進位制數字表示的Unicode字元

    >>> re.search(r'\u00A9','©')
    
    <re.Match object; span=(0, 1), match='©'>

小結

如果你真的讀完了這些例項,我敢說你對正則表示式會有一定的理解了吧。下篇會重點講解python 中的正則表示式庫函式,對中文的處理等,敬請期待~

參考文件

  1. 維基百科—正則表示式

系列文章列表