正则表达式(Regex)完全指南
正则表达式(简称 regex)是一种允许你匹配具有特定模式的字符串的语法。你可以将其视为一种高级的文本搜索快捷方式,但正则表达式还增加了使用量词、模式集合、特殊字符和捕获组来创建极其高级的搜索模式的功能。
你可以在任何需要查询基于字符串的数据时使用正则表达式,例如:
- 分析命令行输出
- 解析用户输入
- 检查服务器或程序日志
- 处理具有一致语法的文本文件,例如 CSV
- 读取配置文件
- 搜索和重构代码
虽然理论上无需正则表达式即可完成所有这些任务,但当正则表达式出现时,它们就成为完成所有这些任务的超能力。
在本指南中我们将介绍:
- 正则表达式是什么样的?
- 如何读取和编写正则表达式
- 如何使用正则表达式
- 什么是“正则表达式标志”?
- 什么是“正则表达式组”?
正则表达式是什么样的?
在其最简单的形式中,正则表达式的使用可能看起来像这样:
此截图来自regex101 网站。以后所有截图都将以此网站为参考。
在“Test”示例中,字母 test 构成了搜索模式,与简单搜索相同。
然而,这些正则表达式并不总是如此简单。以下正则表达式匹配 3 个数字,后跟一个“-”,再后跟 3 个数字,再后跟一个“-”,最后以 4 个数字结尾。
你知道,就像电话号码一样:
^(?:\d{3}-){2}\d{4}$
这个正则表达式可能看起来很复杂,但要记住两点:
- 我们将在本文中教你如何阅读和编写这些内容
- 这是编写该正则表达式的相当复杂的方法。
事实上,大多数正则表达式都可以用多种方式编写,就像其他形式的编程一样。例如,上面的代码可以改写成一个更长但更易读的版本:
^[0-9]{3}-[0-9]{3}-[0-9]{4}$
大多数语言都提供了使用正则表达式搜索和替换字符串的内置方法。然而,每种语言可能根据其自身需求,拥有不同的语法。
在本文中,我们将重点介绍 Regex 的 ECMAScript 变体,它在 JavaScript 中使用,并且与其他语言的正则表达式实现有很多共同之处。
如何读取(和编写)正则表达式
量词
正则表达式量词检查应该搜索某个字符的次数。
以下是所有量词的列表:
a|b
- 匹配“a”或“b”?
- 零个或一个+
- 一个或多个*
- 零个或多个{N}
- 恰好 N 次(其中 N 是一个数字){N,}
- N 次或更多次(其中 N 是一个数字){N,M}
- N 到 M 次之间(其中 N 和 M 为数字,且 N < M)*?
- 零个或多个,但第一个匹配后停止
例如,以下正则表达式:
Hello|Goodbye
匹配字符串“Hello”和“Goodbye”。
同时:
Hey?
将跟踪“y”从零到一次,因此将与“He”和“Hey”匹配。
或者:
Hello{1,3}
将匹配“Hello”、“Helloo”、“Hellooo”,但不会匹配“Helloooo”,因为它正在寻找 1 到 3 次之间的字母“o”。
这些甚至可以相互结合:
He?llo{2}
这里我们寻找的是“e”和字母“o”乘以 2 的零到一个实例的字符串,因此这将匹配“Helloo”和“Hlloo”。
贪婪匹配
我们在上一个列表中提到的正则表达式量词之一是+
符号。该符号匹配一个或多个字符。这意味着:
Hi+
将匹配从“Hi”到“Hiiiiiiiiiiiiiiii”的所有内容。这是因为所有量词默认都被视为“贪婪”。
但是,如果使用问号符号 ( ?
) 将其更改为“懒惰”,则行为会发生变化。
Hi+?
现在,i
匹配器会尝试尽可能少地匹配。由于+
图标表示“一个或多个”,它只会匹配一个“i”。这意味着,如果我们输入字符串“Hiiiiiiiiiiii”,则只会匹配“Hi”。
虽然它本身并不是特别有用,但当与诸如符号之类的更宽泛的匹配项结合使用时.
,它就变得非常重要,我们将在下一节中介绍。.
符号在正则表达式中用于查找“任意字符”。
现在如果你使用:
H.*llo
您可以匹配从“Hillo”到“Hello”到“Hellollollo”的所有内容。
但是,如果您只想匹配最后一个示例中的“Hello”,该怎么办?
好吧,只需使用 a 使搜索变得懒惰?
,它就会按我们想要的方式工作:
H.*?llo
图案集合
模式集合允许你搜索要匹配的字符集合。例如,使用以下正则表达式:
My favorite vowel is [aeiou]
您可以匹配以下字符串:
My favorite vowel is a
My favorite vowel is e
My favorite vowel is i
My favorite vowel is o
My favorite vowel is u
但没有别的了。
以下是最常见的图案集合列表:
[A-Z]
- 匹配从“A”到“Z”的任意大写字符[a-z]
- 匹配从“a”到“z”的任意小写字符[0-9]
- 匹配任意数字[asdf]
- 匹配任意字符“a”、“s”、“d”或“f”[^asdf]
- 匹配不属于以下任何字符:“a”、“s”、“d”或“f”
您甚至可以将它们组合在一起:
[0-9A-Z]
- 匹配任意数字或“A”至“Z”的大写字母[^a-z]
- 匹配任何非小写字母
通用代币
并非所有字符都如此容易识别。虽然像“a”到“z”这样的键可以用正则表达式匹配,但换行符呢?
“换行符”是您按“Enter”键添加新行时输入的字符。
.
- 任何角色\n
- 换行符\t
- 制表符\s
- 任何空白字符(包括\t
,\n
以及其他一些字符)\S
- 任何非空白字符\w
- 任何单词字符(大写和小写拉丁字母、数字 0-9 和_
)\W
- 任何非单词字符(标记的逆\w
)\b
\w
- 单词边界:和之间的边界\W
,但匹配中间的字符\B
- 非词边界:\b
^
- 一行的开头$
- 一行的结尾\\
- 文字字符“\”
因此,如果您想删除每个以新单词开头的字符,您可以使用类似以下正则表达式:
\s.
并将结果替换为空字符串。执行此操作后,结果如下:
Hello world how are you
变为:
Helloorldowreou
与集合合并
不过,这些 token 本身并不那么有用!假设我们要删除任何大写字母或空格。当然,我们可以这样写
[A-Z]|\s
但实际上我们可以将它们合并在一起并将我们的\s
令牌放入集合中:
[A-Z\s]
词边界
在我们的标记列表中,我们提到了\b
匹配单词边界。我想花点时间解释一下它与其他标记的作用有何不同。
给定一个像“This is a string”这样的字符串,你可能期望空格字符被匹配——然而事实并非如此。相反,它匹配的是字母和空格之间的字符:
这可能有点难以理解,但简单地匹配单词边界并不常见。相反,你可能会使用类似下面的代码来匹配完整的单词:
\b\w+\b
您可以像这样解释该正则表达式:
“一个单词边界。然后,一个或多个‘单词’字符。最后,另一个单词边界”。
开始和结束线
我们讨论的另外两个标记是^
和$
。它们分别标记一行的开始和结束。
因此,如果您想找到第一个单词,您可以执行以下操作:
^\w+
匹配一个或多个“单词”字符,但仅限于紧接在行首之后的字符。记住,“单词”字符是指任何大小写拉丁字母、数字 0-9 以及 的字符_
。
同样,如果你想找到最后一个单词,你的正则表达式可能看起来像这样:
\w+$
但是,仅仅因为这些标记通常结束一行并不意味着它们后面不能有字符。
例如,如果我们想找到换行符之间的每个空格字符来充当基本的JavaScript 压缩器,该怎么办?
好吧,我们可以使用以下正则表达式说“查找一行结束后的所有空格字符”:
$\s+
字符转义
虽然标记非常有用,但在尝试匹配实际包含标记的字符串时,它们可能会带来一些复杂性。例如,假设您在博客文章中有以下字符串:
"The newline character is '\n'"
或者,你想找出这篇博文中所有使用“\n”字符串的地方。你可以使用 转义字符\
。这意味着你的正则表达式可能如下所示:
\\n
如何使用正则表达式
然而,正则表达式不仅仅用于查找字符串。你还可以通过其他方法使用它们来修改或处理字符串。
虽然许多语言都有类似的方法,但我们以 JavaScript 为例。
使用正则表达式创建和搜索
首先,让我们看看正则表达式字符串是如何构造的。
在 JavaScript(以及许多其他语言)中,我们将正则表达式放在//
块内。搜索小写字母的正则表达式如下所示:
/[a-z]/
然后,此语法会生成一个 RegExp 对象,我们可以使用该对象和内置方法(例如exec
)来匹配字符串。
/[a-z]/.exec("a"); // Returns ["a"]
/[a-z]/.exec("0"); // Returns null
然后我们可以利用这个真实性来确定正则表达式是否匹配,就像我们在这个例子的第 3 行中所做的那样:
我们也可以使用RegExp
想要转换为正则表达式的字符串来调用构造函数:
const regex = new RegExp("[a-z]"); // Same as /[a-z]/
用正则表达式替换字符串
您还可以使用正则表达式来搜索和替换文件内容。假设您想将任何问候语替换为“再见”。您可以这样做:
function youSayHelloISayGoodbye(str) {
str = str.replace("Hello", "Goodbye");
str = str.replace("Hi", "Goodbye");
str = str.replace("Hey", "Goodbye"); str = str.replace("hello", "Goodbye");
str = str.replace("hi", "Goodbye");
str = str.replace("hey", "Goodbye");
return str;
}
还有一种更简单的替代方法,使用正则表达式:
function youSayHelloISayGoodbye(str) {
str = str.replace(/[Hh]ello|[Hh]i|[Hh]ey/, "Goodbye");
return str;
}
但是,您可能会注意到,如果您运行youSayHelloISayGoodbye
“Hello, Hi there”:它将不会匹配多个输入:
如果对字符串“Hello, Hi there”使用正则表达式 /[Hh]ello|[Hh]i|[Hh]ey/,则默认情况下它将只匹配“Hello”。
在这里,我们应该看到“Hello”和“Hi”都匹配,但是没有。
这是因为我们需要利用正则表达式“标志”进行多次匹配。
旗帜
正则表达式标志是对现有正则表达式的修饰符。这些标志始终附加在正则表达式定义中的最后一个正斜杠之后。
以下是一些可供您使用的标志的简短列表。
g
- 全局,匹配多次m
- 强制 $ 和 ^ 分别匹配每个换行符i
- 使正则表达式不区分大小写
这意味着我们可以重写以下正则表达式:
/[Hh]ello|[Hh]i|[Hh]ey/
要使用不区分大小写的标志:
/Hello|Hi|Hey/i
有了这个标志,这个正则表达式现在将匹配:
Hello
HEY
Hi
HeLLo
或任何其他经过修改的变体。
具有字符串替换功能的全局正则表达式标志
正如我们之前提到的,如果执行不带任何标志的正则表达式替换,它将仅替换第一个结果:
let str = "Hello, hi there!";
str = str.replace(/[Hh]ello|[Hh]i|[Hh]ey/, "Goodbye");
console.log(str); // Will output "Goodbye, hi there"
但是,如果传递该global
标志,则会匹配正则表达式匹配的每个问候语实例:
let str = "Hello, hi there!";
str = str.replace(/[Hh]ello|[Hh]i|[Hh]ey/g, "Goodbye");
console.log(str); // Will output "Goodbye, Goodbye there"
关于 JavaScript 全局标志的说明
exec
当使用全局 JavaScript 正则表达式时,您可能会在多次运行命令时遇到一些奇怪的行为。
特别是,如果使用全局正则表达式运行,它将每隔一次exec
返回:null
这是因为,正如MDN 解释的那样:
JavaScript RegExp对象在设置了全局或粘性标志时是有状态的……它们存储上一个匹配项的lastIndex。在内部使用此功能,exec() 可用于迭代文本字符串中的多个匹配项……
该exec
命令会尝试向前查找lastIndex
。由于lastIndex
设置为字符串的长度,它会尝试将""
空字符串与您的正则表达式进行匹配,直到它exec
再次被另一个命令重置。虽然此功能在特定情况下很有用,但它常常会让新用户感到困惑。
为了解决这个问题,我们可以lastIndex
在运行每个exec
命令之前简单地分配为 0:
团体
使用正则表达式搜索时,一次搜索多个匹配项会很有帮助。这时“组”就派上用场了。组允许您一次搜索多个项目。
在这里,我们可以看到与两者的匹配Testing 123
,并且Tests 123
没有在正则表达式中重复“123”匹配器。
/(Testing|tests) 123/ig
(...)
- 任意三个字符匹配成组(?:...)
- 匹配任意三个字符的非捕获组
当“替换”成为等式的一部分时,这两者之间的差异通常出现在对话中。
例如,使用上面的正则表达式,我们可以使用以下 JavaScript 将文本替换为“Testing 234”和“tests 234”:
const regex = /(Testing|tests) 123/ig;
let str = `
Testing 123
Tests 123
`;
str = str.replace(regex, '$1 234');
console.log(str); // Testing 234\nTests 234"
我们用 来$1
指代第一个捕获组,(Testing|tests)
。我们也可以匹配多个组,例如(Testing|tests)
和(123)
:
const regex = /(Testing|tests) (123)/ig;
let str = `
Testing 123
Tests 123
`;
str = str.replace(regex, '$1 #$2');
console.log(str); // Testing #123\nTests #123"
但是,这仅适用于捕获组。如果我们更改:
/(Testing|tests) (123)/ig
成为:
/(?:Testing|tests) (123)/ig;
那么就只有一个捕获组——(123)
相反,上面的相同代码将输出不同的内容:
const regex = /(?:Testing|tests) (123)/ig;
let str = `
Testing 123
Tests 123
`;
str = str.replace(regex, '$1');
console.log(str); // "123\n123"
命名捕获组
虽然捕获组很棒,但当捕获组数量较多时,很容易让人感到困惑。$3
和之间的区别$5
并不总是一目了然。
为了解决这个问题,正则表达式有一个称为“命名捕获组”的概念
(?<name>...)
- 名为“name”的命名捕获组,匹配任意三个字符
您可以在正则表达式中使用它们来创建一个名为“num”的组,该组匹配三个数字:
/Testing (?<num>\d{3})/
然后,您可以像这样使用它来替换:
const regex = /Testing (?<num>\d{3})/
let str = "Testing 123";
str = str.replace(regex, "Hello $<num>")
console.log(str); // "Hello 123"
命名反向引用
有时在查询内部引用命名的捕获组会很有用。这时“反向引用”就可以发挥作用了。
\k<name>
在搜索查询中引用命名捕获组“名称”
假设您想要匹配:
Hello there James. James, how are you doing?
但不是:
Hello there James. Frank, how are you doing?
你可以编写一个正则表达式来重复单词“James”,如下所示:
/.*James. James,.*/
更好的替代方案可能是这样的:
/.*(?<name>James). \k<name>,.*/
现在,您不再需要硬编码两个名称,而只需要一个。
前瞻组和后瞻组
前瞻组和后瞻组非常强大,但经常被误解。
前瞻和后瞻有四种不同的类型:
(?!)
- 负面前瞻(?=)
- 积极展望(?<=)
- 积极回顾(?<!)
- 负面回顾
前瞻的工作原理就像它听起来的那样:它要么查看某物是否在前瞻组之后,要么查看它是否不在前瞻组之后,这取决于它是正数还是负数。
因此,像这样使用负向前瞻:
/B(?!A)/
将允许您匹配BC
但不允许BA
。
你甚至可以将它们与^
和$
标记组合起来,尝试匹配完整的字符串。例如,以下正则表达式将匹配任何不以“Test”开头的字符串
/^(?!Test).*$/gm
同样,我们可以将其切换为正向预测,以强制我们的字符串必须以“Test”开头
/^(?=Test).*$/gm
整合起来
正则表达式非常强大,可以用于各种字符串操作。了解它们可以帮助您重构代码库、编写快速的语言脚本等等!
让我们回到最初的电话号码正则表达式并尝试再次理解它:
^(?:\d{3}-){2}\d{4}$
请记住,此正则表达式旨在匹配如下电话号码:
555-555-5555
这里的正则表达式是:
- 使用
^
和$
定义正则表达式行的开始和结束。 - 使用非捕获组来查找三个数字,然后是破折号
- 重复此组两次,以匹配
555-555-
- 重复此组两次,以匹配
- 查找电话号码的最后 4 位数字
希望本文能帮助您更好地了解正则表达式。如果您想快速了解一些常用正则表达式的定义,请查看我们的速查表。
文章来源:https://dev.to/coderpad/the-complete-guide-to-regular-expressions-regex-1m6