解析巨大的文件shell(或其他脚本语言)

时间:2015-09-02 12:20:35

标签: python bash performance shell

我正在尝试解析一个巨大的文件(大约13 GB)并将其转换为csv(也可以将它转置为两个或三个)。 该文件在一行上有记录,这就是为什么它有500.000.000行。此外,属性可能因记录而异 - 某些列可能会出现,有些列可能会出现。 我想出了一个用于转置它的shell脚本,但处理1.000.000行需要12分钟,因此解析孔文件需要100个小时。

shell脚本如下:

#############################################
# Define the usage
#############################################

gUsage="
usage: %> `basename $0` <Run Date> <Input path> <Preprocessing path> <filename> 

where
    Input path:    Generic folder where the input file is for transposing
    Preprocessing path:   Generic folder where the processed file will be moved
    filename:   Template for filename

"

ls_current_date=`date +'%Y-%m-%d'`
ls_current_time=`date +'%H%M%S'`
ls_run_name="${ls_current_date}"_"${ls_current_time}"

i=-1
j=0
d=-1

# Check number of parameters 
if [ $# -ne 4  ]; then
    echo "" 
    echo "ERROR: Expecting 4 parameters" 
    echo "$gUsage" 
    exit
fi


ls_current_date=`date +'%Y-%m-%d'`
ls_current_time=`date +'%H%M%S'`
ls_run_name="${ls_current_date}"_"${ls_current_time}"




#############################################
# VN Declare & Check User Parameters + input files existence
#############################################



p_InputPath=$2
p_PreprocessingPath=$3
p_filename=$4

echo "Start time : $ls_run_name " > "${p_PreprocessingPath}/log.txt"
echo " Starting the transposing process..." >> "${p_PreprocessingPath}/log.txt"
echo "  " >> "${p_PreprocessingPath}/log.txt"
echo "  " >> "${p_PreprocessingPath}/log.txt"
### Parameter 1 is the Run Date will test for TODAY (today's date in the format YYYY-MM-DD)

if [ "$1" -eq "TODAY" ]; then
  p_Rundate=`date +'%Y-%m-%d'` 
else
 p_Rundate=$1
fi


echo "*************************************************************" 
echo "Checking File Existence" 
echo "*************************************************************"   

ODSM_FILE="$p_InputPath/$p_filename"

if [ -f $ODSM_FILE ]; 
then
   echo "Source file ODSM found: $ODSM_FILE !" 
else
   echo "ERROR: source file ODSM_FILE does not exist or does not match the pattern $ODSM_FILE." 
   exit
fi

#Define the header of the file
header="entry-id;kmMsisdn;serialNumber;kmSubscriptionType;kmSubscriptionType2;kmVoiceTan;kmDataTan;kmPaymentMethod;kmMccsDate;kmCustomerBlocked;kmNetworkOperatorBlocked;kmBlockedNetwork;kmMmpNoStatus;kmMmpM3cCreditLimit;kmMmpM3cStatus;kmMmpM3cStatusDate;kmMmpM3cRegistrationDate;creatorsName;createTimestamp;modifiersName;modifyTimestamp;kmBrandName;objectClass;cn;kmBlockedServices;kmServiceProvider" 
delimiter=";"
number_col=$(grep -o "$delimiter" <<< "$header" | wc -l)
number_col2=`expr "$number_col + 1" | bc`

#Create the new file 
v=$(basename $p_filename)
name=${v%.*}
extension=${v#*.}
p_shortFileName=$name
#Insert Header in file

p_newFileName="${p_PreprocessingPath}/${p_shortFileName}_Transposed.csv"
echo $header > $p_newFileName

#Create the matrix with the columns and their values


declare -A a
#Parse line by line the file
while read -r line;
do  
    var=$line
    #echo $line
    Column_Name=${var%:*}
    Column_Value=${var#*:}
    var="# entry-id"
    if [[ "$Column_Name" == "$var" && $Column_Value -ne 1 ]];
    then
        ((i++))
        if [ $i -gt 0 ];
        then
            z=$(($i-1))
            #Write the previous loaded record

            echo ${a[$z,0]} ${a[$z,1]} ${a[$z,2]} ${a[$z,3]} ${a[$z,4]} ${a[$z,5]} ${a[$z,6]} ${a[$z,7]} ${a[$z,8]} ${a[$z,9]} ${a[$z,10]} ${a[$z,11]} ${a[$z,12]} ${a[$z,13]} ${a[$z,14]} ${a[$z,15]} ${a[$z,16]} ${a[$z,17]} ${a[$z,18]} ${a[$z,19]} ${a[$z,20]} ${a[$z,21]} ${a[$z,22]} ${a[$z,23]} ${a[$z,24]} ${a[$z,25]} >> $p_newFileName

        fi
        c=0
        a[$i,0]=";"
        a[$i,1]=";"
        a[$i,2]=";"
        a[$i,3]=";"
        a[$i,4]=";"
        a[$i,5]=";"
        a[$i,6]=";"
        a[$i,7]=";"
        a[$i,8]=";"
        a[$i,9]=";"
        a[$i,10]=";"
        a[$i,11]=";"
        a[$i,12]=";"
        a[$i,13]=";"
        a[$i,14]=";"
        a[$i,15]=";"
        a[$i,16]=";"
        a[$i,17]=";"
        a[$i,18]=";"
        a[$i,19]=";"
        a[$i,20]=";"
        a[$i,21]=";"
        a[$i,22]=";"
        a[$i,23]=";"
        a[$i,24]=";"
        a[$i,25]=";"
        a[$i,26]=" "

        a[$i,0]="$Column_Value ;"
        #v[$i]=$i

    elif [[ $Column_Name == "kmMsisdn" && $i -gt -1 ]];
    then
        a[$i,1]="$Column_Value ;"
    elif [[ $Column_Name == "serialNumber" && $i -gt -1 ]];
    then
        a[$i,2]="$Column_Value ;"
    elif [[ $Column_Name == "kmSubscriptionType" && $i -gt -1 ]];
    then
        a[$i,3]="$Column_Value ;"
    elif [[ $Column_Name == "kmSubscriptionType2" && $i -gt -1 ]];
    then
        a[$i,4]="$Column_Value ;"
    elif [[ $Column_Name == "kmVoiceTan" && $i -gt -1 ]];
    then
        a[$i,5]="$Column_Value ;"
    elif [[ $Column_Name == "kmDataTan" && $i -gt -1 ]];
    then
        a[$i,6]="$Column_Value ;"
    elif [[ $Column_Name == "kmPaymentMethod" && $i -gt -1 ]];
    then
        a[$i,7]="$Column_Value ;"
    elif [[ $Column_Name == "kmMccsDate" && $i -gt -1 ]];
    then
        a[$i,8]="$Column_Value ;"
    elif [[ $Column_Name == "kmCustomerBlocked" && $i -gt -1 ]];
    then
        a[$i,9]="$Column_Value ;"
    elif [[ $Column_Name == "kmNetworkOperatorBlocked" && $i -gt -1 ]];
    then
        a[$i,10]="$Column_Value ;"
    elif [[ $Column_Name == "kmBlockedNetwork" && $i -gt -1 ]];
    then
        a[$i,11]="$Column_Value ;"
    elif [[ $Column_Name == "kmMmpNoStatus" && $i -gt -1 ]];
    then
        a[$i,12]="$Column_Value ;"
    elif [[ $Column_Name == "kmMmpM3cCreditLimit" && $i -gt -1 ]];
    then
        a[$i,13]="$Column_Value ;"
    elif [[ $Column_Name == "kmMmpM3cStatus" && $i -gt -1 ]];
    then
        a[$i,14]="$Column_Value ;"
    elif [[ $Column_Name == "kmMmpM3cStatusDate" && $i -gt -1 ]];
    then
        a[$i,15]="$Column_Value ;"
    elif [[ $Column_Name == "kmMmpM3cRegistrationDate" && $i -gt -1 ]];
    then
        a[$i,16]="$Column_Value ;"
    elif [[ $Column_Name == "creatorsName" && $i -gt -1 ]];
    then
        a[$i,17]="$Column_Value ;"
    elif [[ $Column_Name == "createTimestamp" && $i -gt -1 ]];
    then
        a[$i,18]="$Column_Value ;"
    elif [[ $Column_Name == "modifiersName" && $i -gt -1 ]];
    then
        a[$i,19]="$Column_Value ;"
    elif [[ $Column_Name == "modifyTimestamp" && $i -gt -1 ]];
    then
        a[$i,20]="$Column_Value ;"
    elif [[ $Column_Name == "kmBrandName" && $i -gt -1 ]];
    then
        a[$i,21]="$Column_Value ;"
    elif [[ $Column_Name == "objectClass" && $i -gt -1 ]];
    then
        if [ $c -eq 0 ];
        then 
        a[$i,22]="$Column_Value ;"
        ((c++))
        else
        a[$i,22]="$Column_Value+${a[$i,22]}"
        ((c++))
        fi
    elif [[ $Column_Name == "cn" && $i -gt -1 ]];
    then
        a[$i,23]="$Column_Value ;"
    elif [[ $Column_Name == "kmBlockedServices" && $i -gt -1 ]];
    then
        a[$i,24]="$Column_Value ;"
    elif [[ $Column_Name == "kmServiceProvider" && $i -gt -1 ]];
    then
        a[$i,25]="$Column_Value "
    fi
done < $ODSM_FILE 
#Write the last line of the matrix
echo ${a[$i,0]} ${a[$i,1]} ${a[$i,2]} ${a[$i,3]} ${a[$i,4]} ${a[$i,5]} ${a[$i,6]} ${a[$i,7]} ${a[$i,8]} ${a[$i,9]} ${a[$i,10]} ${a[$i,11]} ${a[$i,12]} ${a[$i,13]} ${a[$i,14]} ${a[$i,15]} ${a[$i,16]} ${a[$i,17]} ${a[$i,18]} ${a[$i,19]} ${a[$i,20]} ${a[$i,21]} ${a[$i,22]} ${a[$i,23]} ${a[$i,24]} ${a[$i,25]} >> $p_newFileName


echo "Created transposed file:  $p_newFileName ."

ls_current_date2=`date +'%Y-%m-%d'`
ls_current_time2=`date +'%H%M%S'`
ls_run_name2="${ls_current_date2}"_"${ls_current_time2}"
echo "Completed " 
echo "End time : $ls_run_name2 " >> "${p_PreprocessingPath}/log.txt"
`

您可以在下面找到该文件的示例(条目1是文件的标题,我根本不需要它。)

version: 1

# entry-id: 1
dn: ou=CONNECTIONS,c=NL,o=Mobile
modifyTimestamp: 20130223124344Z
modifiersName: cn=directory manager
aci: (targetattr = "*") 

# entry-id: 3
dn: kmmsisdn=31653440000,ou=CONNECTIONS,c=NL,o=Mobile
modifyTimestamp: 20331210121726Z
modifiersName: cn=directory manager
cn: MCCS
kmBrandName: VOID
kmBlockedNetwork: N
kmNetworkOperatorBlocked: N
kmCustomerBlocked: N
kmMsisdn: 31653440000
objectClass: top
objectClass: device
objectClass: kmConnection
serialNumber: 204084400000000
kmServiceProvider: 1
kmVoiceTan: 25
kmSubscriptionType: FLEXI
kmPaymentMethod: ABO
kmMccsDate: 22/03/2004
nsUniqueId: 2b72cfe9-f8b221d9-80088800-00000000

# entry-id: 4
dn: kmmsisdn=31153128215,ou=CONNECTIONS,c=NL,o=Mobile
modifyTimestamp: 22231210103328Z
modifiersName: cn=directory manager
cn: MCCS
kmMmpM3cStatusDate: 12/01/2012
kmMmpM3cStatus: Potential
kmBrandName: VOID
kmBlockedNetwork: N
kmNetworkOperatorBlocked: N
kmCustomerBlocked: N
kmMsisdn: 31153128215
objectClass: top
objectClass: device
objectClass: kmConnection
objectClass: kmMultiMediaPortalService
serialNumber: 214283011000000
kmServiceProvider: 1
kmVoiceTan: 25
kmSubscriptionType: FLEXI
kmPaymentMethod: ABO
kmMccsDate: 22/03/2004
nsUniqueId: 92723fea-f8e211d9-8011000-01110000

如果使用shell脚本无法实现这一点。你能否建议一些可以更快地完成它的东西(perl,python)。我不知道任何其他脚本语言,但我可以学习:)。

5 个答案:

答案 0 :(得分:2)

我在评论中说shell read很慢,每个记录打开输出一次。

你的shell脚本版看起来似乎永远不会清空它的关联数组,但也永远不会重用旧的。因此,最终你的shell将使用大量内存,因为它将每个记录的条目键入记录计数器。

您只是将记录从空行分隔的块重新格式化为单行,其中字段用空格分隔。这并不难,也不需要将以前的记录保存在内存中。

我的想法和Walter A一样。这个awk程序是解决问题的最重要方法。

在将记录打印到csv行后,请注意delete a,以清除字段。

awk   -vOFS=' ; ' -F'\\s*:\\s*' '/^#/{print; this_is_for_debugging }
    function output_rec(){ print a["kmMsisdn"], a["serialNumber"], a["kmSubscriptionType"], a["objectClass"] }
    /^$/ { output_rec(); delete a;next}
    END  { output_rec() }
    {  sub(/\s+$/, "", $2);  # strip trailing whitespace if needed
       if ($1 == "objectClass" && a[$1] )
           { a[$1]= (a[$1] "+" $2) } else { a[$1]=$2; }
    }' foo.etl

我会留给你打印剩下的字段。 (它们已经被a[$1] = $2语句解析为“objectClass”条件的else块。)

在空白上拆分*:空格*意味着我们不必在第二个字段的开头处去除空白。显然-F arg需要加倍的反斜杠。添加NF&lt; = 2的检查可能是个好主意,以确保没有任何行有多个:

样本输入的输出

 ;  ;  ; 
# entry-id: 1
 ;  ;  ; 
# entry-id: 3
31653440000 ; 204084400000000 ; FLEXI ; top+device+kmConnection
# entry-id: 4
31153128215 ; 214283011000000 ; FLEXI ; top+device+kmConnection+kmMultiMediaPortalService

为避免在打印标题行和打印字段之间出现数据重复,您可以将字段名称放在数组中,并在两个地方循环显示。

我原本以为-v RS='\n\n'会使每个块成为AWK记录。实际上,FS='\n'可能仍然有用。然后,您可以遍历字段(每条记录的行),并将其拆分为:。如果记录包含:是不可能的,就像您的shell脚本所假设的那样,split分割很容易(与我们-F设置{{1}相同}})。

(在您的shell版本中,使用FS删除最长的后缀(包括所有Column_Name=${var%%:*} s),而不是最短的。或者使用:

这可能更好用perl编写,因为它对于awk程序来说变得笨重。 perl可以更容易地在行上的第一个IFS=: read Column_Name Column_Value进行拆分。

答案 1 :(得分:1)

awk -vOFS=' ; ' -F: '
 function output_rec(){ gsub(/[ \t]+$/, "",$2);
 print a["entry-id"],a["kmMsisdn"],a["kmSubscriptionType"],a["kmSubscriptionType2"],a["kmVoiceTan"],a["kmDataTan"],a["kmPaymentMethod"],a["kmMccsDate"],a["kmCustomerBlocked"],a["kmNetworkOperatorBlocked"],a["kmBlockedNetwork"],a["kmMmpNoStatus"],a["kmMmpM3cCreditLimit"],a["kmMmpM3cStatus"],a["kmMmpM3cStatusDate"],a["kmMmpM3cRegistrationDate"],a["creatorsName"],a["createTimestamp"],a["modifiersName"],a["modifyTimestamp"],a["kmBrandName"],a["objectClass"],a["cn"],a["kmBlockedServices"],a["kmServiceProvider"]}
 /entry-id/ {output_rec(); delete a;a["entry-id"]=$2;next}
 END  { output_rec() }
   {gsub(/[ \t]+$/, "",$2);
   if ($1 == "objectClass" && a[$1] ) { a[$1]= (a[$1]"+"$2) } else { a[$1]=$2; } }' $ODSM_FILE >> $p_newFileName

答案 2 :(得分:1)

使用perl我会这样做:

#!/usr/bin/env perl
use strict;
use warnings;

use Text::CSV;

#configure output columns and ordering.
my @output_cols = qw (
    entry-id kmMsisdn serialNumber
    kmSubscriptionType kmSubscriptionType2
    kmVoiceTan kmDataTan kmPaymentMethod
    kmMccsDate kmCustomerBlocked
    kmNetworkOperatorBlocked kmBlockedNetwork
    kmMmpNoStatus kmMmpM3cCreditLimit
    kmMmpM3cStatus kmMmpM3cStatusDate
    kmMmpM3cRegistrationDate creatorsName
    createTimestamp modifiersName
    modifyTimestamp kmBrandName
    objectClass cn
    kmBlockedServices kmServiceProvider
);

#set up our csv engine - separator of ';' particularly. 
#eol will put a linefeed after each line (might want "\r\n" on DOS)
my $csv = Text::CSV->new(
    {   sep_char => ';',
        eol      => "\n",
        binary   => 1
    }
);

#open output
open( my $output, '>', 'output_file.csv' ) or die $!;
#print header row. 
$csv->print( $output, \@output_cols );
#set columns, so print_hr knows ordering. 
$csv->column_names(@output_cols);

#set record separator to double linefeed
local $/ = "\n\n";

#iterate the 'magic' filehandle. 
#this either reads data piped on `STDIN` _or_ a list of files specified on 
#command line. 
#e.g. myscript.pl file_to_process 
#or 
#cat file_to_process | myscript.pl
#this thus emulates awk/grep/sed etc.
#NB works one record at a time - so a chunk all the way to a double line feed. 

while (<>) {
    #pattern match the key-value pairs on this chunk of data (record).
    #multi-line block.
    #because this regex will return a list of paired values (note - "g" and "m" flags), we can
    #insert it directly into a hash (associative array)
    my %row = m/^(?:# )?([-\w]+): (.*)$/mg;

    #skip if this row is incomplete. Might need to be entry-id? 
    next unless $row{'kmMsisdn'};
    $csv->print_hr( $output, \%row );
}
close ( $output );

这会产生:

entry-id;kmMsisdn;serialNumber;kmSubscriptionType;kmSubscriptionType2;kmVoiceTan;kmDataTan;kmPaymentMethod;kmMccsDate;kmCustomerBlocked;kmNetworkOperatorBlocked;kmBlockedNetwork;kmMmpNoStatus;kmMmpM3cCreditLimit;kmMmpM3cStatus;kmMmpM3cStatusDate;kmMmpM3cRegistrationDate;creatorsName;createTimestamp;modifiersName;modifyTimestamp;kmBrandName;objectClass;cn;kmBlockedServices;kmServiceProvider
3;31653440000;204084400000000;FLEXI;;25;;ABO;22/03/2004;N;N;N;;;;;;;;"cn=directory manager";20331210121726Z;VOID;kmConnection;MCCS;;1
4;31153128215;214283011000000;FLEXI;;25;;ABO;22/03/2004;N;N;N;;;Potential;12/01/2012;;;;"cn=directory manager";22231210103328Z;VOID;kmMultiMediaPortalService;MCCS;;1

注意:由于我们正在使用while ( <> ) {,因此我们可以像使用awk / sed一样使用此脚本。 perl将该运算符用作:

  • 数据传输
  • 打开在命令行中指定的文件并阅读它们。

所以你可以:

./myscript.pl filename1 filename2

somecommand_to_generate_data | ./myscript.pl

答案 3 :(得分:0)

这是一个令人印象深刻的shell脚本,但是你解决的问题并不适合传统的shell脚本。我想用echo 并且所有文件写入的输出重定向都会显着减慢 事情发生了。使用适当的编程语言,您可以缓冲您的 文件写入 - 一次读取多行。

你已经提到了Perl和Python,这些正是我的意思 建议。这两种语言都被系统管理员使用,尽管Python似乎更受数据科学家的青睐。我倾向于两者,Python也是我个人的最爱,因为我喜欢它的语法,与伪代码的相似性以及我使用的大多数库都易于使用 - 并且阅读。

学习选择哪种语言,祝你好运。 (关于哪种语言最好的讨论可能会导致这个问题因过于基于意见而被关闭)。

答案 4 :(得分:0)

你可以用awk试试。
awk具有关联数组,因此您可以将-F: '{row[$1]=$2}'之类的内容用于普通行。 您可以在有新设置时打印/重置。

/entry-id/ '{print row["kmMsisdn"], " , ", row["serialNumber"], " , ", row["kmSubscriptionType]}'

并且在支持时通过删除它{delete row}使数组为空。

它应该比你当前的版本快很多。

编辑

我看了@Peter的答案,刚刚编辑了他的解决方案 我添加了其他字段,使用$ ODSM_FILE,$ p_newFileName,并更改了查找新记录的逻辑:
每行后都有一个入口号为
感谢彼得的awk代码和他的解释。

  awk -vOFS=' ; ' -F'\\s*:\\s*' ' BEGIN {
                a["entry-id"]="entry-id"; 
                a["kmMsisdn"]="kmMsisdn"; 
                a["serialNumber"]="serialNumber";
                a["kmSubscriptionType"]="kmSubscriptionType";
                a["kmSubscriptionType2"]="kmSubscriptionType2";
                a["kmVoiceTan"]="kmVoiceTan";                  
                a["kmDataTan"]="kmDataTan";                    
                a["kmPaymentMethod"]="kmPaymentMethod";        
                a["kmMccsDate"]="kmMccsDate";                  
                a["kmCustomerBlocked"]="kmCustomerBlocked";    
                a["kmNetworkOperatorBlocked"]="kmNetworkOperatorBlocked";
                a["kmBlockedNetwork"]="kmBlockedNetwork";                
                a["kmMmpNoStatus"]="kmMmpNoStatus";                      
                a["kmMmpM3cCreditLimit"]="kmMmpM3cCreditLimit";          
                a["kmMmpM3cStatus"]="kmMmpM3cStatus";                    
                a["kmMmpM3cStatusDate"]="kmMmpM3cStatusDate";            
                a["kmMmpM3cRegistrationDate"]="kmMmpM3cRegistrationDate";
                a["creatorsName"]="creatorsName";                        
                a["createTimestamp"]="createTimestamp";                  
                a["modifiersName"]="modifiersName";
                a["modifyTimestamp"]="modifyTimestamp";
                a["kmBrandName"]="kmBrandName";
                a["objectClass"]="objectClass";
                a["cn"]="cn";
                a["kmBlockedServices"]="kmBlockedServices";
                a["kmServiceProvider"]="kmServiceProvider";
        }
        function output_rec(){ print a["entry-id"],
                a["kmMsisdn"],
                a["serialNumber"],
                a["kmSubscriptionType"],
                a["kmSubscriptionType2"],
                a["kmVoiceTan"],
                a["kmDataTan"],
                a["kmPaymentMethod"],
                a["kmMccsDate"],
                a["kmCustomerBlocked"],
                a["kmNetworkOperatorBlocked"],
                a["kmBlockedNetwork"],
                a["kmMmpNoStatus"],
                a["kmMmpM3cCreditLimit"],
                a["kmMmpM3cStatus"],
                a["kmMmpM3cStatusDate"],
                a["kmMmpM3cRegistrationDate"],
                a["creatorsName"],
                a["createTimestamp"],
                a["modifiersName"],
                a["modifyTimestamp"],
                a["kmBrandName"],
                a["objectClass"],
                a["cn"],
                a["kmBlockedServices"],
                a["kmServiceProvider"] }
        END  { output_rec() }
        /^$/ { next }
        /entry-id/ {output_rec();delete a; a["entry-id"]=$2;next}
        {
           sub(/\s*$/, "", $2); # strip trailing whitespace
           if ($1 == "objectClass") { a[$1]= (a[$1]"+"$2) } else { a[$1]=$2; }
        }'  $ODSM_FILE > $p_newFileName

我用25.000行数据测试它,awk代码比原始代码快30倍。对于1100万行的输入文件,awk解决方案在我的系统上需要40秒 @Peter:干得好!