• Python——正则表达式


    此篇文章结合小甲鱼的笔记和视频整理。

    1 编译

    Python 通过 re 模块为正则表达式引擎提供一个接口,同时允许你将正则表达式编译成模式对象,并用它们来进行匹配。

    正则表达式被编译为模式对象,该对象拥有各种方法供你操作字符串,如查找模式匹配或者执行字符串替换。

    >>> import re
    >>> p = re.compile('ab*', re.IGNORECASE)

    正则表达式作为一个字符串参数传给 re.compile()。由于正则表达式并不是 Python 的核心部分,因此没有为它提供特殊的语法支持,所以正则表达式只能以字符串的形式表示。相反,re 模块仅仅是作为 C 的扩展模块包含在 Python 中,就像 socket 模块和 zlib 模块。当你将正则表达式编译之后,你就得到一个模式对象。那你拿他可以用来做什么呢?模式对象拥有很多方法和属性,我们下边列举最重要的几个来讲:

    方法 功能
    match() 判断一个正则表达式是否从开始处匹配一个字符串
    search() 遍历字符串,找到正则表达式匹配的第一个位置
    findall() 遍历字符串,找到正则表达式匹配的所有位置,并以列表的形式返回
    finditer() 遍历字符串,找到正则表达式匹配的所有位置,并以迭代器的形式返回

    如果没有找到任何匹配的话,match() 和 search() 会返回 None;如果匹配成功,则会返回一个匹配对象(match object),包含所有匹配的信息:例如从哪儿开始,到哪儿结束,匹配的子字符串等等。

    举例说明如下:

    >>> import re
    >>> p = re.compile('[a-z]+')
    >>> print(p.match(""))
    None
    >>> m = p.match('fishc')
    >>> m
    <_sre.SRE_Match object; span=(0, 5), match='fishc'>

    在这个例子中,match() 返回一个匹配对象,我们将其存放在变量 m 中,以便日后使用。接下来让我们来看看匹配对象里边有哪些信息吧。匹配对象包含了很多方法和属性,以下几个是最重要的:

    方法 功能
    group() 返回匹配的字符串
    start() 返回匹配的开始位置
    end() 返回匹配的结束位置
    span() 返回一个元组表示匹配位置(开始,结束)

    如上述例子中的m:

    >>> m.group()
    'fishc'
    >>> m.start()
    0
    >>> m.end()
    5
    >>> m.span()
    (0, 5)

    由于 match() 只检查正则表达式是否在字符串的起始位置匹配,所以 start() 总是返回 0。然而,search() 方法可就不一样了:

    >>> print(p.match('^_^fishc'))
    None
    >>> m = p.search('^_^fishc')
    KeyboardInterrupt
    >>> m = p.search('^_^fishc')
    >>> print(m)
    <_sre.SRE_Match object; span=(3, 8), match='fishc'>
    >>> m.group()
    'fishc'
    >>> m.span()
    (3, 8)


    有两个方法可以返回所有的匹配结果,一个是 findall(),findall() 返回的是一个列表;另一个是 finditer(),findall() 需要在返回前先创建一个列表,而 finditer() 则是将匹配对象作为一个迭代器返回。

    使用正则表达式也并非一定要创建模式对象,然后调用它的匹配方法。因为,re 模块同时还提供了一些全局函数,例如 match(),search(),findall(),sub() 等等。这些函数的第一个参数是正则表达式字符串,其他参数跟模式对象同名的方法采用一样的参数;返回值也一样,同样是返回 None 或者匹配对象。其实,这些函数只是帮你自动创建一个模式对象,并调用相关的函数(上一篇的内容,还记得吗?)。它们还将编译好的模式对象存放在缓存中,以便将来可以快速地直接调用。
    那我们到底是应该直接使用这些模块级别的函数呢,还是先编译一个模式对象,再调用模式对象的方法呢?这其实取决于正则表达式的使用频率,如果说我们这个程序只是偶尔使用到正则表达式,那么全局函数是比较方便的;如果我们的程序是大量的使用正则表达式(例如在一个循环中使用),那么建议你使用后一种方法,因为预编译的话可以节省一些函数调用。但如果是在循环外部,由于得益于内部缓存机制,两者效率相差无几。

    2 编译标志

    编译标志让你可以修改正则表达式的工作方式。在 re 模块下,编译标志均有两个名字:完整名和简写。)。另外,多个标志还可以同时使用(通过“|”),如:re.I | re.M 就是同时设置 I 和 M 标志。

    下边列举一些支持的编译标志(详解解释参考《Python3 如何优雅地使用正则表达式(详解三)》):

    标志 含义
    ASCII, A 使得转义符号如 w,,s 和 d 只能匹配 ASCII 字符
    DOTALL, S 使得 . 匹配任何符号,包括换行符
    IGNORECASE, I 匹配的时候不区分大小写
    LOCALE, L 支持当前的语言(区域)设置
    MULTILINE, M 多行匹配,影响 ^ 和 $
    VERBOSE, X (for 'extended') 启用详细的正则表达式

    3 分组

    通常在实际的应用过程中,我们除了需要知道一个正则表达式是否匹配之外,还需要更多的信息。对于比较复杂的内容,正则表达式通常使用分组的方式分别对不同内容进行匹配。在正则表达式中,使用元字符 ( ) 来划分组。( ) 元字符跟数学表达式中的小括号含义差不多;它们将包含在内部的表达式组合在一起,所以你可以对一个组的内容使用重复操作的元字符,例如 *,+,? 或者 {m, n}。

    例如,(ab)* 会匹配零个或者多个 ab:

    >>> p = re.compile('(ab)*')
    >>> print(p.match('ababababab').span())
    (0, 10)

    使用 ( ) 表示的子组我们还可以对它进行按层次索引,可以将索引值作为参数传递给这些方法:group(),start(),end() 和 span()。序号 0 表示第一个分组(这个是默认分组,一直存在的,所以不传入参数相当于默认值 0):

    >>> p = re.compile('(a)b')
    >>> m = p.match('ab')
    >>> m.group()
    'ab'
    >>> m.group(0)
    'ab'

    子组的索引值是从左到右进行编号,子组也允许嵌套,因此我们可以通过从左往右来统计左括号 ( 来确定子组的序号。

    >>> p = re.compile('(a(b)c)d')
    >>> m = p.match('abcd')
    >>> m.group(0)
    'abcd'
    >>> m.group(1)
    'abc'
    >>> m.group(2)
    'b'

    group() 方法可以一次传入多个子组的序号:

    >>> m.group(2,1,2)
    ('b', 'abc', 'b')

    通过 groups() 方法可以一次性返回所有的子组匹配的字符串:

    >>> m.groups()
    ('abc', 'b')


    还有一个反向引用的概念需要介绍。反向引用指的是你可以在后面的位置使用先前匹配过的内容,用法是反斜杠加上数字。例如 1 表示引用前边成功匹配的序号为 1 的子组。

    >>> p = re.compile(r'(w+)s+1')
    >>> p.search('Paris in the the spring').group()
    'the the'

    4 非捕获命名组

    精心设计的正则表达式可能会划分很多组,这些组不仅可以匹配相关的子串,还能够对正则表达式本身进行分组和结构化。在复杂的正则表达式中,由于有太多的组,因此通过组的序号来跟踪和使用会变得困难。有两个新的功能可以帮你解决这个问题——非捕获组和命名组——它们都使用了一个公共的正则表达式扩展语法。

    有时候你只是需要用一个组来表示部分正则表达式,你并不需要这个组去匹配任何东西,这时你可以通过非捕获组来明确表示你的意图。非捕获组的语法是 (?:...),这个 ... 你可以替换为任何正则表达式。

    接着举一个找到网站中的IP地址并打印出来的例子:

    import urllib.request
    import re
    
    def open_url(url):
        req = urllib.request.Request(url)
        req.add_header('User-Agent','Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36')
    
        page = urllib.request.urlopen(req)
        html = page.read().decode('utf-8')
    
        return html
    
    def get_ip(html):
        p = r'(?:(?:[0,1]?d?d|2[0-4]d|25[0-5]).){3}(?:2[0-4]d|25[0-5]|[0,1]?d?d)'
        iplist = re.findall(p,html)
        for each in iplist:
            print(each)
    
    if __name__ =='__main__':
        url = "http://www.data5u.com/"
        get_ip(open_url(url))
    

     

    上述例子中如果不适用非捕获命名组,打印出来的结果将会是:

    ('174.', '174', '213')
    ('145.', '145', '45')
    ('117.', '117', '11')
    ('137.', '137', '137')
    ('133.', '133', '127')
    ('130.', '130', '222')
    ('226.', '226', '101')
    ('240.', '240', '254')
    ('191.', '191', '5')
    ('134.', '134', '57')
    ('17.', '17', '33')
    ('131.', '131', '117')
    ('4.', '4', '82')
    ('226.', '226', '31')
    ('141.', '141', '193')
    ('192.', '192', '101')
    ('25.', '25', '116')
    ('18.', '18', '35')
    ('18.', '18', '60')
    ('93.', '93', '250')

    使用了非捕获命名组后的结果如下:

    113.236.174.213
    52.166.145.45
    139.59.117.11
    203.189.137.137
    1.238.133.127
    114.27.130.222
    52.21.226.101
    159.192.240.254
    66.70.191.5
    190.147.134.57
    101.129.17.33
    116.197.131.117
    39.46.4.82
    149.56.226.31
    203.189.141.193
    202.162.192.101
    125.77.25.116
    122.72.18.35
    122.72.18.60
    40.71.93.250

    除了你不能从非捕获组获得匹配的内容之外,其他的非捕获组跟普通子组没有什么区别了。你可以在里边放任何东西,使用重复功能的元字符,或者跟其他子组进行嵌套(捕获的或者非捕获的子组都可以)。
    当你需要修改一个现有的模式的时候,(?:...) 是非常有用的。原始是添加一个非捕获组并不会影响到其他(捕获)组的序号。值得一提的是,在搜索的速度上,捕获组和非捕获组的速度是没有任何区别的。

    5 命名组

    命名组。普通子组我们使用序列来访问它们,命名组则可以使用一个有意义的名字来进行访问。
    命名组的语法是 Python 特有的扩展语法:(?P<name>)。很明显,< > 里边的 name 就是命名组的名字啦。命名组除了有一个名字标识之外,跟其他捕获组是一样的。
    匹配对象的所有方法不仅可以处理那些由数字引用的捕获组,还可以处理通过字符串引用的命名组。除了使用名字访问,命名组仍然可以使用数字序号进行访问:

    >>> import re
    >>> p = re.compile(r'(?P<word>w+)')
    >>> m = p.search( '(((( Lots of punctuation )))' )
    >>> m.group('word')
    'Lots'
    >>> m.group(1)
    'Lots'
    >>> m
    <_sre.SRE_Match object; span=(5, 9), match='Lots'>

    正则表达式中,反向引用的语法像 (...)1 是使用序号的方式来访问子组;在命名组里,显然也是有对应的变体:使用名字来代替序号。其扩展语法是 (?P=name),含义是该 name 指向的组需要在当前位置再次引用。那么搜索两个单词的正则表达式可以写成 (w+)s+1,也可以写成 (?P<word>w+)s+(?P=word):

    >>> p = re.compile(r'(?P<word>w+)s+(?P=word)')
    >>> p.search('Paris in the the spring').group()
    'the the'

    6 前向断言

    前向断言可以分为前向肯定断言和前向否定断言两种形式。

    (?=...)

    前向肯定断言。如果当前包含的正则表达式(这里以 ... 表示)在当前位置成功匹配,则代表成功,否则失败。一旦该部分正则表达式被匹配引擎尝试过,就不会继续进行匹配了;剩下的模式在此断言开始的地方继续尝试。


    (?!...)

    前向否定断言。这跟前向肯定断言相反(不匹配则表示成功,匹配表示失败)。
    为了使大家更易懂,我们举个例子来证明这玩意是真的很有用。大家考虑一个简单的正则表达式模式,这个模式的作用是匹配一个文件名。我们都知道,文件名是用 . 将名字和扩展名分隔开的。例如在 fishc.txt 中,fishc 是文件的名字,.txt 是扩展名。这个正则表达式其实挺简单的:.*[.].*$
    注意,这里用于分隔的 . 是一个元字符,所以我们使用 [.] 剥夺了它的特殊功能。还有 $,我们使用 $ 确保字符串剩余的部分都包含在扩展名中。所以这个正则表达式可以匹配 fishc.txt,foo.bar,autoexec.bat,sendmail.cf,printers.conf 等。现在我们来考虑一种复杂一点的情况,如果你想匹配扩展名不是 bat 的文件,你的正则表达式应该怎么写呢?

    我们先来看下你有可能写错的尝试:

    .*[.][^b].*$

    这里为了排除 bat,我们先尝试排除扩展名的第一个字符为非 b。但这是错误的开始,因为 foo.bar 后缀名的第一个字符也是 b。

    为了弥补刚刚的错误,我们试了这一招:

    .*[.]([^b]..|.[^a].|..[^t])$

    我们不得不承认,这个正则表达式变得很难看......但这样第一个字符不是 b,第二个字符不是 a,第三个字符不是 t......这样正好可以接受 foo.bar,排除 autoexec.bat。但问题又来了,这样的正则表达式要求扩展名必须是三个字符,比如sendmail.cf 就会被排除掉。

    好吧,我们接着修复问题:

    .*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

    在第三次尝试中,我们让第二个和第三个字符变成可选的。这样就可以匹配稍短的扩展名,比如 sendmail.cf。

    不得不承认,我们把事情搞砸了,现在的正则表达式变得艰涩难懂外加奇丑无比!!

    更惨的是如果需求改变了,例如你想同时排除 bat 和 exe 扩展名,这个正则表达式模式就变得更加复杂了......

    当当当当!主角登场,其实,一个前向否定断言就可以解决你的难题:

    .*[.](?!bat$).*$

    我们来解释一下这个前向否定断言的含义:如果正则表达式 bat 在当前位置不匹配,尝试剩下的部分正则表达式;如果 bat匹配成功,整个正则表达式将会失败(因为是前向否定断言嘛^_^)。(?!bat$) 末尾的 $ 是为了确保可以正常匹配像sample.batch 这种以 bat 开始的扩展名。

    同样,有了前向否定断言,要同时排除 bat 和 exe 扩展名,也变得相当容易:

    .*[.](?!bat$|exe$).*$

    7 修改字符串的几种方法

    正则表达式使用以下方法修改字符串:

    方法 用途
    split() 在正则表达式匹配的地方进行分割,并返回一个列表
    sub() 找到所有匹配的子字符串,并替换为新的内容
    subn() 跟 sub() 干一样的勾当,但返回新的字符串以及替换的数目

    详细用法参考《Python3 如何优雅地使用正则表达式(详解六)

  • 相关阅读:
    addEventListener
    mac截屏
    SVG Use(转)
    关于 Content-Type:application/x-www-form-urlencoded 和 Content-Type:multipart/related(转)
    promise
    apt-get update 出现E: Could not get lock /var/lib/apt/lists/lock问题的解决
    js设计模式——6.模板方法模式与职责链模式
    js设计模式——5.状态模式
    js设计模式——4.迭代器模式
    js实现超简单sku组合算法
  • 原文地址:https://www.cnblogs.com/wwf828/p/7689021.html
Copyright © 2020-2023  润新知