模式匹配循环很慢,加快速度的方法?

时间:2014-04-03 16:18:49

标签: arrays perl file loops

好的,所以我在bash中编写了一个脚本,通过搜索用户名或IP地址来显示ftp连接的整个流程。我让它将数据读入数组,搜索条件,然后将该进程ID与其他进程匹配,以便我得到整个流程。

然而,表现非常缓慢,而且根据专家交流社区的其他人的建议,我决定尝试使用perl。我尽可能多地学习,但还有很长的路要走。我正在尝试搜索条件,获取该行的进程ID,然后将所有行读入与该进程ID匹配的数组中,这样我基本上就可以得到整个ftp连接流。

我假设我会从文件中读取每一行,在其上进行模式匹配,如果它与我正在搜索的IP地址匹配,那么我会将该行复制到数组中。我接着想到,在我将这些行读入数组之后,我将返回并从每个行中获取进程ID,对文件执行另一次搜索并将与进程ID匹配的所有行放入新数组中,然后打印出阵列。

我有以下代码,用于匹配文件的行,基于它是否与数组中的模式匹配。

数组@pids对数据有以下几种,但比这还要多几百个:

4682
4690
4692
4693
4696
5320

如果我正在读的行中有这个数字,那么我将它推到一个新数组。一旦到达文件的末尾,它就会返回到文件的开头,并处理匹配数组@pids的下一个元素。然后我将新数组打印到文件中。

不幸的是,循环是永远的,有什么方法可以加快速度吗?我假设是因为我一遍又一遍地浏览文件,使事情有点重复,但不确定我应该怎么做。

seek INPUT, 0, 0;

my @flow;
my $count = 0;
my $pid_count = 0;

foreach my $mPID(@pids){
    while(my $line = <INPUT>){
        if ($line =~ /$mPID/){
            push @flow, $line;
        }
    }
    push @flow, "###############\n";
    seek INPUT, 0, 0;
}

open (OUTPUT, '>'.$output) or die "Couldn't read $output.\n";
print OUTPUT @flow;
close (OUTPUT);

以下是来自的数据示例:

Dec  1 23:59:03 ftp1 ftpd[4152]: PASV
Dec  1 23:59:04 ftp1 ftpd[4152]: NLST
Dec  1 23:59:04 ftp1 ftpd[4152]: FTP session closed
Dec  1 23:59:05 ftp1 ftpd[4682]: USER test1
Dec  1 23:59:05 ftp1 ftpd[4682]: PASS password
Dec  1 23:59:08 ftp1 ftpd[4682]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prd

数据示例我正在获取与IP匹配的所有pid:

Dec  1 23:59:08 ftp1 ftpd[4682]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prd
Dec  1 23:59:10 ftp1 ftpd[4690]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prod1
Dec  1 23:59:10 ftp1 ftpd[4692]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prod
Dec  1 23:59:11 ftp1 ftpd[4693]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], test1
Dec  1 23:59:14 ftp1 ftpd[4696]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], test1
Dec  1 23:59:40 ftp1 ftpd[5320]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], test1
Dec  1 23:59:47 ftp1 ftpd[5325]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prd
Dec  1 23:59:48 ftp1 ftpd[5328]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prod1
Dec  1 23:59:49 ftp1 ftpd[5329]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prod
Dec  1 23:59:49 ftp1 ftpd[5330]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], test1
Dec  2 00:00:09 ftp1 ftpd[9876]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], test1
Dec  2 00:00:25 ftp1 ftpd[12830]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], test1
Dec  2 00:00:25 ftp1 ftpd[12832]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prd
Dec  2 00:00:27 ftp1 ftpd[12850]: FTP LOGIN FROM 192.168.0.2 [192.168.0.2], prod1

谢谢!

4 个答案:

答案 0 :(得分:3)

任何时候你在循环中有一个循环,这可能是一个性能问题。我们假设你有1000个pid和100万行日志文件。对每个pid循环遍历文件中的每一行是1000 * 1百万,这是1 BILLION DOLLARS!!! Err ...迭代。

现在你要检查每一行是否有每个PID。如果你是手工做,你就不会这样做。您可以在线上扫描看起来像PID的内容,看看它们是否在您的列表中。 PID很容易识别,它们是整数,所以让我们这样做。我们可以开始简单,只匹配数字。

#!/usr/bin/perl

use strict;
use warnings;
use autodie;

# Some test PIDs
my @pids = (
    12,
    1123,
    1234
);

# Put the PIDs into a hash.  Each line which matches will be stored.
my %pids = map { $_ => [] } @pids;

# Loop through the lines
while(my $line = <DATA>) {
    # Look for a PID
    if(my($pid) = $line =~ m{ \[ \s*(\d+) \]: }x) {
        # Push it into the appropriate PID slot if it's on our list
        push @{$pids{$pid}}, $line if $pids{$pid};
    }
}

# Output the PIDs which have matching lines
for my $pid (keys %pids) {
    my $lines = $pids{$pid};
    next if !@$lines;

    print "PID: $pid\n";
    print @$lines;
    print "##################\n";

}


# Some test lines
__DATA__
Dec  1 23:59:03 ftp1 ftpd[  12]: PASV
Dec  1 23:59:04 ftp1 ftpd[1123]: NLST
Dec  1 23:59:04 ftp1 ftpd[3114]: FTP session closed
Dec  1 23:59:05 ftp1 ftpd[9999]: USER test1
Dec  1 23:59:05 ftp1 ftpd[ 123]: PASS password

现在你只需要遍历文件一次。由于PID列表很小(最大PID通常在几万,但即使一百万也不是那么大)存储匹配的每一行不太可能占用大量内存,所以它没问题存储所有匹配的行。如果输出顺序无关紧要,您可以避免存储这些行并将它们打印为匹配grep

while(my $line = <DATA>) {
    # Look for a PID
    if(my($pid) = $line =~ m{ \[ \s*(\d+) \]: }x) {
        print $line if $pids{$pid};
    }
}

关于PID匹配的说明。在您的原始示例中,您只是想查看pid是否在行中的任何位置$line =~ /$mPID/。这是个问题。 PID 123将匹配ftpd[1234]。 PID 59将匹配23:59:04

通过查找整个数字然后查看它们是否在列表中,这可以使我们远离前者。 ftpd[1234]无法与PID 123匹配。但它并不能避免我们意外地匹配评论中的日期或其他数字。根据您提供的示例行,我使用限制性更强的$line =~ m{ \[ \s*(\d+) \]: }x在正确的位置查找PID。

您必须查看数据以确定您是否可以逃避这一点。如果没有,您至少可以将该行中的所有数字与my @pids = $line =~ m{ (\d+) }gx )匹配。

答案 1 :(得分:2)

你应该从数组中构建一个正则表达式,比如

my $pids = join '|', @pids;
$pids = qr/$pids/;

然后你只需要对输入文件的每一行进行一次比较。

open my $out_fh, '>', $output or die qq{Couldn't open "$output" for writing: $!\n};

while (my $line = <$in_fh>) {
  print $out_fh, $line if $line =~ $pids;
}

close $out_fh;

另请注意,您应该使用具有有意义名称的词法文件句柄,以及open的三参数形式。

如果您需要按照PID值的顺序对输出进行排序,那么还有一些工作要做,但很有可能。


<强>更新

如果您需要将输出分成每个PID的组,那么您必须在打印前将输出存储在哈希中,如下所示

my $pids = join '|', @pids;
$pids = qr/($pids)/;

my %output;

while (my $line = <$in_fh>) {
  push @{ $output{$1} }, $line if $line =~ $pids;
}

open my $out_fh, '>', $output or die qq{Couldn't open "$output" for writing: $!\n};

for my $pid (@pids) {
  next unless my $lines = $output{$pid};
  print $out_fh $_ for @$lines;
  print $out_fh "###############\n";
}

close $out_fh;

注意这些解决方案都未经过beyind编译测试,因为创建一组测试数据需要大量工作。


更新2

此程序使用更新后的问题中的新数据。

use strict;
use warnings;

my $outfile = 'result.txt';

my @pids = qw/ 4682 4690 4692 4693 4696 5320 /;
my $pids = join '|', @pids;
$pids = qr/\b($pids)\b/;

open my $in_fh, 'logfile.txt' or die $!;

my %output;
while (my $line = <$in_fh>) {
  push @{ $output{$1} }, $line if $line =~ $pids;
}


open my $out_fh, '>', $outfile or die qq{Couldn't open "$outfile" for writing: $!\n};

for my $pid (@pids) {
  next unless my $lines = $output{$pid};
  print $out_fh $_ for @$lines;
  print $out_fh "###############\n";
}

close $out_fh;

<强>输出

Dec  1 23:59:05 ftp1 ftpd[4682]: USER test1
Dec  1 23:59:05 ftp1 ftpd[4682]: PASS password
###############

答案 2 :(得分:2)

我对你的问题的理解是你有:

  • FTP日志
  • IP地址

并且您希望跟踪从该IP启动的会话。您可以像这样在我的日志文件中一次性完成此操作(我根据您之前询问的another question构建了一些示例数据):

#!/usr/bin/perl

use strict;
use warnings;

use Data::Dumper;
use Regexp::Common qw(net);

my $ip = '192.0.2.0';

my (%pid, %session);
while (<DATA>) {
    chomp;

    if (/ftpd\[(\d+)\]:\s+(?:USER|PASS)/) {
        push @{ $session{$1} }, $_;
    }
    elsif (/ftpd\[(\d+)\]:\s+FTP LOGIN FROM ($RE{net}{IPv4})/) {
        if ($2 eq $ip) {
            $pid{$1} = 1;
            push @{ $session{$1} }, $_;
        }
        else {
            delete $session{$1};
        }
    }
    elsif (/ftpd\[(\d+)\]:/) {
        push @{ $session{$1} }, $_ if exists $pid{$1};
    }
}

print Dumper \%session;

__DATA__
Dec  1 23:59:03 sslmftp1 ftpd[4152]: USER xxxxxx
Dec  1 23:59:03 sslmftp1 ftpd[4152]: PASS password
Dec  1 23:59:03 sslmftp1 ftpd[4152]: FTP LOGIN FROM 192.0.2.0 [192.0.2.0], xxxxxx
Dec  1 23:59:03 sslmftp1 ftpd[4152]: PWD
Dec  1 23:59:03 sslmftp1 ftpd[4152]: CWD /test/data/872507/
Dec  1 23:59:03 sslmftp1 ftpd[4152]: TYPE Image`
Dec  1 23:59:03 sslmftp1 ftpd[4152]: PASV
Dec  1 23:59:04 sslmftp1 ftpd[4152]: NLST
Dec  1 23:59:04 sslmftp1 ftpd[4152]: FTP session closed
Dec  1 23:59:05 sslmftp1 ftpd[4683]: USER xxxxxx
Dec  1 23:59:05 sslmftp1 ftpd[4683]: PASS password
Dec  1 23:59:05 sslmftp1 ftpd[4683]: FTP LOGIN FROM 192.0.2.1 [192.0.2.1], xxxxxx
Dec  1 23:59:05 sslmftp1 ftpd[4683]: PWD
Dec  1 23:59:05 sslmftp1 ftpd[4683]: CWD /test/data/944837/
Dec  1 23:59:05 sslmftp1 ftpd[4683]: TYPE Image
Dec  1 23:59:06 sslmftp1 ftpd[4925]: USER xxxxxx
Dec  1 23:59:06 sslmftp1 ftpd[4925]: PASS password
Dec  1 23:59:06 sslmftp1 ftpd[4925]: FTP LOGIN FROM 192.0.2.0 [192.0.2.0], xxxxxx
Dec  1 23:59:07 sslmftp1 ftpd[4925]: PWD
Dec  1 23:59:08 sslmftp1 ftpd[4925]: CWD /test/data/944837/
Dec  1 23:59:09 sslmftp1 ftpd[4925]: TYPE Image

输出:

$VAR1 = {
          '4152' => [
                      'Dec  1 23:59:03 sslmftp1 ftpd[4152]: USER xxxxxx  ',
                      'Dec  1 23:59:03 sslmftp1 ftpd[4152]: PASS password  ',
                      'Dec  1 23:59:03 sslmftp1 ftpd[4152]: FTP LOGIN FROM 192.0.2.0 [192.0.2.0], xxxxxx  ',
                      'Dec  1 23:59:03 sslmftp1 ftpd[4152]: PWD  ',
                      'Dec  1 23:59:03 sslmftp1 ftpd[4152]: CWD /test/data/872507/  ',
                      'Dec  1 23:59:03 sslmftp1 ftpd[4152]: TYPE Image`',
                      'Dec  1 23:59:03 sslmftp1 ftpd[4152]: PASV',
                      'Dec  1 23:59:04 sslmftp1 ftpd[4152]: NLST',
                      'Dec  1 23:59:04 sslmftp1 ftpd[4152]: FTP session closed'
                    ],
          '4925' => [
                      'Dec  1 23:59:06 sslmftp1 ftpd[4925]: USER xxxxxx ',
                      'Dec  1 23:59:06 sslmftp1 ftpd[4925]: PASS password',
                      'Dec  1 23:59:06 sslmftp1 ftpd[4925]: FTP LOGIN FROM 192.0.2.0 [192.0.2.0], xxxxxx ',
                      'Dec  1 23:59:07 sslmftp1 ftpd[4925]: PWD',
                      'Dec  1 23:59:08 sslmftp1 ftpd[4925]: CWD /test/data/944837/',
                      'Dec  1 23:59:09 sslmftp1 ftpd[4925]: TYPE Image'
                    ]
        };

现在,您在散列中由$ip启动每个会话的行,并将会话PID作为键。我只是用Data::Dumper打印它,但您可以随意操作散列。

答案 3 :(得分:1)

读取文件比循环遍历数组要慢。如果输入不是太大,则应将其加载到数组中并循环遍历该数组:

@input = <INPUT>;
foreach my $mPID(@pids){
    foreach my $line (@input) {
        ...

如果输入太大,那么也许你可以颠倒循环的顺序,这样你仍然只能读取文件一次:

while(my $line = <INPUT>){
    foreach my $mPID(@pids){
        if ($line =~ /$mPID/){
            push @{$flow{$mPid}}, $line;
        }
    }
}

open (OUTPUT, '>'.$output) or die "Couldn't read $output.\n";
foreach my $mPid (@pids) {
    if (@{$flow{$mPid}}) {
        print OUTPUT @{$flow{$mPid}}, "################\n";
    }
}
close (OUTPUT);