本文为《爬着学Python》系列第六篇文章。
本文也算是系列教程的第二篇不规则更新。
在讨论Python正则表达式之前,我想先说说Python的原始字符串(Raw String)。我建议各位在Python正则式的pattern中一律使用原始字符串,本文的目的就在于讨论为什么要用原始字符串以及使用过程中需要注意的事项。
什么是原始字符串
原始字符串的Python提供的一种内置的用于简化转义的字符串形式。用法很简单,在定义字符串时加个r或者R就可以r'string'
。
>>> a = r'hello'
>>> type(a)
可以看到,即使我们把a
定义成原始字符串,但是它的类型依然是字符串。而且不仅如此
>>> a = r'hello'
>>> b = 'hello'
>>> a is b
True
我们看到如果我们再定义一个普通字符串,他们在内存中引用的是同一个对象。目前,我们基本可以推测,所谓原始字符串,其实质就是普通字符串。
既然一样,那么问题来了,那要它何用呢?
原始字符串的作用
可以说,原始字符串就是为了Python正则表达式而存在的,可以说也是Python的语法糖。我们先试试看最常见的例子,如何在Python正则中匹配\
这个符号呢。下文中除特殊说明待处理的原字符串都默认设置为a\b\/c/d
>>> import re
>>> BASE = 'a\b\/c/d'
>>> print(BASE)
\/c/d
我们注意到输出这个字符串时,\b
作为转义字符正常工作着,本来应该输出的a被退格了。
现在我们要尝试匹配BASE
中的\
该如何做呢?
>>> re.findall('\', BASE)
File "", line 1
re.findall('\', a)
^
SyntaxError: EOL while scanning string literal
上面的这个做法显然是在卖萌,程序是必然会报错的,原因在于\'
会被转义为'
,而且我们知道
>>> re.findall('', BASE)
['', '', '', '', '', '', '', '']
也就是说,转义后的'
是作为字符串的内容,不能作为标识字符串结束的边界。
正确的做法是
>>> re.findall('\\\\', BASE)
['\\']
结果是我们匹配到了一个\
,也就是说,待处理字符串中作为转义字符使用的\
是匹配不到的。我们为了匹配一个\
,需要用上四个\
。
那为什么我们的pattern需要四个\
呢?原因在于
>>> print('\\\\')
\\
我们的pattern字符串'\\'
会转义成\
,于是'\\\\'
在正则匹配函数中先被理解为'\\'
,而'\\'
用来匹配待处理字符串,则再一次被理解为用\
来匹配字符串。
也就是说,正则表达式中进行了两次转义。第一次将字符串转义成pattern,在pattern匹配时则再次转义。为什么需要两次转义呢?
根本原因在于,字符串中的转义规则和正则表达式中的转义规则不一样。举例来说,'\b'
在一般的字符串里面会转义成退格,在正则表达式里面会转义成单词边界。如果不约定好转义规则,'\b'
到底应该转义成什么呢?于是Python中给出的解决办法是,先进行普通字符串的转义,将转义后的字符串作为参数传入正则匹配函数中,在正则匹配的过程中再进行正则表达式的转义。
这样的做法消除了歧义,但是带来了麻烦,造成了\
泛滥的现象,这会给正则表达式带来极大的不便利。像用'\\\\'
来匹配\
的处理办法看上去太丑陋了。为了简化理解和操作,Python提供了原始字符串
>>> re.findall(r'\\', BASE)
['\\']
这使得我们可以使用匹配r'\\'
来匹配\
,这就显得容易理解多了,\
在字符串中需要转义,这是我们能接受的。
原始字符串和普通字符串唯一的区别在于——原始字符串中的\
都是默认经过转义的。
例外
但是,我要说但是,r'\'
这种字符串还是不能出现的
>>> print(r'\')
File "", line 1
print(r'\')
^
SyntaxError: EOL while scanning string literal
也就是说,在原始字符串中,\
依然会对引号进行转义(看上去)
>>> print(r"\'")
\'
>>> print(repr(r"\'"))
"\\'"
>>> print("\\'")
\'
通过repr[1]函数可以清楚地看到,原始字符串中的\
是经过自动转义的,因此先还原成\\
,又在输出函数中再次转义成\
。
总之,想要用原始字符串只输出\
几乎是不可能的,这也是不推荐大家在正则表达式以外的地方使用原始字符串的原因。我们可以简单理解为原始字符串是正则表达式中为了简化而专用的字符串形式。在正则式以外的地方能避免原始字符串带来的歧义就尽量不用。
测试
现在回到我们一直用的BASE
,如何匹配其中的\/
这两个字符呢?我们需要的输出是\/
或者'\\/'
以下是我们的结论
>>> re.findall('\\\\/', BASE)
['\\/']
>>> re.findall(r'\\/', BASE)
['\\/']
链接
- 6.2. re — Regular expression operations — Python 3.6.2 documentation
- In context of Python Raw string - Stack Overflow
TODO
-
repr函数 ↩