正则表达式是描述文本形状的迷你语言:普通字符匹配自身,元字符描述"什么类型、重复几次、锚在哪"。Perl 的
//就是匹配操作符,无需任何导入。
my $line = '10.0.0.5 - - [12/Aug/2026:03:14:51] "GET /api/order HTTP/1.1" 200 12ms'; if ($line =~ /GET/) { print "这是一条 GET 请求\n"; }
$line =~ /模式/ 返回真假,守卫写法 next unless $line =~ /ERROR/; 是日志脚本的日常。
| 符号 | 含义 | 日志示例 |
|---|---|---|
\d \s \w |
数字、空白、单词字符 | 状态码 \d{3} |
. |
任意字符(换行除外) | 时间戳粗匹配 |
+ * ? |
1+、0+、0或1 次 | \d+ 一串数字 |
{m,n} |
m 到 n 次 | IP 每段 \d{1,3} |
^ $ |
行首、行尾锚 | ^ERROR 行首才认 |
[...] |
字符集合任选一个 | [45]\d\d 4xx/5xx |
(...) |
分组与捕获 | 提取字段的主力 |
| `\ | ` | 或 |
实战组合:
$line =~ /^(\d{1,3}(?:\.\d{1,3}){3})/; # 行首 IP $line =~ m{"(GET|POST) (\S+)}; # 请求方法与路径 $line =~ /\[(\d{2})\/(\w{3})\/(\d{4})/; # 日期三段
两点值得注意:URL 里常有斜杠,用 m{...} 换定界符省得转义;(?:...) 是只分组不捕获,避免占用捕获组编号。
if ($line =~ /" (\d{3}) /) { $status = $1; }
未命中时 $1 保留上一次的旧值——这是新手统计出离谱数字的经典来源。要么把匹配与取值写在同一个 if 里,要么用 4.2 节的列表上下文一次取全。
💡 关键直觉:正则的阅读单位不是字符而是"字段"。写之前先用自然语言描述字段边界("从引号后到空格"),再翻译成符号,命中率会高得多。
理解锚点最好的方式是看一次翻车。需求:统计 ERROR 日志里某服务的出现次数,有人写了 /error/(没加锚、没加大小写意识),结果把 DEBUG 行里的 "errorHandler initialized" 也数了进去,报出的错误量是真实的四倍。修复后:
next unless /^ERROR\b/; # 行首锚 + 单词边界 $svc_count{$2}++ if /\] (\w+):/; # 第二个字段才是服务名
三处细节各挡住一类误报:^ 挡住行中部巧合;\b 挡住 ERRORLOG 这类前缀撞车;显式定位"冒号前的单词"挡住同名子串。给初学者的推论是:每写一个模式,先问"它会在哪误命中"——把日志里最吵的 DEBUG 行拿来试一遍,误报当场现形。
+ 和 * 默认贪婪(尽量多吃),加 ? 变非贪婪(尽量少吃)。提取 HTML 式尖括号内容是最直观的对照:
my $s = '<b>粗体</b>普通<b>再来</b>'; my @greedy = $s =~ /<b>(.+)<\/b>/g; # 贪婪:"粗体</b>普通<b>再来" 一整条 my @lazy = $s =~ /<b>(.+?)<\/b>/g; # 非贪婪:"粗体" "再来" 各一条
日志字段的引号内容同理:/"(.+?)"/ 拿到的是第一对引号里的内容,贪婪版会把两对引号之间连皮带骨抓走。经验法则只有一句:分隔符之间的内容,量词一律写非贪婪;要"吃到行尾"才用贪婪(如 ^(.*):\s+(\d+)$ 里第一个 .*)。
除了 m{} 换定界符,五个常用修饰符值得一次记全:
$blk =~ /^ERROR/m; # m:多行模式,^ $ 逐行生效 $text =~ /name=".+?"/s; # s:让 . 也匹配换行,跨行提取必备 $cmd =~ /usage/i; # i:忽略大小写 $all =~ /(\d+ms)/g; # g:列表上下文下取出全部命中而非首个 /x # x:模式里允许空白与注释,长模式分段写
x 修饰符是长模式的救星,把一坨符号排版成可读的段落:
$line =~ m{ ^(\d{1,3}(?:\.\d{1,3}){3}) # 1: 客户端 IP \s+\S+\s+\S+ # 身份两列(跳过) \[([^\]]+)\] # 2: 时间戳整体 "\w+\s+(\S+?)\s+[^"]*" # 3: 请求路径 }x;
注释即文档,半年后回看不用重新解码。工坊规矩:超过 60 个字符的模式,必须换 x 风格重写。
最后给一张"字段到模式"的对照速查,覆盖日志解析九成场景,写新模式前先来这里找形状:
IP 地址 ^(\d{1,3}(?:\.\d{1,3}){3}) 日期时间 \[(\d{2})/(\w{3})/(\d{4}):(\d{2}:\d{2}:\d{2}) 请求行 "(\w+) (\S+) HTTP/[\d.]+" 状态码 (\d{3}) 字节数/耗时 (\d+)(ms|s)? 带引号内容 "([^"]*)" # 排除型,防贪婪越界 键值对 (\w+)=("[^"]*"|\S+) UUID/trace ([0-9a-f-]{36})
右列每个形状都值得抄进自己的 snippets 文件。用时贴过来改定界符即可,比每次现场推导又快又稳——熟练工和新手的差距,往往就是这张表有没有。搭配记忆的还有一个习惯:每收编一个新形状,用一行真实日志当注释附在旁边,形状与样本互相印证,下次改动立刻有对照物。
=~ // 是匹配操作符,守卫式 next if /.../ 最常用\d \s \w 加量词能覆盖日志字段的九成描述需求^ $ 锚住行首行尾,防止匹配到行中部的巧合片段(?:...) 只分组不捕获,不污染捕获组编号$1 在未命中时保留旧值,取值必须与判定同处