爬虫入门系列目录:
- 爬虫入门系列(一):快速理解HTTP协议
- 爬虫入门系列(二):优雅的HTTP库requests
- 爬虫入门系列(三):用 requests 构建知乎 API
- 爬虫入门系列(四):HTML文本解析库BeautifulSoup
- 爬虫入门系列(五):正则表达式完全指南(上)
- 爬虫入门系列(六):正则表达式完全指南(下)
正则表达式是一种更为强大的字符串匹配、字符串查找、字符串替换等操作工具。上篇讲解了正则表达式的基本概念和语法以及re
模块的基本使用方式,这节来详细说说 re
模块作为 Python 正则表达式引擎提供了哪些便利性操作。
>>> import re
正则表达式的所有操作都是围绕着匹配对象(Match)进行的,只有表达式与字符串匹配才有可能进行后续操作。判断匹配与否有两个方法,分别是 re.match() 和 re.search(),两者有什么区别呢?
re.match(pattern, string)
match 方法从字符串的起始位置开始检查,如果刚好有一个子字符串与正则表达式相匹配,则返回一个Match对象,只要起始位置不匹配则退出,不再往后检查了,返回 None
>>> re.match(r"b.r", "foobar") # 不匹配
>>> re.match(r"b.r", "barfoo") # 匹配
<_sre.SRE_Match object at 0x102f05b28>
>>>
re.search(pattern, string)
search 方法虽然也是从起始位置开始检查,但是它在起始位置不匹配的时候会一直尝试往后检查,直到匹配为止,如果到字符串的末尾还没有匹配,则返回 None
>>> re.search(r"b.r", "foobar") # 匹配
<_sre.SRE_Match object at 0x000000000254D578>
>>> re.match(r"b.r", "foobr") # 不匹配
两者接收参数都是一样的,第一个参数是正则表达式,第二个是预匹配的字符串。另外,不管是 search 还是 match,一旦找到了匹配的子字符串,就立刻停止往后找,哪怕字符串中有多个可匹配的子字符串,例如
>>> re.search(r"f.o", "foobarfeobar").group()
'foo'
两者的差异使得他们在应用场景上也不一样,如果是检查文本是否匹配某种模式,比如,检查字符串是不是有效的邮箱地址,则可以使用 match 来判断:
>>> rex = r"[\w]+@[\w]+\.[\w]+$"
>>> re.match(rex, "123@qq.com") # 匹配
<_sre.SRE_Match object at 0x102f05bf8>
>>> re.match(rex, "the email is 123@qq.com") # 不匹配
>>>
尽管第二个字符串中包含有邮件地址,但字符串整体不能当作一个邮件地址来使用,在网页上填邮件地址时,显然第二种写法是无效的。
通常,search 方法可用于判断字符串中是否包含有与正则表达式相匹配的子字符串,还可以从中提出匹配的子字符串,例如:
>>> rex = r"[\w]+@[\w]+\.[\w]+"
>>> m = re.search(rex, "the email is 123@qq.com .")
>>> m is None
False
>>> m.group()
'123@qq.com'
>>>
细心的你可能已经发现了,上面例子与前面例子的正则表达式写法有细微区别,前者多一个元字符 $
,它的目的是用于完全匹配字符串。因为不加 $,那么下面这种情况用match方法也匹配,显示这在表单验证时是无法满足要求的。
>>> rex = r"[\w]+@[\w]+\.[\w]+"
>>> re.match(rex, "123@qq.com is my email")
<_sre.SRE_Match object at 0x10cadebf8>
>>>
那么有没有可能不加$,就可以判断是否完全匹配字符串呢?在 Python3 中,re.fullmatch
就可以满足这样的需求。
>>> rex = r"[\w]+@[\w]+\.[\w]+"
>>> re.fullmatch(rex, "123@qq.com is my email") # 不匹配
>>> re.fullmatch(rex, "123@qq.com") # 匹配
<_sre.SRE_Match object; span=(0, 10), match='123@qq.com'>
虽然二者都可以通过 group() 提取出匹配的子字符串,但是,如果字符串中有多个匹配的子字符串时,两个方法都不行,因为它们都是在一旦匹配了第一个子字符串,就不再往后匹配了。
>>> m = re.search(rex, "email is 123@qq.com, anthor email is abc@gmail.com !")
>>> m.group()
'123@qq.com'
那么如何把文本中的所有匹配的邮件地址提取出来呢?re 模块为我们准备了 re.findall() 和 re.finditer() 这两个方法,它们会返回文本中所有与正则表达式相匹配的内容。前者返回的是一个列表(list)对象,后者返回的是一个迭代器(iterator)。
re.findall(pattern, string)
>>> emails = re.findall(rex, "email is 123@qq.com, anthor email is abc@gmail.com")
>>> emails
['123@qq.com', 'abc@gmail.com']
findall 返回的对象是由匹配的子字符串组成的列表,它返回了所有匹配的邮件地址。
re.finditer(pattern, string)
>>> emails = re.finditer(rex, "email is 123@qq.com, anthor email is abc@gmail.com")
>>> emails
<callable-iterator object at 0x0000000002592390>
>>> for e in emails:
... print(e.group())
...
123@qq.com
abc@gmail.com
finditer 返回的对象是由 Match 对象组成的迭代器,因为里面的元素是Match对象,所以要获取里面的邮件地址还需要调用group方法来提取。关于列表和迭代器的区别,此文不做介绍,可以查看公众号“Python之禅”的历史文章。
re.split
我们都知道字符串有一个split方法,可根据某个子串分隔字符串,如:
>>> "this is a string.".split(" ")
['this', 'is', 'a', 'string.']
但该方法有一个缺陷,比如上面的字符串,根据空格分隔字符串时,字符串后面多一个点,如果用 re.split 就可以避免这种情况。
>>> words = re.split(r"\W+", "this is a string.")
>>> words
['this', 'is', 'a', 'string', '']
>>> list(filter(lambda x: x, words))
['this', 'is', 'a', 'string']
>>>
re.split是一种更为高级的字符串分隔操作的方法。在这里,split根据非字母正则来分隔字符串,但凡是 string.split 没法处理的问题,可以考虑使用re模块下的split方法来处理。此外,正则表达式中如果有分组括号,那么返回结果又不一致,这个可以留给大家查阅文档,某些场景用得着。
re.sub(pattern, repl, string)
re.split是一种更为高级的字符串分隔操作的方法。在这里,split根据非字母正则来分隔字符串,但凡是 string.split 没法处理的问题,可以考虑使用re模块下的split方法来处理。此外,正则表达式中如果有分组括号,那么返回结果又不一致,这个可以留给大家查阅文档,某些场景用得着。
把所有邮箱地址替换成 admin@qq.com
>>> rex = r"[\w]+@[\w]+\.[\w]+" # 邮件地址正则
>>> re.sub(rex, "admin@qq.com", "234@qq.com, 456@qq.com ")
'admin@qq.com, admin@qq.com '
>>>
另外一个例子,就是上次讲过的将 img 标签的 src 路径替换成绝对完整的URL地址
html = """
...
![](/images/category.png)
this is anthor words
![](http://foofish.net/images/js_framework.png)
"""
如果用字符串的replace方法是没法实现了,这时需要用到正则表达式的 re.sub,正则表达式应用了非贪婪模式,使用了一个分组,用于提取 src 的路径。
rex = r'.*?![]((.*?))'
这里我们要把替换目标 repl 作为函数来处理。
def fun(m):
img_tag = m.group()
src = m.group(1)
if not src.startswith("http:"):
full_src = "http://foofish.net" + src
else:
full_src = src
new_img_tag = img_tag.replace(src, full_src)
return new_img_tag
引擎会自动把所有匹配的结果应用到该函数中,函数的参数就是每一个匹配的Match对象,通过 group(1) 提取分组后判断是否为一个完整的URL路径,只有是不完整的我们才替换,否则还是按照原来的方式返回。
new_html = re.compile(rex).sub(fun, html)
print(new_html)
# 输出
...
![](http://foofish.net/images/category.png)
this is anthor words
![](http://foofish.net/images/js_framework.png)
如果还想知道替换次数是多少,那么可以使用 re.subn
方法,这个方法具体使用可以参考文档,留着读者自己思考。
此外,以上方法都有一个默认的 flag 参数,该参数用于改变匹配的行为,常用的可选值有:
- re.I(IGNORECASE): 忽略大小写(括号内的单词为完整写法,两种方式都支持)
- re.M(MULTILINE): 多行模式,改变'^'和'$'的行为
- re.S(DOTALL): 改变'.'的行为,默认 . 只能匹配除换行之外的字符,加上它就可以匹配换行了
例如:
>>> re.match(r"foo", "FoObar", re.I)
<_sre.SRE_Match object; span=(0, 3), match='FoO'>
>>>
以上介绍的都是 re 模块下面的方法,其实,这些只不过是一些简便方法,例如 re.match 方法
re.match(r'foo', 'foo bar')
等价于
pattern = re.compile(r'foo')
pattern.match('foo bar')
那么,后者有什么好处呢?为了提高正则匹配的速度,它可以重复利用正则对象,如果一个正则表达式需要匹配多个字符串,那么就推荐后者,先编译在去匹配。更多使用方式可以参考文档 https://docs.python.org/3/library/re.html