Regular Expressions (Regex):正则表达式,软件工程中最为强大,且广泛适用,令人信服的技术之一。从验证电子邮件地址到执行复杂的代码重构器,正则表达式的用途非常广泛,是任何软件工程师工具箱中必不可少的条目。
什么是正则表达式?
正则表达式(或Regex,或Regexp)是使用字符序列描述复杂搜索模式的一种方式。
然而,专门的Regex语法由于其复杂性使得有些表达式变得不可访问。例如,下面的这个基本的正则表达式,它表示24小时制HH / MM格式的时间。
如果你觉得这看上去略显复杂,别担心,当我们完成这个教程时,理解这个表达式将会是小菜一碟。
Learn once, write anywhere
几乎任何编程语言都可以使用Regex。Regex的知识对于验证用户输入,与Unix shell进行交互,在你喜欢的文本编辑器中搜索/重构代码,执行数据库文本搜索等等都非常有用。
在本教程中,我将尝试在各种场景、语言和环境中对Regex的语法和使用进行简明易懂的介绍。
此Web应用程序是我用于构建、测试和调试Regex最喜欢的工具。我强烈推荐大家使用它来测试我们将在本教程中介绍的表达式。
本教程中的示例源代码可以在Github存储库中找到——https://github.com/triestpa/You-Should-Learn-Regex
0 – 匹配任何数字行
我们将从一个非常简单的例子开始——匹配任何只包含数字的行。
让我们一点一点的解释吧。
-
^ ——表示一行的开始。
-
[0-9] ——匹配0到9之间的数字
-
+ ——匹配前一个表达式的一个或多个实例。
-
$ ——表示行尾。
我们可以用伪英文重写这个Regex为[start of line][one or more digits][end of line]。
很简单,不是吗?
我们可以用d替换[0-9],结果相同(匹配所有数字)。
这个表达式(和一般的正则表达式)的伟大之处在于它无需太多修改,就可以用到任何编程语言中。
为了演示,我们先快速了解如何使用16种最受欢迎的编程语言对文本文件执行此简单的Regex搜索。
我们使用以下输入文件(test.txt)为例。
每个脚本都将使用这个正则表达式读取并搜索test.txt文件,并将结果('1234', '5362', '1')输出到控制台。
语言范例 0.0 – Java / Node.js / Type
0.1 – Python
0.2 – R
0.3 – Ruby
0.4 – Haskell
0.5 – Perl
0.6 – PHP
0.7 – Go
0.8 – Java
0.9 – Kotlin
0.10 – Scala
0.11 – Swift
0.12 – Rust
0.13 – C#
0.14 – C++
0.15 – Bash
以十六种语言编写出相同的操作是一个有趣的练习,但是,接下来在本教程中,我们将主要使用Java和Python(最后还有一点Bash),因为这些语言(在我看来)倾向于产生最清晰和更可读的实现。
1 – 年份匹配
我们来看看另外一个简单的例子——匹配二十或二十一世纪中任何有效的一年。
我们使用b而不是^和$来开始和结束这个正则表达式。b表示单词边界,或两个单词之间的空格。这允许我们在文本块(而不是代码行)中匹配年份,这对于搜索如段落文本非常有用。
-
b ——字边界
-
(19|20) ——使用或(|)操作数匹配’19′或’20′。
-
d{2}——两位数,与[0-9]{2}相同
-
b ——字边界
1.0 – 真实示例 – 计数年份请注意b不同于s,s是用于空格字符的代码。b搜索一个单词字符前面或者后面没有另一个字符的地方,因此它搜索单词字符的缺失,而s明确搜索空格字符。b特别适用于我们想要匹配特定序列/单词的情况,而不是特定序列/单词之前或之后有空格的情况。
我们可以在Python脚本中使用此表达式来查找维基百科历史部分的文章中提及20或21世纪内年份的次数。
上述脚本将按照提及的次数依次打印年份。
2 – 匹配时间
现在我们要定义一个正则表达式来匹配24小时格式(MM:HH,如16:59)的任何时间。
-
b——字边界
-
[01]——0或1
-
?——表示上述模式是可选的。
-
[0-9]—— 0到9之间的任何数字
-
|——OR操作数
-
2[0-3]——2,后面跟0和3之间的任何数字(即20-23)
-
:——匹配:字符
-
[0-5]——0到5之间的任何数字
-
d——0到9之间的任何数字(与[0-9]相同)
-
b ——字边界
2.0 – 捕获组
-
(0?[1-9]|[12]d|3[01])——匹配1到31之间的任何数字(前面的0是可选的)
-
([/-])——匹配分隔符/或-
-
(0?[1-9]|1[012])—— 匹配1到12之间的数字
-
2——匹配第二个捕获组(分隔符)
-
d{4}——匹配任意4位数(0000 – 9999)
-
$1——第一个捕获组:日期。
-
$2——第二个捕捉组:分隔符。
-
$3——第三个捕获组:月份。
-
$4——第四个捕获组:年份。
-
^——输入开始
-
[^@s]——匹配除@和空格s之外的任何字符
-
+——1+次数
-
@——匹配’@'符号
-
[^@s]+——匹配除@和空格之外的任何字符,1+次数
-
.——匹配’.'字符。
-
w{2,6}——匹配任何字符(字母,数字或下划线),2-6次
-
$——输入结束
-
/——匹配/符号(我们有转义/字符)
-
*+——匹配一个或多个*符号(再次,我们使用来转义*字符)。
-
(.*)——匹配任何字符(除了换行符n),任意次数
-
*+——匹配一个或多个*字符
-
/——匹配关闭/符号。
-
(https?://)——匹配http(s)
-
(www.)?——可选的“www”前缀
-
(?<domain>[-a-zA-Z0-9@:%._+~#=]{2,256}——匹配有效的域名
-
.[a-z]{2,6})——匹配域扩展扩展名(即“.com”或“.org”)
-
(?<path>/[-a-zA-Z0-9@:%_/+.~#?&=]*)?——匹配URL路径(/posts)、查询字符串(?limit=1)和/或文件扩展名(.html),这些都是可选的。
6.0 – 命名捕获组
-
^——开始行。
-
.+——匹配任何字符(字母,数字,符号),除了n(换行)之外,1+次数。
-
.——匹配 ‘.’字符。
-
(?i)——表示下一个序列不区分大小写。
-
(png|jpg|jpeg|gif|webp)——匹配常见的图像文件扩展名
-
$——结束行
你可能已经注意到上述模式中有了新内容—— 我们在括号 ( ... )中封装小时和分钟的捕获片段。这允许我们将模式的每个部分定义为捕获组。
捕获组允许我们单独提取、转换和重新排列每个匹配模式的片段。
2.1 – 真实示例 – 时间分析
例如,在上述24小时模式中,我们定义了两个捕获组—— 时和分。
我们可以轻松地提取这些捕获组。
以下是我们如何使用Java将24小时制的时间分解成小时和分钟。
第0个捕获组始终是整个匹配表达式。
上述脚本将产生以下输出。
作为额外的训练,你可以尝试修改此脚本,将24小时制转换为12小时制(am/pm)。
3 – 匹配日期
现在我们来匹配一个DAY/MONTH/YEAR样式的日期模式。
这个有点长,但它看起来与我们上面讲过的有些类似。
这里唯一新的概念是,我们使用2来匹配第二个捕获组,即分隔符(/或-)。这使得我们能够避免重复模式匹配规范,并且要求分隔符是一致的(如果第一个分隔符是/,那么第二个分隔符也必须一样)。
3.0 – 捕获组替换
通过使用捕获组,我们可以动态地重组和转换我们的字符串输入。
引用捕获组的标准方法是使用$或符号,以及捕获组的索引(请记住捕获组元素是完整的捕获文本)。
3.1 – 真实示例 – 日期格式转换
假设我们的任务是将使用国际日期格式(DAY/MONTH/YEAR)的文档集合转换为美式(MONTH/DAY/YEAR)日期样式。
我们可以通过替换模式$3$2$1$2$4或32124使用上述正则表达式。
让我们分解捕捉组。
替换模式(32124)简单地交换了表达式中月份和日期的内容。
以下是我们如何在Java中进行这种转换:
上述脚本将打印Today's date is 09/18/2017到控制台。
同样的脚本在Python中是这样的:
4 – 电子邮件验证
正则表达式也可用于输入验证。
以上是一个(过于简单的)Regex,用来匹配电子邮件地址。
假设我们要创建一个简单的Java函数以检查输入是否为有效的电子邮件。
此脚本的输出应为
注意——在现实应用程序中,使用Regex验证电子邮件地址对于许多情况,例如用户注册,是不够的。但是一旦你确认输入的文本是电子邮件地址,那么你应该始终遵循发送确认/激活电子邮件的标准做法。
4.1 – 完整的电子邮件Regex
这是一个非常简单的例子,它忽略了许多非常重要的电子邮件有效性边缘情况,例如无效的开始/结束字符以及连续的周期。我真的不建议在你的应用程序中使用上述表达式;最好是使用一个有信誉的电子邮件验证库或继续探索更完整的电子邮件验证Regex。
例如,下面是一个来自emailregex.com的更高级的表达式,它匹配99%的RFC 5322兼容的电子邮件地址。
不过今天我们不打算深入探讨。
5 – 代码注释模式匹配
Regex最有用的特殊用法之一是可以成为代码重构器。大多数代码编辑器支持基于Regex的查找/替换操作。一个格式正确的Regex替换可以将繁琐的需要半小时忙碌的工作变成一个漂亮的Regex重构魔法。
不要编写脚本来执行这些操作,试着在你选择的文本编辑器中去做。几乎每个文本编辑器都支持基于Regex的查找和替换。
以下是一些受欢迎的编辑器指南。
Sublime中的Regex替换——http://docs.sublimetext.info/en/latest/search_and_replace/search_and_replace_overview.html#using-regular-expressions-in-sublime-text
Vim中的Regex替换——http://vimregex.com/#backreferences
VSCode中的Regex替换——https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options
Emacs中的Regex替换——https://www.gnu.org/software/emacs/manual/html_node/emacs/Regexp-Replace.html
5.0 – 提取单行CSS注释
如果我们想要查找CSS文件中的所有单行注释怎么办?
CSS注释以/* Comment Here */的格式出现。
要捕获任何单行CSS注释,我们可以使用以下表达式。
注意,我们已经在上面的表达式中定义了三个捕获组:开放字符((/*+)),注释内容((.*))和结束字符((*+/))。
5.1 – 真实示例 – 将单行注释转换为多行注释
我们可以使用此表达式通过执行以下替换将单行注释转换为多行注释。
在这里,我们只是在每个捕获组之间添加了一个换行符n。
尝试在有以下内容的文件上执行此替换。
替换将产生相同的文件,但每个单行注释转换为多行注释。
5.2 – 真实示例 – 标准化CSS注释开头
假设我们有一个又大又杂乱的CSS文件,是由几个不同的人写的。在这个文件中,有些注释以/*开头,有些以/**开头,还有些以/*****开头。
让我们来写一个Regex替换以标准化所有的单行CSS注释,以/*开头。
为了做到这一点,我们将扩展表达式,只匹配以两个或更多星号开头的注释。
这个表达式与原来的非常相似。主要区别在于开头我们用*{2,}替换了*+。*{2,}语法表示*的“两个或多个”实例。
为了规范每个注释的开头,我们可以通过以下替代。
让我们在以下测试CSS文件上运行此替换。
结果将是与标准注释开头相同的文件。
6 – 匹配网址
另一个非常有用的Regex是在文本中匹配URL。
下面是一个来自Stack Overflow的URL匹配表达式的示例。
你注意到没有,一些捕获组现在以?<name>标识符开头。这是命名捕获组的语法,可以使得数据提取更加清晰。
6.1 – 真实示例 – 从Web页面上的URL解析域名
以下是我们如何使用命名捕获组来提取使用Python语言的网页中每个URL的域名。
脚本将打印在原始网页HTML内容中找到的每个域名。
7 – 命令行的用法
许多Unix命令行实用程序也支持Regex!我们将介绍如何使用grep查找特定文件,以及使用sed替换文本文件内容。
7.0 – 真实示例 – 用grep匹配图像文件
我们将定义另一个基本的Regex,这次是用于匹配图像文件。
以下是如何列出Downloads目录中所有图像文件的方法。
-
ls ~/Downloads——列出Downloads目录中的文件
-
|——将输出管道输送到下一个命令
-
grep -E——使用正则表达式过滤输入
7.1 – 真实例子 – 用sed进行电子邮件替换
bash命令中正则表达式的另一个好处是在文本文件中修改电子邮件。
这可以通过使用sed命令以及前面的电子邮件Regex的修改版本完成。
-
sed——Unix的“流编辑器”实用程序,允许强大的文本文件转换。
-
-E——使用扩展的Regex模式匹配
-
-i——原位替换文件流
-
's/^(.*?s|)——将行的开头包装在捕获组中
-
[^@]+@[^s]+——电子邮件Regex的简化版本。
-
/1{redacted}/g'——用{redacted}替换每个电子邮件地址。
-
test.txt——对test.txt文件执行操作。
我们可以在一个示例test.txt文件上运行上面的替换命令。
命令运行后,电子邮件将从test.txt文件中进行编辑。
Myemail is{redacted}
警告——此命令将自动从你传递的任何test.txt中删除所有电子邮件地址,因此,在运行它的时候要小心,因为此操作无法逆转。要在终端中预览结果,而不是替换原来的文本,只需省略-i标志。
注意——尽管上述命令适用于大多数Linux发行版,但是macOS使用BSD实现是sed,它在其支持的Regex语法中受到更多的限制。要在MacOS上使用sed,并具有体面的正则表达式支持,我建议使用brew install gnu-sed安装sed的GNU实现,然后从命令行使用gsed而不是sed。
8 – 什么时候不使用Regex
好的,知道Regex是一个强大又灵活的工具了吧?!那么,有没有应该避免编写Regex的时候?有!
8.0 – 语言解析
解析结构化语言,从英语到Java到JSON,使用正则表达式都是一种真正的痛苦。
当数据源中的边缘情况或次要语法错误导致表达式失败时,将导致最终(或即时)的灾难,出于此目的去编写你自己的正则表达式可能会让你心情沮丧。
强化的解析器几乎可用于所有机器可读的语言,而NLP工具可用于人类语言——我强烈建议你使用其中一种,而不是尝试编写自己的语言。
8.1 – 安全 – 输入过滤和黑名单
使用Regex过滤用户输入(例如来自Web表单),以及防止黑客向应用程序发送恶意命令(例如SQL注入),看上去似乎很诱人。
在这里使用自定义的Regex是不明智的,因为它很难覆盖每个潜在的攻击向量或恶意命令。例如,黑客可以使用替代字符编码绕过编写得不全面的输入黑名单过滤器。
这是另一个实例,对此我强烈建议你使用经过良好测试的库和/或服务,以及使用白名单而不是黑名单,以保护你的应用程序免受恶意输入。
8.2 – 性能密集的应用程序
正则表达式的匹配速度可以从不是非常快到极慢的范围变动,取决于表达式写得怎么样。对于大多数用例来说,这很好,特别是如果匹配的文本很短(例如电子邮件地址表单)的话。然而,对于高性能服务器应用程序,正则表达式会成为性能瓶颈,特别是如果表达式写得不好或被搜索的文本很长的话。
8.3 – 对于不需要Regex的地方
正则表达式是一个非常有用的工具,但这并不意味着你应该在任何地方使用它。
如果问题有替代的解决方案,解决方案更简单和/或不需要使用Regex,那么请不要只是为了显摆而使用Regex。Regex很棒,但它也是最不可读的编程工具之一,而且很容易出现边缘情况和bug。
过度使用Regex会让你的同事(以及需要工作在你的代码上的任何人)生气恼怒,甚至恨不得揍你一顿。
结论
我希望这是对Regex的许多用途的一个有用的介绍。
还有很多Regex的用例是我们没有涵盖的。例如,可以在PostgreSQL查询中使用Regex来动态地搜索数据库中的文本模式。
我们还漏下了许多强大的Regex语法特性没有覆盖,如lookahead,lookbehind,atomic groups,recursion和subroutines。