Perl 以其强大的正则表达式引擎而闻名,它为文本处理提供了无与伦比的灵活性和力量。

Perl 正则表达式

正则表达式 (Regular Expression,简称 Regex 或 Regexp) 是一种用来描述、匹配字符串模式的强大工具。在 Perl 中,正则表达式是语言的核心部分,被广泛用于字符串搜索、替换、分割和数据验证。

1. 基本概念与操作符

Perl 主要通过以下三个操作符来使用正则表达式:

  • 匹配操作符 m// (或 /): 用于查找字符串中是否包含某个模式。
  • 替换操作符 s///: 用于查找并替换字符串中的模式。
  • 转换操作符 tr/// (或 y///): 用于将字符串中的字符一对一地转换。

默认情况下,这些操作符作用于特殊变量 $_

示例:基本匹配

1
2
3
4
my $text = "Hello, world!";
if ($text =~ /world/) {
print "找到了 'world'。\n";
}

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
2
3
4
5
6
7
8
9
my $email = "user@example.com";
if ($email =~ /(\w+)@(\w+\.\w+)/) {
print "用户名: $1\n"; # $1 捕获 "user"
print "域名: $2\n"; # $2 捕获 "example.com"
}

my $url = "http://www.google.com";
$url =~ s#^(https?://)(www\.)?([^/]+)/?#$1$3#; # 将 www. 去掉
print "修改后的 URL: $url\n"; # 输出 "http://google.com"

注意这里 s/// 使用了 # 作为分隔符,因为模式中包含 /,这样可以避免转义 \/

您好!您提到的“命令分组”在正则表达式中通常指的是使用圆括号 () 来将正则表达式的一部分组合成一个逻辑单元。这在 Perl 兼容正则表达式(PCRE)中是一个非常核心且强大的特性,在 grep -P 中也同样适用。

分组的主要作用有:

  1. 应用量词到多个字符:将一组字符视为一个整体,然后对其应用量词。
  2. 定义选择(或)的范围:控制 | 操作符的作用范围。
  3. 捕获匹配的子字符串:将匹配到的内容存储起来,以便后续引用。
  4. 非捕获分组:只用于分组逻辑,不捕获内容,可以提高效率。

下面我们通过 grep -P 的实例来详细讲解。

1. 基本分组:应用量词或定义选择范围

这是 () 最基本的用法,将一组字符作为一个整体来处理。

示例 1:应用量词到多个字符

查找文件中所有连续重复的 “ab” 字符串,例如 “abab” 或 “ababab”。

1
grep -P '(ab)+' my_file.txt

如果没有分组ab+ 只会匹配 “abbbb…”,因为 + 只作用于它前面的单个字符 b

示例 2:定义选择(或)的范围

场景:查找文件中所有包含 “you” “me” 或 “her” 的行,但它们必须是作为 “love” 后面的一部分。

1
2
3
4
5
mugster@mug:~$ echo 'loveyou love you love me love her likeher' | grep -Po 'love\s(you|me|her)'
love you
love me
love 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: 只输出匹配到的部分。
    • \(\): 匹配字面括号。
    • (.*?): 捕获组,使用非贪婪匹配 .*? 匹配括号内的所有内容。
  • 输出:只会输出 (内容) 这样的完整匹配。如果想只输出括号内的内容,需要结合更高级的技巧(如 \Kperl -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
2
3
4
5
6
7
# 假设字符串 "abbbbc"
# 贪婪匹配:
grep -P 'a.*b$' my_string.txt # 匹配 "abbb" (回溯)

# 原子组:
grep -P 'a(?>.*)b$' my_string.txt # 不会匹配 "abbbbc"
# 解释:(?>.*) 匹配了 "bbbbc",然后 b$ 无法匹配,整个模式失败,不会回溯。
  • **a(?>.\*)b$**:(?>.*) 会尽可能多地匹配,一旦匹配了 bbbc,它就“锁定”了,不会回溯让出 b 给后面的 b$。所以如果字符串是 abbbbc,它会匹配失败。
  • 应用:主要用于性能优化,避免在复杂模式中出现“灾难性回溯”。

命名分组(Named Capturing Groups)是正则表达式中的一个高级特性,它能让你的模式更具可读性,并且更易于维护。

简单来说,命名分组就是给一个捕获组起一个有意义的名字,而不是仅仅依赖于它在模式中的顺序号($1, $2 等)。


5. 命名分组

在 Perl 兼容正则表达式(PCRE)中,有两种主要的命名分组语法:

  1. **(?<name>...)**:最常用和推荐的语法。
  2. **(?'name'...)**:另一种可选语法。

命名分组提供了比传统捕获组 (()) 更大的优势:

  1. 增强可读性:模式本身就具有了文档功能。例如,(^\d{4}) 只告诉我们捕获了一个四位数字,而 (?<year>\d{4}) 则明确地告诉我们捕获的是年份。
  2. 提高可维护性:如果你在模式中添加或删除了捕获组,传统捕获组的编号(\1, \2)会随之改变,这可能导致你的代码出错。而命名分组则不会受影响,你可以随时调整其他部分。
  3. 更清晰的反向引用:在模式内部,你可以使用**\k<name>**语法来引用之前捕获的内容,这比使用编号(\1)更清晰。

假设我们想从一个日期字符串 “2025-08-06” 中提取年、月、日。

1. 使用传统分组

1
2
3
4
5
6
# 模式:
my $pattern = '(\d{4})-(\d{2})-(\d{2})';
# 匹配结果:
# $1 = '2025'
# $2 = '08'
# $3 = '06'
  • 反向引用:如果要引用年,你需要用 \1;如果想引用月,用 \2。如果之后你在前面添加了一个新的捕获组,\2 就会变成 \3,你需要修改所有相关的反向引用。

2. 使用命名分组

1
2
3
4
5
6
7
# 模式:
my $pattern = '(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})';
# 匹配结果:
# $1 = '2025' # 仍然有编号
# $2 = '08'
# $3 = '06'
# %- = ('year' => '2025', 'month' => '08', 'day' => '06')
  • 可读性:模式 (?<year>\d{4}) 一目了然地告诉了我们它在捕获什么。
  • 反向引用:在模式内部,你可以使用 \k<year>\k<month>\k<day> 来引用捕获的内容。

grep -P 中的应用

grep -P 完全支持命名分组的语法。虽然它不像在 Perl 脚本中那样可以直接通过名称来访问捕获的内容,但你可以在反向引用中使用它。

示例:使用命名分组查找重复的单词

1
2
3
4
5
# 使用传统分组
grep -P '\b(\w+)\s+\1\b' my_file.txt

# 使用命名分组
grep -P '\b(?<word>\w+)\s+\k<word>\b' my_file.txt
  • 解释
    • (?<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
2
3
4
5
6
7
my $str = "apple,banana,apple,orange";
$str =~ s/apple/fruit/g; # 全局替换
print "$str\n"; # 输出 "fruit,banana,fruit,orange"

my $date = "2025-08-03";
$date =~ s/(\d{4})-(\d{2})-(\d{2})/$3\/$2\/$1/; # 调整日期格式
print "$date\n"; # 输出 "03/08/2025"

6. 转换操作符 tr///y///

  • 语法: tr/查找字符集/替换字符集/修饰符y/查找字符集/替换字符集/修饰符
  • 作用: 将字符串中 查找字符集 中的每一个字符,一对一地替换为 替换字符集 中对应位置的字符。
  • 修饰符:
    • c (complement): 转换 查找字符集 中没有的字符。
    • d (delete): 删除 查找字符集 中没有在 替换字符集 中找到对应字符的那些字符。
    • s (squash): 将连续的重复字符压缩成一个。

示例:

1
2
3
4
5
6
7
8
9
10
11
my $word = "Hello World";
$word =~ tr/aeiou/AEIOU/; # 将小写元音转换为大写
print "$word\n"; # 输出 "HEllO WORLd"

my $data = " a b c ";
$data =~ tr/ //s; # 压缩连续空格为一个
print "$data\n"; # 输出 " a b c "

my $num = "123-abc-456";
$num =~ tr/0-9//d; # 删除所有数字
print "$num\n"; # 输出 "-abc-"

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
2
3
4
5
my $text = "apples and oranges. I like apples.";
# 匹配所有后面跟着空格或句号的 'apples'
if ($text =~ /apples(?=[\s\.])/) {
print "匹配到后面是空格或句号的 'apples'\n";
}

8. 贪婪与非贪婪匹配 (Greedy vs. Non-Greedy)

  • 贪婪 (Greedy): 默认行为。量词(*, +, ?, {n,}, {n,m})会尽可能多地匹配字符。
    • 示例:/a.*b/ 匹配 "axbyb" 中的整个 "axbyb"
  • 非贪婪 (Non-Greedy / Reluctant): 在量词后加上 ?,使其尽可能少地匹配字符。
    • 示例:/a.*?b/ 匹配 "axbyb" 中的 "axb"

示例:

1
2
3
4
5
6
my $html = "<b>hello</b> <i>world</i>";
# 贪婪匹配:会匹配从第一个<b>到最后一个</b>的所有内容
$html =~ /<.*>/; # 匹配 "<b>hello</b> <i>world</i>"

# 非贪婪匹配:会匹配最近的配对标签
$html =~ /<.*?>/; # 第一次匹配 "<b>hello</b>"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

mugster@mug:~$ echo "<b>hello</b><i>i'm beizi</i><u>hahaha</u>" |grep -Po '(?<=<\w>).*(?=</\w+>)'
hello</b><i>i'm beizi</i><u>hahaha
mugster@mug:~$ echo "<b>hello</b><i>i'm beizi</i><u>hahaha</u>" |grep -Po '(?<=<\w>).*?(?=</\w+>)'
hello
i'm beizi
hahaha

mugster@mug:~$ echo 1234567 | grep -Po '\d+'
1234567
mugster@mug:~$ echo 1234 | grep -Po '\d+?'
1
2
3
4

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
    2
    grep -P '(?<=\w+)\d' file.txt
    # 会报错:lookbehind assertion is not fixed length

    固定长度的可以:

    1
    grep -P '(?<=abc)\d' file.txt

    如果需要完全支持复杂 lookbehind,请考虑使用 Perl 本身或 pcregrepPython re(Python 3.8+)。

  • -E 表示使用 扩展正则(ERE),它不支持 \w\s 这类 Perl风格元字符

    \w\s-E 模式下只是普通字符,会被当作 ws 匹配;所以在 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
2
3
4
5
6
7
8
9
10
11
mugster@mug:~$ grep -Pv '^\s*#(?!\s*\w[\w\s]*=)' /etc/samba/smb.conf
grep -Pv '^\h*#(?!\s*\w[^=]*=).*$' /etc/samba/smb.conf
# 以下达不到效果,可能跟零宽断言特性有关
grep -Pv '^\h*#\s*(?!\w[^=]*=).*$' /etc/samba/smb.conf
# 更好的方式,以防有 "key="的内容
grep -Pv '^\h*#(?!\s*\w[^=]*=\s*\w+)' /etc/samba/smb.conf
# 使用肯定匹配
mugster@mug:~$ grep -P '^\h*(#\h*)?\w[^=]*=.*$' /etc/samba/smb.conf

grep -P '^\s*[^#]|^\s*#\s*[^=]+=' /etc/samba/smb.conf
grep -P '^\s*[^#]|^\s*#\s*\w[\w\s.-]*\s*=' /etc/samba/smb.conf
1
2
3
4
5
6
7
8
9
10
11
12
# 会显示分节注释
mugster@mug:~$ grep -P '^\s*#\s*\w[\w\s]*=|\w[\w\s]*=|\s*\[.*]' /etc/samba/smb.conf
#======================= Global Settings =======================
[global]
workgroup = WORKGROUP
server string = %h server (Samba, Ubuntu)
; interfaces = 127.0.0.0/8 eth0
; bind interfaces only = yes
log file = /var/log/samba/log.%m
max log size = 1000
logging = file
# ...
  • 提取(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*\(.*?\):'