Python正则表达式笔记

Python中的re模块主要进行正则表达式的匹配,以及匹配后进行的相关操作。本文借助帮助文档和模块源码,从分析类方法和类接口的角度介绍了正则模块的使用方式,并在最后引入了几个实例。

什么是正则表达式

经典意义上的正则表达式regular expressions)是计算理论中的一个概念,它是用正则运算符( ,,\cup,\circ,^{*} )构造描述语言的表达式。简单来说,用正则表达式可以描述一类具有某个共同的结构特征的字符串。比如, (01)0(0\cup1)0^{*} 表示的是由一个 00 或一个 11 后面跟着任意个 00 的所有字符串。关于计算理论的知识可以参阅我的这一篇博客:计算理论之正则语言

在Python中,或者编程语言中,正则表达式的运算符变得更多,语法也更简练,更易于表现字符串的结构特征。我们可以使用多种多样的符号表示字符,甚至可以引用表达式中的一部分作为它的另一部分。这极大地增强了正则表达式的功能,使得用户在处理文本时更加的方便、快捷。

Python中的正则模块

在Python中,关于正则表达式的内容被封装进一个称为re的模块中,这个模块包括以下内容:

CLASSES
    builtins.Exception(builtins.BaseException)
        error
    builtins.object
        Match
        Pattern

FUNCTIONS
    compile(pattern, flags=0)
    escape(pattern)
    findall(pattern, string, flags=0)
    finditer(pattern, string, flags=0)
    fullmatch(pattern, string, flags=0)
    match(pattern, string, flags=0)
    purge()
    search(pattern, string, flags=0)
    split(pattern, string, maxsplit=0, flags=0)
    sub(pattern, repl, string, count=0, flags=0)
    subn(pattern, repl, string, count=0, flags=0)
    template(pattern, flags=0)

DATA
    A = <RegexFlag.ASCII: 256>
    ASCII = <RegexFlag.ASCII: 256>
    DOTALL = <RegexFlag.DOTALL: 16>
    I = <RegexFlag.IGNORECASE: 2>
    IGNORECASE = <RegexFlag.IGNORECASE: 2>
    L = <RegexFlag.LOCALE: 4>
    LOCALE = <RegexFlag.LOCALE: 4>
    M = <RegexFlag.MULTILINE: 8>
    MULTILINE = <RegexFlag.MULTILINE: 8>
    S = <RegexFlag.DOTALL: 16>
    U = <RegexFlag.UNICODE: 32>
    UNICODE = <RegexFlag.UNICODE: 32>
    VERBOSE = <RegexFlag.VERBOSE: 64>
    X = <RegexFlag.VERBOSE: 64>

可以看到,这个模块(实际上re模块建立于几个基模块之上,但我们可以暂且可以看做一个)定义了3个类、12个函数,以及14个常量(这只是帮助文档中展示的公开接口,实际上不论是类、函数,还是常量,数量都会更多)。由于正则表达式工作的时候一般不需要用户自行创建模块中的对象,而是从函数中接收字符串,并自动地从这些字符串中创建对象,因此我们只需了解正则模块的大致工作模式即可。

首先,从作用上来说,正则模块主要完成正则表达式对字符串的匹配,然后可以在此基础上进行进一步的操作。因此我们分两个部分介绍该模块,一个是匹配,另一个是操作。

(注:re模块可以处理两种类型的字符串,bytes patternsstring patterns。前者是Python中的bytes对象,后者则是str对象。两者在处理时基本相似,因此在本文中暂不做区别分析。)

匹配

几乎所有的正则表达式函数(除compile()escape()purge()以外)都至少接收两个字符串型参数,一个叫pattern,表示用户给定的正则表达式,另一个叫string,表示待匹配的文本。正则模块将会在string中寻找和pattern匹配的部分。

在匹配之前,正则模块会先将pattern通过compile()函数转变为内建的Pattern对象(当然,传一个Pattern对象作为pattern也是可以的),然后使用Pattern类中定义的方法进行之后的操作。对于大部分方法,会将匹配到的内容以Match对象的形式返回。

能够被合法转变为Pattern对象的字符串有以下几种:

  1. 仅包含普通字符的字符串,比如"AbcDe""19260817"等。
  2. 仅包含特殊字符的字符串。
    特殊字符包括以下几种:
    1. '.',匹配除换行符以外的任意字符。
    2. '^',匹配一个字符串的开始。这里请注意,它并不匹配字符,而是匹配位置。
    3. '$',匹配字符串的结尾,或字符串结尾处换行符之前。同样,它也只匹配位置而不匹配字符。
    4. '*',贪心地匹配它之前正则表达式的0次或更多次重复。
      这里“贪心”的意思是尽可能向后匹配更多的字符。
    5. '+',贪心地匹配它之前正则表达式的1次或更多次重复。
    6. '?',贪心地匹配它之前正则表达式的0次或1次出现。
      如果'?'跟在'*''+''?'后面,则会取消它们的贪心模式。
    7. '{m,n}',贪心地匹配它之前正则表达式的m次到n次出现。它的贪心模式同样也可以被'?'取消。
    8. '\',转义它之后的字符。它既可以表示正常的转义字符,如'\n'等,也可以转义特殊字符。
    9. '[]',表示匹配一组字符中的任意一个。
      '[^A]',表示匹配表达式A代表的字符串集合的补集。
    10. 'A|B',匹配表达式A或表达式B
    11. '(...)',匹配括号内的正则表达式,且会将括号内匹配到的内容作为一个“组”(之后会介绍)。
    12. '(?aiLmsux)',放在字符串的开头,设置flag变量(之后会介绍)。
    13. '(?:...)',匹配表达式...但不会将匹配到的内容作为一个“组”。
    14. '(?P<name>...)',匹配并分组的同时,给这个组命名为name
    15. '(?P=name)',匹配命名为name的组的内容。
    16. '(?#...)',注释,不参与匹配。
    17. 'A(?=B)',断言(之后会详细介绍),匹配满足表达式A且其后紧跟着的字符串满足表达式B的内容。
    18. 'A(?!B)',断言,匹配满足表达式A且其后紧跟着的内容不满足表达式B的内容。
    19. '(?<=B)A',断言,匹配满足表达式A且之前的内容满足表达式B的内容。
    20. '(?<!B)A',断言,匹配满足表达式A且之前的内容不满足表达式B的内容。
    21. '(?(id/name)yes|no)',如果组号为id或者组名为name的组匹配成功,则用yes去匹配,否则用no
  3. 同时包含普通字符和特殊字符的字符串。

刚才我们提到了“转义字符”的概念,它是由一个'\'字符再紧接着一个字符组成的用来表示一个用常规方法不太好表示的字符的写法。下面是由'\'和特殊字符组成的转义字符,如果是由一般字符和'\'组成转义字符(不包括'\n''\r'等常用的转义字符),则仍表示那个字符本身。

  1. '\number',表示组号为number的组。
  2. '\A',匹配字符串的开头,相当于'^'
  3. '\Z',匹配字符串的末尾。
  4. '\b',匹配单词开头或结尾处的空串。
  5. '\B',匹配非单词开头或结尾处的空串。
  6. '\d',匹配数字,相当于'[0-9]'
  7. '\D',匹配非数字,相当于'[^\d]'
  8. '\s',匹配空白字符,相当于'[ \t\n\r\f\v]'
  9. '\w',匹配单词字符,相当于'[a-zA-Z0-9_]'
  10. '\W',匹配非单词字符,相当于'[^\w]'

由于正则表达式内部也有自己定义的转义字符,而我们平常使用的转义字符在放入一对''中时会默认被转义,因此如果想表示正则表达式中的转义字符,需要使用'\\'表示一个'\',或者使用raw strings

下面介绍“分组”的概念。

Match对象会将匹配到pattern的出现根据用户自定义的若干对'('')'符号分成等量的“组”(group),每一组的内容都是pattern这一次出现的一个子串。这些组之间可以引用,也可以嵌套,但不可以在组被定义之前引用它,比如'(abc)\\2(def)'这样的pattern就是不合法的。引用方式除了组号(从1开始)之外,还可以引用组名(由'(?P<name>...)'定义,由(?P=name)引用)。

下面介绍flag变量。

正则模块定义了一些默认情况以外的匹配模式,这些模式以若干个flag的形式被保存。这些flag有:

  1. A/ASCII,仅对string patterns使用,使'\w', '\W', '\b', '\B', '\d', '\D', '\s', '\S'仅匹配ASCII编码的对应字符。例如,'(?a)\s'不会匹配全角空格,而'\s'是可以的。
  2. I/IGNORECASE,匹配的时候不区分大小写。
  3. L/LOCALE,仅对bytes patterns使用,让'\w', '\W', '\b', '\B'根据当前的语言环境去匹配。(这个flag变量不常用,官方也不建议使用)
  4. M/MULTILINE,使得'^'匹配字符串的开头和每行的开头;使'$'匹配字符串的末尾和每行的末尾(默认情况下,'^'仅匹配字符串的开头,'$'仅匹配字符串的末尾或字符串末尾的换行符之前)。
  5. S/DOTALL,使得'.'匹配任何字符,包括换行符。
  6. X/VERBOSE,允许pattern出现为了美观而多余的不参与匹配的空格。且每一个换行符前若出现一个普通的'#'符号(放在'[]'内部或者和'\'连用的不算),则将从'#'到换行符的内容看成注释。(笔者认为这个flag不是很常用,除非你十分想在正则表达式内部写注释)
  7. U/UNICODE,和A/ASCII类似,使得那些字符仅匹配Unicode编码的对应版本(这个在Python3中是多余的,因为Python3默认就是用Unicode匹配)。这个flag同样无法用于bytes patterns

如上文所述,我们可以通过在pattern前部加上'(?aiLmsux)'来指定匹配时的flag变量。其中'aiLmsux'分别代表上述的七个flagflag可以同时指定多个,如'(?ai)'。此外,在re模块的大多数函数中,我们也可以通过传入re.A的形式来指定flag

下面介绍正则表达式中的断言(assertion)。

有的时候,我们需要匹配那些前后内容满足一定条件的字符串,比如前面带有字母的空格、后面不跟着数字的字母等等。我们希望匹配的同时不把前后的所谓“条件字符”也加进来,因此引入了断言。断言提供了一个使某些内容参与条件判断但不加入匹配结果的pattern写法。

断言又分为两种,一种叫lookahead assertion,即先行断言,另一种叫lookbehind assertion,即后行断言。顾名思义,前者判断之后的字符,后者判断之前的。对于先行断言,我们有两种:(?=...)'(?!...)'。前者叫做“零宽正向先行断言”,表示判断其后紧跟着的字符是否匹配...,后者叫“零宽负向先行断言”,表示判断其后紧跟着的字符是否不匹配...。类似的,(?<=...)'(?<!...)'就表示判断前面紧跟着的。有一点需要注意的是,在Python中,不允许出现不定长的断言,即'(?<=a*bc)d'是不可以的,但'(?<=abc)d'可以。

操作

在讲述正则模块中的各函数之前,先介绍一下正则模块中的Match类,它是大部分函数返回的对象。

Match类拥有以下的公开属性值:

  1. pos,正则表达式匹配内容的第一个字符的下标。
  2. endpos,正则表达式匹配内容的最后一个字符的下标。
  3. lastgroup,最后一个组的组名,(无命名或者无组则返回None)。
  4. lastindex,最后一个组的编号。
  5. pos,开始匹配处的下标。
  6. re,传递给Match对象的Pattern对象。
  7. string,传递给Match对象的字符串。

Match类的接口函数如下:

  1. expand(template),用Match对象匹配到的组替换template中的对应内容,template中可使用\n\1,或\g<name>等的形式。
  2. group([group1, ...]),返回组号对应的组(字符串或字符串元组)。组号为0表示匹配的整个子串。Python3.6以后支持使用下标形式去访问,即,对于一个Match对象mm[3]m.group(3)的含义相同。
  3. groups(default=None),返回匹配到的所有组,default指定当对应组匹配失败时返回的值。
  4. groupdict(default=None),以{group_name: match_string, ...}的形式返回匹配。
  5. start(group)end(group),返回匹配字符串在原子串中的下标。group默认取0,也就是整个子串。当组存在但是对匹配没有贡献的时候会返回-1
  6. span([group]),返回起始点和结束点的元组,相当于(m.start(group), m.end(group))

下面正式介绍正则模块中的接口函数:

  1. compile(pattern, flags=0),根据给定的字符串pattern,返回其对应的Pattern对象。
  2. escape(pattern),接收一个字符串,将其中的特殊符号保留其原始意义(自动加上'\')并返回。
  3. search(pattern, string, flags=0),在string找到pattern第一处匹配,返回对应的Match对象。
  4. findall(pattern, string, flags=0),找出patternstring中的所有出现,以列表形式返回。列表的每一项是一个字符串或着一个元组(如果用户定义了组,会将组以元组形式返回,而不是返回整个出现)。
  5. finditer(pattern, string, flags=0),与findall类似,返回一个迭代器。
  6. sub(pattern, repl, string, count=0, flags=0),根据replpatternstring中对应的匹配做相应替换并返回。repl可以是一个与pattern类似的支持正则模块语法的字符串,也可以是一个函数,该函数接受一个Match对象,返回一个字符串。
  7. subn(pattern, repl, string, count=0, flags=0),和sub类似,但是返回一个元组,元组包括替换后的字符串和替换的次数。
  8. purge(),清理正则表达式缓存。

常用的正则表达式举例

以下从几个实例出发介绍正则表达式,以加深理解。

比如,我们要解析一段HTML中的标签,假设传进来的是一个字符串s,包含有若干个形如'<tag>text</tag>'的标签,我们需要将这些标签替换为'tag:text'的形式并返回。

比如一段文本:

<composer>Wolfgang Amadeus Mozart</composer>
<author>Samuel Beckett</author>
<city>London</city>

我们希望将其解析为:

composer:Wolfgang Amadeus Mozart
author:Samuel Beckett
city:London

我们可以这样编写函数:

import re
def func(s):
    pattern = '<(.*?)>(.*?)</\\1>'
    repl = '\\1:\\2'
    return re.sub(pattern, repl, s)

代码中,我们使用'<(.*?)>(.*?)</\\1>'作为pattern,它将匹配一个完整的标签,且将标签分出两个组,第一组内容为标签的名字,第二组为标签的内容。替换的时候只要分别引用一下然后中间加上':'即可。

pattern中出现了两处'.*?',这是一个非常常用的写法,它能以非贪心的方式匹配几乎任何内容。

下面我们讨论一个稍微难一些的例子。

比如,现在有一段文字,包含有若干个单词,比如我们现在要找到其中以'abc'开头的单词。

这个比较简单,答案直接给出了:'\\b(abc\w*)'

那么,如果我们要匹配不以'abc'开头的单词呢?

我们可以利用断言,匹配那些在单词开头后面不紧跟着'abc'的内容,即:'\\b(?!abc)(\w+)'

那么,如果我们要匹配不含有'abc'的单词呢?

这个可能不太容易想到,我们可以依次匹配单词中的每一个字符,检验它之后是不是紧跟着'abc',因此答案为:'\\b((?:(?!abc)\w)+)\\b'。(事实上,那个(?:...)的括号可以不加,但为了使用findall函数的方便还是加了)

学习资料推荐

第一推荐官方文档,虽然是英文版本,但足够权威和全面。此外,在github上面有一个学习正则表达式的项目learn-regex,有中文版本,可以用作为辅助资料。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章