Regex 并没有那么难 (2023) - 精简 Regex 核心概念
Regex 并没有那么难
2023年7月11日 星期二
Regex 因为过于复杂而声名狼藉。这很正常,但我也认为,如果你专注于 Regex 的某个核心子集,它并没有那么难。 大部分复杂性来自于各种难以记忆的“快捷方式”。 如果你忽略这些,这种语言本身就相当小巧,并且可以在各种编程语言中移植。
学习 Regex 是值得的,因为你可以用非常少的代码完成 很多 工作。 如果我尝试使用常规的过程式代码来复制我的 Regex 所做的事情,通常会非常冗长、充满缺陷且速度明显较慢。 往往需要花费数小时甚至数天才能做得比编写几分钟的 Regex 更好。
注意: 像 Rust 这样的某些语言具有 parser combinators,它们在大多数我关心的方面都可以和 Regex 一样好,甚至更好。 然而,我仍然经常选择 Regex,因为它需要我记住的东西更少。 所有主流编程语言都支持一个 Regex 的核心子集。
你需要了解四个主要概念:
- Character sets(字符集)
- Repetition(重复)
- Groups(分组)
|
、^
和$
operators(操作符)
在这里,我将重点介绍 Regex 语言的一个子集,它并不难理解或记住。 在整个过程中,我还会告诉你应该忽略什么。 这些东西大多数是快捷方式,它们以大量的复杂性为代价来节省一些冗长。 我宁愿冗长也不要复杂,所以我坚持使用这个子集。
Character Sets(字符集)
Character set 是 Regex 中可用的最小文本匹配单元。 它只是一个字符。
Single characters(单个字符)
a
匹配一个字符,总是小写 a
。 aaa
是 3 个连续的 character sets,每个只匹配 a
。 abc
也是如此,但第二个和第三个分别匹配 b
和 c
。
Ranges(范围)
匹配一组字符中的一个。
[a]
— 与a
相同[abc]
— 匹配a
、b
或c
。[a-c]
— 相同,但使用-
指定字符范围[a-z]
— 任何小写字符[a-zA-Z]
— 任何小写或大写字符[a-zA-Z0-9!@#$%^&*()-]
— 字母数字字符加上这些符号:!@#$%^&*()-
请注意,在最后一点中,-
是如何放在最后的。 还要注意,^
不是范围内的第一个字符,如果 ^
作为 character set 或 Regex 中的第一个字符出现,则 ^
可以成为一个 operator。
这里与布尔逻辑有一个相似之处:
ab
表示“a
ANDb
”[ab]
表示 “a
ORb
”
你可以使用 groups 和 negation 构建更复杂的逻辑。
Negation (^
)(否定)
我稍后会提到这个 operator,但在 character sets 的上下文中,它表示“除了这些之外的所有内容”。
例子:
[^ab]
表示“除了a
或b
之外的所有内容”[ab^]
表示 “a
、b
或^
”。^
必须是第一个字符才能具有特殊含义。
[Ignore this stuff](忽略这些东西)
这些东西是不必要的复杂。它们以大量的复杂性为代价来节省一些冗长。
\w
、\s
等。— 这些是像[a-zA-Z0-9]
这样的 ranges 的快捷方式。 忽略它们,因为它们不可移植。 大多数编程语言都在某种程度上拥有它们,但它们很难记住。 有些语言使用不同的语法,例如:word:
,它几乎和显式写出来一样长。.
— 点号 (.
) 匹配任何字符,但并非总是如此。 有时它不匹配换行符。 在某些编程语言中,它永远不会匹配换行符。 我经常因为.
的行为不如我所想而受到困扰。 最好完全忽略它。 相反,使用范围否定,例如[^%]
,如果你知道%
字符不会出现。 更明确一点没什么坏处。
Repetition(重复)
这些 operators 更改紧接在前的 character set 以匹配特定次数:
?
— 零次或一次*
— 零次或多次+
— 一次或多次
所有这些也适用于整个 groups。
[Ignore this stuff](忽略这些东西)
这些是不必要的复杂。你可以通过其他方式完成相同的事情。
- Non-greedy matching,
*?
和+?
。 当你使用.
character set 时,这会经常出现。 相反,你通常可以使用更严格的 negation character set,如[^%]
。 - Repetition ranges,即
{1,2}
。 只需复制你的 pattern 或在 group 上使用?
或*
。
Groups(分组)
Group 基本上是一个子 Regex。 Groups 有三个常见的用途:
1. Repeat a sub-pattern(重复一个子模式)
例如,此 pattern ([0-9][0-9]?[0-9]][.])+
匹配一个、两个或三个数字,后跟一个 .
,并且还匹配此 pattern 的重复模式。 这将匹配一个 IP address(即使不完全正确)。
2. Substitutions(替换)
最常见的 Regex 操作是 match 和 substitute。 但是,subtitution 的 API 因 host langauge 而异。
- Methods — 在 C#、Java、Python 等中,通常会有一个名为
sub
、substitute
或replace
的 method 或 function。 sed
style — 在 sed、Perl 和 bash 中,它的流程类似于s/pattern/replacement/
,其中前导s
表示“substitute”。
在这两种情况下,你都可以使用 $1
或 \1
。 在文档中查找哪个适合。
3. Extract text(提取文本)
你可以提取 group 匹配的文本。
0
— 整个 Regex match1
-∞ — 由 1 索引的 group 匹配的文本。 第一组括号是 group1
,第二组是2
,依此类推。
不可移植的部分是,访问 groups 的 API 在每种编程语言中几乎总是不同的。 尽管如此,group extraction 非常有用,所以只需查找一下。
最常见的 APIs 如下所示:
Match.group(1)
— Python、C#、Java 等提供了一种来自主要编程语言的 method,用于从 match object 中提取 group。 确切的 method 名称通常类似于group
或getGroup
。$1
— Perl 将在本地 scope 中设置诸如$1
和$2
之类的变量。 大多数编程语言都不能这样做,但是你会看到该语法出现,例如,对于 replacements,通常可以在 substitution 文本中使用$1
或\1
。
如果这些 APIs 不存在,或者如果你不想记住它,你可以通过 subtitution 复制 extraction。 例如,在 Python 中,你可以执行 re.sub("([^\n]*\\.foo)[^\n]*", "$1", input_str)
来提取第一个 group。
[Ignore this stuff](忽略这些东西)
在 groups 的开头有一些 operators,例如 (?:
,它们可以表示各种东西,例如“non-capturing group”或“look-ahead”或“look-behind”。 这些是相当高级的,通常你可以不用了解它们。
The |
、^
和 $
Operators(操作符)
|
operator 是 OR,但适用于整个 Regex 或 groups。
foo|bar
匹配foo
或bar
(foo|bar)+
在其上添加一些 repetition,例如,它匹配barfoobarfoo
^
只有当它是第一个字符时才重要:
- 在 pattern 中第一个 — 从字符串或行的开头开始匹配。 例如,
^foo
将匹配foobar
,但不匹配barfoo
。- 警告:某些 Regex APIs 的行为总是像 pattern 始终被
^
和$
包围一样。 你可以通过试错很容易地对此进行测试。
- 警告:某些 Regex APIs 的行为总是像 pattern 始终被
- 在 character set 中第一个 — negation,匹配除这些字符之外的所有字符
$
字符仅表示“结束”,并且仅在 top-level Regex 中使用。
Conclusion(结论)
始终只坚持使用 Regex 的这个子集并不是一个坏主意,因为它主要可以在各种编程语言中移植。 这意味着需要记住的东西更少,因此你在将信息塞入你的大脑方面获得了很大的“性价比”。 确实存在的怪癖相对较少,并且通常由于它们提供的价值而值得付出努力。
关于可移植性 — 大多数现代实现都试图复制 Perl Regex 的某个子集。 我在此处概述的子集在当今的主要编程语言中非常一致。 但是,如果你使用的是 sed
和 grep
之类的旧工具,它们是大约在 Perl 开发 Regex 概念的同时创建的,你可能会遇到一些意外。 但是,较新的实现相当稳定。
太多时候人们完全拒绝 Regex,这很可惜,因为它是一种非常强大的文本处理语言。 稍微了解一下 Regex 知识会很有帮助。 我希望这有帮助!