强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Perl 完全指南 / 第 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
-Cinode 变更时间-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文件锁定

练习

  1. 编写脚本统计文件的行数、单词数、字符数(类似 wc
  2. 使用 File::Find 查找指定目录下的所有 .pl 文件
  3. 编写一个简单的文件备份脚本(复制+重命名)
  4. 使用 Path::Tiny 实现目录同步(比较修改时间后复制更新的文件)
  5. 实现一个简单的文件锁保护的计数器

扩展阅读