第 12 章:文件与目录操作
第 12 章:文件与目录操作
“Unix 哲学:一切皆文件”
文件 I/O 是 Perl 最核心的能力之一。本章涵盖文件读写、目录操作、文件测试以及现代的 Path::Tiny 模块。
12.1 文件句柄基础
打开与关闭
use strict;
use warnings;
# 三参数 open(推荐)
open my $fh, '<', 'data.txt' or die "无法打开: $!\n";
while (my $line = <$fh>) {
chomp $line;
print "> $line\n";
}
close $fh;
打开模式
| 模式 | 含义 | 示例 |
|---|
< | 只读 | open my $fh, '<', 'file' |
> | 写入(覆盖) | open my $fh, '>', 'file' |
>> | 追加 | open my $fh, '>>', 'file' |
+< | 读写 | open my $fh, '+<', 'file' |
+> | 读写(创建/截断) | open my $fh, '+>', 'file' |
| ` | -` | 管道输出 |
| `- | ` | 管道输入 |
# 写入文件
open my $out, '>', 'output.txt' or die "写入失败: $!\n";
print $out "第一行\n";
print $out "第二行\n";
close $out;
# 追加
open my $log, '>>', 'app.log' or die "追加失败: $!\n";
print $log "[" . localtime . "] 新日志\n";
close $log;
# 管道读取命令输出
open my $pipe, '-|', 'df -h' or die "管道失败: $!\n";
while (<$pipe>) {
print if /dev/;
}
close $pipe;
12.2 读取文件
逐行读取
# 逐行读取(推荐)
open my $fh, '<', 'data.txt' or die $!;
while (my $line = <$fh>) {
chomp $line;
# 处理 $line
}
close $fh;
# 使用 $_ (简洁写法)
open my $fh2, '<', 'data.txt' or die $!;
while (<$fh2>) {
chomp;
print "$_\n";
}
close $fh2;
一次读取整个文件(Slurp 模式)
# 方式 1:修改 $/
{
local $/;
open my $fh, '<', 'data.txt' or die $!;
my $content = <$fh>;
close $fh;
}
# 方式 2:Path::Tiny(推荐)
use Path::Tiny;
my $content = path('data.txt')->slurp_utf8;
# 方式 3:File::Slurper(推荐)
use File::Slurper qw(read_text);
my $content = read_text('data.txt');
按记录读取
# 默认按行读取($/ = "\n")
# 按段落读取
{
local $/ = ""; # 空行分隔段落
open my $fh, '<', 'data.txt' or die $!;
while (my $para = <$fh>) {
print "段落: $para---\n";
}
close $fh;
}
# 按固定字节数读取
{
local $/ = \1024; # 每次读 1024 字节
open my $fh, '<:raw', 'binary.dat' or die $!;
while (my $chunk = <$fh>) {
# 处理 $chunk
}
close $fh;
}
12.3 写入文件
# 格式化写入
open my $fh, '>', 'report.txt' or die $!;
printf $fh "%-20s %10s %10s\n", "名称", "数量", "金额";
printf $fh "%-20s %10d %10.2f\n", "苹果", 100, 350.50;
printf $fh "%-20s %10d %10.2f\n", "香蕉", 200, 180.00;
close $fh;
# print 列表
open my $fh2, '>', 'data.csv' or die $!;
print $fh2 join(",", qw(Name Age City)), "\n";
print $fh2 join(",", "张三", 30, "北京"), "\n";
close $fh2;
12.4 文件测试运算符
| 运算符 | 含义 | 示例 |
|---|
-e | 文件存在 | -e $file |
-f | 普通文件 | -f $file |
-d | 目录 | -d $dir |
-l | 符号链接 | -l $file |
-r | 可读 | -r $file |
-w | 可写 | -w $file |
-x | 可执行 | -x $file |
-s | 文件大小(非空) | -s $file |
-z | 文件为空 | -z $file |
-T | 文本文件 | -T $file |
-B | 二进制文件 | -B $file |
-M | 修改时间(天) | -M $file |
-A | 访问时间(天) | -A $file |
-C | inode 变更时间 | -C $file |
my $file = "data.txt";
if (-e $file) {
print "文件存在\n";
print "是普通文件\n" if -f $file;
print "是目录\n" if -d $file;
print "可读\n" if -r $file;
print "可写\n" if -w $file;
printf "大小: %d 字节\n", -s $file;
printf "修改于 %.1f 天前\n", -M $file;
} else {
print "文件不存在\n";
}
# 文件测试可以用于文件句柄
open my $fh, '<', $file or die $!;
print "文件句柄可读\n" if -r $fh;
12.5 文件操作
use File::Copy qw(copy move);
use File::Path qw(make_path remove_tree);
# 复制
copy('source.txt', 'dest.txt') or die "复制失败: $!";
# 移动/重命名
move('old.txt', 'new.txt') or die "移动失败: $!";
# 删除
unlink 'temp.txt' or warn "删除失败: $!";
# 创建目录
make_path('/path/to/new/dir', { mode => 0755 });
# 删除目录
remove_tree('/path/to/dir', { verbose => 1 });
# 获取文件信息
my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
$atime, $mtime, $ctime) = stat($file);
# 获取绝对路径
use Cwd qw(abs_path);
my $abs = abs_path('data.txt');
12.6 目录操作
# 打开目录
opendir my $dh, '.' or die "无法打开目录: $!\n";
# 读取目录内容
while (my $entry = readdir $dh) {
next if $entry =~ /^\./; # 跳过隐藏文件
print "$entry\n";
}
closedir $dh;
# glob 模式匹配
my @perl_files = glob("*.pl");
my @all_pms = glob("**/*.pm");
# 使用 File::Find 遍历目录树
use File::Find;
find(sub {
return unless -f $_; # 只处理文件
return unless /\.log$/; # 只处理 .log 文件
print "$File::Find::name\n"; # 完整路径
}, '/var/log');
12.7 二进制文件处理
# 读取二进制文件
open my $fh, '<:raw', 'image.jpg' or die $!;
binmode $fh;
my $data;
read($fh, $data, -s $fh);
close $fh;
# 写入二进制文件
open my $out, '>:raw', 'copy.jpg' or die $!;
binmode $out;
print $out $data;
close $out;
# 按字节处理
open my $fh2, '<:raw', 'file.bin' or die $!;
while (read($fh2, my $byte, 1)) {
printf "%02X ", ord($byte);
}
close $fh2;
12.8 Path::Tiny — 现代文件操作
use Path::Tiny;
# 读取
my $content = path('file.txt')->slurp_utf8;
# 写入
path('output.txt')->spew_utf8("Hello, World!\n");
# 追加
path('log.txt')->append_utf8("新日志\n");
# 目录遍行
for my $file (path('.')->children) {
print $file->basename . ($file->is_dir ? "/" : "") . "\n";
}
# 递归遍行
path('.')->visit(sub {
my ($path) = @_;
print "$path\n" if $path->is_file;
}, { recurse => 1 });
# 创建临时文件
my $temp = Path::Tiny->tempfile;
$temp->spew("临时数据");
# 路径操作
my $p = path('/home/user/file.txt');
print $p->parent, "\n"; # /home/user
print $p->basename, "\n"; # file.txt
print $p->extension, "\n"; # txt
print $p->sibling('data.csv'), "\n"; # /home/user/data.csv
12.9 锁定文件
use Fcntl qw(:flock);
# 排他锁(写锁)
open my $fh, '>>', 'shared.dat' or die $!;
flock($fh, LOCK_EX) or die "无法获取锁: $!";
print $fh "新数据\n";
close $fh; # 自动释放锁
# 共享锁(读锁)
open my $fh2, '<', 'shared.dat' or die $!;
flock($fh2, LOCK_SH) or die "无法获取锁: $!";
while (<$fh2>) {
print;
}
close $fh2;
12.10 业务场景:日志文件轮转
#!/usr/bin/env perl
use strict;
use warnings;
use Path::Tiny;
use File::Copy qw(move);
sub rotate_log {
my ($log_file, $max_backups) = @_;
$max_backups //= 5;
return unless -f $log_file && -s $log_file > 0;
# 轮转已有备份
for my $i (reverse 1 .. $max_backups - 1) {
my $old = "$log_file.$i";
my $new = "$log_file." . ($i + 1);
move($old, $new) if -f $old;
}
# 当前日志变为 .1
move($log_file, "$log_file.1");
# 删除超出的备份
unlink "$log_file." . ($max_backups + 1)
if -f "$log_file." . ($max_backups + 1);
}
sub write_log {
my ($log_file, $message) = @_;
open my $fh, '>>:encoding(UTF-8)', $log_file or die $!;
printf $fh "[%s] %s\n", scalar localtime, $message;
close $fh;
}
# 使用
my $LOG = "app.log";
rotate_log($LOG);
write_log($LOG, "应用启动");
write_log($LOG, "处理请求");
本章小结
| 要点 | 内容 |
|---|
open my $fh | 三参数 open 是推荐写法 |
$! | 错误信息变量 |
| 文件测试 | -e -f -d -r -w -s -M |
File::Find | 递归遍历目录 |
Path::Tiny | 现代文件操作首选 |
Flock | 文件锁定 |
练习
- 编写脚本统计文件的行数、单词数、字符数(类似
wc) - 使用
File::Find 查找指定目录下的所有 .pl 文件 - 编写一个简单的文件备份脚本(复制+重命名)
- 使用 Path::Tiny 实现目录同步(比较修改时间后复制更新的文件)
- 实现一个简单的文件锁保护的计数器
扩展阅读