python的正则表达式re、regex模块

该模块提供了与Perl中类似的正则表达式匹配操作。就其本质而言,正则表达式(或re)是一种小型的、高度专业化的编程语言。正则就是用一些具有特殊含义的符号组合到一起(称为正则表达式)来描述字符或者字符串的方法。或者说:正则就是用来描述一类事物的规则。(在python中)它内嵌在python中,并通过re模块实现,正则表达式模式被编译成一系列的字节码,然后由用C语言编写的匹配引擎执行。

官网:https://docs.python.org/3.6/library/re.html

生活中处处都是正则:

    比如我们描述:4条腿

      你可能会想到的是四条腿的动物或者桌子,椅子等

    继续描述:4条腿,活的

          就只剩下四条腿的动物这一类了

正则字符

正则表达式有自己的特殊意义字符和语法,用于描述同一类的普通字符,和python字符串一样,用反斜杠转义,这就可能造成反斜杠困扰。假如你需要匹配文本中的字符"\",那么使用编程语言表示的正则表达式里将需要4个反斜杠"\\\\":前两个和后两个分别用于在编程语言(python)里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠,这样才能匹配python的字符"\",Python里的原生字符串(普通字符串,里面的字符不做特殊字符解析)很好地解决了这个问题,这个例子中的正则表达式可以使用r"\\"表示。同样,匹配一个数字的"\\d"可以写成r"\d"。有了原生字符串,你再也不用担心是不是漏写了反斜杠,写出来的表达式也更直观。

普通字符:大多数数字字符和字母都会和自身匹配。

元字符:正则表达式的特殊字符,有 . ^ $ * + ? { } [ ] | ( ) \

正则表达式元字符与语法

 

中文正匹配:[\u4E00-\u9FA5]

匹配模式

1、compile()

编译正则表达式模式,返回一个对象的模式。(可以把那些常用的正则表达式编译成正则表达式对象,这样可以提高一点效率。)

格式:

re.compile(pattern,flags=0)

pattern: 编译时用的正则表达式字符串。

flags 编译标志位,用于修改正则表达式的匹配方式,如:是否区分大小写,多行匹配等。常用的flags有:

 

 2、escape(pattern)  

如果你需要操作的文本中含有正则的元字符,你在写正则的时候需要将元字符加上反斜扛 \ 去匹配自身, 而当这样的字符很多时,写出来的正则表达式就看起来很乱而且写起来也挺麻烦的,这个时候你可以使用这个函数。

3、findall()

re.findall遍历匹配,可以获取字符串中所有匹配的字符串,返回一个列表。

 格式:

re.findall(pattern, string, flags=0)

import re
s = ".+\d123abc.+\d123"
# s2 = ".+2123abc.+3123"

# regex_str = re.escape(".+\d123")
# 查看转义后的字符
# print(regex_str)
# output> \.\+\\d123

# 查看匹配到的结果
# print(re.findall(regex_str, s))

'''类似于'''
# pattern = re.compile('\.\+\\\\d123') #经python处理特殊字符,得到的正则表达式是:'\.\+\\d123'。然后正则解析'\.\+\\d123'。
# pattern2 = re.compile('\.\+\\d123')  #经python处理特殊字符,得到的正则表达式是:'\.\+\d123'。然后正则解析'\.\+\d123'。
pattern = re.compile(r'\.\+\\d123')  #字符串前面加r表示是普通字符,python不会对特殊字符处理,得到的正则表达式是:'\.\+\\d123'。然后正则解析'\.\+\\d123'。
print(pattern.findall(s))
# print(pattern2.findall(s2))

4、finditer(pattern, string, flags=0)   

参数和作用与 findall 一样,不同之处在于 findall 返回一个列表, finditer 返回一个顺序访问每一个匹配结果(Match对象)的迭代器。找到 RE 匹配的所有子串,并把 它们作为一个迭代器返回。

import re
iter = re.finditer(r'\d+','12 drumm44ers drumming, 11 ... 10 ...')
for i in iter:
    print(i)
    print(i.group()) #返回Match对象匹配的字符串。
    print(i.span()) #返回一个元组包含匹配 (开始,结束) 的位置。

5、match()

从字符串的开头进行匹配, 匹配成功就返回一个匹配对象,匹配失败就返回None。

import re
print(re.match('com','comwww.runcomoob').group())
print(re.match('com','Comwww.runcomoob',re.I).group())

6、search()

 格式:

re.search(pattern, string, flags=0)

re.search函数会在字符串内查找模式匹配,只要找到第一个匹配然后返回,如果字符串没有匹配,则返回None。

print(re.search('\dcom','www.4comrunoob.5com').group())
执行结果如下:
4com

*注:match和search一旦匹配成功,就是一个match object对象,而match object对象有以下方法:

  • group() 返回被 RE 匹配的字符串
  • start() 返回匹配开始的位置
  • end() 返回匹配结束的位置
  • span() 返回一个元组包含匹配 (开始,结束) 的位置
  • group() 返回re整体匹配的字符串,可以一次输入多个组号,对应组号匹配的字符串。

a. group()返回re整体匹配的字符串,
b. group (n,m) 返回组号为n,m所匹配的字符串,如果组号不存在,则返回indexError异常
c.groups()groups() 方法返回一个包含正则表达式中所有小组字符串的元组,从 1 到所含的小组号,通常groups()不需要参数,返回一个元组,元组中的元就是正则表达式中定义的组。

import re
a = "123abc456"
print(re.search("([0-9]*)([a-z]*)([0-9]*)",a).group(0))   #123abc456,返回整体
print(re.search("([0-9]*)([a-z]*)([0-9]*)",a).group(1))   #123
print(re.search("([0-9]*)([a-z]*)([0-9]*)",a).group(2))   #abc
print(re.search("([0-9]*)([a-z]*)([0-9]*)",a).group(3))   #456
###group(1) 列出第一个括号匹配部分,group(2) 列出第二个括号匹配部分,group(3) 列出第三个括号匹配部分。###

7、 purge()   

当你在程序中使用 re 模块,无论是先使用 compile 还是直接使用比如 findall 来使用正则表达式操作文本,re 模块都会将正则表达式先编译一下, 并且会将编译过后的正则表达式放到缓存中,这样下次使用同样的正则表达式的时候就不需要再次编译, 因为编译其实是很费时的,这样可以提升效率,而默认缓存的正则表达式的个数是 100, 当你需要频繁使用少量正则表达式的时候,缓存可以提升效率,而使用的正则表达式过多时,缓存带来的优势就不明显了 ,这个函数的作用是清除缓存中的正则表达式,可能在你需要优化占用内存的时候会用到。

8、split()

按照能够匹配的子串将string分割后返回列表。

可以使用re.split来分割字符串,如:re.split(r'\s+', text);将字符串按空格分割成一个单词列表。

格式:

re.split(pattern, string[, maxsplit])

maxsplit用于指定最大分割次数,不指定将全部分割。

import re
print(re.split('\d+','one1two29three3four4five5'))
print(re.split('\d','one1two29three3four4five5'))

9、sub()

使用re替换string中每一个匹配的子串后返回替换后的字符串。

格式:

re.sub(pattern, repl, string, count)

import re
text = "JGood is a handsome boy, he is cool, clever, and so on..."
print(re.sub(r'\s+', '-', text))

10、subn()

 返回替换后的记过和替换次数

格式:

subn(pattern, repl, string, count=0, flags=0)

import re
print(re.sub("g.t","have",'I get A,  I got B ,I gut C'))
print(re.subn("g.t","have",'I get A,  I got B ,I gut C'))
print(re.subn("g.t","have",'I get A,  I got B ,I gut C',1))

re 内置对象的用法

SRE_Pattern:这个对象是一个编译后的正则表达式,编译后不仅能够复用和提升效率,同时也能够获得一些其他的关于正则表达式的信息

属性

  • flags         编译时指定的模式
  • groupindex    以正则表达式中有别名的组的别名为键、以该组对应的编号为值的字典,没有别名的组不包含在内。
  • groups        正则表达式中分组的数量
  • pattern       编译时用的正则表达式
import re
string = "123abc456"
pattern = re.compile("(?P<int1>[0-9]*)(?P<name>[a-z]*)([0-9]*)")
print(type(pattern))
print(pattern.groups)
print(pattern.flags)
print(pattern.pattern)
print(pattern.groupindex)

SRE_Match: 这个对象会保存本次匹配的结果,包含很多关于匹配过程以及匹配结果的信息

属性:

  • endpos       本次搜索结束位置索引
  • lastgroup    本次搜索匹配到的最后一个分组的别名
  • lastindex    本次搜索匹配到的最后一个分组的索引
  • pos          本次搜索开始位置索引
  • re           本次搜索使用的 SRE_Pattern 对象
  • regs         列表,元素为元组,包含本次搜索匹配到的所有分组的起止位置
  • string       本次搜索操作的字符串

函数:

  • end([group=0])               返回指定分组的结束位置,默认返回正则表达式所匹配到的最后一个字符的索引
  • expand(template)             根据模版返回相应的字符串,类似与 sub 函数里面的 repl, 可使用 \1 或者 \g<name> 来选择分组
  • group([group1, ...])         根据提供的索引或名字返回响应分组的内容,默认返回 start() 到 end() 之间的字符串, 提供多个参数将返回一个元组
  • groupdict([default=None])    返回 返回一个包含所有匹配到的命名分组的字典,没有命名的分组不包含在内,key 为组名, value 为匹配到的内容,参数 default 为没有参与本次匹配的命名分组提供默认值
  • groups([default=None])       以元组形式返回每一个分组匹配到的字符串,包括没有参与匹配的分组,其值为 default
  • span([group])                返回指定分组的起止位置组成的元组,默认返回由 start() 和 end() 组成的元组
  • start([group])               返回指定分组的开始位置,默认返回正则表达式所匹配到的第一个字符的索引
import re
s = 'Hello, Mr.Gumby : 2016/10/26'
m = re.search('''(?:        # 构造一个不捕获分组 用于使用 |
              (?P<name>\w+\.\w+)    # 匹配 Mr.Gumby
              |     # 或
              (?P<no>\s+\.\w+) # 一个匹配不到的命名分组
              )
              .*? # 匹配  : 
              (\d+) # 匹配 2016
              ''',
              s, re.X)

# 返回指定分组的结束位置,默认返回正则表达式所匹配到的最后一个字符的索引
print(m.end())
# output> 22

# 根据模版返回相应的字符串,类似与 sub 函数里面的 repl, 可使用 \1 或者 \g<name> 来选择分组
print(m.expand("my name is \\1"))
# output> my name is Mr.Gumby

# 根据提供的索引或名字返回响应分组的内容,默认返回 start() 到 end() 之间的字符串, 提供多个参数将返回一个元组
print(m.group())
# output> Mr.Gumby : 2016
print(m.group(1,2))
# output> ('Mr.Gumby', None)

# 返回 返回一个包含所有匹配到的命名分组的字典,没有命名的分组不包含在内,key 为组名, value 为匹配到的内容,参数 default 为没有参与本次匹配的命名分组提供默认值
print(m.groupdict('default_string'))
# output> {'name': 'Mr.Gumby', 'no': 'default_string'}

# 以元组形式返回每一个分组匹配到的字符串,包括没有参与匹配的分组,其值为 default
print(m.groups('default_string'))
# output> ('Mr.Gumby', 'default_string', '2016')

# 返回指定分组的起止未知组成的元组,默认返回由 start() 和 end() 组成的元组
print(m.span(3))
# output> (18, 22)

# 返回指定分组的开始位置,默认返回正则表达式所匹配到的第一个字符的索引
print(m.start(3))
# output> 18

分组用法

python 的正则表达式中用小括号 "(" 表示分组,按照每个分组中前半部分出现的顺序 "(" 判定分组的索引,索引从 1 开始,每个分组在访问的时候可以使用索引,也可以使用别名。

import re
s = 'Hello, Mr.Gumby : 2016/10/26'
p = re.compile(r"(?P<name>\w+\.\w+).*?(\d+)(?#comment)")
m = p.search(s)
print(m.group())

# 使用别名访问
print(m.group('name'))
# output> Mr.Gumby
# 使用分组访问
print(m.group(2))
# output> 2016

有时候可能只是为了把正则表达式分组,而不需要捕获其中的内容,这时候可以使用非捕获分组。

import re
s = 'Hello, Mr.Gumby : 2016/10/26'
p = re.compile("""
                (?:  # 非捕获分组标志 用于使用 |
                    (?P<name>\w+\.\w+)
                    |
                    (\d+/)
                )
                """, re.X)
m = p.search(s)
# 使用非捕获分组
# 此分组将不计入 SRE_Pattern 的 分组计数
print(p.groups)
# output> 2

# 不计入 SRE_Match 的分组
print(m.groups())
# output> ('Mr.Gumby', None)

如果你在写正则的时候需要在正则里面重复书写某个表达式,那么你可以使用正则的引用分组功能,需要注意的是引用的不是前面分组的 正则表达式 而是捕获到的 内容,并且引用的分组不算在分组总数中。

import re
s = 'Hello, Mr.Gumby : 2016/2016/26'
p = re.compile("""
                (?:  # 非捕获分组标志 用于使用 |
                    (?P<name>\w+\.\w+)
                    |
                    (\d+/)
                )
                .*?(?P<number>\d+)/(?P=number)/
                """, re.X)
m = p.search(s)
# 使用引用分组
# 此分组将不计入 SRE_Pattern 的 分组计数
print(p.groups)
# output> 3

# 不计入 SRE_Match 的分组
print(m.groups())
# output> ('Mr.Gumby', None, '2016')

# 查看匹配到的字符串
print(m.group())
# output> Mr.Gumby : 2016/2016/

引用分组 \1表示第一个分组 \2表示第二个分组

import re
a = "123abc456"
pattern = re.compile("([0-9]*)([a-z]*)([0-9]*)")  #123abc456,返回整体
print(pattern.sub(r'\2\1',a))

环视用法

环视还有其他的名字,例如 界定、断言、预搜索等,叫法不一。

环视是一种特殊的正则语法,它匹配的不是字符串,而是 位置,其实就是使用正则来说明这个位置的左右应该是什么或者应该不是什么,然后去寻找这个位置。

环视的语法有四种,见第一小节元字符,基本用法如下。

import re
s = 'Hello, Mr.Gumby : 2016/10/26  Hello,r.Gumby : 2016/10/26'

# 不加环视限定
print(re.compile("(?P<name>\w+\.\w+)").findall(s))
# output> ['Mr.Gumby', 'r.Gumby']

# 环视表达式所在位置 左边为 "Hello, "
print(re.compile("(?<=Hello, )(?P<name>\w+\.\w+)").findall(s))
# output> ['Mr.Gumby']

# 环视表达式所在位置 左边不为 ","
print(re.compile("(?<!,)(?P<name>\w+\.\w+)").findall(s))
# output> ['Mr.Gumby']

# 环视表达式所在位置 右边为 "M"
print(re.compile("(?=M)(?P<name>\w+\.\w+)").findall(s))
# output> ['Mr.Gumby']

# 环视表达式所在位置 右边不为 r
print(re.compile("(?!r)(?P<name>\w+\.\w+)").findall(s))
# output> ['Mr.Gumby']

贪婪与非贪婪

贪婪:再整个表达式匹配成功的前提下,尽可能多的匹配

非贪婪:在整个表达式匹配成功的前提下,尽可能少的匹配

import re
str1 = "aaa<p>hello</p>bbb<p>world</p>ccc"
pattern = re.compile('<p>.*</p>') #匹配任意字符0到无限次
print(pattern.findall(str1))  #贪婪模式

# pattern = re.compile('<p>.*?</p>') #匹配任意字符0到无限次
# print(pattern.findall(str1))  #非贪婪模式

小练习

import re

#使用|,先匹配的先生效,|左边是匹配小数,而findall最终结果是查看分组,所有即使匹配成功小数也不会存入结果
#而不是小数时,就去匹配(-?\d+),匹配到的自然就是,非小数的数,在此处即整数
#
print(re.findall(r"-?\d+\.\d*|(-?\d+)","1-2*(60+(-40.35/5)-(-4*3))")) #找出所有整数['1', '-2', '60', '', '5', '-4', '3']

#找到所有数字:
print(re.findall('\D?(\-?\d+\.?\d*)',"1-2*(60+(-40.35/5)-(-4*3))")) # ['1','2','60','-40.35','5','-4','3']

#计算器作业参考:http://www.cnblogs.com/wupeiqi/articles/4949995.html
expression='1-2*((60+2*(-3-40.0/5)*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))'

content=re.search(r'\(([\-\+\*\/]*\d+\.?\d*)+\)',expression).group() #(-3-40.0/5)
print(content)
# -- encoding:utf-8 --
"""
Greate by ibf on 2019
"""
'''导入与父目录的父目录同级的模块'''
# !/usr/bin/env python
# -*- coding:utf-8 -*-
"""
该计算器思路:
    1、递归寻找表达式中只含有 数字和运算符的表达式,并计算结果
    2、由于整数计算会忽略小数,所有的数字都认为是浮点型操作,以此来保留小数
使用技术:
    1、正则表达式
    2、递归

执行流程如下:
******************** 请计算表达式: 1 - 2 * ( (60-30 +(-40.0/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2) ) ********************
before: ['1-2*((60-30+(-40.0/5)*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))']
-40.0/5=-8.0
after: ['1-2*((60-30+-8.0*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))']
========== 上一次计算结束 ==========
before: ['1-2*((60-30+-8.0*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))']
9-2*5/3+7/3*99/4*2998+10*568/14=173545.880953
after: ['1-2*((60-30+-8.0*173545.880953)-(-4*3)/(16-3*2))']
========== 上一次计算结束 ==========
before: ['1-2*((60-30+-8.0*173545.880953)-(-4*3)/(16-3*2))']
60-30+-8.0*173545.880953=-1388337.04762
after: ['1-2*(-1388337.04762-(-4*3)/(16-3*2))']
========== 上一次计算结束 ==========
before: ['1-2*(-1388337.04762-(-4*3)/(16-3*2))']
-4*3=-12.0
after: ['1-2*(-1388337.04762--12.0/(16-3*2))']
========== 上一次计算结束 ==========
before: ['1-2*(-1388337.04762--12.0/(16-3*2))']
16-3*2=10.0
after: ['1-2*(-1388337.04762--12.0/10.0)']
========== 上一次计算结束 ==========
before: ['1-2*(-1388337.04762--12.0/10.0)']
-1388337.04762--12.0/10.0=-1388335.84762
after: ['1-2*-1388335.84762']
========== 上一次计算结束 ==========
我的计算结果: 2776672.69524
"""

import re

def compute_mul_div(arg):
    """ 操作乘除
    :param expression:表达式
    :return:计算结果
    """

    val = arg[0]
    mch = re.search('\d+\.*\d*[\*\/]+[\+\-]?\d+\.*\d*', val)
    if not mch:
        return
    content = re.search('\d+\.*\d*[\*\/]+[\+\-]?\d+\.*\d*', val).group()

    if len(content.split('*')) > 1:
        n1, n2 = content.split('*')
        value = float(n1) * float(n2)
    else:
        n1, n2 = content.split('/')
        value = float(n1) / float(n2)

    before, after = re.split('\d+\.*\d*[\*\/]+[\+\-]?\d+\.*\d*', val, 1)
    new_str = "%s%s%s" % (before, value, after)
    arg[0] = new_str
    compute_mul_div(arg)


def compute_add_sub(arg):
    """ 操作加减
    :param expression:表达式
    :return:计算结果
    """
    while True:
        if arg[0].__contains__('+-') or arg[0].__contains__("++") or arg[0].__contains__('-+') or arg[0].__contains__(
                "--"):
            arg[0] = arg[0].replace('+-', '-')
            arg[0] = arg[0].replace('++', '+')
            arg[0] = arg[0].replace('-+', '-')
            arg[0] = arg[0].replace('--', '+')
        else:
            break

    if arg[0].startswith('-'):
        arg[1] += 1
        arg[0] = arg[0].replace('-', '&')
        arg[0] = arg[0].replace('+', '-')
        arg[0] = arg[0].replace('&', '+')
        arg[0] = arg[0][1:]
    val = arg[0]
    mch = re.search('\d+\.*\d*[\+\-]{1}\d+\.*\d*', val)
    if not mch:
        return
    content = re.search('\d+\.*\d*[\+\-]{1}\d+\.*\d*', val).group()
    if len(content.split('+')) > 1:
        n1, n2 = content.split('+')
        value = float(n1) + float(n2)
    else:
        n1, n2 = content.split('-')
        value = float(n1) - float(n2)

    before, after = re.split('\d+\.*\d*[\+\-]{1}\d+\.*\d*', val, 1)
    new_str = "%s%s%s" % (before, value, after)
    arg[0] = new_str
    compute_add_sub(arg)


def compute(expression):
    """ 操作加减乘除
    :param expression:表达式
    :return:计算结果
    """
    inp = [expression, 0]

    # 处理表达式中的乘除
    compute_mul_div(inp)

    # 处理
    compute_add_sub(inp)
    if divmod(inp[1], 2)[1] == 1:
        result = float(inp[0])
        result = result * -1
    else:
        result = float(inp[0])
    return result


def exec_bracket(expression):
    """ 递归处理括号,并计算
    :param expression: 表达式
    :return:最终计算结果
    """
    # 如果表达式中已经没有括号,则直接调用负责计算的函数,将表达式结果返回,如:2*1-82+444
    if not re.search('\(([\+\-\*\/]*\d+\.*\d*){2,}\)', expression):
        final = compute(expression)
        return final
    # 获取 第一个 只含有 数字/小数 和 操作符 的括号
    # 如:
    #    ['1-2*((60-30+(-40.0/5)*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))']
    #    找出:(-40.0/5)
    content = re.search('\(([\+\-\*\/]*\d+\.*\d*){2,}\)', expression).group()

    # 分割表达式,即:
    # 将['1-2*((60-30+(-40.0/5)*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))']
    # 分割更三部分:['1-2*((60-30+(    (-40.0/5)      *(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))']
    before, nothing, after = re.split('\(([\+\-\*\/]*\d+\.*\d*){2,}\)', expression, 1)

    print('before:', expression)
    
    content = content[1:len(content) - 1]

    # 计算,提取的表示 (-40.0/5),并活的结果,即:-40.0/5=-8.0
    ret = compute(content)

    print('%s=%s' % (content, ret))
    

    # 将执行结果拼接,['1-2*((60-30+(      -8.0     *(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))']
    expression = "%s%s%s" % (before, ret, after)
    print('after:', expression)
    
    print("=" * 10, '上一次计算结束', "=" * 10)
    

    # 循环继续下次括号处理操作,本次携带者的是已被处理后的表达式,即:
    # ['1-2*((60-30+   -8.0  *(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))']

    # 如此周而复始的操作,直到表达式中不再含有括号
    return exec_bracket(expression)


# 使用 __name__ 的目的:
#   只有执行 python index.py 时,以下代码才执行
#   如果其他人导入该模块,以下代码不执行
if __name__ == "__main__":
    # print '*'*20,"请计算表达式:", "1 - 2 * ( (60-30 +(-40.0/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2) )" ,'*'*20
    # inpp = '1 - 2 * ( (60-30 +(-40.0/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2) ) '
    inpp = "1-2*-30/-12*(-20+200*-3/-200*-300-100)"
    # inpp = "1-5*980.0"
    inpp = re.sub('\s*', '', inpp)
    # 表达式保存在列表中
    result = exec_bracket(inpp)
    print(result)
基于递归与正则表达式实现计算器

更强大的python正则表达式模块 -- regex

Python自带了正则表达式引擎(内置的re模块),但是不支持一些高级特性,比如下面这几个:

  • 固化分组    Atomic grouping
  • 占有优先量词    Possessive quantifiers
  • 可变长度的逆序环视    Variable-length lookbehind
  • 递归匹配    Recursive patterns
  • (起始/继续)位置锚\G    Search anchor

幸好,在2009年,Matthew Barnett写了一个更强大正则表达式引擎——regex模块这是一个Python的第三方模块。

除了上面这几个高级特性,还有很多有趣、有用的东西,本文大致介绍一下,很多内容取自regex的文档。

无论是编程还是文本处理,regex模块都是一件利器。

用一个指标可以大致了解它的复杂性,re模块有4270行C语言代码,而regex模块有24513行C语言代码。

这个模块以后可能被收进Python标准库。目前(2015年)它还在不断发展,经常发布bug修正版,不过感觉用在一般生产环境应该没什么问题。

一、安装regex

官方文档:https://pypi.org/project/regex/

regex支持Python 2.5+和Python 3.1+,可以用pip命令安装:

pip install regex

PyPy 2.6+也可以使用这个模块。

regex基本兼容re模块,现有的程序可以很容易切换到regex模块:

import regex as re

二、一些有趣的特性

完整的Unicode支持

1,支持最新的Unicode标准,这一点经常比Python本身还及时。

2,支持Unicode代码属性,包括scripts和blocks。

      如:\p{Cyrillic}表示西里尔字符(scripts),\p{InCyrillic}表示西里尔区块(blocks)。

3,支持完整的Unicode字符大小写匹配,详见此文

      如:ss可匹配ßcliff(这里的ff是一个字符)可匹配CLIFF(FF是两个字符)。

      不需要的话可以关闭此特性。不支持Unicode组合字符与单一字符的大小写匹配,所以感觉这个特性不太实用。

4,regex.WORD标志开启后:

      作用1:\b、\B采用Unicode的分界规则,详见此文

                   如:开启后\b.+?\b可搜索到3.4;关闭后小数点.成为分界符,于是只能搜到['3', '.', '4']

      作用2:采用Unicode的换行符。除了传统的\r、\n,Unicode还有一些换行符,开启后作用于.MULTILINE和.DOTALL模式。

5,\X匹配Unicode的单个字形(grapheme)。

      Unicode有时用多个字符组合在一起表示一个字形,详见此文

      \X匹配一个字形,如:^\X$可以匹配'\u0041\u0308'

单词起始位置、单词结束位置

\b是单词分界位置,但不能区分是起始还是结束位置。

regex用\m表示单词起始位置,用\M表示单词结束位置

(?|...|...)
重置分支匹配中的捕获组编号。

>>> regex.match(r"(?|(first)|(second))", "first").groups()
('first',)
>>> regex.match(r"(?|(first)|(second))", "second").groups()
('second',)

两次匹配都是把捕获到的内容放到编号为1捕获组中,在某些情况很方便。

(?flags-flags:...)

局部范围的flag控制。在re模块,flag只能作用于整个表达式,现在可以作用于局部范围了:

>>> regex.search(r"<B>(?i:good)</B>", "<B>GOOD</B>")
<regex.Match object; span=(0, 11), match='<B>GOOD</B>'>

在这个例子里,忽略大小写模式只作用于标签之间的单词。

(?i:)是打开忽略大小写,(?-i:)则是关闭忽略大小写。

如果有多个flag挨着写既可,如(?is-f:),减号左边的是打开,减号右边的是关闭。

除了局部范围的flag,还有全局范围的flag控制,如 (?si-f)<B>good</B>

re模块也支持这个,可以参见Python文档。

把flags写进表达式、而不是以函数参数的方式声明,方便直观且不易出错。

(?(DEFINE)...)

定义可重复使用的子句

>>> regex.search(r'(?(DEFINE)(?P<quant>\d+)(?P<item>\w+))(?&quant) (?&item)', '5 elephants')
<regex.Match object; span=(0, 11), match='5 elephants'>

此例中,定义之后,(?&quant)表示\d+(?&item)表示\w+。如果子句很复杂,能省不少事。

partial matches

部分匹配。可用于验证用户输入,当输入不合法字符时,立刻给出提示。

可以pickle编译后的正则表达式对象

如果正则表达式很复杂或数量很多,编译需要较长时间。

这时可以把编译好的正则式用pickle存储到文件里,下次使用直接pickle.load()就不必再次编译了。

除了以上这些,还有很多新特性(匹配控制、便利方法等等),这里就不介绍了,请自行查阅文档。

三、模糊匹配

regex有模糊匹配(fuzzy matching)功能,能针对字符进行模糊匹配,提供了3种模糊匹配:

  • i,模糊插入
  • d,模糊删除
  • s,模糊替换

以及e,包括以上三种模糊

举个例子:

>>> regex.findall('(?:hello){s<=2}', 'hallo')
['hallo']

(?:hello){s<=2}的意思是:匹配hello,其中最多容许有两个字符的错误。

于是可以成功匹配hallo。

这里只简单介绍一下模糊匹配,详情还是参见文档吧。

四、两种工作模式

regex有Version 0Version 1两个工作模式,其中的Version 0基本兼容现有的re模块,以下是区别:

  Version 0  (基本兼容re模块) Version 1
启用方法

设置.VERSION0或.V0标志,或者在表达式里写上(?V0)。

设置.VERSION1或.V1标志,或者在表达式里写上(?V1)。

零宽匹配

像re模块那样处理:

.split 不能在零宽匹配处切割字符串。

.sub 在匹配零宽之后向前传动一个位置。

PerlPCRE那样处理

.split 可以在零宽匹配处切割字符串。

.sub 采用正确的行为。

内联flag 内联flag只能作用于整个表达式,不可关闭。 内联flag可以作用于局部表达式,可以关闭。
字符组 只支持简单的字符组。 字符组里可以有嵌套的集合,也可以做集合运算(并集、交集、差集、对称差集)。
大小写匹配

默认支持普通的Unicode字符大小写,如Й可匹配й

这与Python3里re模块的默认行为相同。

默认支持完整的Unicode字符大小写,如ss可匹配ß

可惜不支持Unicode组合字符与单一字符的大小写匹配,所以感觉这个特性不太实用。可以在表达式里写上(?-f)关闭此特性。

如果什么设置都不做,会采用regex.DEFAULT_VERSION指定的模式。在目前,regex.DEFAULT_VERSION的默认值是regex.V0。

如果想默认使用V1模式,这样就可以了:

import regex
regex.DEFAULT_VERSION = regex.V1

V1模式默认开启.FULLCASE(完整的忽略大小写匹配)。通常用不上这个,所以在忽略大小写匹配时用(?-f)关闭.FULLCASE即可,这样速度更快一点,例如:(?i-f)tag

其中零宽匹配的替换操作差异比如明显。绝大多数正则引擎采用的是Perl流派的作法,于是Version 1也朝着Perl的方向改过去了。

复制代码
>>> # Version 0 behaviour (like re)
>>> regex.sub('(?V0).*', 'x', 'test')
'x'
>>> regex.sub('(?V0).*?', '|', 'test')
'|t|e|s|t|'

>>> # Version 1 behaviour (like Perl)
>>> regex.sub('(?V1).*', 'x', 'test')
'xx'
>>> regex.sub('(?V1).*?', '|', 'test')
'|||||||||'
复制代码

re模块对零宽匹配的实现可能是有误的(见issue1647489);

而V0零宽匹配的搜索和替换会出现不一致的行为(搜索采用V1的方式,替换采用re模块的方式);

在Python 3.7+环境下,re和regex模块的行为同时做了改变,据称是采用了“正确”的方式处理零宽匹配:总是返回第一个匹配(字符串或零宽),但是如果是零宽并且之前匹配的还是零宽则忽略这和3.6-的re模块、regex的V0模式、V1模式都略有不同。

说着挺吓人的,在实际使用中3.6- re、3.7+ re、V0、V1之间极少出现不兼容的现象。

五、Version 0模式和re模块不兼容之处

上面说了“Version 0基本兼容re模块”,说说不兼容的地方:

1、对零宽匹配的处理。

regex修复了re模块的搜索bug(见issue1647489),但是也带来了不兼容的问题。

在re中,用".*?"搜索"test"返回:['', '', '', '', ''],也就是:最前、字母之间的3个位置、最后,总共5个位置。

在regex中,则返回:['', 't', '', 'e', '', 's', '', 't', '']

在实际使用中,这个问题几乎不会造成不兼容的情况,所以基本可以忽略此差异。

2、\s的范围。

在re中,\s在这一带的范围是0x09 ~ 0x0D,0x1C ~ 0x1E。

在regex中,\s采用的是Unicode 6.3+标准的\p{Whitespace},在这一带的范围有所缩小,只有:0x09 ~ 0x0D。

十六进制 十进制 英文说明 中文说明
0x09 9 HT (horizontal tab) 水平制表符
0x0A 10 LF (NL line feed, new line) 换行键
0x0B 11 VT (vertical tab) 垂直制表符
0x0C 12 FF (NP form feed, new page) 换页键
0x0D 13 CR (carriage return) 回车键
 ...  ...  ...  ...
0x1C 28 FS (file separator) 文件分割符
0x1D 29 GS (group separator) 分组符
0x1E 30 RS (record separator) 记录分离符

 

除此之外,可能还有未知的不兼容之处。

理解正则表达式中的(?R)递归

先来个最简单的正则表达式递归 

字符串 :abc123dsf654wre485wer652 
传统作法:\w{3}\d{3}\w{3}\d{3}\w{3}\d{3}\w{3}\d{3} 
递归做法:(\w{3}\d{3}|(?R))*

当然这个例子不太合适,只能说明正则的递归用法罢了 
其实还可以用(\w{3}\d{3})*,

你看递归的好处,精悍短少(有力),传统作法无法比拟。

在网上找了一番正则递归的资料,最终谷歌了一下,还是在PHP官网上面找到我想要的示例。

<?php
$string = "some text (aaa(b(c1)(c2)d)e)(test) more text";
preg_match_all("/\((([^()]*|(?R))*)\)/", $string, $matches);
echo '<pre>';
print_r($matches);
echo '</pre>';
?>

看了示例之后,瞬间明白了,原理很简单

/\((([^()]*|(?R))*)\)/

为了清晰,我格式化一下

/
    \(    #看到这里的转义左括号没有?这里是入口,意味着这个正则会从左括号开始匹配
        ( #这里是分组用
            (
                [^()]*|(?R)  #这里就是递归,相当于[^()]*|(\((([^()]*|递归....
            )* #这对括号加上一个星*表明,我要重复递归这件事。
        )
    \)
/

相对于这行字符串:some text (aaa(b(c1)(c2)d)e)(test) more text 
过程是这样的(用word搞了一番): 
这里写图片描述

你看第八步,是否有疑问? 
为什么 “第二个对应右括号” 前面的d也被匹配了呢? 
答案:因为正则的第二个星号,第二个星号做的事情是重复这个星号前面括号的内容,也就是还没到”第二个对应右括号”之前,这个’d’被星号重复匹配了 。 
简而言之就是: 匹配左括号 匹配文字 或者 递归(这一块吃掉后面的对对括号的内容,其实我管你递归怎么搞,你搞完后,之后的在我这一层级就按我的来)匹配右括号

正则表达式的平衡组

一般正则教程中对平衡组的介绍

如果想要匹配可嵌套的层次性结构的话,就得使用平衡组了。举个例子吧,如何把“xx <aa <bbb> <bbb> aa> yy”这样的字符串里,最长的尖括号内的内容捕获出来?

这里需要用到以下的语法构造:

(?<group>) 把捕获的内容命名为group,并压入堆栈
(?<-group>) 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败
(?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分
(?!) 顺序否定环视,由于没有后缀表达式,试图匹配总是失败
如果你不是一个程序员(或者你是一个对堆栈的概念不熟的程序员),你就这样理解上面的三种语法吧:第一个就是在黑板上写一个(或再写一个)"group",第二个就是从黑板上擦掉一个"group",第三个就是看黑板上写的还有没有"group",如果有就继续匹配yes部分,否则就匹配no部分。

我们需要做的是每碰到了左括号,就在黑板上写一个"group",每碰到一个右括号,就擦掉一个,到了最后就看看黑板上还有没有-如果有那就证明左括号比右括号多,那匹配就应该失败(为了能看得更清楚一点,我用了(?'group')的语法): 

<                 #最外层的左括号
  [^<>]*          #最外层的左括号后面的不是括号的内容
  (
    (
      (?'Open'<)  #碰到了左括号,在黑板上写一个"Open"
      [^<>>]*     #匹配左括号后面的不是括号的内容
    )+
    (
      (?'-Open'>) #碰到了右括号,擦掉一个"Open"
      [^<>]*      #匹配右括号后面不是括号的内容
    )+
  )*
  (?(Open)(?!))   #在遇到最外层的右括号前面,判断黑板上还有没有没擦掉的"Open";如果有,则匹配失败
>                 #最外层的右括号

看了上面的介绍,你明白了吗?在我未理解正则表达式匹配原理之前,看上面对于平衡组的介绍,似懂非懂,且只能当做模板记住,而不能灵活运用。因此查阅大量有关正则方面的资料,这里尤其感谢lxcnn的技术文档及《精通正则表达式》这本书,让我对正则表达式有了更深入、更系统的理解,因此,在它们的基础之上,我就结合自己的学习经历做个小结,一来做为学习笔记存档,另外,如果能解决你的疑惑,也是件让人高兴的事。

我先暂不分析上面的代码,先讲解一下关于平衡组相关的概念及知识。

下面表达式匹配测试工具为:Expresso,本站也提供它的完美破解版下载。点击下载

平衡组的概念及作用

平衡组,故名思义,平衡即对称,主要是结合几种正则语法规则,提供对配对出现的嵌套结构的匹配。平衡组有狭义与广义两种定义,狭义平衡组指(?Expression) 语法,而广义平衡组并不是固定的语法规则,而是几种语法规则的综合运用,我们平时所说的平衡组通常指的是广义平衡组。本文中如无特殊说明,平衡组这种简写指的是广义平衡组。

平衡组的匹配原理

平衡组的匹配原理可以用堆栈来解释,先举个例子,再根据例子进行解释。

源字符串:a+(b*(c+d))/e+f-(g/(h-i))*j

正则表达式:((?<Open>\()|(?<−Open>((?<Open>\()|(?<−Open>)|[^()])*(?(Open)(?!))\)

需求说明:匹配成对出现的()中的内容

输出:(b*(c+d)) 和 (g/(h-i))

我将上面正则表达式代码分行写,并加上注释,这样看起来有层次,而且方便 

 \(               #普通字符“(”
   (              #分组构造,用来限定量词“*”修饰范围
     (?<Open>\()  #命名捕获组,遇到开括弧“Open”计数加1
     |            #分支结构
     (?<-Open>\)) #狭义平衡组,遇到闭括弧“Open”计数减1
     |            #分支结构
     [^()]+       #非括弧的其它任意字符
   )*             #以上子串出现0次或任意多次
   (?(Open)(?!))  #判断是否还有“Open”,有则说明不配对,什么都不匹配
 \)              #普通闭括弧

对于一个嵌套结构而言,开始和结束标记都是确定的,对于本例开始为“(”,结束为“)”,那么接下来就是考察中间的结构,中间的字符可以划分为三类,一类是“(”,一类是“)”,其余的就是除这两个字符以外的任意字符。

那么平衡组的匹配原理就是这样的

1、先找到第一个“(”,作为匹配的开始。即上面的第1行,匹配了:a+(b*(c+d))/e+f-(g/(h-i))*j (红色显示部分)

2、在第1步以后,每匹配到一个“(”,就入栈一个Open捕获组,计数加1

3、在第1步以后,每匹配到一个“)”,就出栈最近入栈的Open捕获组,计数减1

也就是讲,上面的第一行正则“\(”匹配了:a+(b*(c+d))/e+f-(g/(h-i))*j (红色显示部分)
然后,匹配到c前面的“(”,此时,计数加1;继续匹配,匹配到d后面的“)”,计算减1;——注意喽:此时堆栈中的计数是0,正则还是会向前继续匹配的,但是,如果匹配到“)”的话,比如,这个例子中d))(红色显示的括号)——引擎此时将控制权交给(?(Open)(?!)),判断堆栈中是否为0,如果为0,则执行匹配“no”分支,由于这个条件判断结构中没有“no”分支,所以什么都不做,把控制权交给接下来的“\)”
这个正则表达式“\)”可匹配接下来的),即b))(红色显示的括号)

4、后面的 (?(Open)(?!))用来保证堆栈中Open捕获组计数是否为0,也就是“(”和“)”是配对出现的

5、最后的“)”,作为匹配的结束

匹配过程

首先匹配第一个“(”,然后一直匹配,直到出现以下两种情况之一时,把控制权交给(?(Open)(?!)):

a)堆栈中Open计数已为0,此时再遇到“)”

b)匹配到字符串结束符

这时控制权交给(?(Open)(?!)),判断Open是否有匹配,由于此时计数为0,没有匹配,那么就匹配“no”分支,由于这个条件判断结构中没有“no”分支,所以什么都不做,把控制权交给接下来的“\)”

如果上面遇到的是情况a),那么此时“\)”可以匹配接下来的“)”,匹配成功;
如果上面遇到的是情况b),那么此时会进行回溯,直到“\)”匹配成功为止,否则报告整个表达式匹配失败。

由于.NET中的狭义平衡组“(?<Close-Open>Expression)”结构,可以动态的对堆栈中捕获组进行计数,匹配到一个开始标记,入栈,计数加1,匹配到一个结束标记,出栈,计数减1,最后再判断堆栈中是否还有Open,有则说明开始和结束标记不配对出现,不匹配,进行回溯或报告匹配失败;如果没有,则说明开始和结束标记配对出现,继续进行后面子表达式的匹配。

需要对“(?!)”进行一下说明,它属于顺序否定环视,完整的语法是“(?!Expression)”。由于这里的“Expression”不存在,表示这里不是一个位置,所以试图尝试匹配总是失败的,作用就是在Open不配对出现时,报告匹配失败。

python3实现正则表达式平衡组

python是不支持正则平衡组的。所以只能自己写代码实现了。代码如下:

import re

'''
堆栈方式,循环查找标签名称
'''
def loopTab(tagName,content):
    #需要匹配的标签名称
    tagBegin = "<"+tagName
    tagEnd = "</"+tagName
    tagRegex = r"("+tagBegin+"|"+tagEnd+")"

    #标签名称的数量决定着,匹配的结尾在哪里
    #遇到开头标签,就加一,遇到结尾标签就减一
    #直到减到0的时候就是结束的时候
    tagNum = 0;

    pattern = re.compile(tagRegex,re.M|re.S)
    mather = pattern.search(content)

    #循环匹配开头标签和结尾标签
    while mather:
        group = mather.group()
        if group == tagBegin:
            #开头标签,数量加1
            tagNum = tagNum + 1
        else:
            #结尾标签,数量减1
            tagNum = tagNum - 1

        #标签数量为0,循环结束,范围结尾处的下标。
        if tagNum == 0:
            return mather.span()[1] + 2

        #下一次循环
        mather = pattern.search(content,mather.span()[1])

    #如果标签不匹配,返回0。
    return 0;

'''
平衡组查找
'''
def balenceGroup(regexHead,text):
    #解析标签名称
    tabName =  re.compile(r"<(.+?)\s",re.S).search(regexHead).group(1)
    #使用贪婪模式,尽可能多的找到内容,然后从找到的内容中筛选
    regex = regexHead + ".*</"+tabName+">"
    mather = re.compile(regex,re.M|re.S).search(text)
    if mather:
        divContent = mather.group()
        return divContent[:loopTab(tabName,divContent)]
    return ""


text = '''  <div id="1">
                <div id="2">
                    <div id="3">
                        aaa
                    </div>
                </div>
                <div id="4">
                    bbb
                </div>
            </div>
            <div id="5">
                ccc
            </div>
        '''


regex = r"<div id=\"1\">"
result = balenceGroup(regex,text)
print(result)
View Code

 

posted @ 2019-06-05 16:14  码迷-wjz  阅读(2151)  评论(0)    收藏  举报