介紹
正則表達式是一個規則比對的語法,可以很方便的比對文本資料是否符合特定規律,在找到文本的規律後,我們就可以進行提取、替代等操作,舉例來說,我們可以使用正則表達式來
- 檢查輸入是否符合給定的模式, 例如,我們可以檢查使用者輸入的電子信箱是否是有效的電子郵件地址
- 在一段文本中尋找模式外觀; 例如,檢查是否 “顏色”出現在文本中
- 提取文本的特定部分
- 替換部分文本; 例如,將“顏色”的更改為“紅色”
- 將較大的文本拆分,例如,通過點、逗號將文本拆分
而python的re模組便是可以用python來操作正則表達式的模組,舉例來說
pattern = re.compile(r'(?<!John\\s)Doe')
results = pattern.findall("John Doe, Calvin Doe, Hobbes Doe")
以上便是一個使用python操作正則表達式的例子,我們首先使用正則表達式的語法去表達我們想要比對怎樣的文本規律,在這個例子我們要比對r'(?<!John\s)Doe’這個規律,至於這個規律是什麼意思,我們等一下會加以解釋,而接下來針對這個規律,我們可以命令程式去發現全部符合這個規律的文字(findall)。
所以要用python使用正則表達式,我們需要了解正則表達式的語法,同時了解re模組有那些支援的操作方式。
接下來會分為
- 基本的正則表達式
- re的操作
- 進階概念-group
- 進階概念-look around
正則表達式基本語法
在介紹正則表達式的語法之前,我們先介紹一個可以用來學習正則表達式的網站,regex101,在左側,讓我們選擇python。其中上方的輸入格可以輸入正則語法,下方輸入文本,即可即時看到比對的結果!
字符比對
最簡單的規律便是直接比對字符,例如我們想要比對”蘋果”這個單詞,則讓我們在下方輸入以下文本:社畜喜歡吃蘋果,然後在上方輸入蘋果,我們即可看到蘋果被比對出來。
保留規則運算符
試試看以下的例子,如果我們的文本是社畜喜歡吃(蘋果),這時候我們想要比對(蘋果),如果我們直接輸入(蘋果),會發現沒有辦法成功比對,主要的原因在於,()是正則表達式的保留符合,就像是python裏面的一些關鍵字(像是if True等等),()在正則表達式裡面有特別的功用,所以在這個例子,它並不會被當作字符來做比對。
常見的規則運算符如下
- Backslash \
- Caret ^
- Dollar sign $
- Dot .
- Pipe symbol |
- Question mark ?
- Asterisk *
- Plus sign +
- Opening parenthesis ()
- Opening square bracket []
- The opening curly brace {}
那在上述例子,我們要如何比對(蘋果)?
用\來轉義
在這個情況我們可以用\來轉義,只要在規則運算符前面加上\即可將運算符當作字符來比對
reg = r”\(蘋果\)”
文本=社畜喜歡吃(蘋果)
這時候就比對的到了
用[]來比對集合
美式英文與英式英文對於執照這個單子有不同的拼法:
美式英文: licence
英式英文: license
如果我們同時想要從文本中提取執照的英文字,要怎麼做?我們可以發現這兩個拼法只有在倒數第二單字使用不同的字母,我們要如何讓程式可以同時配對這兩個字母?用中括號就可以達到這個效果!
想要同時比對: licen[cs]e
中括號代表c或是s都可以
用-簡寫比對集合
-這個符號在中括號裡面有特殊的功用,[a-z]可以代表所有的小寫字母,而這個手法也可以混用,例如[a-zA-Z]就是同時比對大小寫字母。更多例子如下:
想要比對數字: [0-9]
想要比對小寫字母 [a-z]
想要比對大寫字母 [A-Z]
想要同時比對大小寫字母與數字 [a-zA-Z0-9]
用^表達not in
在中括號裡面的最前方加上^代表not in,是比對沒在中括號裡面的字母,例子如下
想要比對非數字[^0-9]
想要比對非小寫字母[^a-z]
想要比對非大寫字母[^A-Z]
用.來代表任意字符
其中.這個符號可以當作萬用字符,可以比對任何的字符,像是字、特殊符號、空格等等,除了換行之外都可以被比對到,
所以想要比對任意字符,可以使用.
用|來表達或的規律
前面講得[]只能進行單一字符的多重選擇,如果我們有兩個expression想要同時比對,像是我們同時想要比對yes與no兩個規律,要怎麼做? 我們可以使用|這個特殊運算符,它可以使得正則引擎同時比對yes與no兩個規律,只要其中一個符合變回傳。
想要比對yes or no -> yes|no
常用的字符集
有些常用的字符集已經被內建了,像是
\d -> 比對數字 相當於[0-9]
\D -> 比對非數字 相當於[^0-9]
\s -> 比對空格
\S -> 比對非空格
\w -> 任何字母與數字
\W -> 任何非(字母與數字)
比對多次-量詞
我們可以在比對的字符或是規律(後面會提到group的概念),加上量詞,告述引擎這個規律要重複幾次,一些常見的量詞如下。
? -> (出現一次或是沒出現)
\* -> (沒出現或是出現無限次)
\+ -> (出現1次到無限次)
{n, m} -> 出現n~m次
貪婪與非貪婪配對
在比對時,有兩種比對方式,一種是貪婪比對,一種是非貪婪比對,假設我們考慮下列的例子 aabcccb,如果用.*b是比對的話,可能會發現比對到整串文本,但是我們可能會覺得比對到aab也是符合規律,在這個情況下,第一種比對方式,也就是盡可能發現最長符合規律的字串便是貪婪比對,而第二種就是非貪婪比對,要啟動非貪婪比對,我們可以用以下語法:.*?b
邊界比對
有時候我們需要比對邊界,例如我們想要比對每一行的第一個字母是不是a,這個時候我們可以使用^代表第一行的開頭,^a就是我們想要的規律。以下有更多的例子:
^ 可以比對一行的開頭
$ 可以比對一行的結尾
\b 可以比對字的邊界
\B 可以比對非字的邊界
Python re常用命令
因為\在python以及正則語法中都被視為特殊符號,所以在編譯正則語法時最好加個r
pattern = re.compile(“\\\\”)
這裡等價於
pattern = re.compile(r”\\”)
尋找特定規律 – match
# 尋找特定規律
# 只從文本的最開頭開始比對
import re
pattern = re.compile(r'<HTML>')
pattern.match("<HTML><head>")
#命令也可以直接由re完成上面兩步,編譯的規律會存在暫存裡。
# re.match(r'<HTML>', "<HTML><head>")
尋找特定規律 – search
# 尋找特定規律
# 從任意位置比對
pattern = re.compile(r'<HTML>')
pattern.search("a<HTML><head>")
提取符合規律的文字 – findall
# 提取符合規律的文字
# 回傳符合規律的文字列表
pattern = re.compile(r'<HTML>')
pattern.findall("<HTML>_123_<HTML>")
提取符合規律的文字 – finditer
# 提取符合規律的文字
# 回傳符合規律的文字迭代器
results = pattern.finditer("<HTML>_123_<HTML>")
type(results)
修改文本-split
#修改文本
#根據條件分割文本
pattern = re.compile(r"\\W")
pattern.split("Beautiful is better than-ugly")
修改文本-sub
#修改文本
#根據條件替代文字
pattern = re.compile(r"[0-9]+")
pattern.sub("-", "order0⇢order1⇢order13")
Group
想像下列例子
文本: “執照\s有” 或是 文本: “執照\s沒有”
這樣如果使用下列的規律無法配對 r”執照 有|沒有”,這主要是因爲|讀取到兩個規律,分別為執照 有以及沒有這兩個規則,如果要把後面的 有以及沒有當做兩個獨立的規律,需要加小括號,也就是把後面有|沒有當做獨立的子規律,這時候r”執照\s(yes|no)”,就可以比對到執照兩個字加上空格加上yes或是no。 而這個(yse|no)就是一個capturing group或是說是一個子規律。
group與matchobject
當使用match或是search時,會回傳一個matchObject,同時會存下比對到的group(最多到99個),可以使用matchobject.group方法去調用。
m = re.match(r"(\w+)\s(\w+)", "Isaac Newton, physicist")
m.group(0) # The entire match
m.group(1) # The first parenthesized subgroup.
m.group(2) # The second parenthesized subgroup.
m.group(1, 2) # Multiple arguments give us a tuple.
可以看到m.group(0)會存下整個比對的結果,而接下來的1,2,分別存下比對到的第一個group以及第二個group,而這個編號會跟後面的”回朔”的語法概念有關。
group與findall
前面提到的findall方法,如果我們的regular expression裡面包含了group,則findall會回傳group而非整個比對結果,所以我們可以利用group去指定想要抓到的文本位置。
pattern = re.compile(r"(\w+)\s(\w+)")
pattern.findall("Isaac Newton, physicist")
回朔(backreference)
有了group之後,我們可以用一個特別語法來找到之前配對到的group。這個語法叫做backreference(回朔)。
backreference 的語法是反斜線加上一個數字,這個數字是指定第幾個配對到的group,舉例來說,如果拿(\w)a\1這個語法去配對文本”cacd”,因為\w會配對到c,a會配對接下來的a,而\1會回朔我們第一個group(\w)配對到的東西,也就是c,所以上述語法會配對到,cac。
Name Group
可以看到用回朔蠻沒有可讀性的,尤其是如果group數量很多的話,所以我們可以幫group去名來增加可讀性,語法為?P<>
import re
match = re.search(r'(?P<foo>hello \S+)', 'This is a hello world!')
print(match.group('foo')) # hello world!
可以看到我們命名group為foo,接下來就可以用’foo’來叫它。
在expression呼叫name group
我們可以使用前面提到的group的概念來解下列的問題,如果我們有一連串代碼,格式像是1-a, 20-baer,前面是國家代碼,後面是id代碼,這時候如果我們想把兩個代碼的順序交換,那我們就可以用name group以及sub來做到這件事,如下列:
pattern = re.compile(r"(?P<country>\d+)-(?P<id>\w+)")
pattern.sub(r"\g<id>-\g<country>", "1-a\n20-baer\n34-afcr")
在正則命令裡,要呼叫對應的name group,前面加\g<對應的名子>。
yes – pattern | no-pattern
簡單來說,就是if-else在正則的語法,語法如下
(?(id/name)yes-pattern|no-pattern)
如果滿足條件則執行yes-pattern,沒有則執行no – pattern
下列的例子是,假設我們有一系列的產品,產品有兩種可能的格式 一種是4個字母例如erte,或者是完整的id,前後都會在加-與兩個數字,例如34-erte-22,我們可以用yes – no pattern做這件事。
首先,比對前面兩個數字以及- 也就是前面的(\d\d-),後面加一個?量詞代表出現或是沒出現都可以,然後比對4個字母,最後使用yes pattern去判斷是否第一個group存在,如果存在,則要比對後面的-與兩個數字。
pattern = re.compile(r"(\d\d-)?(\w{3,4})(?(1)(-\d\d))")
pattern.match("34-erte-22")
pattern.search("erte")
Look Around
如果我們想要用規律來配對特定位置,那我們就可以使用Look Around的技巧。詳細看影片的解說。
有四種主要的類別:
- Positive Look ahead
- Negative Look ahead
- Positive Look behind
- Negative Look behind
Positive Look ahead
要使用look ahead,我們會使用語法 ?=
假設現在的文本為 “The quick brown fox jumps over the lazy dog”
我們現在apply (?=fox)看看會有什麼效果
pattern = re.compile(r"fox")
result = pattern.search("The quick brown fox jumps over the lazy dog")
print(result.start(), result.end())
16, 19
如果這時候改成?=fox
pattern = re.compile(r"(?=fox)")
result = pattern.search("The quick brown fox jumps over the lazy dog")
print(result.start(), result.end())
16,16
可以看到這時候配對到fox前面那條線,所以在正則引擎之中,look ahead相當於要求配對到fox前面。
讓我們看看另外一個例子
pattern = re.compile(r"\\w+(?=,)")
pattern.findall("They were three: Felix, Victor, and Carlos")
["Felix", "Victor"]
可以用|把最後一個人名給抓出來
pattern = re.compile(r"\\w+(?=,|\\.)")
pattern.findall("They were three: Felix, Victor, and Carlos")
["Felix", 'Victor', 'Carlos']
Negative Look ahead
negative look ahead跟look ahead有一樣的特性,但是只有subexpression沒有配對到的時候才會發揮作用,語法為?!
pattern = re.compile(r"\\w+(?=,|\\.)")
pattern.findall("They were three: Felix, Victor, and Carlos")
["Felix", 'Victor', 'Carlos']
舉例來說, 我們想要抓到叫做John的人,但不是叫做John Smith,請看以下的例子
pattern = re.compile(r"John(?!\\sSmith)")
result = pattern.finditer("I would rather go out with John McLane than with John Smith or John Bon Jovi")
for i in result:
print(i.start(), i.end())
Look behind
語法:?<=
pattern = re.compile(r"(?<=John\\s)McLane")
result = pattern.finditer("I would rather go out with John McLane the with John Smith or John Bon Jovi")
for i in result:
print(i.start(), i.end())
Negative Look Behind
語法: ?<!
pattern = re.compile(r'(?<!John\\s)Doe')
results = pattern.finditer("John Doe, Calvin Doe, Hobbes Doe")
for result in results:
print(result.start(), result.end())