【问题标题】:Pattern match loop is slow, ways to speed it up?模式匹配循环很慢,如何加快速度?
【发布时间】:2014-05-15 14:55:53
【问题描述】:

好的,所以我在 bash 中编写了一个脚本,通过搜索用户名或 IP 地址来显示 ftp 连接的整个流程。我让它将数据读入一个数组,搜索条件,然后将该进程 id 与其他进程 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

谢谢!

【问题讨论】:

  • 为什么不循环遍历每一行的所有模式?现在写你阅读文件的每一行scalar @pids 次。 IO 通常是这类问题的瓶颈,如果您可以将其最小化的话。
  • 我希望对其进行排序,这就是为什么我在外面有 @pid 循环。
  • 这看起来像XY problem。你描述了如何你试图解决你的问题,但没有描述什么你试图解决。我怀疑有更好的方法来解决这个问题,但没有更多细节,很难说。请告诉我们您到底想达到什么目标,我们或许可以推荐一些更好的方法。
  • @user2208986 如果你使用一个 hashref,它的键是 pid,值是如下 mob 显示的行的 arrayref,你仍然可以保持顺序。
  • (您问了一个先前的问题,因为已删除,描述了您要解决的实际问题;根据您对该问题的问题的描述,不需要数组或多次传递通过文件,这就是我发表最后一条评论的原因)

标签: arrays perl file loops


【解决方案1】:

任何时候您的循环中都有可能存在性能问题的循环。假设您有 1000 个 pid 和 100 万行日志文件。为每个 pid 循环遍历文件中的每一行是 1000 * 100 万,即 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。但它并不能避免我们意外匹配 cmets 中的日期或其他数字。根据您提供的示例行,我使用限制性更强的 $line =~ m{ \[ \s*(\d+) \]: }x 在正确的位置查找 PID。

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

【讨论】:

  • 我将如何修改这个,所以我只得到与特定 IP 地址相关的行。我在想按时间框架匹配它是可行的方法,但我很难将它们过滤掉。
  • @cycloxr 您应该将其作为一个新问题提出。
【解决方案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;

请注意,这些解决方案均未在编译后进行测试,因为创建一组测试数据需要大量工作。


更新 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
###############

【讨论】:

  • pids 正则表达式应该有括号并被锚定,如qr/\b($pids)\b/ 这样'123' 不匹配 '4123' 的 pid。
  • @AndyLester:是的,可能。我正在重新创建 OP 代码的行为。不确定这是否是个好主意。
  • @AndyLester:看过 OP 的输入文件后,我同意你的看法。固定。
  • @AndyLester 感谢您的建议!
【解决方案3】:

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

  • 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 打印了它,但你可以随意操作散列。

【讨论】:

    【解决方案4】:

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

    @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);
    

    【讨论】:

    • 我正在读取的文件有 440932 行长,会不会太大而无法读取到数组中?
    • print OUTPUT $_ for @{$flow{$mPid}}, "################\n" 是一个非常简单的优化
    • 恐怕print for OUTPUT @{$flow{$mPid}}, "################\n不行,用foreach会比原来慢。
    • @Borodin 你对 Perl 的工作原理做了很多假设,其中一些是错误的。 print for @array 可能比 print @array 慢,因为前者是 N 个 op 调用,而后者是 1 个。因为 Perl ops 是高度优化的 C 代码,任何时候你都可以减少 Perl 脚本中的操作数量要赢。参数不会复制到函数中,它们是别名,因此没有完成内存复制。当然,首先要进行基准测试和配置文件。
    • 由于$/,他们的行为略有不同。用-l 运行它,看看我的意思。我打开了benchmark 进行仔细检查,因为在你和硬件之间有几十层,即使你认为你的计算机内部行为如何,你也可能错了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-11-12
    • 1970-01-01
    • 1970-01-01
    • 2015-08-30
    • 2013-08-18
    • 2015-07-17
    • 1970-01-01
    相关资源
    最近更新 更多