如何使用Perl统计文件中的字符数、单词数和行数?

21

使用Perl的好/最佳方法来计算文本文件中字符、单词和行数是什么(不使用wc)?


假设不使用系统调用来使用“wc”命令,你能否完成这个编程任务? - JeeBee
这不是一项作业任务,尽管我承认它看起来像一个。 - NoahD
10个回答

25

这里是 Perl 代码。计算单词数量有一定主观性,但我认为它是任何不是空格的字符序列。

open(FILE, "<file.txt") or die "Could not open file: $!";

my ($lines, $words, $chars) = (0,0,0);

while (<FILE>) {
    $lines++;
    $chars += length($_);
    $words += scalar(split(/\s+/, $_));
}

print("lines=$lines words=$words chars=$chars\n");

简洁明了但不含糊。 - Paul Tomblin
3
为了计算单词数量,你需要使用“scalar(split)”函数;它将会按照 /\s+/ 进行分割,并且去掉开头的空字段,就像 awk 命令一样。 - glenn jackman
作为与格伦相关的注释,您可以使用“length;”而不是“length $_;”,Perl将默认使用$ _。但是,使用split()上的默认值更有益,因为它甚至具有默认的正则表达式。 - Chris Lutz
2
@Paul Tomblin:嘿,你现在开心了吗:perl -ne 'END{print"$. $c $w\n"}$c+=length;$w+=split' - Chas. Owens
@Chas:不这样做的原因有很多,其中包括没有指定编码。请参考我的答案。 - tchrist
显示剩余2条评论

7

对于bmdhacks的答案的变体,可能会产生更好的结果,是使用\s+(甚至更好的\W+)作为分隔符。考虑字符串“The  quick  brown fox”(如果不明显,则有额外的空格)。使用单个空格字符作为分隔符将给出六个单词计数而不是四个。因此,请尝试:

open(FILE, "<file.txt") or die "Could not open file: $!";

my ($lines, $words, $chars) = (0,0,0);

while (<FILE>) {
    $lines++;
    $chars += length($_);
    $words += scalar(split(/\W+/, $_));
}

print("lines=$lines words=$words chars=$chars\n");

使用\W+作为分隔符将阻止标点符号(以及其他内容)被计算为单词。

使用 \W 将 "nit-picking" 分成两个单词。我不知道这是否是正确的行为,但我总是认为连字符单词是一个单词而不是两个。 - Chris Lutz
这是一种“你出钱,你做决定”的事情。就我个人而言,通常会编写适合当前所需“单词”定义的自己的正则表达式。很多时候,split可能不太有用,因为它是一个负匹配。正则表达式通常匹配您想要的字符,这是一个更好的选择。您可以使用m/.../g执行相同类型的操作,并在列表上下文中调用它。 - Nic Gibson
1
这只计算代码点,而不是字符(=字形)。并且它忘记设置编码。 - tchrist
@tchrist - 是的,我知道那个问题,但觉得没必要加上去。你可能是对的。另一方面,好点子 - 我没有想到那个。既然其他答案已经给出了,而且这个问题已经很久了,我打算保持原样,不觉得值得再做修改。 - Nic Gibson

4

3

试试这个Unicode智能版的wc程序。

  • 它会跳过非文件参数(管道、目录、套接字等)。

  • 它假定文本为UTF-8编码。

  • 它将任何Unicode空白字符视为单词分隔符。

  • 如果文件名以.ENCODING结尾,如foo.cp1252foo.latin1foo.utf16等,它也可以接受其他编码。

  • 它还可以处理多种格式的压缩文件。

  • 它提供的计数包括段落、行、单词、字形、字符字节

  • 它理解所有Unicode换行符序列。

  • 它会警告有行终止错误的损坏文本文件。

以下是运行它的示例:

   Paras    Lines    Words   Graphs    Chars    Bytes File
       2     2270    82249   504169   504333   528663 /tmp/ap
       1     2404    11163    63164    63164    66336 /tmp/b3
    uwc: missing linebreak at end of corrupted textfiile /tmp/bad
      1*       2*        4       19       19       19 /tmp/bad
       1       14       52      273      273      293 /tmp/es
      57      383     1369    11997    11997    12001 /tmp/funny
       1   657068  3175429 31205970 31209138 32633834 /tmp/lw
       1        1        4       27       27       27 /tmp/nf.cp1252
       1        1        4       27       27       34 /tmp/nf.euc-jp
       1        1        4       27       27       27 /tmp/nf.latin1
       1        1        4       27       27       27 /tmp/nf.macroman
       1        1        4       27       27       54 /tmp/nf.ucs2
       1        1        4       27       27       56 /tmp/nf.utf16
       1        1        4       27       27       54 /tmp/nf.utf16be
       1        1        4       27       27       54 /tmp/nf.utf16le
       1        1        4       27       27      112 /tmp/nf.utf32
       1        1        4       27       27      108 /tmp/nf.utf32be
       1        1        4       27       27      108 /tmp/nf.utf32le
       1        1        4       27       27       39 /tmp/nf.utf7
       1        1        4       27       27       31 /tmp/nf.utf8
       1    26906   101528   635841   636026   661202 /tmp/o2
     131      346     1370     9590     9590     4486 /tmp/perl5122delta.pod.gz
     291      814     3941    25318    25318     9878 /tmp/perl51310delta.pod.bz2
       1     2551     5345   132655   132655   133178 /tmp/tailsort-pl.utf8
       1       89      334     1784     1784     2094 /tmp/til
       1        4       18       88       88      106 /tmp/w
     276     1736     5773    53782    53782    53804 /tmp/www

Here ya go:

#!/usr/bin/env perl 
#########################################################################
# uniwc - improved version of wc that works correctly with Unicode
#
# Tom Christiansen <tchrist@perl.com>
# Mon Feb 28 15:59:01 MST 2011
#########################################################################

use 5.10.0;

use strict;
use warnings FATAL => "all";
use sigtrap qw[ die untrapped normal-signals ];

use Carp;

$SIG{__WARN__}  = sub {
    confess("FATALIZED WARNING: @_")  unless $^S;
};

$SIG{__DIE__}  = sub {
    confess("UNCAUGHT EXCEPTION: @_")  unless $^S;
};

$| = 1;

my $Errors = 0;
my $Headers = 0;

sub yuck($) {
    my $errmsg = $_[0];
    $errmsg =~ s/(?<=[^\n])\z/\n/;
    print STDERR "$0: $errmsg";
}

process_input(\&countem);

sub countem { 
    my ($_, $file) = @_;

    my (
        @paras, @lines, @words,
        $paracount, $linecount, $wordcount, 
        $grafcount, $charcount, $bytecount,
    );

    if ($charcount = length($_)) {
        $wordcount = eval { @words = split m{ \p{Space}+  }x }; 
        yuck "error splitting words: $@" if $@;

        $linecount = eval { @lines = split m{ \R     }x }; 
        yuck "error splitting lines: $@" if $@;

        $grafcount = 0;
        $grafcount++ while /\X/g;
        #$grafcount = eval { @lines = split m{ \R     }x }; 
        yuck "error splitting lines: $@" if $@;

        $paracount = eval { @paras = split m{ \R{2,} }x }; 
        yuck "error splitting paras: $@" if $@;

        if ($linecount && !/\R\z/) {
            yuck("missing linebreak at end of corrupted textfiile $file");
            $linecount .= "*";
            $paracount .= "*";
        } 
    }

    $bytecount = tell;
    if (-e $file) {
        $bytecount = -s $file;
        if ($bytecount != -s $file) {
            yuck "filesize of $file differs from bytecount\n";
            $Errors++;
        }
    } 
    my $mask = "%8s " x 6 . "%s\n";
    printf  $mask => qw{ Paras Lines Words Graphs Chars Bytes File } unless $Headers++;

    printf $mask => map( { show_undef($_) } 
                                $paracount, $linecount, 
                                $wordcount, $grafcount, 
                                $charcount, $bytecount,
                       ), $file;
} 

sub show_undef {
    my $value = shift;
    return defined($value)
             ? $value
             : "undef";
} 

END { 
    close(STDOUT) || die "$0: can't close STDOUT: $!";
    exit($Errors != 0);
}

sub process_input {

    my $function = shift();

    my $enc;

    if (@ARGV == 0 && -t) {
        warn "$0: reading from stdin, type ^D to end or ^C to kill.\n";
    }

    unshift(@ARGV, "-") if @ARGV == 0;

FILE:

    for my $file (@ARGV) {
        # don't let magic open make an output handle

        next if -e $file && ! -f _;

        my $quasi_filename = fix_extension($file);

        $file = "standard input" if $file eq q(-);
        $quasi_filename =~ s/^(?=\s*[>|])/< /;

        no strict "refs";
        my $fh = $file;   # is *so* a lexical filehandle! ☺
        unless (open($fh, $quasi_filename)) {
            yuck("couldn't open $quasi_filename: $!");
            next FILE;
        }
        set_encoding($fh, $file) || next FILE;

        my $whole_file = eval {
            use warnings "FATAL" => "all";
            local $/;
            scalar <$fh>;
        };

        if ($@) {
            $@ =~ s/ at \K.*? line \d+.*/$file line $./;
            yuck($@);
            next FILE;
        }

        $function->($whole_file, $file);

        unless (close $fh) {
            yuck("couldn't close $quasi_filename at line $.: $!");
            next FILE;
        }

    } # foreach file

}

sub set_encoding(*$) {
    my ($handle, $path) = @_;

    my $enc_name = "utf8";

    if ($path && $path =~ m{ \. ([^\s.]+) \z }x) {
        my $ext = $1;
        die unless defined $ext;
        require Encode;
        if (my $enc_obj = Encode::find_encoding($ext)) {
            my $name = $enc_obj->name || $ext;
            $enc_name = "encoding($name)";
        }
    }

    return 1 if eval {
        use warnings FATAL => "all";
        no strict "refs";
        binmode($handle, ":$enc_name");
        1;
    };

    for ($@) {
        s/ at .* line \d+\.//;
        s/$/ for $path/;
    }

    yuck("set_encoding: $@");

    return undef;
}

sub fix_extension {
    my $path = shift();
    my %Compress = (
        Z       =>  "zcat",
        z       => "gzcat",            # for uncompressing
        gz      => "gzcat",
        bz      => "bzcat",
        bz2     => "bzcat",
        bzip    => "bzcat",
        bzip2   => "bzcat",
        lzma    => "lzcat",
    );

    if ($path =~ m{ \. ( [^.\s] +) \z }x) {
        if (my $prog = $Compress{$1}) {
            return "$prog $path |";
        } 
    } 

    return $path;

}


2
我在谷歌搜索字符计数解决方案时发现了这个。虽然我对perl知之甚少,所以这里的一些东西可能不正确,但这是我对newt解决方案的调整。
首先,有一个内置的行计数变量,所以我只使用了它。这可能更有效率。 目前,字符计数包括换行符,这可能不是你想要的,所以我删掉了$_的换行符。 Perl还抱怨split()的方式(隐式分割,请参见:为什么Perl抱怨“使用隐式分割到@_已过时”?),所以我进行了调整。 我的输入文件是UTF-8格式,因此我以此打开它们。如果输入文件包含非ASCII字符,则这可能有助于获得正确的字符计数。
以下是代码:
open(FILE, "<:encoding(UTF-8)", "file.txt") or die "Could not open file: $!";

my ($lines, $words, $chars) = (0,0,0);
my @wordcounter;
while (<FILE>) {
    chomp($_);
    $chars += length($_);
    @wordcounter = split(/\W+/, $_);
    $words += @wordcounter;
}
$lines = $.;
close FILE;
print "\nlines=$lines, words=$words, chars=$chars\n";

2
这里有一个名为Perl Power Tools的项目,其目标是重建所有Unix二进制工具,主要是为那些没有Unix的操作系统而设计。是的,他们也重建了wc。实现可能有些过度,但它是POSIX兼容的。
当你看到GNU兼容实现的true时,会有点荒谬。

大多数花哨的“真实”实现都是POD。仍然荒谬。 - Chris Lutz
Schwern:我一直在重新实现相当多的PPT,以适用于Unicode智能化。最近我已经重新实现了`cat-v / od-c' ','expand','fmt','grep','look','rev','sort'和'wc'。所有这些都比原版更加改进了。 - tchrist

1

按固定大小块读取文件可能比逐行读取更有效。 wc 二进制文件可以实现此功能。

#!/usr/bin/env perl

use constant BLOCK_SIZE => 16384;

for my $file (@ARGV) {
    open my $fh, '<', $file or do {
        warn "couldn't open $file: $!\n";
        continue;
    };

    my ($chars, $words, $lines) = (0, 0, 0);

    my ($new_word, $new_line);
    while ((my $size = sysread $fh, local $_, BLOCK_SIZE) > 0) {
        $chars += $size;
        $words += /\s+/g;
        $words-- if $new_word && /\A\s/;
        $lines += () = /\n/g;

        $new_word = /\s\Z/;
        $new_line = /\n\Z/;
    }
    $lines-- if $new_line;

    print "\t$lines\t$words\t$chars\t$file\n";
}

2
我不确定这会给你带来任何好处。在底层,Perl的<>运算符使用缓冲IO。你所做的只是用需要解释的东西重写了内置的东西。 - Nic Gibson
真的。至少在我的5.8.8安装中,Perl每次缓冲4096字节,并且手动执行此操作没有性能优势 - 正如您所怀疑的那样,如果有什么区别,它实际上更糟糕。不过,我喜欢提醒人们要考虑低级问题 :) - ephemient
1
那么,对于跨越块边界的UTF-8字符,你如何处理呢? - tchrist

1
为了能够计算字符而不是字节,请考虑以下内容:
(请尝试使用中文或西里尔字母,并将文件保存为utf8格式)
use utf8;

my $file='file.txt';
my $LAYER = ':encoding(UTF-8)';
open( my $fh, '<', $file )
  || die( "$file couldn't be opened: $!" );
binmode( $fh, $LAYER );
read $fh, my $txt, -s $file;
close $fh;

print length $txt,$/;
use bytes;
print length $txt,$/;

Perl默认使用系统语言环境。如果您的系统是现代化的,那么系统语言环境将是UTF-8编码,因此Perl IO默认为UTF-8。如果不是,则您可能应该使用系统语言环境而不是强制使用UTF-8模式... - ephemient
错误,短暂。Perl默认使用系统区域设置,但为了向后兼容性将字符128-255打印为“?”。要正确打印UTF-8,应该在使用文件句柄之前说binmode($fh, ":utf8");。在这种情况下,“use utf8;”是无用的 - 它告诉Perl源代码可以是UTF-8,除非您有变量名称如$áccent或$ümlats否则这是不必要的。 - Chris Lutz
@Chris 我的 Perl 5.8 和 5.10 都记录了 -C SDL 作为默认设置,而 perl -e 'print "\xe2\x81\x89\n"' 输出的是预期的 "⁉",而不是你所期望的 "???"。 - ephemient
我认为这三个十六进制值正在组合成一个UTF-8字符。而UTF-8字符将在Perl中打印。只是128-255之间的字符不会打印。在我的机器上尝试任何一个这三个十六进制代码都会给我“?”,而在其前面加上binmode(STDOUT, ":utf8");则会给我“â”对于\xe2和其他两个字符则会给出非打印字符。就我所知,我没有任何默认设置为“-C”。 - Chris Lutz
echo $'\xe2\x80\x99' | perl -ne'print length,$/' 输出 4,而 echo $'\xe2\x80\x99' | perl -CSDL -ne'print length,$/' 输出 2,所以我一定记错了,Chris 是正确的。 - ephemient
@Berov:你的想法是正确的!但是看看我的解决方案,这是一个更加精细的版本。 - tchrist

1

非认真回答:

system("wc foo");

ITYM: 我的 ($lines, $words, $chars) = split(' ', wc foo); - ysth

0

这对Perl初学者可能有所帮助。

我尝试模拟MS Word中的计数功能,并添加了一项在Linux中未显示的功能。

  • 行数
  • 单词数
  • 包括空格在内的字符数
  • 不包括空格在内的字符数(wc命令不会在输出中给出此项,但Microsoft Word会显示它)。

以下是链接地址:在文件中计算单词、字符和行数


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接