While循环性能:极慢

时间:2019-09-18 18:53:56

标签: linux bash perl

我有以下input.txt和parts.txt文件:

input.txt
CAR*BMW*X1*BUMBER*PLATE~
CAR*AUDI*A5*HOOD~
CAR*MAZDA*CX3*QNX*DIGITAL~
CAR*BMW*X5*SEAT~
SUV*FORD*EXPLORER*GLASS*SAFE~
CAR*FORD*FUSION*QNX~
CAR*GM*YUKON**~
parts.txt
BLACKBERRY
GOOGLE
NXP

在用Red Hat Linux服务器编写的bash代码下面,这需要很长时间。例如,我输入的文件大小为10MB,花费了3个小时来完成该过程。

#!/bin/bash
segment=CAR
position=3
a=0
b=0
while IFS='*' read -r -d'~' -a data; do
    if [ "${data[0]}" = "$segment" ]; then
        if [ ${#data[$position]} -gt 0 ]; then
           data[$position]=$(shuf -n1 "/tmp/parts.txt")
        b=$((b+1))
        fi
    a=$((a+1))
    fi
    # and output the data
     (IFS=*; printf "%s~" "${data[*]}";)  >> /tgt/output.txt
done < /src/input.txt 
output.txt
CAR*BMW*X1*BLACKBERRY*PLATE~
CAR*AUDI*A5*NXP~
CAR*MAZDA*CX3*NXP*DIGITAL~
CAR*BMW*X5*GOOGLE~
SUV*FORD*EXPLORER*GLASS*SAFE~
CAR*FORD*FUSION*BLACKBERRY~
CAR*GM*YUKON**~

代码说明: 对于input.txt文件中的所有“ CAR”段,我正在尝试使用shuf命令使用来自parts.txt文件的随机数据更新该行中的第3位。 行(input.txt)中的每个字段都由*分隔,行定界符为〜。

问题:我们可以改善上述while语句的性能吗? 我尝试使用下面的代码一次性写入output.txt,而不是在while循环中多次写入,但这仍然需要10MB input.txt文件的时间

 (IFS=*; printf "%s~" "${data[*]}";)
done < input.txt > output.txt 

我在网上搜索,每个人都在说Pearl在这种情况下效果很好。我们可以使用Pearl命令编写while循环吗?

3 个答案:

答案 0 :(得分:2)

进行优化时,第一步是确定读取输入文件花了多长时间,而对其不执行任何操作。在我的系统上,一个10MB的文件只需要百分之几秒。

因此,现在我们知道将花费最少的时间,我们需要研究优化策略。在示例代码中,您将打开parts.txt并从文件系统中为输入文件中的每个记录读取该文件。因此,您正在大量扩展所需的工作量。如果您可以将零件文件保留在内存中,并为输入文件中的每条记录获取一个随机元素,那就更好了。

您可以进行的下一个优化是避免在每次需要零件时都重新排列零件清单。最好抓取随机元素,而不是随机元素。

对于不是以CAR开头的任何记录,您也可以跳过任何处理,但这似乎是次要的优势。

无论如何,以下实现这些目标:

#!/usr/bin/env perl

use strict;
use warnings;
use Getopt::Long;
use Time::HiRes qw(time);

my ($parts_file, $input_file, $output_file) = ('parts.txt', 'input.txt', 'output.txt');

GetOptions(
    "parts=s",  \$parts_file,
    "input=s",  \$input_file,
    "output=s", \$output_file,
);

my $t0 = time;
chomp(
    my @parts = do {
        open my $fh, '<', $parts_file or die "Cannot open $parts_file: $!\n";
        <$fh>;
    }
);

open my $input_fh, '<', $input_file or die "Cannot open $input_file for input: $!\n";
local $/ = '~';

open my $out_fh,   '>', $output_file or die "Cannot open $output_file for output: $!\n";

my $rec_count = 0;
while (my $rec = <$input_fh>) {
    chomp $rec;
    $rec =~ s{^
        (CAR\*(?:[^*]+\*){2})
        [^*]+
    }{
        $1 . $parts[int(rand(@parts))]
    }xe;
    ++$rec_count;
    print $out_fh "$rec$/";
}

close $out_fh or die "Cannot close output file $output_file: $!\n";
printf "Elapsed time: %-.03f\nRecords: %d\n", time-$t0, $rec_count;

在我的系统上,包含488321条记录(大约10MB)的文件需要0.588秒的时间来处理。

根据您自己的需要,您将需要使用此Perl脚本并对其进行修改,以更强大地处理文件名和文件系统路径。不过,这并不是所提问题的一部分。该代码的主要目的是演示可以在哪里进行优化。例如,将工作移出循环;我们只打开一次零件文件,我们只读取一次,并且我们从不随机播放;我们只是从内存的零件列表中随机抽取一个项目。

由于命令行“单行代码”是如此方便,因此我们应该看看是否可以简化为一个。通常,通过使用-l-a-p-F-e开关,可以在Perl“单线”中实现等效功能(I放任其流向多行):

perl -l0176  -apF'\*' -e '
    BEGIN{
        local $/ = "\n";
        chomp(@parts = do {open $fh, "<", shift(@ARGV); <$fh>})
    }
    $F[0] =~ m/^CAR/ && $F[3] =~ s/^\w+$/$parts[int(rand(@parts))]/e;
    $_ = join("*", @F);
' parts.txt input.txt >output.txt

这是它的工作方式:

-p开关告诉Perl在STDIN上遍历命令行中指定的文件中的每一行,或者如果未指定则遍历。对于每一行,将其值放入$_中,然后继续进行下一行,将$_的内容打印到STDOUT。这使我们有机会修改$_,以便将更改写入STDOUT。但是我们使用-l开关,它可以指定代表不同记录分隔符的八进制值。在这种情况下,我们为~字符使用八进制值。这导致-p遍历由~而不是\n分隔的记录。此外,-l开关还会在输入上剥离记录分隔符,并在输出上取代记录分隔符。

但是,我们也使用-a-F开关。 -a告诉Perl将输入自动拆分到@F数组中,而-F让我们指定我们要在*字符上自动拆分。由于-F接受PCRE模式,并且*被视为PCRE中的量词,因此我们使用反斜杠对其进行转义。

接下来,-e开关说要评估以下字符串作为代码。最后,我们可以讨论代码字符串。首先有一个BEGIN{...}块,它从@ARGV移出一个值,并将其用作要从中读取零件清单的文件的名称。一旦该文件名被移开,脚本后面的-p开关将不考虑读取该文件名(BEGIN块发生在隐式-p循环之前)。因此,只需考虑BEGIN{...}块中的代码即可将记录分隔符暂时设置为换行符,将零件文件读入数组,然后再次将记录分隔符放回为~

现在,我们可以继续通过begin块。 @F已成为保存给定记录中的字段的容器。您希望交换的第四个字段(偏移量3)。检查第一个字段(偏移量0)是否以CAR开头。如果是这样,则将第4个字段的内容设置为我们部件数组中的随机元素,但前提是该字段包含一个或多个字符。

然后,我们将由星号分隔的字段重新合并在一起,并将结果分配回$_。我们的工作完成了。借助-p开关,Perl将$_的内容写入STDOUT,然后附加记录分隔符~

最后,我们首先在命令行中指定零件文件的路径,然后指定输入文件的路径,然后将STDOUT重定向到我们的输出文件。

答案 1 :(得分:2)

我认为

awk是您的答案

awk 'BEGIN{while(getline<"parts.txt")r[++i]=$0;
           FS=OFS="*";
           RS=ORS="~";
           srand()}
     $1=="CAR"&&$4{$4=r[1+int(i*rand())]}
     1' input.txt >output.txt

说明:

r[]是一个仅包含parts.txt的所有行的数组。

输入和输出字段以及记录分隔符设置为与input.txt文件的格式匹配。

srand()植入rand()函数(具有一天中的时间),因此您每次不会获得相同的随机元素序列。

如果满足更改第四字段的条件,则第四字段将更改为r的随机元素。

最后的1仅导致打印行,无论更改还是未更改。

答案 2 :(得分:2)

我完全同意,除了bash之外,还有其他语言会变得更加容易和快捷。

仍然,有些日子我无法抗拒挑战。使Shell脚本快速运行的关键是在Shell中执行尽可能少的操作。尝试找到一种使用外部实用程序批量工作而不是逐行工作的方法。

以下shell脚本是一个粗略的示例。为了避免在shell中循环,它做了几件事:

  • shuf的Gnu版本提供了-r标志,以生成从输入中获取的(可能是无限的)随机行序列,而不是对输入进行混洗。

    < / li>
  • paste命令对两个输入流进行逐行连接。 (不幸的是,它没有办法在最短的流结束时停止,因此您不能将其与无限流一起使用。这会迫使对输入文本进行额外的尴尬扫描,以便计算行数。) / p>

  • 可以将标准“第一个字段为CAR且第四个字段不为空”编码为单个正则表达式。这样,我们就可以通过一次调用sed来进行所有选择和替换。

  • 输入文件使用~而不是换行符来分隔记录,这对于大多数Linux文本文件工具来说都是尴尬的。我们可以使用tr '~' '\n'将波浪号转换为换行符,并使用tr '\n' '~'将波浪线最后转换为换行符。

所以这是脚本:

# Count the number of "lines" in the input:
count=$(tr '~' '\n' <input.txt | wc -l)
# (paste) Paste together a column of random parts with the original input;
# (sed)   then substitute  what is now the fifth column with the new first column
#         if the criteria are met.
# (cut)   Finally strip out the column of random parts and
# (tr)    restore the record terminator ~ to return to the original format:
paste -d '*' <(shuf -rn$count parts.txt) \
             <(tr '~' '\n' <input.txt) |
sed -E 's/^([^*]+)([*]CAR([*][^*]+){2}[*])[^*]+/\1\2\1/' |
cut -f2- -d'*' |
tr '\n' '~'

这是一个示例运行:

# The input is 500,000 lines -- about 10MB -- created at random
# from the short input data in the question
$ tr '~' '\n' < input.txt | wc
500000  500000 10498615
$ tr '~' '\n' < input.txt | head
CAR*BMW*X5*SEAT
SUV*FORD*EXPLORER*GLASS*SAFE
CAR*GM*YUKON**
CAR*BMW*X1*BUMBER*PLATE
SUV*FORD*EXPLORER*GLASS*SAFE
SUV*FORD*EXPLORER*GLASS*SAFE
CAR*AUDI*A5*HOOD
CAR*AUDI*A5*HOOD
CAR*AUDI*A5*HOOD
CAR*FORD*FUSION*QNX

# The script takes a couple of seconds
$ time ./xform.sh > output.txt

real    0m1.517s
user    0m1.690s
sys     0m0.121s

# It seems to do the right thing:
$ tr '~' '\n' < output.txt | head
CAR*BMW*X5*NXP
SUV*FORD*EXPLORER*GLASS*SAFE
CAR*GM*YUKON**
CAR*BMW*X1*GOOGLE*PLATE
SUV*FORD*EXPLORER*GLASS*SAFE
SUV*FORD*EXPLORER*GLASS*SAFE
CAR*AUDI*A5*GOOGLE
CAR*AUDI*A5*BLACKBERRY
CAR*AUDI*A5*BLACKBERRY
CAR*FORD*FUSION*NXP

这是上述脚本的一个版本,要求您指定字段0的值(“ $segment”)和要替换的字段号(“ $position”)作为脚本参数。它完全缺乏对参数有效性的检查,并且也不提供默认值。健壮的脚本会做得更好。希望它能对如何参数化脚本提供一些想法。 (通过使用提供的参数构建sed正则表达式来完成此操作。)

#!/bin/bash
# $1 is the string to match in field 0. It must not contain / nor any regex
# metacharacter.
# $2 is the number of the field to substitute. It must be > 0.
# Make the sed command:
sedcmd='s/^([^*]*)([*]'$1'[*]([^*]*[*]){'$(($2-1))'})([^*]+)/\1\2\1/'
# Count the number of "lines" in the input:
count=$(tr '~' '\n' <input.txt | wc -l)
# (paste) Paste together a column of random parts with the original input;
# (sed)   then substitute  what is now the (position+1) column with the new first column
#         if the criteria are met.
# (cut)   Finally strip out the column of random parts and
# (tr)    restore the record terminator ~ to return to the original format:
paste -d '*' <(shuf -rn$count parts.txt) \
             <(tr '~' '\n' <input.txt) |
sed -E "$sedcmd" |
cut -f2- -d'*' |
tr '\n' '~'

用法:

$ time ./xform.sh CAR 3 > output.txt

real    0m1.519s
user    0m1.712s
sys     0m0.120s