引言
刚接触正则表达式,我也曾被它们天书似的符号组合给吓住,但经过一段时间的深入学习,发现它并没有想象中那么可怕,只要多实践,多理解,也是可以轻松搞定的。
而且我发现带着问题去学习,求知欲会驱使着你往前走,不知不觉就懂了。
下面就是我在学习中提出的几个问题,在后面会依次进行讨论。由于正则表达式涉及到的内容确实非常多,分成两篇来阐述。
- 什么是正则表达式?
- 正则表达式可以干什么?
- 正则表达式的语法以及在 python 中这些语法是如何使用的?
- 正则表达式如何处理中文字符?
- python 的正则表达式库中有哪些重要的函数?
什么是正则表达式?
正则表达式使用单个字符串来描述,匹配一系列符合某个句法规则的字符串。
— *
先来划重点:
- 正则表达式的表现形式是 单个字符串
- 它用来执行匹配的动作
- 匹配的对象也是字符串
语言总是有些苍白的,必须要结合实例才能理解的更清楚,先来看一个例子:
>>> 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 中的正则表达式库函数,对中文的处理等,敬请期待~