“代码胜于雄辩。”
——林纳斯·托瓦兹(Linus Torvalds)
许多编程语言和操作系统都支持正则表达式(regular expression):定义搜索模式的一组字符串。正则表达式可用于检索文件或其他数据中是否存在指定的复杂模式。例如,可使用正则表达式匹配文件中所有的数字。本章将学习如何定义正则表达式,将其传入类UNIX操作系统以用来检索文件的grep命令。该命令会返回文件中与指定模式匹配的文本。我们还将学习在Python中使用正则表达式检索字符串。
开始之前,先创建一个名为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文件,使其持久存在。
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']
我们还可以在正则表达式中加入特殊字符来匹配复杂模式,特殊字符并不匹配单个字符,而是定义一条规则。例如,可使用补字符号 ^ 创建一个正则表达式,表示只有模式位于行的起始位置时才匹配成功:
# 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']
将正则表达式的多个字符放在方括号中,即可定义一个匹配多个字符的模式。如果在正则表达式中加入[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']
可使用[[: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']
星号符*可让正则表达式支持匹配重复字符。加上星号符之后,星号前面的元素可匹配零或多次。例如,可使用星号匹配后面接任意个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中所有被双下划线包围的内容(每个均为玩家需要输入答案进行替代的内容),以列表形式返回。然后,对列表中的元素进行循环,通过每个提示来要求玩家提供一个新的单词。之后,创建一个新的字符串,将提示替换为玩家输入的词。循环结束后,打印替换完成后的新字符串。
我们可以在正则表达式中对字符进行转义(忽略字符的意义,直接进行匹配)。在正则表达式中的字符前加上一个反斜杠即可进行转义:
# 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)
>> ['$']
找到匹配模式的正则表达式是一件很困难的事。可前往http://theselftaughtprogrammer. io/regex了解有助于创建正则表达式的工具。
正则表达式:定义检索模式的字符串序列。
菜单:代码中隐藏的信息。
贪婪匹配:尽量多地匹配文本的正则表达式。
非贪婪匹配:尽可能少地进行文本匹配的正则表达式。
本文摘自《Python编程无师自通——专业程序员的养成》