2つのCSVファイルを読み込んで、ハッシュを使って、同一のキー項目を元に照合を行うスクリプトです。
[2-3-5.ハッシュを使った1対1マッチング(照合)]を変形した処理で、「マスタファイル」と「トランザクションファイル」の両方から統合した1つのファイルを作成する処理として[2-3-3.マージ処理]で取り上げましたが、同様の処理についてハッシュを使って行う方法を以下に挙げます。[2-3-3.マージ処理]と異なる点は、入力データがキー順に並べ替えてある必要がない点です。
注意すべき点は、出力ファイルにあわせてファイルハンドルを設定する点です。この例では、出力ファイルは1つのため、対応するファイルハンドルも1つになっています。仮に「マッチング(照合)したファイル」と「トランザクションファイル」を1つのファイルに出力し、「マスタオンリーファイル」と区別する場合は、ファイルハンドルを2つ設定することになります。マッチング(照合)用のスクリプトをそのまま持ってきて、ファイル名だけを変更すると誤った出力結果になるので注意する必要があります。
「1対1マッチング(照合)」の変形だからといって、複数の異なる出力ファイルのファイルハンドルに対して、同一のデータを指定しないでください。処理の結果が保証されなくなりますので、十分注意してください。
# merge_hash.rb # 内容 : マージプログラム(2ファイルの1対1マッチングをした後、1ファイルに統合する) # 前提 : マスタファイルとトランザクションファイルの両方とも # マージするキーごとに昇順に並べ替えておく必要はない # Copyright (c) 2012-2015 Mitsuo Minagawa, All rights reserved. # (minagawa@fb3.so-net.ne.jp) # 使用方法 : c:\>ruby merge_hash.rb # # ファイルのオープン in1_file = open("master.txt","r") #マスターファイル in2_file = open("transaction.txt","r") #トランザクションファイル out1_file = open("merge.txt","w") #マージファイル # マスターデータを入れるハッシュ data1 = Hash.new # トランザクションデータを入れるハッシュ data2 = Hash.new # マージしたデータを入れるハッシュ out1 = Hash.new # マスターファイルの入力 while (line1 = in1_file.gets) line1.chomp! # タブ区切りのとき # in1 = line1.split("\t",-1) # カンマ区切りのとき in1 = (line1 + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')} # キー項目が単独のとき in1_key = in1[0] # キー項目が複数のとき # 1番目と2番目と3番目の項目がキーとなる場合 # in1_key = in1[0] + in1[1] + in1[2] # マスターデータを格納した配列をハッシュに入れる data1[in1_key] = in1 end # トランザクションファイルの入力 while (line2 = in2_file.gets) line2.chomp! # タブ区切りのとき # in2 = line2.split("\t",-1) # カンマ区切りのとき in2 = (line2 + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')} # キー項目が単独のとき in2_key = in2[0] # キー項目が複数のとき # 1番目と2番目と3番目の項目がキーとなる場合 # in2_key = in2[0] + in2[1] + in2[2] # トランザクションデータを格納した配列をハッシュに入れる data2[in2_key] = in2 end #マスターのデータを優先させる場合 #data1.sort_by{|key| key }.each{|key,value| # out1[key] = data1[key] #} #data2.sort_by{|key| key }.each{|key,value| # ハッシュのキーが存在しない場合(トランザクションにしかないとき) # unless (out1.include?(key)) # out1[key] = data2[key] # end #} #トランザクションのデータを優先させる場合 data2.sort_by{|key| key }.each{|key,value| out1[key] = data2[key] } data1.sort_by{|key| key }.each{|key,value| #ハッシュのキーが存在しない場合(マスタにしかないとき) unless (out1.include?(key)) out1[key] = data1[key] end } # マージしたデータを出力する out1.sort_by{|key| key }.each{|key,value| w_temp = value.join("\t") out1_file.print "#{w_temp}\n" } # ファイルのクローズ in1_file.close in2_file.close out1_file.close
それでは、スクリプトの解説を個別に行っていきましょう。
マスタファイルの入力
ここではマスタファイルを入力し、CSVファイルを配列にセットした上で、CSVファイルの中でキーとなる項目をハッシュのキーとし、CSVファイルを配列にしたものをハッシュの値としてセットしています。なお、キーとなる項目は1番目の項目を想定していますが、必要であれば、異なる項目に変更します。
# マスターファイルの入力 while (line1 = in1_file.gets) line1.chomp! # タブ区切りのとき # in1 = line1.split("\t",-1) # カンマ区切りのとき in1 = (line1 + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')} # キー項目が単独のとき in1_key = in1[0] # キー項目が複数のとき # 1番目と2番目と3番目の項目がキーとなる場合 # in1_key = in1[0] + in1[1] + in1[2] # マスターデータを格納した配列をハッシュに入れる data1[in1_key] = in1 end
上記では、入力したCSVファイルを各項目をすべてハッシュの値としてセットしていますが、入力したCSVファイルの各項目の一部をハッシュの値としてセットする場合は、
data1[in1_key] = in1
の部分を以下のように変更します。
data1[in1_key] = [in1[1],in1[2]]
トランザクションファイルの入力
次にトランザクションファイルを入力し、CSVファイルを配列にセットした上で、CSVファイルの中でキーとなる項目をハッシュのキーとし、CSVファイルを配列にしたものをハッシュの値としてセットしています。なお、キーとなる項目は1番目の項目を想定していますが、必要であれば、異なる項目に変更します。
# トランザクションファイルの入力 while (line2 = in2_file.gets) line2.chomp! # タブ区切りのとき # in2 = line2.split("\t",-1) # カンマ区切りのとき in2 = (line2 + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')} # キー項目が単独のとき in2_key = in2[0] # キー項目が複数のとき # 1番目と2番目と3番目の項目がキーとなる場合 # in2_key = in2[0] + in2[1] + in2[2] # トランザクションデータを格納した配列をハッシュに入れる data2[in2_key] = in2 end
上記では、入力したCSVファイルを各項目をすべてハッシュの値としてセットしていますが、入力したCSVファイルの各項目の一部をハッシュの値としてセットする場合は、
data2[in2_key] = in2
の部分を以下のように変更します。
data2[in2_key] = [in2[1],in2[2]]
マッチング(2つのファイルの照合)処理
次に入力した2つのデータを照合し、マスタファイルとトランザクションファイルの両方に存在する場合、どちらのデータを優先させるかによって、処理が変わります。
マスタファイルを優先させる場合は、以下のようにします。まず、マスタのデータを出力用のハッシュにセットした後に、マスタファイルにない場合だけ、トランザクションファイルのデータをセットします。
マスターのデータを優先させる場合 data1.sort_by{|key| key }.each{|key,value| out1[key] = data1[key] } data2.sort_by{|key| key }.each{|key,value| ハッシュのキーが存在しない場合(トランザクションにしかないとき) unless (out1.include?(key)) out1[key] = data2[key] end }
逆にトランザクションファイルを優先させる場合は、以下のようにします。まず、トランザクションのデータを出力用のハッシュにセットした後に、トランザクションファイルにない場合だけ、マスタファイルのデータをセットします。
#トランザクションのデータを優先させる場合 data2.sort_by{|key| key }.each{|key,value| out1[key] = data2[key] } data1.sort_by{|key| key }.each{|key,value| #ハッシュのキーが存在しない場合(マスタにしかないとき) unless (out1.include?(key)) out1[key] = data1[key] end }
出力処理
最後にマージしたハッシュからデータを出力します。
# マージしたデータを出力する out1.sort_by{|key| key }.each{|key,value| w_temp = value.join("\t") out1_file.print "#{w_temp}\n" }
その他注意すべきこと
その他、注意すべき点は入力データがタブ区切りなどの場合とキーが複数ある場合の処理です。
上記のスクリプトでは、入力ファイルがCSVファイルであれば、どのようなものでも対応できるようになっています。具体的には以下の箇所になります。
CSV形式の入力データを配列に変換する
# カンマ区切りのとき in1 = (line1 + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')}
上記のスクリプトはCSV2形式以外の引用符(")を使用する場合を含め、あらゆるCSVファイルに対応できるようになっています(CSV2形式などについては、[3-1-1.固定長データとCSVデータ]を参照してください)が、CSV2形式のCSVファイルであれば、上記のスクリプトは以下のように簡略化できます。
1.入力データがCSV2形式の場合、
in1 = line1.split(",",-1)
とします。
2.入力データがタブ区切りの場合、
in1 = line1.split("\t",-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