爬虫入门系列(六):正则表达式完全指南(下)

爬虫入门系列目录:

  1. 爬虫入门系列(一):快速理解HTTP协议
  2. 爬虫入门系列(二):优雅的HTTP库requests
  3. 爬虫入门系列(三):用 requests 构建知乎 API
  4. 爬虫入门系列(四):HTML文本解析库BeautifulSoup
  5. 爬虫入门系列(五):正则表达式完全指南(上)
  6. 爬虫入门系列(六):正则表达式完全指南(下)

正则表达式是一种更为强大的字符串匹配、字符串查找、字符串替换等操作工具。上篇讲解了正则表达式的基本概念和语法以及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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容