perl regex
Perl 以其强大的正则表达式引擎而闻名,它为文本处理提供了无与伦比的灵活性和力量。
Perl 正则表达式
正则表达式 (Regular Expression,简称 Regex 或 Regexp) 是一种用来描述、匹配字符串模式的强大工具。在 Perl 中,正则表达式是语言的核心部分,被广泛用于字符串搜索、替换、分割和数据验证。
1. 基本概念与操作符
Perl 主要通过以下三个操作符来使用正则表达式:
- 匹配操作符
m//
(或/
): 用于查找字符串中是否包含某个模式。 - 替换操作符
s///
: 用于查找并替换字符串中的模式。 - 转换操作符
tr///
(或y///
): 用于将字符串中的字符一对一地转换。
默认情况下,这些操作符作用于特殊变量 $_
。
示例:基本匹配
1 | my $text = "Hello, world!"; |
2. 元字符 (Metacharacters) - 构建模式的基石
元字符是正则表达式中具有特殊含义的字符,它们不代表自身,而是表示一种模式。
2.1. 锚点 (Anchors)
锚点用于指定匹配模式在字符串中的位置。
^
: 匹配字符串的开头。- 示例:
/^Hello/
匹配以 “Hello” 开头的字符串。
- 示例:
$
: 匹配字符串的结尾。- 示例:
/world!$/
匹配以 “world!” 结尾的字符串。
- 示例:
\b
: 匹配单词边界。单词边界是非字母数字字符(或字符串的开头/结尾)和字母数字字符之间的位置。- 示例:
/\bcat\b/
匹配独立的单词 “cat”。"The cat sat"
匹配,"category"
不匹配。
- 示例:
\B
: 匹配非单词边界。- 示例:
/\Bcat\B/
匹配嵌在单词中的 “cat”。"category"
匹配,"The cat sat"
不匹配。
- 示例:
2.2. 量词 (Quantifiers)
量词用于指定前一个字符或组出现的次数。
\*
: 匹配前一个元素零次或多次。- 示例:
/a*b/
匹配 “b”, “ab”, “aaab”。
- 示例:
+
: 匹配前一个元素一次或多次。- 示例:
/a+b/
匹配 “ab”, “aaab”,但不匹配 “b”。
- 示例:
?
: 匹配前一个元素零次或一次。- 示例:
/colou?r/
匹配 “color” 和 “colour”。
- 示例:
{n}
: 匹配前一个元素恰好n
次。- 示例:
/a{3}b/
匹配 “aaab”。
- 示例:
{n,}
: 匹配前一个元素至少n
次。- 示例:
/a{2,}b/
匹配 “aab”, “aaab”, “aaaab” 等。
- 示例:
{n,m}
: 匹配前一个元素至少n
次,最多m
次。- 示例:
/a{2,4}b/
匹配 “aab”, “aaab”, “aaaab”。
- 示例:
2.3. 字符类 (Character Classes)
字符类用于匹配一组字符中的任意一个。
[abc]
: 匹配方括号中列出的任意一个字符。- 示例:
/[aeiou]/
匹配任何一个小写元音字母。
- 示例:
[^abc]
: 匹配不在方括号中列出的任意一个字符。- 示例:
/[^0-9]/
匹配任何非数字字符。
- 示例:
[a-z]
: 匹配指定范围内的任意一个字符。- 示例:
/[a-zA-Z0-9]/
匹配任何字母或数字。
- 示例:
.
: 匹配除换行符\n
之外的任意一个字符。- 示例:
/a.b/
匹配 “axb”, “a?b”, “a3b”。
- 示例:
2.4. 预定义字符类 (Predefined Character Classes)
这些是常用的字符类的简写形式。
\d
: 匹配任意一个数字字符 (等价于[0-9]
)。\D
: 匹配任意一个非数字字符** (等价于[^0-9]
)。
\w
: 匹配任意一个单词字符 (字母、数字或下划线_
,等价于[a-zA-Z0-9_]
)。\W
: 匹配任意一个非单词字符** (等价于[^a-zA-Z0-9_]
)。
\s
: 匹配任意一个空白字符 (包括空格、制表符\t
、换行符\n
、回车符\r
、换页符\f
等)。\S
: 匹配任意一个非空白字符**。
2.5. 选择 (Alternation)
|
: 匹配左边或右边的模式。- 示例:
/cat|dog/
匹配 “cat” 或 “dog”。
- 示例:
2.6. 分组 (Grouping)
()
: 用于将多个字符组合成一个逻辑单元,可以对其应用量词或进行捕获。- 示例:
/(ab)+/
匹配 “ab”, “abab”, “ababab”。 - 捕获组: 分组还会捕获匹配到的子字符串,用于后续引用(见。
- 示例:
2.7. 转义 (Escaping)
\
: 用于将元字符转义为其字面含义,或用于创建特殊序列。- 示例:
/2\.5/
匹配字面值 “2.5” (因为.
是元字符)。 - 示例:
/\$/
匹配字面值$
符号。 - 示例:
/\n/
匹配换行符。
- 示例:
3. 捕获组 (Capture Groups)
使用圆括号 ()
创建捕获组,正则表达式引擎会“记住”每个捕获组匹配到的文本。这些捕获到的文本可以在后续的模式匹配、替换或脚本中使用。
- 在替换中使用: 通过
$1
,$2
,$3
… 来引用。 - 在匹配后使用: 通过
$1
,$2
,$3
… 或\1
,\2
,\3
…(在模式内部引用)来访问。 - 非捕获组:
(?:...)
创建一个分组,但不捕获匹配到的文本。这在只需要分组逻辑而不需要捕获时很有用,可以提高效率。
示例:捕获与替换
1 | my $email = "user@example.com"; |
注意这里 s///
使用了 #
作为分隔符,因为模式中包含 /
,这样可以避免转义 \/
。
您好!您提到的“命令分组”在正则表达式中通常指的是使用圆括号 ()
来将正则表达式的一部分组合成一个逻辑单元。这在 Perl 兼容正则表达式(PCRE)中是一个非常核心且强大的特性,在 grep -P
中也同样适用。
分组的主要作用有:
- 应用量词到多个字符:将一组字符视为一个整体,然后对其应用量词。
- 定义选择(或)的范围:控制
|
操作符的作用范围。 - 捕获匹配的子字符串:将匹配到的内容存储起来,以便后续引用。
- 非捕获分组:只用于分组逻辑,不捕获内容,可以提高效率。
下面我们通过 grep -P
的实例来详细讲解。
1. 基本分组:应用量词或定义选择范围
这是 ()
最基本的用法,将一组字符作为一个整体来处理。
示例 1:应用量词到多个字符
查找文件中所有连续重复的 “ab” 字符串,例如 “abab” 或 “ababab”。
1 | grep -P '(ab)+' my_file.txt |
如果没有分组:ab+
只会匹配 “abbbb…”,因为 +
只作用于它前面的单个字符 b
。
示例 2:定义选择(或)的范围
场景:查找文件中所有包含 “you” “me” 或 “her” 的行,但它们必须是作为 “love” 后面的一部分。
1 | mugster@mug:~$ echo 'loveyou love you love me love her likeher' | grep -Po 'love\s(you|me|her)' |
2. 捕获组 (Capturing Groups)
这是分组最常用的功能之一。使用 ()
括起来的部分会捕获匹配到的子字符串,并存储起来。虽然 grep -P
不像 Perl 脚本那样直接暴露 $1
, $2
等变量供输出(除非结合 -o
和 \K
或 \G
),但它们在模式内部的反向引用中非常有用。
示例 3:反向引用
场景:查找文件中所有重复单词的行,例如 “hello hello” 或 “world world”。
1 | grep -P '\b(\w+)\s+\1\b' my_file.txt |
示例 4:结合 -o
选项提取特定部分
场景:从日志中提取所有括号内的内容。
1 | grep -P -o '\((.*?)\)' my_log.log |
- 解释:
-o
: 只输出匹配到的部分。\(
和\)
: 匹配字面括号。(.*?)
: 捕获组,使用非贪婪匹配.*?
匹配括号内的所有内容。
- 输出:只会输出
(内容)
这样的完整匹配。如果想只输出括号内的内容,需要结合更高级的技巧(如\K
或perl -ne
)。
3. 非捕获组 (Non-Capturing Groups)
使用 (?:...)
来创建分组,但它不会捕获匹配到的内容。这在只需要分组逻辑(如应用量词或选择范围)而不需要捕获时非常有用,可以稍微提高性能。
示例 5:非捕获组的应用
场景:查找文件中包含 “apple” 或 “banana” 的行,后面跟着 “juice”。我们不需要捕获 “apple” 或 “banana”。
1 | grep -P '(?:apple|banana) juice' my_file.txt |
- 解释:
(?:apple|banana)
: 这是一个非捕获组。它将 “apple” 和 “banana” 组合起来进行选择,但不会将匹配到的 “apple” 或 “banana” 存储为捕获组。juice
: 匹配字面字符串。
- **对比
(apple|banana) juice
**:如果使用捕获组,$1
会捕获 “apple” 或 “banana”。如果不需要这个信息,非捕获组更高效。
4. 原子组 (Atomic Groups) / 占有型量词 (Possessive Quantifiers)
虽然不是严格意义上的“分组”,但原子组 (?>...)
是一个高级特性,它与分组和回溯密切相关。它会匹配尽可能多的内容,并且一旦匹配成功,就不会回溯。这可以防止指数级回溯导致的性能问题。
示例 6:原子组(防止回溯)
场景:匹配一个以 “a” 开头,后面跟着多个 “b”,最后以 “b” 结尾的字符串。
1 | # 假设字符串 "abbbbc" |
- **
a(?>.\*)b$
**:(?>.*)
会尽可能多地匹配,一旦匹配了bbbc
,它就“锁定”了,不会回溯让出b
给后面的b$
。所以如果字符串是abbbbc
,它会匹配失败。 - 应用:主要用于性能优化,避免在复杂模式中出现“灾难性回溯”。
命名分组(Named Capturing Groups)是正则表达式中的一个高级特性,它能让你的模式更具可读性,并且更易于维护。
简单来说,命名分组就是给一个捕获组起一个有意义的名字,而不是仅仅依赖于它在模式中的顺序号($1
, $2
等)。
5. 命名分组
在 Perl 兼容正则表达式(PCRE)中,有两种主要的命名分组语法:
- **
(?<name>...)
**:最常用和推荐的语法。 - **
(?'name'...)
**:另一种可选语法。
命名分组提供了比传统捕获组 (()
) 更大的优势:
- 增强可读性:模式本身就具有了文档功能。例如,
(^\d{4})
只告诉我们捕获了一个四位数字,而(?<year>\d{4})
则明确地告诉我们捕获的是年份。 - 提高可维护性:如果你在模式中添加或删除了捕获组,传统捕获组的编号(
\1
,\2
)会随之改变,这可能导致你的代码出错。而命名分组则不会受影响,你可以随时调整其他部分。 - 更清晰的反向引用:在模式内部,你可以使用**
\k<name>
**语法来引用之前捕获的内容,这比使用编号(\1
)更清晰。
假设我们想从一个日期字符串 “2025-08-06” 中提取年、月、日。
1. 使用传统分组
1 | # 模式: |
- 反向引用:如果要引用年,你需要用
\1
;如果想引用月,用\2
。如果之后你在前面添加了一个新的捕获组,\2
就会变成\3
,你需要修改所有相关的反向引用。
2. 使用命名分组
1 | # 模式: |
- 可读性:模式
(?<year>\d{4})
一目了然地告诉了我们它在捕获什么。 - 反向引用:在模式内部,你可以使用
\k<year>
、\k<month>
、\k<day>
来引用捕获的内容。
在 grep -P
中的应用
grep -P
完全支持命名分组的语法。虽然它不像在 Perl 脚本中那样可以直接通过名称来访问捕获的内容,但你可以在反向引用中使用它。
示例:使用命名分组查找重复的单词
1 | # 使用传统分组 |
- 解释:
(?<word>\w+)
:捕获一个单词并命名为word
。\k<word>
:引用名为word
的捕获组匹配到的内容。
对比:两种命令都能得到相同的结果,但第二个命令通过 (?<word>...)
和 \k<word>
使得模式的意图更加清晰。如果模式变得更复杂,这种优势会更加明显。
在编写复杂的正则表达式时,优先使用命名分组。它能极大地提升代码的可读性和可维护性,是正则表达式从功能性到工程化的一大进步。
总结
在 grep -P
中,“分组”主要通过 ()
来实现,它提供了模式的结构化、量词的精确应用、选择范围的控制以及捕获子字符串的能力。理解捕获组、非捕获组以及它们与反向引用、贪婪/非贪婪匹配和回溯的关系,是掌握 Perl 正则表达式高级用法的关键。
4. 正则表达式修饰符 (Modifiers)
修饰符放在正则表达式的末尾,用于改变匹配行为。
i
(ignore case): 不区分大小写匹配。- 示例:
/hello/i
匹配 “hello”, “Hello”, “HELLO”。
- 示例:
g
(global): 全局匹配。在匹配操作中,查找所有匹配项;在替换操作中,替换所有匹配项。- 示例:
$text =~ s/a/X/g;
将$text
中所有的 “a” 替换为 “X”。
- 示例:
s
(single line / dotall): 使.
匹配包括换行符在内的所有字符。- 示例:
/foo.bar/s
即使中间有换行,也能匹配 “foo\nbar”。
- 示例:
m
(multi-line): 使^
和$
匹配每行的开头和结尾(而不仅仅是整个字符串的开头和结尾)。- 示例:
m/^start$/m
将匹配多行字符串中每一行独立的 “start”。
- 示例:
x
(extended / ignore whitespace): 忽略模式中的空白字符和#
后的注释,提高可读性。- 示例:
/^a.b$/x
可以写成/^ a . b $/x # 匹配以a开头,b结尾的三个字符
- 示例:
o
(once): 仅编译一次正则表达式。如果正则表达式中包含变量,并且变量的值在后续的循环中保持不变,使用o
可以提高效率。- 示例:
while (<FILE>) { print if /$variable/o; }
- 示例:
5. 替换操作符 s///
- 语法:
s/模式/替换字符串/修饰符
- 作用: 在字符串中查找
模式
的匹配项,并用替换字符串
替换它们。 - 修饰符: 最常用的是
g
(全局替换) 和i
(不区分大小写)。 - 替换字符串: 可以包含字面字符、变量、捕获组引用 (
$1
,$2
)。
示例:
1 | my $str = "apple,banana,apple,orange"; |
6. 转换操作符 tr///
或 y///
- 语法:
tr/查找字符集/替换字符集/修饰符
或y/查找字符集/替换字符集/修饰符
- 作用: 将字符串中
查找字符集
中的每一个字符,一对一地替换为替换字符集
中对应位置的字符。 - 修饰符:
c
(complement): 转换查找字符集
中没有的字符。d
(delete): 删除查找字符集
中没有在替换字符集
中找到对应字符的那些字符。s
(squash): 将连续的重复字符压缩成一个。
示例:
1 | my $word = "Hello World"; |
7. 高级特性:零宽度断言 (Lookarounds)
零宽度断言匹配的是一个位置,而不是字符,它们不消耗字符串中的字符。
(?=pattern)
(Positive Lookahead): 正向先行断言。匹配后面跟着pattern
的位置。- 示例:
/foo(?=bar)/
匹配 “foobar” 中的 “foo”,但只匹配 “foo” 而不包含 “bar”。
- 示例:
(?!pattern)
(Negative Lookahead): 负向先行断言。匹配后面没有跟着pattern
的位置。- 示例:
/foo(?!bar)/
匹配 “foo” 而不是 “foobar” 中的 “foo”。
- 示例:
(?<=pattern)
(Positive Lookbehind): 正向后行断言。匹配前面是pattern
的位置。- 示例:
/(?<=foo)bar/
匹配 “foobar” 中的 “bar”,但只匹配 “bar” 而不包含 “foo”。
- 示例:
(?<!pattern)
(Negative Lookbehind): 负向后行断言。匹配前面不是pattern
的位置。- 示例:
/(?<!foo)bar/
匹配 “abcbar” 中的 “bar”,但不匹配 “foobar” 中的 “bar”。
- 示例:
示例:
1 | my $text = "apples and oranges. I like apples."; |
8. 贪婪与非贪婪匹配 (Greedy vs. Non-Greedy)
- 贪婪 (Greedy): 默认行为。量词(
*
,+
,?
,{n,}
,{n,m}
)会尽可能多地匹配字符。- 示例:
/a.*b/
匹配"axbyb"
中的整个"axbyb"
。
- 示例:
- 非贪婪 (Non-Greedy / Reluctant): 在量词后加上
?
,使其尽可能少地匹配字符。- 示例:
/a.*?b/
匹配"axbyb"
中的"axb"
。
- 示例:
示例:
1 | my $html = "<b>hello</b> <i>world</i>"; |
1 |
|
9. 回溯引用 \1
10. 条件判断 (?(1)...|...)
11. 递归匹配 (?R)
12. Unicode 匹配 \p{Han}
13. 可读模式 (?x)
14. 性能与最佳实践
use re 'debug'
: 用于调试正则表达式的匹配过程,非常有用。use strict; use warnings;
: 始终使用,有助于编写健壮的代码。- 锚点优化: 如果知道模式在字符串的开头或结尾,使用
^
和$
可以显著提高效率。 - 避免不必要的捕获: 如果不需要捕获组的内容,使用非捕获组
(?:...)
可以减少开销。 - 选择合适的量词:
+
通常比*
更高效,因为它避免了零长度匹配的情况。 - 预编译: 对于在循环中多次使用的正则表达式,如果模式是固定的,使用
qr//
(quote regex) 操作符预编译可以提高效率,或者使用o
修饰符。 - 可读性: 使用
x
修饰符添加注释和空白,使复杂模式更易读。 - 逐步构建和测试: 对于复杂的正则表达式,从小部分开始构建,并逐步测试每个部分。
❗ 注意事项(grep/PCRE 限制)
grep -P
使用的是 PCRE,但有些版本(特别是 GNU grep)不支持可变长度后行断言(lookbehind):1
2grep -P '(?<=\w+)\d' file.txt
# 会报错:lookbehind assertion is not fixed length固定长度的可以:
1
grep -P '(?<=abc)\d' file.txt
如果需要完全支持复杂 lookbehind,请考虑使用 Perl 本身或
pcregrep
、Python re
(Python 3.8+)。-E
表示使用 扩展正则(ERE),它不支持\w
、\s
这类 Perl风格元字符;\w
和\s
在-E
模式下只是普通字符,会被当作w
和s
匹配;所以在grep -E
中并不会起作用,会错配或不配;1
2
3
4
5# 使用 -P(Perl正则)
grep -P '^\s*#\s*\w[\w\s]*=' a.txt
# 用 -E,需替换为 POSIX 兼容写法
grep -E '^[[:space:]]*#[[:space:]]*[[:alnum:]_][[:alnum:]_[:space:]]*=' a.txt[:space:]
等价于\s
;[:alnum:]_
等价于\w
(字母数字下划线);但这写法较复杂,不如
-P
简洁直观。
10. 实例
- 提取samba配置中的配置项,及被注释的配置项
1 | mugster@mug:~$ grep -Pv '^\s*#(?!\s*\w[\w\s]*=)' /etc/samba/smb.conf |
1 | # 会显示分节注释 |
- 提取(Nginx 日志)ipv4地址
1 | grep -Po '\b(?:\d{1,3}\.){3}\d{1,3}\b' /var/log/nginx/access.log |
- 提取
<tag>内容</tag>
内容, 典型应用:简单 XML 抽取任务
1 | grep -Po '(?<=<tag>).*?(?=</tag>)' config.xml |
- 查看 Git diff 中被删除的函数(Python)
1 | git diff | grep -P '^-.*def\s+\w+\s*\(.*?\):' |