第 8 章:正则表达式
第 8 章:正则表达式
“有些人遇到问题时会想:我知道了,用正则表达式。现在他们有了两个问题。” — Jamie Zawinski
尽管如此,正则表达式仍是 Perl 最强大的特性之一。Perl 的正则引擎是所有语言中最先进的之一。
8.1 基础匹配
m// 匹配运算符
use strict;
use warnings;
my $string = "Hello, Perl 5.40!";
# 基本匹配
if ($string =~ m/Perl/) {
print "找到 Perl!\n";
}
# =~ 绑定到变量,!~ 不匹配
if ($string !~ /Python/) {
print "没有找到 Python\n";
}
# m// 可以省略 m,使用其他分隔符
if ($string =~ m{Perl}) { }
if ($string =~ m(Perl)) { }
if ($string =~ m|Perl|) { }
| 元字符 | 含义 | 示例 | 匹配 |
|---|
. | 任意字符(除换行) | a.c | “abc”, “a1c” |
^ | 行首 | ^Hello | “Hello world” |
$ | 行尾 | world$ | “Hello world” |
\b | 单词边界 | \bcat\b | “the cat” 中的 “cat” |
\B | 非单词边界 | \Bcat\B | “concatenate” 中的 “cat” |
\d | 数字 [0-9] | \d+ | “123” |
\D | 非数字 | \D+ | “abc” |
\w | 单词字符 [a-zA-Z0-9_] | \w+ | “hello_123” |
\W | 非单词字符 | \W+ | “!@#” |
\s | 空白字符 | \s+ | " \t\n" |
\S | 非空白字符 | \S+ | “hello” |
\t | 制表符 | \t | Tab |
\n | 换行符 | \n | 换行 |
量词(Quantifiers)
| 量词 | 含义 | 示例 | 匹配 |
|---|
* | 0 次或多次 | ab*c | “ac”, “abc”, “abbc” |
+ | 1 次或多次 | ab+c | “abc”, “abbc” (不匹配 “ac”) |
? | 0 次或 1 次 | colou?r | “color”, “colour” |
{n} | 恰好 n 次 | a{3} | “aaa” |
{n,} | 至少 n 次 | a{2,} | “aa”, “aaa”, “aaaa” |
{n,m} | n 到 m 次 | a{2,4} | “aa”, “aaa”, “aaaa” |
*? | 非贪婪 | a.*?b | 尽可能少匹配 |
+? | 非贪婪 | a+? | 尽可能少匹配 |
# 贪婪 vs 非贪婪
my $html = '<b>bold</b> and <i>italic</i>';
# 贪婪匹配(默认)
if ($html =~ m/<.*>/) {
print "贪婪: $&\n"; # <b>bold</b> and <i>italic</i>
}
# 非贪婪匹配
if ($html =~ m/<.*?>/) {
print "非贪婪: $&\n"; # <b>
}
8.2 字符类
# 自定义字符类
/[aeiou]/ # 元音字母
/[0-9]/ # 数字(等价于 \d)
/[a-zA-Z]/ # 字母
/[^aeiou]/ # 非元音(^ 在字符类中表示取反)
/[\w.]+@[\w.]+/ # 简单邮箱模式
# 预定义字符类
/\d{3}-\d{4}/ # 电话号码:123-4567
/\w+\s+\w+/ # 两个单词
/[[:alpha:]]+/ # POSIX 字母类
/[[:digit:]]+/ # POSIX 数字类
/[[:space:]]+/ # POSIX 空白类
8.3 捕获组
基本捕获
my $date = "2026-05-10";
if ($date =~ /(\d{4})-(\d{2})-(\d{2})/) {
my $year = $1; # 2026
my $month = $2; # 05
my $day = $3; # 10
print "年: $year, 月: $month, 日: $day\n";
}
命名捕获组(Perl 5.10+)
my $email = "[email protected]";
if ($email =~ /(?<user>[\w.]+)@(?<domain>[\w.]+)/) {
print "用户: $+{user}\n"; # user
print "域名: $+{domain}\n"; # example.com
}
# 别名捕获
if ($email =~ /(?<user>[\w.]+)@(?<domain>[\w.]+)/) {
print "用户: $+{user}\n";
}
非捕获组
# (?:...) 不捕获,只分组
my $str = "http://example.com";
if ($str =~ m{(https?)://(?:www\.)?([^/]+)}) {
print "协议: $1\n"; # http
print "域名: $2\n"; # example.com
}
捕获变量
| 变量 | 含义 |
|---|
$1, $2, ... | 第 n 个捕获组 |
$+{name} | 命名捕获组 |
$& | 整个匹配 |
$` | 匹配前的部分 |
$' | 匹配后的部分 |
@- | 匹配起始位置 |
@+ | 匹配结束位置 |
my $text = "Email: [email protected], Phone: 123-4567";
if ($text =~ /user@example\.com/) {
print "匹配: $&\n"; # [email protected]
print "之前: $`\n"; # Email:
print "之后: $'\n"; # , Phone: 123-4567
}
8.4 替换(s///)
my $text = "Hello, World!";
# 基本替换
$text =~ s/World/Perl/;
print "$text\n"; # Hello, Perl!
# 全局替换
my $str = "aaa bbb aaa ccc aaa";
$str =~ s/aaa/xxx/g;
print "$str\n"; # xxx bbb xxx ccc xxx
# 大小写不敏感替换
$str = "Hello HELLO hello";
$str =~ s/hello/Hi/gi;
print "$str\n"; # Hi Hi Hi
替换修饰符
| 修饰符 | 含义 |
|---|
g | 全局替换 |
i | 大小写不敏感 |
m | 多行模式(^ $ 匹配行首行尾) |
s | 单行模式(. 匹配换行符) |
e | 替换部分作为代码执行 |
ee | 替换部分执行两次(嵌套 eval) |
r | 返回替换结果,不修改原串(Perl 5.14+) |
# /e 修饰符 - 替换部分作为代码
my $price = "价格: 100 元";
$price =~ s/(\d+)/$1 * 1.1/e; # 涨价 10%
print "$price\n"; # 价格: 110 元
# /r 修饰符 - 非破坏性替换
my $original = "Hello World";
my $modified = $original =~ s/World/Perl/r;
print "$original\n"; # Hello World(未修改)
print "$modified\n"; # Hello Perl
8.5 匹配修饰符
| 修饰符 | 含义 |
|---|
i | 大小写不敏感 |
m | 多行模式 |
s | 单行模式(. 匹配 \n) |
x | 扩展模式(允许空格和注释) |
a | ASCII 模式 |
u | Unicode 模式 |
l | Locale 模式 |
# /x 允许注释和空格
my $phone = qr/
(\d{3}) # 区号
[-.\s]? # 分隔符(可选)
(\d{4}) # 号码
/x;
if ("123-4567" =~ $phone) {
print "区号: $1, 号码: $2\n";
}
8.6 qr// — 预编译正则
# 预编译正则表达式
my $email_re = qr/
^
([\w.]+) # 用户名
@
([\w.]+) # 域名
\.
(\w{2,}) # 顶级域名
$
/x;
# 多次使用
for my $addr ("user\@example.com", "bad@@addr", "test\@site.org") {
if ($addr =~ $email_re) {
print "有效: $1\@$2.$3\n";
} else {
print "无效: $addr\n";
}
}
8.7 正则高级特性
零宽断言(Lookaround)
# 正向前瞻 (?=pattern)
"Hello World" =~ /Hello(?= World)/; # 匹配 "Hello"(后面是 World)
# 负向前瞻 (?!pattern)
"Hello World" =~ /Hello(?! World)/; # 不匹配
# 正向后瞻 (?<=pattern)
"Hello World" =~ /(?<=Hello )World/; # 匹配 "World"(前面是 Hello )
# 负向后瞻 (?<!pattern)
"Hello World" =~ /(?<!Hello )World/; # 不匹配
# 实用示例:数字千分位格式化
my $num = "1234567890";
1 while $num =~ s/(\d)(\d{3})(?!\d)/$1,$2/;
print "$num\n"; # 1,234,567,890
回溯控制
# 占有量词 (?>...) — 不回溯
"aaaa" =~ /(?>a+)a/; # 不匹配!(a+ 消费所有 a,不回溯)
# (?|...) 分支重置
"abc-123" =~ /(?|(\d+)|(\w+))/; # $1 总是有值
8.8 split 与正则
# split 使用正则分割字符串
my $csv_line = "apple,banana,cherry";
my @fields = split /,/, $csv_line;
# 带限制
my @limited = split /,/, "a,b,c,d,e", 3; # ("a", "b", "c,d,e")
# 按空白分割
my @words = split /\s+/, " hello world ";
# 捕获括号也会返回
my @parts = split /(-)/, "2026-05-10"; # ("2026", "-", "05", "-", "10")
8.9 业务场景:日志解析器
#!/usr/bin/env perl
use strict;
use warnings;
# 解析 Nginx 访问日志
my $log_re = qr/
^(?<ip>\d+\.\d+\.\d+\.\d+) # IP 地址
\s+-\s+ # 分隔符
\S+ # 用户标识
\s+\[(?<time>[^\]]+)\] # 时间
\s+"(?<method>\w+) # 请求方法
\s+(?<path>\S+) # 请求路径
\s+\S+" # HTTP 版本
\s+(?<status>\d{3}) # 状态码
\s+(?<size>\d+) # 响应大小
/x;
my %stats;
while (my $line = <DATA>) {
if ($line =~ $log_re) {
$stats{$+{status}}++;
}
}
print "状态码统计:\n";
for my $code (sort keys %stats) {
printf " %s: %d 次\n", $code, $stats{$code};
}
__DATA__
192.168.1.1 - - [10/May/2026:10:00:00 +0800] "GET /index.html HTTP/1.1" 200 1234
192.168.1.2 - - [10/May/2026:10:00:01 +0800] "GET /missing HTTP/1.1" 404 567
192.168.1.1 - - [10/May/2026:10:00:02 +0800] "POST /api/data HTTP/1.1" 200 890
192.168.1.3 - - [10/May/2026:10:00:03 +0800] "GET /admin HTTP/1.1" 403 0
本章小结
| 要点 | 内容 |
|---|
=~ / !~ | 绑定匹配/不匹配 |
m// | 匹配运算符 |
s/// | 替换运算符 |
| 捕获组 | $1, $2 和 (?<name>...) |
| 修饰符 | i 大小写、g 全局、x 注释、e 代码 |
qr// | 预编译正则 |
| 零宽断言 | 前瞻/后瞻断言 |
练习
- 编写正则匹配中国手机号(1 开头,11 位数字)
- 编写正则提取 HTML 标签中的内容
- 使用
/e 修饰符实现"将匹配到的数字翻倍" - 编写正则解析
key=value 格式的配置文件 - 实现一个简单的 Markdown 转 HTML 转换器(支持标题、粗体、链接)
扩展阅读