把第 4、5 章的解析与统计内核装进 LogUtil 模块:package 声明身份,Exporter 导出接口,1 表示返回真。三件套齐了,脚本侧只剩一行 use。
package LogUtil; # 文件名必须是 LogUtil.pm use strict; use warnings; use Exporter 'import'; # 5.36 前后都兼容的导出写法 our @EXPORT_OK = qw(parse_line aggregate report_line); # EXPORT_OK:调用方点名才导入 my $ACC_RE = qr{ ^(\S+)\ \[(\S+)\] "\w+\ (\S+)"\ (\d{3})\ (\d+)ms$ }x; sub parse_line { my ($line) = @_; chomp $line; return unless $line =~ $ACC_RE; return { ip => $1, time => $2, api => $3, status => $4, ms => $5 }; } sub aggregate { my ($stat, $rec) = @_; $stat->{ $rec->{api} }[0]++; $stat->{ $rec->{api} }[1] += $rec->{ms}; return $stat; } sub report_line { my ($api, $n, $sum) = @_; return sprintf "%-40s %8d %10d\n", $api, $n, $sum / $n; } 1; # 模块最后一行必须返回真值
@EXPORT_OK 是白名单:调用方写 use LogUtil qw(parse_line) 才拿到它。另一个数组 @EXPORT 是自动导入——听起来方便,实际是名字污染源,新模块一律用 EXPORT_OK。
#!/usr/bin/perl use strict; use warnings; use lib '.'; # 把当前目录加进模块搜索路径 use LogUtil qw(parse_line aggregate); my %stat; while (my $line = <>) { my $rec = parse_line($line); aggregate(\%stat, $rec) if $rec; }
use lib '.' 只在实验阶段用;正式环境把模块放进标准搜索路径,或者用第 6.3 节的方式发布。

⚠️ 常见坑:模块文件结尾忘了
1;,加载时 Perl 报 "did not return a true value",新手常以为是语法错误查半天。任何非零字面量都行,但惯例就是1;。
模块一旦离开脚本目录,就该自带说明书与体检。Pod 文档写在模块尾部,perldoc 直接可读:
__END__ =head1 NAME LogUtil - 访问日志解析与统计 =head1 SYNOPSIS use LogUtil qw(parse_line aggregate); my $rec = parse_line($line) or next; aggregate(\%stat, $rec); =head1 DESCRIPTION parse_line 接受一行日志,返回哈希引用;无法解析返回 undef。 =cut
接口文档写 SYNOPSIS 的最大好处是逼自己用一遍"用户视角"——写着别扭,多半说明接口设计有问题,比 code review 更早暴露坏味道。测试则放在相邻的 t/ 目录,prove 一条命令跑全套(写法在第 7.3 节展开)。至此模块的三件配套齐了:代码、文档、测试,缺任何一件,它都只是"碰巧能跑的文件"。
LogUtil 现在装了解析与统计两块,继续长下去会装进报表格式化、告警判断,最后变成谁都依赖、谁都不敢动的巨石。拆分的信号有三个:一是 use 它的脚本从来只用一半函数,说明里面住着两个主题;二是修改报表格式也要重测解析逻辑,说明改动传播过宽;三是名字开始出现 And、UtilPlus 这类拼接词。出现任意一条,沿"解析(纯函数)/ 统计(有状态聚合)/ 呈现(输出格式)"的边界拆开,每个模块保持"一句话能说清自己干什么"。判断拆不拆的最终标准始终是改动局部性:让一次需求变更只碰一个模块。
模块被第二个项目使用的那一刻起,接口变更就成了跨项目事件,版本号是最轻量的协调机制。约定俗成的语义是:改接口必升次版本号(1.1 到 1.2),破坏性重设计升主版本(1.x 到 2.0),纯内部优化不动号。模块内声明与调用侧检查:
package LogUtil; our $VERSION = '1.02'; # 调用侧锁最低版本,过低直接报错而非运行到一半行为异常 use LogUtil 1.02 qw(parse_line aggregate);
配套的纪律是变更日志:模块目录下留一个 CHANGES 文件,每个版本号一行、写清改了什么接口、调用方需要动哪里。成本一分钟,换来的是接手者不用 diff 全部代码猜"上次升级动了什么"。这套小流程与 6.3 节的依赖锁定互为镜像——你依赖别人的模块时希望对方有版本纪律,自己的模块被依赖时就把同样的纪律还回去。最日常的落点其实很轻:模块顶部 our $VERSION 一行、CHANGES 一行,共两处改动;坚持下来,"这个脚本要用哪个版本的 LogUtil"就永远是一个有答案的问题,而不是一场考古。版本号的另一个副作用是提醒:对外接口一旦被人依赖,"随手改"就不再成立——每次想改签名时先想版本要不要跳、别人的代码要不要跟着动,这三秒的迟疑正是模块作者责任感的具体形态。
1;use lib 只用于本地实验,正式部署走标准路径