Perl:有效地计算许多字符串中的许多单词

时间:2015-05-29 13:41:56

标签: python regex perl loops

我经常发现自己需要计算单词出现在多个文本字符串中的次数。当我这样做时,我想知道每个单词在每个文本字符串中出现的次数。

我不相信我的方法非常有效,你能给我的任何帮助都会很棒。

通常,我会编写一个循环,(1)从txt文件中提取文本作为文本字符串,(2)执行另一个循环,循环遍历我想要使用正则表达式计算的单词来检查有多少每次将计数推送到数组时出现给定单词的次数,(3)将逗号分隔的计数数组打印到文件中。

以下是一个例子:

#create array that holds the list of words I'm looking to count;
@word_list = qw(word1 word2 word3 word4);

#create array that holds the names of the txt files I want to count;
$data_loc = "/data/txt_files_for_counting/"
opendir(DIR1,"$data_loc")||die "CAN'T OPEN DIRECTORY";
my @file_names=readdir(DIR1);


#create place to save results;
$out_path_name = "/output/my_counts.csv";
open (OUT_FILE, ">>", $out_path_name);

#run the loops;
foreach $file(@file_names){
    if ($file=~/^\./)
        {next;}
    #Pull in text from txt filea;
    {
        $P_file = $data_loc."/".$file;
        open (B, "$P_file") or die "can't open the file: $P_file: $!"; 
        $text_of_txt_file = do {local $/; <B>}; 
        close B or die "CANNOT CLOSE $P_file: $!";      
    }

    #preserve the filename so counts are interpretable;
    print OUT_FILE $file;

    foreach $wl_word(@word_list){
        #use regular expression to search for term without any context;
        @finds_p = ();
        @finds_p = $text_of_txt_file =~ m/\b$wl_word\b/g;
        $N_finds = @finds_p;
        print OUT_FILE ",".$N_finds;
    }
    print OUT_FILE ",\n";
}
close(OUT_FILE);

我发现这种方法非常低效(慢),因为txt文件的数量和我想要计算的单词数量增长。

有更有效的方法吗?

是否有perl包执行此操作?

在python中它会更有效吗? (例如,是否有一个python包可以执行此操作?)

谢谢!

编辑:注意,我不想计算单词的数量,而是计算某些单词的存在。因此,这个问题“What's the fastest way to count the number of words in a string in Perl?”中的答案并不十分适用。除非我遗漏了什么。

5 个答案:

答案 0 :(得分:3)

这是我对你的代码编写方式的看法。我将花一些时间解释我的选择,然后更新

  • 始终 use strictuse warnings位于您编写的每个 Perl程序的顶部。您还必须使用my 声明每个变量尽可能接近其第一个使用点。这是一个必不可少的习惯,因为它会揭示许多简单的错误。

  • 不要评论不言自明的源代码。鼓励评论一切都是20世纪70年代的遗产,并成为编写糟糕代码的借口。大多数情况下,正确使用标识符和空格将比任何评论更好地解释程序的功能

  • 使用open的三参数形式是正确的,但您也应该使用词法文件句柄。如果程序无法在不访问文件的情况下无法合理地继续,则检查每个open的结果并调用die至关重要。 die字符串必须包含变量$!的值,以说明为什么 open失败

  • 如果您的程序打开了很多文件,那么使用autodie编译指示通常会更方便,它会隐式检查每个IO操作

  • 您应该阅读perldoc perlstyle以熟悉大多数Perl程序员所熟悉的格式。像

    这样的神器
    if ($file=~/^\./)
            {next;}
    

    应该只是

    next if $file =~ /^\./;
    
  • 您已抓住do { local $/; ... }成语将整个文件读入内存,但您的范围有限。你的块

    {
        $P_file = $data_loc."/".$file;
        open (B, "$P_file") or die "can't open the file: $P_file: $!";
        $text_of_txt_file = do {local $/; <B>}; 
        close B or die "CANNOT CLOSE $P_file: $!";      
    }
    

    写得更好

    my $text_of_txt_file = do {
      open my $fh, '<', $file;
      local $/;
      <$fh>;
    };
    
  • 不是循环遍历单词列表,而是从单词列表构建正则表达式更快更简洁。我的下面的程序显示了这个

use strict;
use warnings;
use 5.010;
use autodie;

use constant DATA_LOC    => '/data/txt_files_for_counting/';
use constant OUTPUT_FILE => '/output/my_counts.csv';

my @word_list = qw(word1 word2 word3 word4);
my $word_re   = join '|', map quotemeta, @word_list;
$word_re      = qr/$word_re/;

chdir DATA_LOC;

my @text_files = grep -f, glob '*.*';

my @find_counts;

for my $file ( @text_files ) {

  next if $file =~ /^\./;

  my $text = do {
    open my $in_fh, '<', $file;
    local $/;
    <$in_fh>
  }; 

  my $n_finds = $text =~ /\b$word_re\b/g;
  push @find_counts, $n_finds;
}

open my $out_fh, '>', OUTPUT_FILE;
print $out_fh join(',', @find_counts), "\n";
close $out_fh;

答案 1 :(得分:2)

首先关闭 - 您正在使用opendir做什么 - 我不会并且会建议glob

否则 - 还有另一个有用的技巧。为你的“单词”编译一个正则表达式。这有用的原因是因为 - 在正则表达式中使用变量,它需要每次重新编译正则表达式 - 以防变量发生变化。如果它是静态的,那么你就不再需要了。

use strict;
use warnings;
use autodie;

my @words = ( "word1", "word2", "word3", "word4", "word5 word6" );
my $words_regex = join( "|", map ( quotemeta, @words  ));
$words_regex = qr/\b($words_regex)\b/;

open( my $output, ">", "/output/my_counts.csv" );

foreach my $file ( glob("/data/txt_files_for_counting") ) {
    open( my $input, "<", $file );
    my %count_of;
    while (<$input>) {
        foreach my $match (m/$words_regex/g) {
            $count_of{$match}++;
        }
    }
    print {$output} $file, "\n";
    foreach my $word (@words) {
        print {$output} $word, " => ", $count_of{$word} // 0, "\n"; 
    }
    close ( $input );
}

使用这种方法 - 您不再需要将整个文件“啜饮”到内存中以进行处理。 (这可能不是一个很大的优势,取决于文件的大小)。

当输入数据时:

word1
word2
word3 word4 word5 word6 word2 word5 word4
word4 word5 word word 45 sdasdfasf
word5 word6 
sdfasdf
sadf

输出:

word1 => 1
word2 => 2
word3 => 1
word4 => 3
word5 word6 => 2

但是我会注意到 - 如果你的正则表达式中有重叠的子字符串,那么这将无法正常工作 - 尽管如此,你只需要一个不同的正则表达式。

答案 2 :(得分:0)

如果你的单词用空格分隔,请使用collections.Counter dict使用python计算所有单词:

from collections import Counter

with open("in.txt") as f:
    counts = Counter(word for line in f for word in line.split())

然后通过按键访问以获取每个单词出现的次数,无论你想要的是什么:

 print(counts["foo"])
 print(count["bar"])
 .....

所以一次传递文件中的单词,你可以得到所有单词的计数,所以如果你有1或10000个字来统计,你只需要构建一次dict。与普通词语不同,您尝试访问的任何单词/键不在词典中而不会引发词汇错误,而是会返回0

如果您只想使用一个集来存储某些单词来存储您想要保留的单词并对每个单词进行查找:

from collections import Counter
words = {"foo","bar","foobar"}
with open("out.txt") as f:
    counts = Counter(word for line in f for word in line.split() if word in words)

只存储单词中的单词计数,集合查找平均为0(1)

如果你想搜索一个短语然后你可以使用sum和in,但是你必须为每个短语做这个,所以多次传递文件:

with open("in.txt") as f:
    count = sum("word1 word2 word3"  in line for line in f)

答案 3 :(得分:0)

您最大的瓶颈是从存储介质读取数据的速度。 Using a small number of parallel processes,您的程序可以在处理其他文件时读取一个文件,从而加快了整个过程。除非文件本身很大,否则这不太可能产生任何好处。

请记住,重叠字符串很难。下面的代码更喜欢最长的匹配。

非并行版本

#!/usr/bin/env perl

use strict;
use warnings;
use File::Spec::Functions qw( catfile );
use Text::CSV_XS;

die "Need directory and extension\n" unless @ARGV == 2;
my ($data_dir, $ext) = @ARGV;

my $pat = join('|',
    map quotemeta,
    sort { (length($b) <=> length($a)) }
    my @words = (
        'Visual Studio',
        'INCLUDE',
        'Visual',
    )
);

my $csv= Text::CSV_XS->new;

opendir my $dir, $data_dir
    or die "Cannot open directory: '$data_dir': $!";

my %wanted_words;

while (my $file = readdir $dir) {
    next unless $file =~ /[.]\Q$ext\E\z/;
    my $path = catfile($data_dir, $file);
    next unless -f $path;
    open my $fh, '<', $path
        or die "Cannot open '$path': $!";
    my $contents = do { local $/; <$fh> };
    close $fh
        or die "Cannot close '$path': $!";
    while ($contents =~ /($pat)/go) {
        $wanted_words{ $file }{ $1 } += 1;
    }
}

for my $file (sort keys %wanted_words) {
    my $file_counts = $wanted_words{ $file };
    my @fields = ($file, sort keys %$file_counts);
    $csv->combine(@fields)
        or die "Failed to combine [@fields]";
    print $csv->string, "\n";
}

对于测试,我在包含Boost安装中的一些临时批处理文件的目录中运行脚本:

C:\...\Temp> perl count.pl . cmdb2_msvc_14.0_vcvarsall_amd64.cmd,INCLUDE,"Visual Studio"
b2_msvc_14.0_vcvarsall_x86.cmd,INCLUDE,"Visual Studio"
b2_msvc_14.0_vcvarsall_x86_arm.cmd,INCLUDE,"Visual Studio"

也就是说,"Visual"的所有出现都会被忽略,而有利于"Visual Studio"

要生成CSV输出,您应该使用Text::CSV_XS中的combine方法,而不是join(',' ...)

使用Parallel :: ForkManager

的版本

这是否能更快地完成任务取决于输入文件的大小和存储介质的速度。如果有改进,进程数可能在N / 2到N之间,其中N是核心数。我没有测试过这个。

#!/usr/bin/env perl

use strict;
use warnings;
use File::Spec::Functions qw( catfile );
use Parallel::ForkManager;
use Text::CSV_XS;

die "Need number of processes, directory, and extension\n" unless @ARGV == 3;
my ($procs, $data_dir, $ext) = @ARGV;

my $pat = join('|',
    map quotemeta,
    sort { (length($b) <=> length($a)) }
    my @words = (
        'Visual Studio',
        'INCLUDE',
        'Visual',
    )
);

my $csv= Text::CSV_XS->new;

opendir my $dir, $data_dir
    or die "Cannot open directory: '$data_dir': $!";

my $fm = Parallel::ForkManager->new($procs);

ENTRY:
while (my $file = readdir $dir) {
    next unless $file =~ /[.]\Q$ext\E\z/;
    my $path = catfile($data_dir, $file);
    next unless -f $path;
    my $pid = $fm->start and next ENTRY;

    my %wanted_words;
    open my $fh, '<', $path
        or die "Cannot open '$path': $!";
    my $contents = do { local $/; <$fh> };
    close $fh
        or die "Cannot close '$path': $!";
    while ($contents =~ /($pat)/go) {
        $wanted_words{ $1 } += 1;
    }
    my @fields = ($file, sort keys %wanted_words);
    $csv->combine(@fields)
        or die "Failed to combine [@fields]";
    print $csv->string, "\n";
    $fm->finish;
}

$fm->wait_all_children;

答案 4 :(得分:-3)

我更愿意使用单线:

$ for file in /data/txt_files_for_counting/*; do perl -F'/\W+/' -nale 'BEGIN { @w = qw(word1 word2 word3 word4) } $h{$_}++ for map { $w = lc $_; grep { $_ eq $w } @w } @F; END { print join ",", $ARGV, map { $h{$_} || 0 } @w; }' "$file"; done