2つのCSVファイルを読み込んで、同一のキー項目を元に照合を行うスクリプトです。2つのファイルは「マスタファイル」と「トランザクションファイル」と呼びます。照合した結果により、3つのファイルに、<ふるいわけ>をします。3つのファイルとは、、該当するキー項目が「マスタファイル」と「トランザクションファイル」の両者に存在する<マッチングファイル>、該当するキー項目が「マスタファイル」にしか存在しない<マスタオンリーファイル>、該当するキー項目が「トランザクションファイル」にしか存在しない<トランザクションオンリーファイル>のことです。
以下のスクリプトは、「マスタファイル」も「トランザクションファイル」もそれぞれ、同一のキーを持つレコードが1つしか存在しない場合のマッチング(照合)処理です。2つのファイルを照合し、それぞれの相違点や一致点が容易に把握できるという点で、「キーブレイク処理」と並んで応用範囲の広いプログラムです。
下記に示したのは、スクリプトの構造を示したPAD図です。この図と実際のスクリプトを比較しながら、内容を理解してください。なお、入力データはキー順に並べ替えてあることが前提となっています。rubyを利用して並べ替えを行う場合については、[2-4.ソート処理]を参照してください。
マッチング(照合)した場合は、「マスタファイル」と「トランザクションファイル」の両方が元データになりますが、どちらのファイルのデータをどのように編集するかは、それぞれのケースによって異なるので、各自工夫してください。
「マスタオンリーの処理」と「トランザクションオンリーの処理」では、それぞれ「マスタファイル」や「トランザクションファイル」からすべてのデータをセットして、出力していますが、必要に応じて、条件をつけたり、集計したり、必要な項目だけをセットするなど、内容を変更して利用することになります。
# matching1.rb # 内容 : マッチングプログラム(2ファイルの1対1マッチングプログラム) # 前提 : マスターファイルとトランザクションファイルの両方を # マッチングするキーごとに昇順に並べ替えておくこと # Copyright (c) 2002-2015 Mitsuo Minagawa, All rights reserved. # (minagawa@fb3.so-net.ne.jp) # 使用方法 : c:\>ruby matching1.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 #トランザクションキー # マスターファイル入力 class S_in1 def dataset(in1_file,in1_eof) if (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] #マスターファイルが終了のとき 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",-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] #トランザクションファイルが終了のとき else in2 = nil #トランザクションファイルの終了判定(in2_eof)をトランザクションキーにセット in2_key = in2_eof end return in2,in2_key end end # マッチング時の処理 def s_match(in1,in2,out1_file) # $total += in2[1].to_i out1 = [in1,in2[1]].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) s_match(in1,in2,out1_file) (in1,in1_key) = s_in1.dataset(in1_file,in1_eof) (in2,in2_key) = s_in2.dataset(in2_file,in2_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
それでは、スクリプトの解説を個別に行っていきましょう。
初期値設定
# 初期値設定 in1_eof = ["ffffffff"].pack("h8") #マスターファイルの終了判定 in2_eof = ["ffffffff"].pack("h8") #トランザクションファイルの終了判定 in1 = nil #マスターデータ in1_key = nil #マスターキー in2 = nil #トランザクションデータ in2_key = nil #トランザクションキー
初期値設定では、文字列の最大値である「$high_value」を設定しておきます。「$high_value」はCOBOLで使用される定数の名称ですが、入力ファイルが終了したことを示すEOFを検出したときに設定することに利用します。また、1レコード分のマスタデータとトランザクションデータをそれぞれ格納しておく配列として「in1」と「in2」を、マスタデータとトランザクションデータのキー項目を格納するために「in1_key」と「in2_key」を用意し、それぞれ初期値として<nil>をセットしておきます。
それ以外に初期値として設定しておくような項目があれば、ここに設定しておきます。
# 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ループ処理を行う場合は、必ず、ループ処理の前に1件目のデータを入力するための処理を行います。
入力処理は、untilループの中で最後でも行うため、クラス(ここでは「S_in1」)として別に設定しています。
クラスの中のメソッドで設定したり、変更したりした項目はそのままでは、クラスの外では利用できませんので、クラスの外で利用したい場合は、「(in1_key,in1) = s_in1.dataset(in1_file,high_value) 」のように式の左辺に「利用したい変数」、式の右辺に「クラスのメソッド」を指定します。また、メソッドやクラスの定義は実際に使用する前に記述しておかないとエラーになりますので、実際に呼び出す前に設定しておきます。
主処理(1対1マッチング処理)
# 主処理 until ((in1_key == in1_eof) && (in2_key == in2_eof)) # マッチングの時(両方のファイルにデータがある) if (in1_key == in2_key) s_match(in1,in2,out1_file) (in1,in1_key) = s_in1.dataset(in1_file,in1_eof) (in2,in2_key) = s_in2.dataset(in2_file,in2_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
untilの条件は、入力データが終了(EOF)したときの条件を入れます。ここでは、マスタファイルとトランザクションファイルの両方のデータがなくなるまで処理を続けていますが、処理内容によってはマスタファイルが終了するだけ、またはトランザクションファイルが終了するだけで処理全体を終了させることがありますので、必要に応じて変更します。
whileのループにしたい場合は、以下のようにwhileの中の条件を「入力データが終了(EOF)しないときの条件」にします。
while ((in1_key != in1_eof) || (in2_key != in2_eof))
キーブレイク処理のように「ファイルハンドルから入力データを1件、読み込む処理」にすると、マスタファイルとトランザクションファイルの両方を常に読み込む処理になってしまうため、マッチング(照合)処理を正しく行うことができなくなります。
untilのループ内では、「マッチングの時」、「マスタオンリーの時」、「トランザクションオンリーの時」のそれぞれに場合分けして処理を行います。ここでも、例えば、マッチングしたときだけのデータが必要であれば、それ以外の処理はデータ入力だけを行うように変更します。
マッチング(照合)時の処理
# マッチング時の処理 def s_match(in1,in2,out1_file) # $total += in2[1].to_i out1 = [in1,in2[1]].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
「マッチング(照合)の時」、「マスタオンリーの時」、「トランザクションオンリーの時」のそれぞれに場合分けしたときの具体的な処理をここで設定しておきます。それぞれの処理の場合、マスタファイルとトランザクションファイルのどちらからどのような項目を持ってくるのかについては、実際の処理にあわせて変更します。
例えば、「マッチング(照合)時の処理」でマスタファイル とトランザクションファイルからデータを持ってくることもあります。また、この例ではすべての項目を出力していますが、必要な項目だけを出力したり、計算結果や内容を変換して出力したりします。
メソッド(method)の記述位置について
Rubyでは、それぞれの処理をメソッドとして作成しています。「def」から「end」までがメソッドの定義になります。メソッドやクラスでは引数としてカッコの中に指定した項目しか利用することができませんので、メソッドやクラスの中で使用する項目があれば、必ず引数として指定しておくことが必要です。また、メソッドやクラスの定義は実際に使用する前に記述しておかないとエラーになりますので、実際に呼び出す前に設定しておきます。この点はPerlと異なる点です。
その他注意すべきこと
その他、注意すべき点は入力データがタブ区切りなどの場合とキーが複数ある場合の処理です。
上記のスクリプトでは、入力ファイルが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 07 77777 08 88888 09 99999
03 33333 05 44444
04 400 06 600 10 1000