2つのCSVファイルを読み込んで、同一のキー項目を元に照合を行うスクリプトです。2つのファイルは「マスタファイル」と「トランザクションファイル」と呼びます。照合した結果により、3つのファイルに、<ふるいわけ>をします。3つのファイルとは、、該当するキー項目が「マスタファイル」と「トランザクションファイル」の両者に存在する<マッチングファイル>、該当するキー項目が「マスタファイル」にしか存在しない<マスタオンリーファイル>、該当するキー項目が「トランザクションファイル」にしか存在しない<トランザクションオンリーファイル>のことです。
「1対nマッチング(照合)」は、「トランザクションファイル」において、同一のキーを持つレコードが複数存在する場合のマッチング(照合)処理です。
1対1マッチング(照合)との相違は、マスタファイルのキーとトランザクションファイルのキーがマッチしたとき、トランザクションファイルについて繰り返し処理を行う点です。繰り返し処理は「until」を利用し、「until」処理の内部でトランザクションファイルを入力しています。「until」処理が終了する条件は、マスタファイルのキーと異なることです。
この例では、「マッチング(照合)したときの処理」を「until」処理の内部で行っていますが、トランザクションファイルの合計値を出力するような場合は、「until」処理を抜けた後に行うことになります。
下記に示したのは、スクリプトの構造を示したPAD図です。この図と実際のスクリプトを比較しながら、内容を理解してください。なお、入力データはキー順に並べ替えてあることが前提となっています。Perlを利用して並べ替えを行う場合については、[2-4.ソート処理]を参照してください。
マッチング(照合)した場合は、「マスタファイル」と「トランザクションファイル」の両方が元データになりますが、どちらのファイルのデータをどのように編集するかは、それぞれのケースによって異なるので、各自工夫してください。
「マスタオンリーの処理」と「トランザクションオンリーの処理」では、それぞれ「マスタファイル」や「トランザクションファイル」からすべてのデータをセットして、出力していますが、必要に応じて、条件をつけたり、集計したり、必要な項目だけをセットするなど、内容を変更して利用してください。
# matching2.rb # 内容 : マッチングプログラム(2ファイルの1対nマッチングプログラム) # 前提 : マスターファイルとトランザクションファイルの両方を # マッチングするキーごとに昇順に並べ替えておくこと # Copyright (c) 2004-2015 Mitsuo Minagawa, All rights reserved. # (minagawa@fb3.so-net.ne.jp) # 使用方法 : c:\>ruby matching2.rb # # ファイルのオープン in1_file = open("master.txt","r") #マスターファイル in2_file = open("transaction.txt","r") #トランザクションファイル out1_file = open("matching.txt","w") #マッチングファイル out2_file = open("masteronly.txt","w") #マスターオンリーファイル out3_file = open("tranonly.txt","w") #トランザクションオンリーファイル # 初期値設定 in1_eof = ["ffffffff"].pack("h8") #マスターファイルの終了判定 in2_eof = ["ffffffff"].pack("h8") #トランザクションファイルの終了判定 in1 = nil #マスターデータ in1_key = nil #マスターキー in2 = nil #トランザクションデータ in2_key = nil #トランザクションキー w_total = 0 #合計 # マスターファイル入力 class S_in1 def dataset(in1_file,in1_eof) if (line1 = in1_file.gets) line1.chomp! #タブ区切りのとき # in1 = line1.split("\t") #カンマ区切りのとき 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] #入力ファイルが終了のとき else in1 = nil #マスターファイルの終了判定(in1_eof)をマスターキーにセット in1_key = in1_eof end return in1,in1_key end end # トランザクションファイル入力 class S_in2 def dataset(in2_file,in2_eof) if (line2 = in2_file.gets) line2.chomp! #タブ区切りのとき # in2 = line2.split("\t") #カンマ区切りのとき 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] #入力ファイルが終了のとき else in2 = nil #トランザクションファイルの終了判定(in2_eof)をトランザクションキーにセット in2_key = in2_eof end return in2,in2_key end end # マッチング時の処理 def s_match(in1_key,w_total,out1_file) out1 = [in1_key,w_total].join("\t") out1_file.print out1,"\n" end # マスタオンリー時の処理 def s_master_only(in1,out2_file) out2 = in1.join("\t") out2_file.print out2,"\n" end # トランザクションオンリー時の処理 def s_trans_only(in2,out3_file) out3 = in2.join("\t") out3_file.print out3,"\n" end # 1件目のデータ入力と配列へのデータセット等 s_in1 = S_in1.new (in1,in1_key) = s_in1.dataset(in1_file,in1_eof) s_in2 = S_in2.new (in2,in2_key) = s_in2.dataset(in2_file,in2_eof) # 主処理 until ((in1_key == in1_eof) && (in2_key == in2_eof)) # マッチングの時(両方のファイルにデータがある) if (in1_key == in2_key) until (in1_key != in2_key) w_total += in2[1].to_i (in2,in2_key) = s_in2.dataset(in2_file,in2_eof) end # マッチング時の処理 s_match(in1_key,w_total,out1_file) w_total = 0 (in1,in1_key) = s_in1.dataset(in1_file,in1_eof) # マスタオンリーの時(マスタファイルだけにデータがある) elsif (in1_key < in2_key) s_master_only(in1,out2_file) (in1,in1_key) = s_in1.dataset(in1_file,in1_eof) # トランザクションオンリーの時(トランザクションファイルだけにデータがある) elsif (in1_key > in2_key) s_trans_only(in2,out3_file) (in2,in2_key) = s_in2.dataset(in2_file,in2_eof) end end # ファイルのクローズ in1_file.close in2_file.close out1_file.close out2_file.close out3_file.close
1対nマッチング処理
それでは、スクリプトの解説を個別に行っていきましょう。
# マッチング時の処理 def s_match(in1_key,w_total,out1_file) out1 = [in1_key,w_total].join("\t") out1_file.print out1,"\n" end # マッチングの時(両方のファイルにデータがある) if (in1_key == in2_key) until (in1_key != in2_key) w_total += in2[1].to_i (in2,in2_key) = s_in2.dataset(in2_file,in2_eof) end # マッチング時の処理 s_match(in1_key,w_total,out1_file) w_total = 0 (in1,in1_key) = s_in1.dataset(in1_file,in1_eof)
一般的には、マッチング(照合)をしたときの処理を「def」を使ったメソッド定義で行いますので、一般には、どちらでも使えるようにマスタファイルの「in1」とトランザクションファイルの「in2」、さらには1件ずつ出力することも多いので、出力ファイルの「out1_file」を引数にしておきます。
その他注意すべきこと
その他、注意すべき点は入力データがタブ区切りなどの場合とキーが複数ある場合の処理です。
上記のスクリプトでは、入力ファイルが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 01,101 01,102 02,200 04,400 04,401 04,402 04,403 06,600 06,601 07,700 08,800 08,800 08,800 09,900 09,900 09,900 09,900 09,900 10,1000 10,1010 10,1020
01 303 02 200 07 700 08 2400 09 4500
03 33333 05 44444
04 400 04 401 04 402 04 403 06 600 06 601 10 1000 10 1010 10 1020