shell脚本中的性能问题

时间:2016-12-20 09:18:44

标签: bash shell unix

我有一个200 MB的制表符分隔文本文件,包含数百万行。在这个文件中,我有一个包含多个位置的列,如美国,英国,澳大利亚等。

现在我想在此列的基础上打破此文件。虽然这段代码对我来说很好,但面临性能问题,因为根据位置将文件拆分成多个文件需要1个多小时。这是代码:

#!/bin/bash

read -p "Please enter the file to split " file
read -p "Enter the Col No. to split " col_no

#set -x

header=`head -1 $file`

cnt=1
while IFS= read -r line
do
        if [ $((cnt++)) -eq 1 ]
        then
                echo "$line" >> /dev/null
        else
                loc=`echo "$line" | cut -f "$col_no"`
                f_name=`echo "file_"$loc".txt"`
                if [ -f "$f_name" ]
                then
                        echo "$line" >> "$f_name";
                else
                        touch "$f_name";
                        echo "file $f_name created.."
                        echo "$line" >> "$f_name";
                        sed -i '1i '"$header"'' "$f_name"
                fi
        fi

done < $file

这里应用的逻辑是我们只读取整个文件一次,并且根据位置,我们正在创建数据并将其附加到它。

请建议对代码进行必要的改进以提高其性能。

以下是样本数据,用冒号而不是制表符分隔。国家/地区代码位于第4列:

ID1:ID2:ID3:ID4:ID5
100:abcd:TEST1:ZA:CCD
200:abcd:TEST2:US:CCD
300:abcd:TEST3:AR:CCD
400:abcd:TEST4:BE:CCD
500:abcd:TEST5:CA:CCD
600:abcd:TEST6:DK:CCD
312:abcd:TEST65:ZA:CCD
1300:abcd:TEST4153:CA:CCD

2 个答案:

答案 0 :(得分:2)

要记住以下几点:

  1. 使用while read阅读文件很慢
  2. 创建子shell并执行外部进程很慢
  3. 这是文本处理工具的工作,例如awk。

    我建议你使用这样的东西:

    # save first line
    NR == 1 {
        header = $0
        next
    }
    
    {
        filename = "file_" $col  ".txt"
    
        # if country code has changed
        if (filename != prev) {
            # close the previous file
            close(prev)
            # if we haven't seen this file yet
            if (!(filename in seen)) {
                print header > filename
            }
            seen[filename]
        }
    
        # print whole line to file
        print >> filename
        prev = filename
    }
    

    使用以下行中的内容运行脚本:

    awk -v col="$col_no" -f script.awk file
    

    其中$col_no是一个shell变量,包含带有国家/地区代码的列号。

    如果您没有太多不同的国家/地区代码,则可以将所有文件保持打开状态,在这种情况下,您可以取消对close(filename)的调用。

    您可以在问题中提供的示例中测试脚本,如下所示:

    awk -F: -v col=4 -f script.awk file
    

    请注意,我已添加-F:以将输入字段分隔符更改为:

答案 1 :(得分:1)

我认为汤姆走在正确的轨道上,但我会稍微简化一下。

Awk在某些方面是神奇的。其中一种方法是,除非您明确关闭它们,否则它将保持所有输入和输出文件句柄处于打开状态。因此,如果您创建一个包含输出文件名的变量,您可以简单地重定向到您的变量并相信awk会将数据发送到您指定的位置,并在输出用完时最终关闭输出文件

(N.B。这个魔术的延伸是除了重定向之外,你可以保持多个PIPES。想象一下,如果你去cmd="gzip -9 > file_"$4".txt.gz"; print | cmd

以下内容拆分您的文件而不向每个输出文件添加标题。

awk -F: 'NR>1 {out="file_"$4".txt"; print > out}' inp.txt

如果添加标题很重要,则需要更多代码。但并不多。

awk -F: 'NR==1{h=$0;next} {out="file_"$4".txt"} !(out in files){print h > out; files[out]} {print > out}' inp.txt

或者,因为这个单线程现在有点长,我们可以将其拆分出来进行解释:

awk -F: '
  NR==1 {h=$0;next}        # Capture the header
  {out="file_"$4".txt"}    # Capture the output file
  !(out in files){         # If we haven't seen this output file before,
    print h > out;         # print the header to it,
    files[out]             # and record the fact that we've seen it.
  }
  {print > out}            # Finally, print our line of input.
' inp.txt

我根据您在问题中提供的输入数据成功测试了这两个脚本。使用这种类型的解决方案,无需对输入数据进行排序 - 每个文件中的输出将按照输出数据中出现该子集记录的顺序。

注意:awk的不同版本将允许您打开不同数量的打开文件。 GNU awk(gawk)有数千个限制 - 远远超过您可能需要处理的国家数量。 BSD awk版本20121220(在FreeBSD中)似乎在21117文件后用完。 BSD awk版本20070501(在OS X El Capitan中)限制为17个文件。

如果您对可能的打开文件数量没有信心,可以尝试使用以下类似的awk版本:

mkdir -p /tmp/i
awk '{o="/tmp/i/file_"NR".txt"; print "hello" > o; printf "\r%d ",NR > "/dev/stderr"}' /dev/random

您还可以测试打开的管道数量:

awk '{o="cat >/dev/null; #"NR; print "hello" | o; printf "\r%d ",NR > "/dev/stderr"}' /dev/random

(如果你有一个/dev/yes或只是吐出文字行的东西,这会比使用/ dev / random进行输入更好。)

我以前在我自己的awk编程中遇到过这个限制,因为当我需要创建许多输出文件时,我总是使用gawk。 :-P