“代碼勝于雄辯。”
——林納斯·托瓦茲(Linus Torvalds)
許多編程語言和操作系統都支持正則表達式(regular expression):定義搜索模式的一組字符串。正則表達式可用于檢索文件或其他數據中是否存在指定的復雜模式。例如,可使用正則表達式匹配文件中所有的數字。本章將學習如何定義正則表達式,將其傳入類UNIX操作系統以用來檢索文件的grep命令。該命令會返回文件中與指定模式匹配的文本。我們還將學習在Python中使用正則表達式檢索字符串。
17.1 初始配置
開始之前,先創建一個名為zen.txt的文件。在命令行中(確保位于zen.txt所在的目錄)輸入命令python3 -c "import this",這會打印出蒂姆·皮特斯(Tim Peters)寫的詩The Zen of Python(Python之禪):
Python之禪
優美勝于丑陋
明了勝于晦澀
簡潔勝于復雜
復雜勝于凌亂
扁平勝于嵌套
間隔勝于緊湊
可讀性很重要
即便假借特例的實用性之名,也不可違背這些規則
不要包容所有錯誤,除非你確定需要這樣做
當存在多種可能,不要嘗試去猜測
而是盡量找一種,最好是唯一一種明顯的解決方案
雖然這并不容易,因為你不是Python之父
做也許好過不做,但不假思索就動手還不如不做
如果你無法向人描述你的方案,那肯定不是一個好方案
命名空間是一種絕妙的理念,我們應當多加利用
旗標-c告訴Python傳入的字符串中包含有Python代碼。然后Python會執行傳入的代碼。Python執行import this之后,將打印The Zen of Python(像上述這首詩一樣隱藏在代碼中的信息,也被稱為彩蛋)。在Bash中輸入exit()函數退出Python,然后將詩的內容復制到文件zen.txt中。
在Ubuntu系統中,grep命令默認在輸出時以紅色字體打印匹配的文本,但是在UNIX系統中則不是這么做的。如果使用的是mac,可以通過在Bash中修改如下環境變量來改變該行為:
# http://tinyurl.com/z9prphe
$ export GREP_OPTIONS='--color=always'
$ export GREP_OPTIONS='--color=always'
要記住,在Bash中直接設置環境變量的方式是不持久的,如果退出Bash,下次再打開時必須重新設置環境變量。因此,可將環境變量添加至.profile文件,使其持久存在。
17.2 簡單匹配
grep命令接受兩個參數:一個正則表達式和檢索正則表達式中定義模式的文件路徑。使用正則表達式進行最簡單的模式匹配,就是簡單匹配,即一個字符串匹配單詞中相同的字符串。舉個例子,在zen.txt文件所在的目錄輸入如下命令:
# http://tinyurl.com/jgh3x4c
$ grep Beautiful zen.txt
>> Beautiful is better than ugly.
上例中執行的命令里,第一個參數Beautiful是一個正則表達式,第二個參數zen.txt是檢索正則表達式的文件。Bash打印了Beautiful is better than ugly.這句話,其中Beautiful為紅色,因為它是正則表達式匹配上的單詞。
如果將上例中的正則表達式從Beautiful修改為beautiful,grep將無法匹配成功:
# http://tinyurl.com/j2z6t2r
$ grep beautiful zen.txt
當然,可以加上旗標-i來忽略大小寫:
# http://tinyurl.com/zchmrdq
$ grep -i beautiful zen.txt
>> Beautiful is better than ugly.
grep命令默認打印匹配文本所在的整行內容。可以添加旗標-o,確保只打印與傳入的模式參數相匹配的文本:
# http://tinyurl.com/zfcdnmx
$ grep -o Beautiful zen.txt
>> Beautiful
也可通過內置模塊re在Python中使用正則表達式。re模塊提供了一個叫findall的方法,將正則表達式和目標文本作為參數傳入,該方法將以列表形式返回文本中與正則表達式匹配的所有元素:
01 # http://tinyurl.com/z9q2286
02
03
04 <strong>import</strong> re
05
06
07 l = "Beautiful is better than ugly."
08
09
10 matches = re.findall("Beautiful", l)
11
12
13 print(matches)
>> ['Beautiful']
本例中findall方法只找到了一處匹配,返回了一個包含匹配結果[Beautiful]的列表。
將re.IGNORECASE作為第3個參數傳入findall,可以讓其忽略大小寫:
01 # http://tinyurl.com/jzeonne
02
03
04 <strong>import</strong> re
05
06
07 l = "Beautiful is better than ugly."
08
09
10 matches = re.findall("beautiful",
11 l,
12 re.IGNORECASE)
13
14
15 print(matches)
>> ['Beautiful']
17.3 匹配起始位置
我們還可以在正則表達式中加入特殊字符來匹配復雜模式,特殊字符并不匹配單個字符,而是定義一條規則。例如,可使用補字符號 ^ 創建一個正則表達式,表示只有模式位于行的起始位置時才匹配成功:
# http://tinyurl.com/gleyzan
$ grep ^If zen.txt
>> If the implementation is hard to explain, it is a bad idea.
>> If the implementation is easy to explain, it may be a good idea.
類似地,還可使用美元符號$來匹配結尾指定模式的文本行:
# http://tinyurl.com/zkvpc2r
$ grep idea.$ zen.txt
>> If the implementation is hard to explain, it is a bad idea.
>> If the implementation is easy to explain, it may be a good idea.
本例中,grep忽略了Namespaces are one honking great idea -- let us do more of those!這行,因為它雖然包含了單詞idea,但并不是以其結尾。
下例是在Python中使用補字符 ^ 的示例(必須傳入re.MULITILINE作為findall的第3個參數,才能在多行文本中找到所有匹配的內容):
01 # http://tinyurl.com/zntqzc9
02
03
04 <strong>import</strong> re
05
06
07 zen = """Although never is
08 often better than
09 *right* now.
10 If the implementation
11 is hard to explain,
12 it's a bad idea.
13 If the implementation
14 is easy to explain,
15 it may be a good
16 idea. Namesapces
17 are one honking
18 great idea -- let's
19 do more of those!
20 """
21
22
23 m = re.findall("^If",
24 zen,
25 re.MULITILINE)
26 print(m)
>> ['If', 'If']
17.4 匹配多個字符
將正則表達式的多個字符放在方括號中,即可定義一個匹配多個字符的模式。如果在正則表達式中加入[abc],則可匹配a、b或c。在下一個示例中,我們不再是直接匹配zen.txt中的文本,而是將字符串以管道形式傳給grep進行匹配。示例如下:
# http://tinyurl.com/jf9qzuz
$ echo Two too. <strong>|</strong> grep -i t[ow]o
>> Two too
echo命令的輸出被作為輸入傳給grep,因此不用再為grep指定文件參數。上述命令將two和too都打印出來,是因為正則表達式均匹配成功:第一個字符為t,中間為o或w,最后是o。
Python實現如下:
01 # http://tinyurl.com/hg9sw3u
02
03
04 <strong>import</strong> re
05
06
07 string = "Two too."
08
09
10 m = re.findall("t[ow]o",
11 string,
12 re.IGNORECASE)
13 print(m)
>> ['Two', 'too']
17.5 匹配數字
可使用[[:digit:]]匹配字符串中的數字:
# http://tinyurl.com/gm8o6gb
$ echo 123 hi 34 hello. <strong>|</strong> grep [[:digit:]]
>> 123 hi 34 hello.
在Python 中使用d匹配數字:
1 # http://tinyurl.com/z3hr4q8
2
3
04 <strong>import</strong> re
05
06
07 line = "123?34 hello?"
08
09
10 m = re.findall("d",
11 line,
12 re.IGNORECASE)
13
14
15 print(m)
>> ['1', '2', '3', '3', '4']
17.6 重復
星號符*可讓正則表達式支持匹配重復字符。加上星號符之后,星號前面的元素可匹配零或多次。例如,可使用星號匹配后面接任意個o的tw:
# http://tinyurl.com/j8vbwq8
$ echo two twoo not too. <strong>|</strong> grep -o two*
>> two
>> twoo
在正則表達式中,句號可匹配任意字符。如果在句號后加一個星號,這將讓正則表達式匹配任意字符零或多次。也可使用句號加星號,來匹配兩個字符之間的所有內容:
# http://tinyurl.com/h5x6cal
$ echo __hello__there <strong>|</strong> grep -o __.*__
>> __hello__
正則表達式__.*__可匹配兩個下劃線之間(包括下劃線)的所有內容。星號是貪婪匹配(greedy),意味著會盡可能多地匹配文本。例如,如果在雙下劃線之間加上更多的單詞,上例中的正則表達式也會匹配從第一個下劃線到最后一個下劃線之間的所有內容:
# http://tinyurl.com/j9v9t24
$ echo __hi__bye__hi__there <strong>|</strong> grep -o __.*__
>> __hi__bye__hi__
如果不想一直貪婪匹配,可以在星號后面加個問號,使得正則表達式變成非貪婪模式(non-greedy)。一個非貪婪的正則表達式會盡可能少地進行匹配。在本例中,將會在碰到第一個雙下線后就結束匹配,而不是匹配第一個和最后一個下劃線之間的所有內容。grep并不支持非貪婪匹配,但是在Python中可以實現:
01 # http://tinyurl.com/j399sq9
02
03
04 <strong>import</strong> re
05
06
07 t = "__one__ __two__ __three__"
08
09
10 found = re.findall("__.*?__", t)
11
12
13 <strong>for</strong> match <strong>in</strong> found:
14 print(match)
>> __one__
>> __two__
>> __three__
我們可通過Python中的非貪婪匹配,來實現游戲Mad Libs(本游戲中會給出一段文本,其中有多個單詞丟失,需要玩家來補全):
01 # http://tinyurl.com/ze6oyua
02
03 <strong>import</strong> re
04
05
06 text = """Giraffes have aroused
07 the curIOSity of __PLURAL_NOUN__
08 since earliest times. The
09 giraffe is the tallest of all
10 living __PLURAL_NOUN__, but
11 scientists are unable to
12 explain how it got its long
13 __PART_OF_THE_BODY__. The
14 giraffe's tremendous height,
15 which might reach __NUMBER__
16 __PLURAL_NOUN__, comes from
17 it legs and __BODYPART__.
18 """
19
20
21 <strong>def</strong> mad_libs(mls):
22 """
23 :param mls:字符串
24 雙下劃線部分的內容要由玩家來補充。
25 雙下劃線不能出現在提示語中,如不能
26 出現 __hint_hint__,只能是 __hint__。
27
28
29
30
31 """
32 hints = re.findall("__.*?__",
33 mls)
34 <strong>if</strong> hints <strong>is</strong> <strong>not</strong> None:
35 <strong>for</strong> word <strong>in</strong> hints:
36 q = "Enter a {}".format(word)
37 new = input(q)
38 mls = mls.replace(word, new, 1)
39 <strong>print</strong>("n")
40 mls = mls.replace("n", "")
41 print(mls)
42 <strong>else</strong>:
43 <strong>print</strong>("invalid mls")
44
45
46 mad_libs(text)
>> enter a __PLURAL_NOUN__
本例中,我們使用re.findall匹配變量text中所有被雙下劃線包圍的內容(每個均為玩家需要輸入答案進行替代的內容),以列表形式返回。然后,對列表中的元素進行循環,通過每個提示來要求玩家提供一個新的單詞。之后,創建一個新的字符串,將提示替換為玩家輸入的詞。循環結束后,打印替換完成后的新字符串。
17.7 轉義
我們可以在正則表達式中對字符進行轉義(忽略字符的意義,直接進行匹配)。在正則表達式中的字符前加上一個反斜杠即可進行轉義:
# http://tinyurl.com/zkbumfj
$ echo I love $ <strong>|</strong> grep \$
>> I love $
通常情況下,美元符號的意思是出現在匹配文本行尾時才有效,但是由于我們進行了轉義,這個正則表達式只是匹配目標文本中的美元符號。
Python實現如下:
01 # http://tinyurl.com/zy7pr41
02
03
04 <strong>import</strong> re
05
06
07 line = "I love $"
08
09
10 m = re.findall("\$",
11 line,
12 re.IGNORECASE)
13
14
15 print(m)
>> ['$']
17.8 正則工具
找到匹配模式的正則表達式是一件很困難的事。可前往http://theselftaughtprogrammer. io/regex了解有助于創建正則表達式的工具。
17.9 術語表
正則表達式:定義檢索模式的字符串序列。
菜單:代碼中隱藏的信息。
貪婪匹配:盡量多地匹配文本的正則表達式。
非貪婪匹配:盡可能少地進行文本匹配的正則表達式。
本文摘自《Python編程無師自通——專業程序員的養成》