【问题标题】:How can I reduce this to a single file open?如何将其减少为打开的单个文件?
【发布时间】:2017-12-03 12:35:07
【问题描述】:

在 Windows 7 中使用 Strawberry Perl 5.22.0。有没有更“perlish”的方式来编写这个 sn-p 代码?我讨厌文件打开部分的重复,但由于需要测试创建时间,我想不出一种方法让它只打开一次。

...
my $x;
my $fh;
my $sentinelfile = "Logging.yes"; #if this file exists then enable logging
my $logfile = "transfers.log";
my $log = 0; #default to NO logging

$log = 1 if -e $sentinelfile; #enable logging if sentinel file exists

if($log){

#logfile remains open after this so remember to close at end of program!
 if (-e $logfile) { #file exists
    open($fh, "<", $logfile); #open for read will NOT create if not exist
        chomp ($x = <$fh>); #grab first row
    close $fh;
    if (((scalar time - $x)/3600/24) > 30) { #when ~30 days since created
        rename($logfile, $logfile . time); #rename existing logfile
        open($fh, ">", $logfile); #open for write and truncate
        print $fh time,"\n"; #save create date
        print $fh "--------------------------------------------------\n";
    } else { #file is not older than 30 days
        open($fh, ">>", $logfile); #open for append
    }
 } else { #file not exist
    open($fh, ">", $logfile); #open new for write
    print $fh time,"\n"; #save create date
    print $fh "--------------------------------------------------\n";
 }

} #if $log
...

回顾一下:日志文件记录东西。文件的第一行包含日志文件的创建日期。第二行包含水平规则。文件的其余部分包含文本。创建文件后大约 30 天,重命名文件并开始一个新文件。在上述代码块之后,日志文件已打开并准备好记录内容。它在程序的其余部分结束时关闭。

【问题讨论】:

  • 为什么不创建一个计划任务来处理每个月的日志轮换?那么这个脚本只需要 1 个打开调用(在附加模式下)。
  • 如果您不想执行计划任务日志轮换,那么我会重写您的代码以使用 2 个单独的子程序;一个用于旋转日志,另一个用于获取日志文件的 ctime。然后你可以有一个像这样的简单语句rotatelog($logfile) if log_ctime($logfile) &gt; 30;
  • @NetMage 下一条语句就是:open( $fh, "&gt;&gt;", $logfile );。您可以向其中添加 die 语句或加载 autodie pragma。这两条语句将是 OP 的 if ($log) { 块中唯一需要的行。
  • 你能举一个需要多次打开的例子吗?由于 Windows 上的 ctime 值不会像在 *nix 系统上那样改变,我们可以检查该值而不是打开和读取文件中的时间戳(如 zdim 所示)。所以文件只需要打开/关闭一次。

标签: windows perl file append


【解决方案1】:

您的代码还有其他非修饰性问题:a) 您从未检查您对 open 的调用是否成功; b)您正在创建竞争条件。该文件可以在-e 检查失败后存在。随后的open $fh, '&gt;' ... 会破坏它; c) 你不检查你的rename 调用是否成功等等。

以下是对现有代码的部分改进:

if ($log) {
    if (open $fh, '<', $logfile) { #file exists
        chomp ($x = <$fh>);
        close $fh
            or die "Failed to close '$logfile': $!";
        if (((time - $x)/3600/24) > 30) {
            my $rotated_logfile = join '.', $logfile, time;
            rename $logfile => $rotated_logfile
                or die "Failed to rename '$logfile' to '$rotated_logfile': $!";
            open $fh, '>', $logfile
                or die "Failed to create '$logfile'";
            print $fh time, "\n", '-' x 50, "\n";
        }
        else {
            open $fh, '>>', $logfile
                or die "Cannot open '$logfile' for appending: $!";
        }
    }
    else {
        open $fh, '>', $logfile
            or die "Cannot to create '$logfile': $!";
        print $fh time, "\n", '-' x 50, "\n";
    }
}

最好将所有离散功能抽象为适当命名的函数。

例如,这是一个完全未经测试的重写:

use autouse Carp => qw( croak );

use constant SENTINEL_FILE => 'Logging.yes';
use constant ENABLE_LOG => -e SENTINEL_FILE;

use constant HEADER_SEPARATOR => '-' x 50;
use constant SECONDS_PER_DAY => 24 * 60 * 60;
use constant ROTATE_AFTER => 30 * SECONDS_PER_DAY;

my $fh;

if (ENABLE_LOG) {
    if (my $age = read_age( $logfile )) {
        if ( is_time_to_rotate( $age ) ) {
            rotate_log( $logfile );
        }
        else {
            $fh = open_log( $logfile );
        }
    }
    unless ($fh) {
        $fh = create_log( $logfile );
    }
}

sub is_time_to_rotate {
    my $age = shift;
    return $age > ROTATE_AFTER;
}

sub rotate_log {
    my $file = shift;

    my $saved_file = join '.', $file, time;

    rename $file => $saved_file
        or croak "Failed to rename '$file' to '$saved_file': $!"

    return;
}

sub create_log {
    my $file = shift;

    open my $fh, '>', $file
        or croak "Failed to create '$file': $!";

    print $fh time, "\n", HEADER_SEPARATOR, "\n"
        or croak "Failed to write header to '$file': $!";

    return $fh;
}

sub open_log {
    my $file = shift;

    open my $fh, '>>', $file
        or croak "Failed to open '$file': $!";

    return $fh;
}

sub read_age {
    my $file = shift;

    open my $fh, '<', $file
        or return;

    defined (my $creation_time = <$fh>)
        or croak "Failed to read creation time from '$file': $!";

    return time - $creation_time;
}

【讨论】:

  • 感谢您的评论。我确实说过这是代码的“sn-p”,而不是真正的程序。当您开始使用它时,我只是在寻找更好的算法。您的 cmets 当然是正确的,但与此讨论无关。
  • 文件打开“+>>”将打开一个文件进行读写/追加。
  • @NetMage 如果有人问我“我怎样才能把我的车从华盛顿开到洛杉矶”,我会建议他们把油加到油箱里然后开车。那也将是“完全不回答最初的问题”。
【解决方案2】:

如果你需要读取文件的一行,重命名它然后使用它,你必须打开它两次。

但是,您也可以不使用第一行。

在 Windows 上,根据perlport (Files and Filesystems)inode 更改时间 时间戳 (ctime)“可能真的”标记文件创建时间。这可能完全适合不会被操纵和移动的日志文件。可以通过-Cfile-test operator获得

my $days_float = -C $filename;

现在您可以针对 30 进行数值测试。然后无需将文件的创建时间打印到其第一行(但如果它对查看或其他工具有用,您也可以这样做)。

还有一个模块Win32API::File::Time,目的是

在 MSWin32 下提供对文件创建、修改和访问时间的最大访问

请务必阅读文档以了解一些注意事项。我没用过,但它似乎是为你的需要量身定做的。

评论中提出了一个很好的观点:显然操作系统在文件被重命名时保留了原始时间戳。在这种情况下,当文件太旧时,将其复制到一个新文件中(使用新名称)并删除它,而不是使用rename。然后重新打开该日志文件,使用新的时间戳。

这是一个完整的例子

archive_log($logfile) if -f $logfile and -C $logfile > 30; 

open my $fh_log, '>>', $logfile or die "Can't open $logfile: $!";

say $fh_log "Log a line";

sub archive_log {
    my ($file) = @_;

    require POSIX; POSIX->import('strftime');
    my $ts = strftime("%Y%m%d_%H:%M:%S", localtime);  # 20170629_12:44:10

    require File::Copy; File::Copy->import('copy');
    my $archive = $file . "_$ts";     
    copy ($file, $archive) or die "Can't copy $file to $archive: $!";
    unlink $file           or die "Can't unlink $file: $!";
}

archive_log 通过复制当前日志将其归档,然后将其删除。 所以在那之后我们可以打开追加,如果不存在则创建文件。

-C 测试文件是否存在,但由于它的输出用于数值测试,我们首先需要-f

由于这种情况每月发生一次,我在运行时加载模块,使用requireimport,一旦日志实际需要轮换。如果您已经使用File::Copy,则无需这样做。至于时间戳,我添加了一些东西以使其成为一个工作示例。

我在 UNIX 上对此进行了测试,将-C 更改为-M 并通过touch -t -c 调整时间戳。

更好的是,为了减少调用者的代码,也将测试完全移动到子中,因为

my $fh_log = open_log($logfile);

say $fh_log "Log a line";

sub open_log {
    my ($file) = @_;
    if (-f $file and -C $file > 30) {
        # code from archive_log() above, to copy and unlink $file
    }
    open my $fh_log, '>>', $file  or die "Can't open $file: $!";
    return $fh_log;
}

注意。在 UNIX 上,文件的创建时间不会保存在任何地方。最接近的概念是上面的ctime,但这当然是不同的。一方面,它会随着许多操作而变化,例如mvlnchmodchownchgrp(可能还有其他)。

【讨论】:

  • 我玩过它,但并不一致。我发现它以某种方式保留了原始创建日期,并且新文件在文件系统中具有该日期。我认为这与 Windows 中的 FAT 文件系统有关,文件删除实际上并没有“删除”文件,它只是在目录结构中设置了一个标志,表示文件条目可以重复使用。这就是文件取消删除实用程序起作用的原因。如果这是一个 *nix 系统,我就不必这样做了。
  • @Mushu 说得好。但是——既然你要重命名这个日志文件,为什么不把它复制到新的并删除旧的(而不是使用rename)呢?然后应该重新创建时间戳。
  • @Mushu 我添加了一个完整的工作示例(现在无法在 Windows 上测试,但它应该可以工作)。
猜你喜欢
  • 2019-10-01
  • 1970-01-01
  • 2011-05-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-11
相关资源
最近更新 更多