正则表达式是定义文本字符串应有形式的文本模式。除其他用途外,使用它们可以进行以下活动:
- 检查输入是否符合给定的模式;例如,我们可以检查在 HTML 公式集中输入的值是否是有效的电子邮件地址
- 在一段文本中查找模式外观;例如,只需一次扫描,即可检查文档中是否出现“颜色”或“颜色”一词
- 提取文本的特定部分;例如,提取地址的邮政编码
- 替换部分文本;例如,将“颜色”或“颜色”的任何外观更改为“红色”
- 将较大的文本拆分为较小的部分,例如,按点、逗号或换行符的任何外观拆分文本
在本章中,我们将从语言不可知的角度学习正则表达式的基础知识。在本章的最后,我们将了解正则表达式是如何工作的,但是我们还不能在 Python 中执行正则表达式。这将在下一章中介绍。由于这个原因,本章中的示例将从理论的角度进行处理,而不是在 Python 中执行。
正则表达式无处不在。它们可以在最新的 Offmatic 套件或 JavaScript 框架中找到,这些 UNIX 工具可以追溯到 70 年代。现代编程语言只有支持正则表达式才能称为完整的。
尽管正则表达式在语言和框架中很流行,但在现代程序员的工具包中还没有普及。经常用来解释这一点的原因之一是他们的学习曲线很难。如果不小心编写正则表达式,它可能很难掌握,而且阅读起来也很复杂。
由于这种复杂性,在互联网论坛上不难找到旧的栗子:
| | “有些人在遇到问题时会想“我知道,我会使用正则表达式。”现在他们有两个问题了。” | | | | --杰米·扎温斯基,1997 |
你可以在找到它 https://groups.google.com/forum/?hl=en#!msg/alt.religation.emacs/DR057Srw5-c/Co-2L2BKn7UJ。
读完这本书,我们将学习如何在编写正则表达式时利用最佳实践来大大简化阅读过程。
尽管正则表达式可以在当今最新和最伟大的编程语言中找到,并且可能会持续很多年,但它们的历史可以追溯到 1943 年,当时神经生理学学家沃伦·麦卡洛赫和沃尔特·皮茨发表了神经活动中内在思想的逻辑演算。本文不仅介绍了正则表达式的起源,而且提出了第一个神经网络的数学模型。
下一步是 1956 年,这一次是由一位数学家采取的。斯蒂芬·克莱恩(Stephen Kleene)撰写了论文《神经网络和有限自动机中事件的表示》,他在论文中创造了术语正则集和正则表达式。
12 年后的 1968 年,一位传奇的计算机科学先驱接受了克莱恩的工作并将其扩展,在论文正则表达式搜索算法中发表了他的研究成果。这位工程师是 Ken Thompson,以设计和实现 Unix、B 编程语言、UTF-8 编码等而闻名。
肯·汤普森的工作不仅仅是写一篇论文。他在 QED 版本中包含了对这些正则表达式的支持。要在 QED 中使用正则表达式进行搜索,必须编写以下内容:
g/<regular expression>/p在前一行代码中,g表示全局搜索,p表示打印。如果我们不写regular expression,而是写了简短的形式re,我们得到了g/re/p,因此,古老的 UNIX 命令行工具grep的诞生。
下一个突出的里程碑是 Henry Spence 发布了第一个非专有的regex库,后来 Larry Wall 创建了脚本语言Perl。Perl 将正则表达式推向主流。
Perl 中的实现向前推进,并对原始正则表达式语法进行了许多修改,创建了所谓的Perl 风格。其余语言或工具中的许多后续实现都基于正则表达式的 Perl 风格。
IEEE 认为他们的 POSIX 标准试图标准化正则表达式的语法和行为,并为其提供更好的 Unicode 支持。这称为正则表达式的 POSIX 风格。
今天,正则表达式的标准 Python 模块re只支持 Perl 风格的正则表达式。正在努力编写一个新的正则表达式模块,该模块在处具有更好的 POSIX 风格支持 https://pypi.python.org/pypi/regex 。这个新模块最终将取代 Python 的re模块实现。在本书中,我们将学习如何仅利用标准的re模块。
正则表达式、正则表达式、正则表达式或正则表达式?
亨利·斯宾塞模糊地将他著名的图书馆称为“regex”或“regexp”。维基百科建议使用regex或regexp作为缩写。著名的行话文件将它们列为regexp、regex 和 reg-ex。
然而,尽管似乎没有一种非常严格的方法来命名正则表达式,但它们是基于数学领域的,称为形式语言,精确就是一切。大多数现代实现都支持不能用正式语言表达的特性,因此它们不是真正的正则表达式。为此,Perl 语言的创建者 Larry Wall 使用了术语正则表达式或正则表达式。
在本书中,我们将模糊地使用上述所有术语,就好像它们是完美的同义词一样。
毫无疑问,任何有经验的开发人员都使用过某种正则表达式。例如,在操作系统控制台中,使用星号(*)或问号(?查找文件并不少见。
问号将单个字符与文件名上的任何值匹配。例如,像file?.xml这样的模式将匹配file1.xml、file2.xml和file3.xml,但它将不匹配file99.xml,因为该模式表示以file开头、后跟任意值的一个字符、以.xml结尾的任何内容都将被匹配。
星号(*)定义了类似的含义。当使用星号时,接受任意数量的具有任意值的字符。在file*.xml的情况下,任何以file开头,后跟任意数量的具有任意值的字符,并以.xml结尾的字符都将被匹配。
在这个表达式中,我们可以找到两种成分:文字(file和.xml)和元字符(?或*)。我们将在本书中学习的正则表达式比我们通常可以在操作系统命令行上找到的简单模式强大得多,但两者都可以共享一个定义:
正则表达式是由普通字符(例如,字母A到z或数字0到9和称为元字符的特殊字符)组成的文本模式。此模式描述应用于文本时匹配的字符串。
让我们来看看我们的第一个正则表达式,它将匹配以
a开头的任何单词:
使用文本和元字符的正则表达式
本书中正则表达式的表示
在本书的下图中,正则表达式将由/符号表示。这是大多数教科书中遵循的 QED 界限。但是,代码示例不会使用这种表示法。
另一方面,即使使用单间距字体,正则表达式的空格也很难计数。为了简化阅读,图中的每个空格将显示为
。
前面的正则表达式再次使用文字和元字符。这里的文字是
和a,元字符是\和w,它们匹配任何字母数字字符(包括下划线),以及*,这将允许对前一个字符进行任意次数的重复,因此,允许对任何单词字符(包括下划线)进行任意次数的重复。
我们将在本章后面介绍元字符,但让我们从理解文字开始。
文字是正则表达式中最简单的模式匹配形式。只要找到文字,他们就会成功。
如果我们使用正则表达式/fox/搜索短语The quick brown fox jumps over the lazy dog,我们将找到一个匹配项:
使用正则表达式进行搜索
但是,如果我们将正则表达式/be/应用于以下短语To be, or not to be,我们也可以得到多个结果,而不是一个:
使用正则表达式搜索多个结果
我们刚刚在上一节中了解到,元字符可以与同一表达式中的文字共存。由于这种共存,我们可以发现一些表达并不意味着我们想要的。例如,如果我们应用表达式/(this is inside)/来搜索文本this is outside (this is inside),我们会发现结果中不包括括号。之所以会出现这种情况,是因为括号是元字符,它们具有特殊的含义。
不正确的未转换元字符
我们可以像使用文字一样使用元字符。有三种机制可以做到这一点:
- 通过在元字符前面加反斜杠来转义元字符。
- 在 python 中,使用
re.escape方法对表达式中可能出现的非字母数字字符进行转义。我们将在第 2 章、正则表达式和 Python中介绍这一点。 - 用\Q 和\E引用:在正则表达式中有第三种引用机制,即用
\Q和\E引用。在支持它们的口味中,简单到包含必须用\Q(开始引用)和\E(结束引用)引用的部分。
但是,Python 目前不支持这一点。
使用反斜杠方法,我们可以将前面的表达式转换为/\(this is inside\)/,并再次将其应用于相同的文本,以使括号包含在结果中:
正则表达式中的转义元字符
在正则表达式中,如果要使用十二个元字符的字面含义,则应将其转义:
- 反斜杠
\ - 插入符号
^ - 美元符号
$ - Dot
. - 管道符号
| - 问号
? - 星号
* - 加号
+ - 左括号
( - 右括号
) - 开口方支架
[ - 开头的花括号
{
在某些情况下,正则表达式引擎将尽最大努力理解它们是否应该具有字面意义,即使它们没有被转义;例如,开头的大括号{只有在后面跟一个数字表示重复时才会被视为元字符,我们将在本章后面学习。
我们将首次使用元字符学习如何利用字符类。字符类(也称为字符集)允许我们定义一个字符,如果该字符集中存在任何已定义的字符,则该字符将匹配。
要定义一个字符类,我们应该使用开头的方括号元字符[,然后是任何接受的字符,最后用结尾的方括号]结束。例如,让我们定义一个正则表达式,它可以匹配英式和美式英语书面形式中的单词“license”:
使用字符类进行搜索
也可以使用字符的范围。这是通过在两个相关字符之间使用连字符(-实现的;例如,要匹配任何小写字母,我们可以使用[a-z]。同样,为了匹配任何单个数字,我们可以定义字符集[0-9]。
字符类的范围可以组合在一起,只需将一个范围放在另一个范围之后,就可以将一个字符与多个范围进行匹配,无需特殊分隔。例如,如果我们想要匹配任何小写或大写字母数字字符,我们可以使用[0-9a-zA-Z](有关更详细的解释,请参见下表)。这也可以使用联合机制编写:[0-9[a-z[A-Z]]]。
要素
|
描述
|
| --- | --- |
| | 匹配以下字符集 |
| 0-9 | 匹配0和9之间的任何内容(0、1、2、3、4、5、6、7、8、9)。 |
| | 或 |
| a-z | 匹配a和z之间的任何内容(a、b、c、d、z) |
| | 或 |
| A-Z | 匹配A和Z之间的任何内容(A、B、C、D、Z) |
| | 字符集结尾 |
还有一种可能性是否定范围。我们可以通过将插入符号(^符号放在开头方括号元字符([)的正后方来反转字符集的含义。如果我们有一个字符类,如表示任何数字的[0-9],则被否定的字符类[^0-9]将匹配任何非数字的字符。然而,重要的是要注意,必须有一个不是数字的字符;例如,/hello[^0-9]/与字符串hello不匹配,因为在
之后必须有一个非数字字符。有一种机制可以做到这一点,称为消极前瞻——这将在第 4 章环顾四周中介绍。
在使用字符类一段时间后,变得很清楚,其中一些非常有用,可能值得一条捷径。
幸运的是,有许多预定义的字符类可以重用,并且其他开发人员已经知道,这使得使用它们的表达式更具可读性。
这些字符不仅是典型字符集的常用快捷方式,而且在不同的上下文中具有不同的含义。匹配任何字母数字字符的字符类\w将根据配置的区域设置和 Unicode 支持匹配不同的字符集。
下表显示了 Python 目前支持的字符类:
|要素
|
说明(对于带有默认标志的正则表达式)
| | --- | --- | |
.| 此元素匹配除换行符\n之外的任何字符 |
|
\d| 此匹配任何十进制数字;这相当于类[0-9] |
|
\D| 此匹配任何非数字字符;这相当于类[^0-9] |
|
\s| 此匹配任何空白字符;这相当于类[\t\n\r\f\v] |
|
\S| 此匹配任何非空白字符;这相当于类[^ \t\n\r\f\v] |
|
\w| 此匹配任何字母数字字符;这相当于类[a-zA-Z0-9_] |
|
\W| 此匹配任何非字母数字字符;这相当于类[^a-zA-Z0-9_] |
Python 中的 POSIX 字符类
POSIX 标准提供了许多字符类的命名,例如,[:alnum:]表示字母数字字符,[:alpha:]表示字母字符,或者[:space:]表示所有空白字符,包括换行符。
所有 POSIX 字符类都遵循相同的[:name:]符号,因此易于识别。但是,Python 目前不支持它们。
如果您遇到其中一个角色,您可以通过利用我们在本节中刚刚研究的角色类的功能来实现相同的功能。例如,对于具有英语语言环境的 ASCII 等价物[:alnum:],我们可以编写[a-zA-Z0-9]。
上表中的第一个点需要特别注意。点可能是最古老的元字符之一,也是使用最多的元字符之一。点可以匹配除换行符以外的任何字符。
不匹配换行符的原因可能是 UNIX。在 UNIX 中,命令行工具通常一行一行地工作,目前可用的正则表达式分别应用于这些行。因此,没有要匹配的换行符。
让我们通过创建一个正则表达式来实践点,该正则表达式匹配除换行符以外的任意值的三个字符:
/…/要素
|
描述
| | --- | --- | | . | 匹配任何字符 | | . | 匹配后跟上一个字符的任何字符 | | . | 匹配后跟上一个字符的任何字符 |
点是一个非常强大的元字符,如果不适当使用,它可能会产生问题。在大多数使用点的情况下,它可能被认为是杀伤力过大(或者只是编写正则表达式时懒惰的一种症状)。
为了更好地定义期望匹配的内容,并向其他读者更简洁地表达正则表达式的意图,建议使用字符类。例如,在使用 Windows 和 UNIX 文件路径时,要匹配除斜杠或反斜杠以外的任何字符,可以使用反斜杠字符集:
[^\/\]要素
|
描述
| | --- | --- | |
[| 匹配一组字符 | |
^| 与此符号的以下字符不匹配 | |
\/| 匹配一个/字符 |
|
\| 匹配一个\字符 |
|
]| 剧终 |
这个字符集明确地告诉您我们打算匹配除 Windows 或 UNIX 文件路径分隔符以外的任何内容。
我们刚刚学习了如何从一组字符中匹配单个字符。现在,我们将学习一种更广泛的方法:如何匹配一组正则表达式。这是使用管道符号|完成的。
让我们首先说,如果我们找到“是”或“否”,我们希望匹配。使用 alternation,它将非常简单:
/yes|no/要素
|
描述
| | --- | --- | | | 匹配以下任一字符集 | |
yes| 字符y、e和s。 |
|
|| 或 | |
no| 字符n和o。 |
另一方面,如果我们想要接受两个以上的值,我们可以继续向替换中添加值,如下所示:
/yes|no|maybe/要素
|
描述
| | --- | --- | | | 匹配以下任一字符集 | |
yes| 字面上的“是” | |
|| 或 | |
no| 字面上的“不” | |
|| 或 | |
maybe| 字面上的“可能” |
当在更大的正则表达式中使用时,我们可能需要在括号中包装我们的替换,以表示只有该部分被替换,而不是整个表达式。例如,如果我们犯了不使用括号的错误,如以下表达式所示:
/Licence: yes|no/要素
|
描述
| | --- | --- | | | 匹配以下任一字符集 | |
Licence: yes| 字符L、i、c、e、n、c、e、:、
、y、e和s |
|
|| 或 | |
no| 字符n和o。 |
我们可能认为我们接受的是Licence: yes或Licence: no,但实际上我们接受的是Licence: yes或no,因为替代已应用于整个正则表达式,而不仅仅是yes|no部分。正确的方法是:
使用交替的正则表达式
到目前为止,我们已经学会了如何在各种时尚中定义单个角色。此时,我们将利用量词和机制来定义如何重复字符、元字符或字符集。
例如,如果我们定义一个\d可以重复多次,我们就可以很容易地为购物车的number of items字段创建一个表单验证器(记住,\d匹配任何十进制数字)。但让我们从头开始,三个基本量词:问号?、加号+和星号*。
象征
|
名称
|
前一个字符的量化
| | --- | --- | --- | |
?| 问号 | 可选(0 或 1 次重复) | |
*| 星号 | 零次或多次 | |
+| 加号 | 一次或多次 | |
{n,m}| 花括号 | 在n和m次之间 |
在上表中,我们可以找到三个基本量词,每个量词都有一个特定的实用程序。问号可用于匹配单词car及其复数形式cars:
/cars?/要素
|
描述
| | --- | --- | |
car| 匹配字符c、a、r和s |
|
s?| 可选地匹配字符s |
在前面的示例中,问号仅应用于字符s,而不是整个单词。量词始终仅应用于前一个标记。
问号量词用法的另一个有趣的例子是匹配一个电话号码,其格式可以是555-555-555、555 555 555或555555555。
我们现在知道了如何利用字符集来接受不同的字符,但是有可能对字符集应用量词吗?是的,量词可以应用于字符、字符集甚至组(我们将在第 3 章、分组中介绍这一特性)。我们可以构造这样的正则表达式来验证电话号码:
/\d+[-\s]?\d+[-\s]?\d+/在下一个表中,我们可以找到前面正则表达式的详细说明:
|要素
|
类型
|
描述
| | --- | --- | --- | |
\d| 预定义字符集 | 任何十进制字符 | |
+| 数量词 | -重复一次或多次 | |
[-\s]| 字符集 | 连字符或空白字符 | |
?| 数量词 | -可能出现也可能不出现 | |
\d| 预定义字符集 | 任何十进制字符 | |
+| 数量词 | -重复一次或多次 | |
[-\s]| 字符集 | 连字符或空白字符 | |
\d| 预定义字符集 | 任何十进制字符 | |
+| 数量词 | -重复一次或多次 |
在本节的开头,我们提到了另一种使用大括号的量词。使用这种语法,我们可以通过在前面的字符后面加上{3}来定义前面的字符必须正好出现三次,也就是说,表达式\w{8}正好指定了八个字母数字。
我们还可以通过提供最小和最大的重复次数来定义一定范围的重复,也就是说,可以使用语法{4,7}定义三到八次。可以忽略最小值或最大值,默认分别为0和无限。要指定最多三次的重复,我们可以使用{,3},也可以使用{3,}建立至少三次的重复。
可读性提示
您可以使用问号代替{,1}。星号*的{0,}和加号+的{1,}也是如此。
其他开发人员会希望您这样做。如果你不遵循这一做法,任何阅读你的表达的人都会失去一些时间,试图弄清楚你想要完成什么样的花哨东西。
下表显示了这四种不同的组合:
|语法
|
描述
| | --- | --- | |
{n}| 前一个字符重复了整整n次。 | |
{n,}| 前一个字符至少重复n次。 | |
{,n}| 前一个字符最多重复n次。 | |
{n,m}| 前面的字符在n和m之间重复(包括两次)。 |
在本章前面,我们创建了一个正则表达式来验证电话号码,其格式可以是555-555-555、555 555 555或555555555。我们定义了一个正则表达式,使用元字符加号/\d+[-\s]?\d+[-\s]?\d+/对其进行验证。它将要求数字(\d重复一次或多次。
让我们通过定义最左边的数字组最多可以包含三个字符来微调正则表达式,而其余的数字组应该正好包含三个数字:
使用量词
我们还没有定义,如果我们将这样的量词/".+"/应用于如下文本:English "Hello", Spanish "Hola"会匹配什么。我们可能期望它与"Hello" and "Hola"匹配,但实际上它将与"Hello", Spanish "Hola"匹配。
这种行为称为贪婪,是 Python 中量词的两种可能行为之一:贪婪和非贪婪(也称为不情愿。
- 量词的贪婪行为默认应用于量词中。贪婪的量词会尽可能多地匹配,以获得最大的匹配结果。
- 非贪婪的行为可以通过在量词中添加额外的问号来请求;例如,
??、*?或+?。标记为不情愿的量词的行为与贪婪的量词完全相反。他们将尽可能地进行最小的比赛。
所有格量词
量词还有第三种行为,所有格行为。目前只有 Java 和.NET 风格的正则表达式支持这种行为。
它们用量词的额外加号表示;例如,?+、*+或++。所有格量词在本书中不会有进一步的介绍。
通过查看下一个图,我们可以更好地理解这个量词是如何工作的。我们将对同一文本应用几乎相同的正则表达式(将量词保留为贪婪或标记为不情愿的除外),得到两个截然不同的结果:
贪婪和不情愿的量词
到目前为止,我们只是试图找出文本中的正则表达式。有时,当需要匹配整行时,我们也可能需要在一行的开头甚至结尾进行匹配。这可以通过边界匹配器实现。
边界匹配器是许多标识符,它们将对应于输入内部的特定位置。下表显示了 Python 中可用的边界匹配器:
|匹配器
|
描述
| | --- | --- | |
^| 在行首匹配 | |
$| 在一行的末尾匹配 | |
\b| 匹配单词边界 | |
\B| 匹配\b的对立面。任何不是单词边界的东西 |
|
\A| 匹配输入的开头 | |
\Z| 匹配输入的结尾 |
这些边界匹配器在不同的上下文中表现不同。例如,单词边界(\b)将直接取决于配置的语言环境,因为不同的语言可能有不同的单词边界,并且行首和行尾边界将根据我们将在下一章研究的某些标志而表现不同。
让我们开始使用边界匹配器,编写一个正则表达式来匹配以“Name:”开头的行。如果您查看上一个表,您可能会注意到表示行首的元字符^的存在。使用它,我们可以编写以下表达式:
/^Name:/要素
|
描述
| | --- | --- | |
^| 匹配行的开头 | |
N| 匹配后跟字符N的 |
|
a| 匹配后跟字符a的 |
|
m| 匹配后跟字符m的 |
|
e| 匹配后跟字符e的 |
|
:| 匹配后跟冒号的符号 |
如果我们想更进一步,继续使用插入符号和美元符号组合来匹配线条的末端,我们应该考虑到,从现在起,我们将与整个线条进行匹配,而不仅仅是试图在线条中找到一个模式。
按照前面的示例,假设我们希望确保在名称之后,直到行尾只有字母字符或空格。我们将通过使用接受的字符设置一个字符集,并允许它们重复任意次数直到行尾,从而匹配整行直到行尾。
/^Name:[\sa-zA-Z]+$/要素
|
描述
| | --- | --- | |
^| 匹配行的开头。 | |
N| 匹配后跟字符N的。 |
|
a| 匹配后跟字符a的。 |
|
m| 匹配后跟字符m的。 |
|
e| 匹配后跟字符e的。 |
|
:| 匹配后跟冒号的符号。 | |
[\sa-zA-Z]| 然后匹配后跟空格或任何字母大小写字符的。 | |
+| 该字符可以重复一次或多次。 | |
$| 直到队伍的尽头。 |
另一个杰出的边界匹配器是单词 boundary\b。它将匹配任何非单词字符(在配置的区域设置中)的字符,因此匹配任何潜在的单词边界。当我们想要处理孤立的单词,并且我们不想用可能分割单词的每个字符(空格、逗号、冒号、连字符等)创建字符集时,这非常有用。例如,我们可以使用以下正则表达式确保单词hello出现在文本中:
/\bhello\b/要素
|
描述
| | --- | --- | |
\b| 匹配单词边界。 | |
h| 匹配后跟字符h的。 |
|
e| 匹配后跟字符e的。 |
|
l| 匹配后跟字符l的。 |
|
l| 匹配后跟字符l的。 |
|
o| 匹配后跟字符o的。 |
|
\b| 然后匹配另一个,后跟单词边界。 |
作为练习,我们可以思考为什么前面的表达式比/hello/更好。原因是这个表达式将匹配一个孤立的单词,而不是包含“hello”的单词,也就是说,/hello/将很容易匹配hello、helloed或Othello;而/\bhello\b/将只匹配hello。
在第一章中,我们了解了正则表达式的重要性,以及它们是如何成为程序员的相关工具的。
我们还从非实用的角度研究了基本正则表达式语法和一些关键特性,如字符类和量词。
在下一章中,我们将跳转到 Python,开始练习re模块。








