2つのCSVファイルを読み込んで、ハッシュを使って、同一のキー項目を元に照合を行うスクリプトです。
[2-3-5.ハッシュを使った1対1マッチング(照合)]を変形した処理で、「マスタファイル」と「トランザクションファイル」の両方から統合した1つのファイルを作成する処理として[2-3-3.マージ処理]で取り上げましたが、同様の処理についてハッシュを使って行う方法を以下に挙げます。[2-3-3.マージ処理]と異なる点は、入力データがキー順に並べ替えてある必要がない点です。
注意すべき点は、出力ファイルにあわせてファイルハンドルを設定する点です。この例では、出力ファイルは1つのため、対応するファイルハンドルも1つになっています。仮に「マッチング(照合)したファイル」と「トランザクションファイル」を1つのファイルに出力し、「マスタオンリーファイル」と区別する場合は、ファイルハンドルを2つ設定することになります。マッチング(照合)用のスクリプトをそのまま持ってきて、ファイル名だけを変更すると誤った出力結果になるので注意する必要があります。
統合された1つのファイルに対して、「マスタファイル」や「トランザクションファイル」からどのようにデータをセットするかは、それぞれのケースによって異なるので、各自工夫してください。
「1対1マッチング(照合)」の変形だからといって、複数の異なる出力ファイルのファイルハンドルに対して、同一のデータを指定しないでください。処理の結果が保証されなくなりますので、十分注意してください。
# merge_hash.pl # 内容 : マージプログラム # (2ファイルの1対1マッチングをした後、1ファイルに統合する) # 前提 : マスタファイルとトランザクションファイルの両方とも # マッチングするキーごとに昇順に並べ替えておく必要はない # Copyright (c) 2012 Mitsuo Minagawa, All rights reserved. # (minagawa@fb3.so-net.ne.jp) # 使用方法 : c:\>perl merge_hash.pl # # マスタデータを入れるハッシュ my %data1; # トランザクションデータを入れるハッシュ my %data2; # マージしたデータを入れるハッシュ my %out1; # マスタデータをハッシュに入れる open(IN1,"master.txt"); while($line1 = <IN1>) { chomp($line1); #---区切り文字により、処理を変更する---------- #タブ区切りのとき # @in1 = split("\t",$line1); #カンマ区切りのとき my $tmp = $line1; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @in1 = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); #----------------- $in1_key = $in1[0]; # 配列のリファレンスを生成し、ハッシュに配列を代入する $data1{$in1_key} = [@in1]; } close(IN1); # トランザクションデータをハッシュに入れる open(IN2,"transaction.txt"); while($line2 = <IN2>) { chomp($line2); #---区切り文字により、処理を変更する---------- #タブ区切りのとき # @in2 = split("\t",$line2); #カンマ区切りのとき my $tmp = $line2; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @in2 = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); #----------------- $in2_key = $in2[0]; # 配列のリファレンスを生成し、ハッシュに配列を代入する $data2{$in2_key} = [@in2]; } close(IN2); #マスタのデータを優先させる場合 #foreach my $key (sort keys %data1) { # $out1{$key} = $data1{$key}; #} #foreach my $key (sort keys %data2) { # ハッシュのキーが存在しない場合(トランザクションにしかないとき) # unless (exists $data1{$key}) { # $out1{$key} = $data2{$key}; # } #} #トランザクションのデータを優先させる場合 foreach my $key (sort keys %data2) { $out1{$key} = $data2{$key}; } foreach my $key (sort keys %data1) { #ハッシュのキーが存在しない場合(マスタにしかないとき) unless (exists $data2{$key}) { $out1{$key} = $data1{$key}; } } open(OUT1,">merge.txt"); # ハッシュのキーをソート(並べ替え)しながら foreach my $key (sort keys %out1) { # 配列のリファレンスを配列に戻して出力する print OUT1 join("\t", @{$out1{$key}})."\n"; } close(OUT1);
それでは、スクリプトの解説を個別に行っていきましょう。
マスタファイルの入力
ここではマスタファイルを入力し、CSVファイルを配列にセットした上で、CSVファイルの中でキーとなる項目をハッシュのキーとし、CSVファイルを配列にしたものをハッシュの値としてセットしています。なお、キーとなる項目は1番目の項目を想定していますが、必要であれば、異なる項目に変更します。
# マスタデータをハッシュに入れる open(IN1,"master.txt"); while($line1 = <IN1>) { chomp($line1); #---区切り文字により、処理を変更する---------- #タブ区切りのとき # @in1 = split("\t",$line1); #カンマ区切りのとき my $tmp = $line1; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @in1 = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); #----------------- $in1_key = $in1[0]; # 配列のリファレンスを生成し、ハッシュに配列を代入する $data1{$in1_key} = [@in1]; } close(IN1);
上記では、入力したCSVファイルを各項目をすべてハッシュの値としてセットしていますが、入力したCSVファイルの各項目の一部をハッシュの値としてセットする場合は、
$data1{$in1_key} = [@in1];
の部分を以下のように変更します。
$data1{$in1_key} = [$in1[1],$in1[2]];
トランザクションファイルの入力
次にトランザクションファイルを入力し、CSVファイルを配列にセットした上で、CSVファイルの中でキーとなる項目をハッシュのキーとし、CSVファイルを配列にしたものをハッシュの値としてセットしています。なお、キーとなる項目は1番目の項目を想定していますが、必要であれば、異なる項目に変更します。
# トランザクションデータをハッシュに入れる open(IN2,"transaction.txt"); while($line2 = <IN2>) { chomp($line2); #---区切り文字により、処理を変更する---------- #タブ区切りのとき # @in2 = split("\t",$line2); #カンマ区切りのとき my $tmp = $line2; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @in2 = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); #----------------- $in2_key = $in2[0]; # 配列のリファレンスを生成し、ハッシュに配列を代入する $data2{$in2_key} = [@in2]; } close(IN2);
上記では、入力したCSVファイルを各項目をすべてハッシュの値としてセットしていますが、入力したCSVファイルの各項目の一部をハッシュの値としてセットする場合は、
$data2{$in2_key} = [@in2];
の部分を以下のように変更します。
$data2{$in2_key} = [$in2[1],$in2[2]];
マッチング(2つのファイルの照合)処理
次に入力した2つのデータを照合し、マスタファイルとトランザクションファイルの両方に存在する場合、どちらのデータを優先させるかによって、処理が変わります。
マスタファイルを優先させる場合は、以下のようにします。まず、マスタのデータを出力用のハッシュにセットした後に、マスタファイルにない場合だけ、トランザクションファイルのデータをセットします。
#マスタのデータを優先させる場合 foreach my $key (sort keys %data1) { $out1{$key} = $data1{$key}; } foreach my $key (sort keys %data2) { # ハッシュのキーが存在しない場合(トランザクションにしかないとき) unless (exists $data1{$key}) { $out1{$key} = $data2{$key}; } }
逆にトランザクションファイルを優先させる場合は、以下のようにします。まず、トランザクションのデータを出力用のハッシュにセットした後に、トランザクションファイルにない場合だけ、マスタファイルのデータをセットします。
#トランザクションのデータを優先させる場合 foreach my $key (sort keys %data2) { $out1{$key} = $data2{$key}; } foreach my $key (sort keys %data1) { #ハッシュのキーが存在しない場合(マスタにしかないとき) unless (exists $data2{$key}) { $out1{$key} = $data1{$key}; } }
出力処理
最後にマージしたハッシュからデータを出力します。その際、「@{$out1{$key}}」とすることで、配列のリファレンスを配列に変換します。
open(OUT1,">merge.txt"); # ハッシュのキーをソート(並べ替え)しながら foreach my $key (sort keys %out1) { # 配列のリファレンスを配列に戻して出力する print OUT1 join("\t", @{$out1{$key}})."\n"; }
その他注意すべきこと
その他、注意すべき点は入力データがタブ区切りなどの場合とキーが複数ある場合の処理です。
上記のスクリプトでは、入力ファイルがCSVファイルであれば、どのようなものでも対応できるようになっています。具体的には以下の箇所になります。
CSV形式の入力データを配列に変換する
my $tmp = $line1; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @in1 = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g);
上記のスクリプトはCSV2形式以外の引用符(")を使用する場合を含め、あらゆるCSVファイルに対応できるようになっています(CSV2形式などについては、[3-1-1.固定長データとCSVデータ]を参照してください)が、このCSVファイルに分解するロジックは、大崎 博基(OHZAKI Hiroki)さんの「Perlメモ」に記載されていたものを参考にしています。また、CSV2形式のCSVファイルであれば、上記のスクリプトは以下のように簡略化できます。
1.入力データがCSV2形式の場合、
@in1 = split(",",$line1,-1);
とします。
2.入力データがタブ区切りの場合、
@in1 = split("\t",$line1,-1);
とします。
3.キーが複数の項目から成り立っている場合は、
「$in1_key = $in1[0]」の部分を
$in1_key = $in1[0].$in1[1].$in1[2];
のように変更します(文字列の連結は「.」(ピリオド)を使います)。
01,11111 02,22222 03,33333 05,44444 07,77777 08,88888 09,99999
01,100 02,200 04,400 06,600 07,700 08,800 09,900 10,1000
01 11111 02 22222 03 33333 04 400 05 44444 06 600 07 77777 08 88888 09 99999 10 1000