CSVファイルを文字列型のキーで昇順にソート(sort:並べ替え)するスクリプトです。並べ替える項目が何番目の項目かを指定し、入力データからキー項目を抽出した後、キー項目を連結し、連結したキー項目とレコード全体をハッシュとしてセットした後、ハッシュを並べ替えています。
ソート(sort:並べ替え)するキーには文字列型以外に数値型があります。文字列型と数値型の違いは、同じ数字でも計算するための数値として扱う場合は数値型として、計算するための数値として扱わない場合は文字列型として扱います。たとえば、昇順に並べ替えると「1,2,3,10,20,30」のような順番になるのが数値型で、文字列型では「1,10,2,20,3,30」のような順番になります。表計算ソフトなどでは、誤解しているケースが多いようですが、文字列型・数値型というのは、データそのものによって決まるのではなく、データをどのように扱う(認識する)かによって決まります。たとえば、郵便番号や電話番号は(ハイフンは除くものとすると)すべて数字から成り立っていますが、通常は文字列型として扱います。一方、点数や長さ、重さなどは通常、数値型として扱います。また、ISBNコードやJANコードなどは通常は文字列型として扱いますが、チェックディジットを計算する場合は、各桁の数字を数値として扱います。
このスクリプトでは文字列型のデータを昇順に並べ替えることを想定していますので、降順のソート(sort:並べ替え)や数値型のソート(sort:並べ替え)には利用できません(降順や数値型のソート(sort:並べ替え)は[2-4-3.CSVファイルの文字列型・数値型/昇順・降順ソート(その1)]と[2-4-4.CSVファイルの文字列型・数値型/昇順・降順ソート(その2)]で解説しています。)。
実際に応用する場合は、入出力データのファイル名を変更し、「sortitem = [1,0]」の部分を変更します。この例では、2番目の項目を第1キーに、1番目の項目を第2キーとして並べ替えるように指定しています。Rubyでは、配列のインデックスは「0」から開始されますので、1番目の項目を「0」、2番目の項目を「1」として指定することに注意してください。
なお、実用的な入力データの件数は指定するキーの数にもよりますが、100万件程度までです。それ以上の入力件数がある場合は、実用にはなりません。
# csvsort.rb # 内容 : CSV形式データのソート(対象ファイルをCSV形式としてソートする) # Copyright (c) 2002-2015 Mitsuo Minagawa, All rights reserved. # (minagawa@fb3.so-net.ne.jp) # 使用方法 : c:\>ruby csvsort.rb # # 入力ファイルを全部読み込む。 in1_file = IO.readlines("input.txt") # 出力ファイル out1_file = open("output.txt","w") # ソートキーの入っている項目番号 sortitem = [1,0] # ソートキーをセットするハッシュのキー hash_key = nil # ソートするレコードをセットするハッシュ sort_rec = Hash.new # ソートキーとなる項目番号からソートキーとなる文字列を作っておく。 #recno番目のレコードがrecordである。 in1_file.each_with_index {|record,recno| record.chomp! # 入力ファイルをカンマで分解する。 in1 = (record + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')} # 入力ファイルをタブで分解する。 # in1 = record.split("\t",-1) #ソートキーとなる項目の内容をタブで連結してハッシュのキーとする。 hash_key = "" sortitem.each {|i| hash_key += in1[i] + "\t" } # 同じ値を持つキーが複数存在する場合に以下のように指定する。 hash_key += sprintf("%08d",recno) # レコードをハッシュの値としてセットする。 sort_rec[hash_key] = record } # ハッシュのキーをソートし、キー順にレコードを出力する。 #sort_rec.sort{|a,b| a[0] <=> b[0]}.each{|key,value| sort_rec.sort_by{|key| key }.each{|key,value| out1_file.print value,"\n" } # ファイルのクローズ out1_file.close
それでは、1つずつ解説していきましょう。
ソート(並べ替え)キーの設定
# ソートキーの入っている項目番号 sortitem = [1,0]
「sortitem」には、CSVファイルの何番目の項目を昇順で並べ替えるかを指定します。この例では、2番目の項目を第1キーに、1番目の項目を第2キーにして並べ替えるようにしていますので、実際には、必要に応じて変更します。何番目かを指定する際には、1番目の項目が0番目になることに注意します。
その他初期値設定
# ソートキーをセットするハッシュのキー hash_key = nil # ソートするレコードをセットするハッシュ sort_rec = Hash.new
それぞれの項目を初期値に設定しています。
「hash_key」は、CSVファイルの「sortitem」で先頭からの順番を指定したキー項目の実際の内容を結合して、各レコードごとにセットします。
「sort_rec」は、ハッシュの値として、CSVファイルの各レコードごとにセットしますが、ハッシュのキーはキー項目の実際の内容を結合したものになります。ただし、各レコードのキーは、同一内容である可能性があるため、そのままセットすると、正常に出力できなくなります(レコード数が少なくなる)。これを防止するため、何番目のレコードかを計算した値を追加しています。
ファイルの入力処理
# 入力ファイルを全部読み込む。 in1_file = IO.readlines("input.txt")
「in1_file」には、CSVファイルをすべて読み込みます。readlinesメソッドによって、全データが一度に「in1_file」にセットされます。「in1_file」は各レコードを要素とする1次元テーブルになります。
CSVファイルを配列にセット
# ソートキーとなる項目番号からソートキーとなる文字列を作っておく。 #recno番目のレコードがrecordである。 in1_file.each_with_index {|record,recno| record.chomp! # 入力ファイルをカンマで分解する。 in1 = (record + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')} # 入力ファイルをタブで分解する。 # in1 = record.split("\t",-1)
「in1_file」に対して、each_with_indexメソッドによって、各レコードごとにCSVファイルをカンマで分解していきます。CSVファイルは、CSV2形式以外の引用符(")を使用するケースを想定しています(CSV2形式などについては、[3-1-1.固定長データとCSVデータ]を参照してください)。CSV2形式のときは「in1 = record.split(",",-1)」とします。また、タブ区切りのファイルの時は、コメントを外して、その前の「in1 = (record + ','). 」からの3行をコメントにします。
ソート(並べ替え)キーを連結して、ハッシュのキーとする
#ソートキーとなる項目の内容をタブで連結してハッシュのキーとする。 hash_key = "" sortitem.each {|i| hash_key += in1[i] + "\t" }
カンマで分解したCSVファイルから「sortitem」で指定した項目の順番でキー項目を抜き出し、タブで連結しています。rubyでは、nilの項目に対して、+=メソッドで連結できないため、事前にスペースを「key」にセットしています。なお、一般のデータにない文字コードであれば、タブ以外の文字データで区切ってもかまいません。
また、並べ替えるキーがアルファベットで大文字と小文字が混在している場合、そのままでは大文字と小文字で別々に並べ替えが行われるため、アルファベット順になりません。その場合は、「hash_key += in1[i] + "\t"」の部分を「hash_key += in1[i].downcase + "\t"」に変更します。downcaseメソッドを使用することでキーをすべて小文字に変換してから並べ替えるため、正しくアルファベット順にすることができます。
同じソート(並べ替え)キーが複数ある場合の対応
# 同じ値を持つキーが複数存在する場合に以下のように指定する。 hash_key += sprintf("%08d",recno)
CSVファイルのキー項目がすべて異なる場合は必要ないのですが、同一のキー項目がある場合、各レコードをハッシュにセットするため、そのままセットすると、データが少なくなってしまうことになります。したがって、同一のキー項目でもすべて異なる内容になるように8桁の連番をキー項目に連結するようにしています。これにより、結果として、同一のキー項目でもレコードの先頭からの順番は維持されるようになります。
レコードをハッシュの値としてセットする
# レコードをハッシュの値としてセットする。 sort_rec[hash_key] = record
各レコードをハッシュにセットします。その場合のキー項目は「sortitem」で指定した項目と8桁のレコード連番を組み合わせたものになります。
ハッシュをソート(並べ替え)して、出力する
# ハッシュのキーをソートし、キー順にレコードを出力する。 #sort_rec.sort{|a,b| a[0] <=> b[0]}.each{|key,value| sort_rec.sort_by{|key| key }.each{|key,value| out1_file.print value,"\n" }
その他注意すべきこと
その他、注意すべき点は入力データがタブ区切りなどの場合です。
上記のスクリプトでは、どのようなCSVファイルにも対応できるようにしていますが、入力ファイルがCSV2形式(CSV2形式については、[3-1-1.固定長データとCSVデータ]を参照してください)のCSVファイルである場合には、
in1 = (record + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')}
となっている箇所を以下のように変更します。
入力データがCSV2形式のCSVファイルの場合、
in1 = record.split(",",-1)
また、入力ファイルがタブ区切りの場合には以下のように変更します。
入力データがタブ区切りの場合、
in1 = record.split("\t",-1)
上記の例では入力ファイルを一気に読み込む方法にしていますが、通常通り、1件ずつ読み込む方法をとる場合は、以下のようにします。
# csvsort_another.rb # 内容 : CSV形式データのソート(対象ファイルをCSV形式としてソートする) # Copyright (c) 2002-2015 Mitsuo Minagawa, All rights reserved. # (minagawa@fb3.so-net.ne.jp) # 使用方法 : c:\>ruby csvsort_another.rb # # 入力ファイルを全部読み込む。 in1_file = open("input.txt","r") # 出力ファイル out1_file = open("output.txt","w") # ソートキーの入っている項目番号 sortitem = [1,0] # ソートキーをセットするハッシュのキー hash_key = nil # ソートするレコードをセットするハッシュ sort_rec = Hash.new # 入力件数を初期設定する。 in1_ctr = 0 # ソートキーとなる項目番号からソートキーとなる文字列を作っておく。 #recno番目のレコードがrecordである。 while (line1 = in1_file.gets) in1_ctr += 1 line1.chomp! # 入力ファイルをカンマで分解する。 in1 = (line1 + ',') .scan(/"([^"\\]*(?:\\.[^"\\]*)*)",|([^,]*),/) .collect{|x,y| y || x.gsub(/(.)/, '\1')} # 入力ファイルをタブで分解する。 # in1 = line1.split("\t",-1) #ソートキーとなる項目の内容をタブで連結してハッシュのキーとする。 hash_key = " " sortitem.each {|i| hash_key += in1[i] + "\t" } # 同じ値を持つキーが複数存在する場合に以下のように指定する。 hash_key += sprintf("%08d",in1_ctr); # レコードをハッシュの値としてセットする。 sort_rec[hash_key] = line1 end # ハッシュのキーをソートし、キー順にレコードを出力する。 #sort_rec.sort{|a,b| a[0] <=> b[0]}.each{|key,value| sort_rec.sort_by{|key| key }.each{|key,value| out1_file.print value,"\n" } # ファイルのクローズ in1_file.close out1_file.close
また、ソート(並べ替え)結果の1行目に見出しを入れる場合などのようにスクリプトの中に出力する全角文字を直接指定する場合は、文字コードの指定を行う必要があります。文字コードの指定は、ruby1.8以前であれば、$KCODEを指定し、ruby1.9以降であれば、マジックコメントを指定します。指定方法については、[2-1-5.スクリプトの中で漢字を使う]を参照してください。
その場合、スクリプトの中に全角文字を指定する場合は、kconvライブラリを使って変換します。たとえば、出力ファイルがタブ区切りの場合、見出しもタブ区切りで出力する場合は、以下のように指定します([3-5-1.文字コードの変換(kconvライブラリ利用)]を参照)。下記の例では見出しをshift_jisで出力するようにしています。
require 'kconv' #見出しなどで全角文字をスクリプト上に指定する場合に必要 ・・・・・ title = Kconv.tosjis("項目1 \t 項目2 \t 項目3 \t 項目4") out1_file.print title,"\n"
また、入力ファイルの文字コードが"shift_jis"か"utf-8"の場合(ruby1.9ではsort_byメソッドと"euc-jp"の相性が良くないようですが)、入力ファイルの文字コード、スクリプトの文字コード、文字コードの指定、openメソッドの文字コードの指定をすべて一致させないと、エラーが表示されることがあります。
具体的にはスクリプト上は以下のような指定になります。
# coding:windows-31j #(または"# coding:utf-8") require 'kconv' #見出しなどで全角文字をスクリプト上に指定する場合に必要 in1_file = open("input.txt","r:shift_jis") #または"in1_file = open("input.txt","r:utf-8")"などとする。
さらに、入力ファイルの文字コードが"utf-8"でも出力ファイルの文字コードをshift_jisにする場合は、ソート(並べ替え)結果を出力する直前に「encodeメソッド」を使って、shift_jisに変換する方法もあります。「encodeメソッド」については、[3-5-3.文字コードの変換(encode利用)]を参照してください。
require 'kconv' #見出しなどを全角文字をスクリプト上に指定する場合に必要 ・・・・・ in1_file = open("input.txt","r:utf-8") out1_file = open("output.txt","w") ・・・・・ title = Kconv.tosjis("項目1 \t 項目2 \t 項目3 \t 項目4") out1_file.print title,"\n" sort_rec.sort_by{|key| key }.each{|key,value| value = value.encode("cp932", "utf-8") # utf8 から Shift_JIS に変換 out1_file.print value,"\n" }
ccc,5555 aaa,7777 bbb,9999 aaa,1111 bbb,3333 bbb,4444 aaa,5555 ccc,1111
aaa,1111 ccc,1111 bbb,3333 bbb,4444 aaa,5555 ccc,5555 aaa,7777 bbb,9999