# 正则匹配

# 正则表达式

:regular expression ,简称为 regex 。是一个描述字符串的文本模式,用于对其它字符串进行部分匹配。

  • 不同软件采用的正则语法可能不同。常见的几种如下:
    • POSIX 风格:常用于类 Unix 系统上的老旧软件,又分为两种:
      • 基本正则(Basic Regular Expression ,BRE)
        • 速记字符集中,不支持 \d、\D 。
        • 限定符中,支持 * ,不支持 +、? 。
      • 扩展正则(Extended Regular Expression ,ERE)
    • Perl 风格:更流行,语法较多,常用于 Perl、Python 等编程语言。
  • 正则表达式包含的字符分为两大类:
    • 普通字符 :指除元字符以外的其它字符。
      • 基本的正则引擎只支持 ASCII 字符,而 Python 的 re 模块支持 Unicode 字符。
    • 元字符:指在正则语法中具有特殊含义的字符。

# 普通字符

  • 正则表达式中的普通字符包括字母、数字、下划线等,它们会与目标字符串中的字符直接匹配。
  • 包括非打印字符,比如:
    \r      # 回车符
    \n      # 换行符
    \t      # 制表符
    \f      # 换页符
    

# 元字符

# 转义字符

  • \ :用于声明一个转义字符。
    • 如果将元字符声明为转义字符,则可以当作普通字符进行匹配。比如 \. 匹配普通字符 .\\ 匹配普通字符 \
    • 在 Python 的 re 模块的正则表达式中,使用 \\\\ 才能匹配普通字符 \ 。如下:
      >>> re.findall('\t', '\t\\')
      ['\t']
      >>> re.findall('\\', '\t\\')
      re.error: bad escape (end of pattern) at position 0
      >>> re.findall('\\\\', '\t\\')
      ['\\']
      >>> re.findall(r'\\', '\t\\')   # 给字符串加上 r 前缀,将所有 \ 转义成 \\
      ['\\']
      

# 字符集

  • [ ] :用于声明字符集。

    • 一个字符集是任意个字符的无序集合,用于匹配目标字符串中,单个属于该字符集的字符。
    • 如果字符集开头为 ^ ,则表示反向匹配,即不匹配字符集中的字符。
    • 在字符集中,连字符 - 用于声明 ASCII 码连续的一串字符,比如 A-Z0-9
      • 如果要匹配普通字符 - ,则要用转义字符 \- ,或者将 - 放在字符集开头或末尾,比如 [0-9-]
    • [] 要转义之后才能当作普通字符进行匹配:
      >>> re.findall(r'\[hi\]', r'[hi]')
      ['[hi]']
      
      但是在字符集中,以下元字符不需要转义,就会当作普通字符进行匹配:
      >>> re.findall(r'[.|()^$*+?{}]', '[.|()[]^$*+?{}]')
      ['.', '|', '(', ')', '^', '$', '*', '+', '?', '{', '}']
      
    • 例:
      >>> re.findall(r'[0-9A-Za-z]', 'A1.')
      ['A', '1']
      >>> re.findall(r'[^0-9A-Za-z]', 'A1.')
      ['.']
      
  • 正则表达式中, . 是一个通配符,可以匹配单个任意字符,默认不包括换行符 \n 。

  • 正则表达式中,以下转义字符被定义为速记字符集:

    \d      # 匹配一个数字,相当于 [0-9]
    \D      # 匹配一个非数字,相当于 [^0-9]
    \w      # 匹配一个字母、数字、下划线,相当于 [0-9A-Za-z_]
    \W      # 匹配一个不匹配 \w 的字符
    \s      # 匹配一个空白字符,相当于 [ \t\n\r\f\v]
    \S      # 匹配一个非空白字符
    
    • 例:
      >>> re.findall(r'\d', 'ab12#你好》')
      ['1', '2']
      >>> re.findall(r'\D', 'ab12#你好》')
      ['a', 'b', '#', '你', '好', '》']
      >>> re.findall(r'\w', 'ab12#你好》')
      ['a', 'b', '1', '2', '你', '好']
      >>> re.findall(r'\W', 'ab12#你好》')  # 使用 Python 的 re 模块时,\w 匹配所有归类为字母的 Unicode 字符,包括单个汉字,但不包括中文的标点符号
      ['#', '》']
      

# 字符组

  • ( ) :用于在正则表达式中声明子表达式,又称为字符组。

    • 支持嵌套。
    • 字符组可以用于从正则匹配的结果中,提取子字符串。如下:
      >>> re.findall(r'(A)BC', 'ABC')     # 声明了字符组时,只返回该字符组的匹配结果,而不是整个正则表达式的匹配结果
      ['A']
      >>> re.findall(r'((A)B)C', 'ABC')   # 声明了多个字符组时,依次返回其匹配结果
      [('AB', 'A')]
      
  • (? ) :字符组的扩展语法,常见的几种如下:

    • (?P<name> ) :给该字符组命名。
      >>> re.findall(r'(?P<id>(A)B)C', 'ABC')
      [('AB', 'A')]
      
      正则匹配时,没必要定义组名。而正则替换时,可以通过字符组的名称或编号,引用其提取的子字符串。编号从 1 开始递增。如下:
      >>> re.sub(r'(?P<id>(A)B)C', r'\g<ID>', 'ABC')  # 组名区分大小写,如果引用不存在的组名则会报错
      IndexError: unknown group name 'ID'
      >>> re.sub(r'(?P<id>(A)B)C', r'\g<id>', 'ABC')
      'AB'
      >>> re.sub(r'(?P<id>(A)B)C', r'\g<1>', 'ABC')
      'AB'
      >>> re.sub(r'(?P<id>(A)B)C', r'\1', 'ABC')
      'AB'
      
    • (?# ) :将该字符组视作注释,可以忽略。
      >>> re.findall(r'(?#test)ABC', 'ABC')
      ['ABC']
      
    • (?: ) :让括号表达式生效,但不捕获字符组。
      >>> re.sub(r'((A)B)C', r'\1', 'ABC')    # 匹配结果中,第一个组是 AB
      'AB'
      >>> re.sub(r'(?:(A)B)C', r'\1', 'ABC')  # 匹配结果中,第一个组是 A
      'A'
      
    • (?= ) :前置断言。如果该字符组匹配当前位置之后的目标字符串,才继续匹配后续的正则表达式。匹配结果中不会包含断言的字符组。
      >>> re.findall(r'^(?=A)\w+', 'ABC')
      ['ABC']
      >>> re.findall(r'^(?=AA)\w+', 'ABC')
      []
      
    • (?! ) :否定的前置断言。如果该字符组匹配当前位置之后的目标字符串,则不继续匹配后续的正则表达式。
      >>> re.findall(r'^(?!A)\w+', 'ABC')
      []
      >>> re.findall(r'^(?!AA)\w+', 'ABC')
      ['ABC']
      
    • (?<= ) :后置断言。如果该字符组匹配当前位置之前的目标字符串,才继续匹配后续的正则表达式。
      >>> re.findall(r'A(?<=A)\w+', 'ABC')
      ['ABC']
      >>>
      >>> re.findall(r'A(?<=AB)\w+', 'ABC')
      []
      
    • (?<! ) :否定的后置断言。
      >>> re.findall(r'A(?<!A)\w+', 'ABC')
      []
      >>> re.findall(r'A(?<!AB)\w+', 'ABC')
      ['ABC']
      

# 或匹配

  • | :用于同时使用两个正则表达式,如果前一个不匹配,则尝试匹配后一个。
    • 例:
      >>> re.findall(r'B|b', 'ABC')
      ['B']
      >>> re.findall(r'abc|ABC', 'ABC')
      ['ABC']
      >>> re.findall(r'(ab|AB)C', 'ABC')
      ['AB']
      

# 定位符

  • 定位符 :用于匹配字符串的边界位置。如下:
    • 分为以下几种:
      ^     # 匹配字符串的开始边界,即第一个字符的之前
      $     # 匹配字符串的结束边界,即最后一个字符之后
      \b    # 匹配单词的开始或结束边界
      \B    # 匹配除 \b 以外的其它边界
      
      • 定位符并不匹配一个实际存在的字符,而是匹配字符之间的边界位置。
      • 如果连续的两个字符,一个匹配 \w ,一个不匹配 \w 或者为 ^ 、$ ,则认为这两个字符之间为单词边界,匹配 \b 。
    • 例:
      >>> re.findall(r'^', 'Hi!')     # 正则表达式只包含定界符时,匹配结果为空字符串
      ['']
      >>> re.findall(r'^Hi', 'Hi!')
      ['Hi']
      >>> re.findall(r'^Hi$', 'Hi!')
      []
      >>> re.findall(r'Hi\b!', 'Hi!')
      ['Hi!']
      >>> re.findall(r'H\Bi!\B', 'Hi!')
      ['Hi!']
      >>> re.findall(r'Hi\b\b\b\b!\B\B$', 'Hi!')  # 可以连续使用多个定位符,匹配同一个边界位置
      ['Hi!']
      

# 限定符

  • 限定符 :用于限制前面一个字符(或字符组)的连续匹配次数。

    • 分为以下几种:
      *   # 连续匹配任意次,包括 0 次
      +   # 连续匹配至少一次
      ?   # 连续匹配至多一次,即匹配 0 次或 1 次
      
    • 例:
      >>> re.findall(r'.*3','123123123')          # 限定符默认采用贪心模式,会尽量匹配最多次
      ['123123123']
      >>> re.findall(r'.*?3','123123123')         # 在限定符之后加上 ? ,比如 *? ,就采用非贪心模式,会尽量匹配最少次
      ['123', '123', '123']
      >>> re.findall(r'(?:\w{2}3)+','123123123')  # 限定符支持修饰字符组,字符组内也支持使用限定符
      ['123123123']
      
  • { } :用于声明限定符表达式,从而比限定符指定更准确的连续匹配次数。

    • 格式如下:
      {m}     # 连续匹配刚好 m 次
      {m,}    # 连续匹配至少 m 次
      {,n}    # 连续匹配至多 n 次
      {m,n}   # 连续匹配至少 m 次且至多 n 次
      
      • m、n 必须都是非负整数,且 m<=n 。
      • 逗号前后不能加空格。
    • 例:
      >>> re.findall(r'e{2}', 'be')
      []
      >>> re.findall(r'e{2}', 'bee')
      ['ee']
      >>> re.findall(r'e{2}', 'beee')
      ['ee']
      >>> re.findall(r'e{2}', 'beeee')
      ['ee', 'ee']
      

# 示例

  • 匹配月份:
    >>> re.findall(r'^0?[1-9]$|^1[0-2]$', '09')
    ['09']
    
  • 匹配一个符合 C 语言命名规范的字符串:
    >>> re.findall(r'^[A-Za-z_]\w*$', 'my_var')
    ['my_var']
    
  • 匹配一个中文姓名:
    >>> re.findall(r'^[\u4e00-\u9fa5]{1,5}$', '张三')
    ['张三']
    

# ♢ re

:Python 的标准库,提供了一些使用正则表达式的方法。

# compile()

re.compile(pattern, flags=0)
  • 作用:创建一个正则表达式对象并返回。
  • 参数:
    • pattern :输入一个 str 或 bytes 类型的正则表达式。
    • flags :标志位,用于控制正则匹配的模式。如下:
      re.A      # ASCII ,当 pattern 为 str 类型时,使 \b、\d、\w、\s 及其大写形式,只匹配 ASCII 字符。默认不启用该模式,会匹配 Unicode 字符
      re.I      # IGNORECASE ,不区分大小写
      re.M      # MULTILINE ,使 ^ 额外匹配换行符的右边界、 $ 额外匹配换行符的左边界,从而对字符串中的每行分别进行正则匹配
      re.S      # DOTALL ,使 . 匹配任何字符,即增加匹配换行符 \n
      
      re.A|re.I # 可以通过或运算符 | 组合启用多个模式
      
  • 使用 re 模块时,可以直接调用 re.findall() 等函数。也可以先用 re.compile() 创建正则表达式对象,再调用其方法,这样便于复用正则表达式。如下:
    >>> import re
    >>> r = re.compile('a+', re.A|re.I)
    >>> r.findall('abAB')
    ['a', 'A']
    >>> re.findall(r, 'abAB')
    ['a', 'A']
    

# findall()

re.findall(pattern, string, flags=0)
  • 作用:找出所有正则匹配的子字符串(没有字符重叠),组成一个 list 返回。
  • 参数:
    • pattern :一个 str、bytes 或 对象类型的正则表达式。
    • string :要进行正则匹配的目标字符串。
  • 例:
    >>> re.findall(r'aa', 'abAB')       # 匹配失败时,返回一个空 list
    []
    >>> re.findall(r'.', 'abAB')        # 匹配多个子字符串时,依次返回
    ['a', 'b', 'A', 'B']
    >>> re.findall(r'(ab)A(B)', 'abAB') # 如果声明了字符组,则只返回字符组的匹配结果,不返回整个正则表达式的匹配结果
    [('ab', 'B')]
    
    >>> re.findall(r'.*', 'abAB\n')     # . 默认不匹配换行符,因此这里会匹配 \n 左侧、右侧的空字符串
    ['abAB', '', '']
    >>> re.findall(r'.+', 'abAB\n')
    ['abAB']
    
re.search(pattern, string, flags=0)
  • 作用:找出第一个正则匹配的子字符串,返回一个 re.Match 对象。匹配失败时,返回 None 。
  • 例:
    >>> re.search(r'ab', 'abAB')
    <re.Match object; span=(0, 2), match='ab'>
    
  • 关于 re.Match 对象。
    • 用法示例:
      >>> match = re.search(r'(ab)A(B)', 'abAB\n')
      >>> if not match:     # 检查匹配是否成功,再进行后续操作
      ...     exit()
      ...
      >>> match
      <re.Match object; span=(0, 4), match='abAB'>  # 这里 match 表示与整个正则表达式匹配的子字符串,而 span 表示该子串的切片范围
      >>> match.span()      # 返回子串的切片范围
      (0, 4)
      
    • 调用 groups() 方法,会将各个字符组的匹配结果组成一个 tuple 返回:
      >>> re.search(r'abAB', 'abAB\n').groups()     # 如果没有声明字符组,则返回一个空 tuple
      ()
      >>> re.search(r'(ab)AB', 'abAB\n').groups()
      ('ab',)
      >>> re.search(r'(ab)A(B)', 'abAB\n').groups()
      ('ab', 'B')
      
    • 调用 groupdict() 方法,会将各个已命名的字符组的匹配结果组成一个 dict 返回:
      >>> re.search(r'(?P<id>ab)A(B)', 'abAB\n').groupdict()
      {'id': 'ab'}
      

# sub()

re.sub(pattern, replace, string, count=0, flags=0)
  • 作用:找出 string 中正则匹配的子字符串,替换成 replace 字符串。
  • 参数:
    • count :替换的最大次数。默认为 0 ,表示不限制。
  • 例:
    >>> re.sub(r'aa', '_', 'abAB\n')        # 如果没有正则匹配的子字符串,则返回原字符串
    'abAB\n'
    >>> re.sub(r'(ab)A(B)', '_', 'abAB\n')  # sub() 会替换与整个正则表达式匹配的子字符串,而不只是字符组
    '_\n'
    >>> re.sub(r'(?P<id>ab)A(B)', r'\g<id>_\2', 'abAB\n')   # 替换时可以引用字符组
    'ab_B\n'
    

# split()

re.split(pattern, string, maxsplit=0, flags=0)
  • 作用:找出 string 中正则匹配的子字符串,用它们将 string 分割成多个字符串,组成一个 list 返回。
  • 参数:
    • maxsplit :分割的最大次数。默认为 0 ,表示不限制。
  • 例:
    >>> re.split(r'\W+', 'www.baidu.com')
    ['www', 'baidu', 'com']