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)]で解説しています。)。
実際に応用する場合は、入出力データのファイル名を変更し、「@sortkey = (1,0)」の部分を変更します。この例では、2番目の項目を第1キーに、1番目の項目を第2キーとして並べ替えるように指定しています。Perlでは、配列のインデックスは「0」から開始されますので、1番目の項目を「0」、2番目の項目を「1」として指定することに注意してください。
なお、実用的な入力データの件数は、ほぼ100万件程度までです。それ以上の入力件数がある場合は、実用にはなりません。
100万件以上のデータをソート(sort:並べ替え)場合には、[2-4-5.大量のCSVファイルのソート(sort:並べ替え)する場合の考慮事項]と[2-4-6.大量のCSVファイルの文字列型/昇順ソート]の両方を参考にして、スクリプトを作成してください(1000万件程度のデータであれば、ソート(sort:並べ替え)できます)。
# csvsort.pl # 内容 : CSV形式データのソート(対象ファイルをCSV形式としてソート(並べ替え)する) # Copyright (c) 2002-2011 Mitsuo Minagawa, All rights reserved. # (minagawa@fb3.so-net.ne.jp) # 使用方法 : c:\>perl csvsort.pl # open(IN1,"input.txt"); open(OUT1,">output.txt"); # ソート(並べ替え)キーの入っている項目番号 @sortkey = (1,0); # ソート(並べ替え)キーをセットするハッシュのキー $key = undef; # ソート(並べ替え)用のキーと1行分レコードをセットするハッシュ %sort_record = (); # 対象ファイルを全部読み込む。 @record = <IN1>; # ソート(並べ替え)キーとなる項目番号から # ソート(並べ替え)キーとなる文字列を作っておく。 #recno番目のレコードがrecordである。 foreach $recno (0..$#record) { chomp($record[$recno]); # 入力ファイルをカンマで分解する。 $tmp = $record[$recno]; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @in1 = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); # 入力ファイルをタブで分解する。 # @in1 = split("\t",$record[$recno],-1); # ソート(並べ替え)キーとなる項目の内容をタブで連結してハッシュのキーとする。 foreach $sortno (@sortkey) { $key .= $in1[$sortno] . "\t"; } # 同じ値を持つキーが複数存在する場合に以下のように指定する。 $key .= sprintf("%08d",$recno); # レコードをハッシュの値としてセットする。 $sort_record{$key} = $record[$recno]; $key = undef; } # ハッシュのキーをソート(並べ替え)し、キー順にレコードを出力する。 foreach $recno (sort keys %sort_record) { print OUT1 "$sort_record{$recno}\n"; } close(IN1); close(OUT1);
それでは、1つずつ解説していきましょう。
ソート(並べ替え)キーの設定
# ソート(並べ替え)キーの入っている項目番号(2番目と1番目の項目) @sortkey = (1,0);
「@sortkey」には、CSVファイルの何番目の項目を昇順で並べ替えるかを指定します。この例では、2番目の項目を第1キーに、1番目の項目を第2キーにして並べ替えるようにしていますので、実際には、必要に応じて変更します。何番目かを指定する際には、1番目の項目を「0」、2番目の項目を「1」と指定することに注意します。
その他初期値設定
# ソート(並べ替え)キーをセットするハッシュのキー $key = undef; # ソート(並べ替え)用のキーと1行分レコードをセットするハッシュ %sort_record = ();
それぞれの項目を初期値に設定しています。
「$key」は、CSVファイルの「@sortkey」で先頭からの順番を指定したキー項目の実際の内容を結合して、セットします。ここでセットしたものをあとで並べ替えを行うハッシュのキーとして設定します。
「%sort_record」は、ハッシュの値として、CSVファイルの各レコードごとにセットしますが、ハッシュのキーはキー項目の実際の内容を結合したものになります。ただし、各レコードのキーは、同一内容である可能性があるため、そのままセットすると、異なるレコードでもキーが同じために後でセットした内容に上書きされて、レコード数が少なくなってしまいます。これを防止するため、何番目のレコードかを計算した値を追加しています。
ファイルの入力処理
# 対象ファイルを全部読み込む @record = <IN1>;
「@record」には、CSVファイルをすべて読み込みます。スカラ変数を左辺にした場合に右辺を「<IN1>」とすると、ファイルからレコードを1行だけ読み込みますが、配列変数を左辺にした場合に右辺を「<IN1>」とすると、ファイルにある全レコードが一度に「@record」にセットされます。「@record」は各レコードを要素とする1次元テーブルになります。
CSVファイルを配列にセット
# ソート(並べ替え)キーとなる項目番号から # ソート(並べ替え)キーとなる文字列を作っておく。 foreach $recno (0..$#record) { # CSV形式の一行を項目に分解し、配列にする chomp($record[$recno]); $tmp = $record[$recno]; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @item = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g);
「@record」に対して、foreachを使って、各レコードごと(「0..$#record」で1件目から最後($#record)までを指定しています)にCSVファイルをカンマで分解していきます。「$recno」には各レコードごとの先頭からの連番(1件目を0とする)が入ります。また、各レコードごとにキー項目をセットして、「$key[$recno]」に入れていきます。
なお、上記のCSVファイルはCSV2形式以外の引用符(")を使用するケースを想定しています。CSV2形式などについては、[3-1-1.固定長データとCSVデータ]を参照してください。
このCSVファイルに分解するロジックは、大崎 博基(OHZAKI Hiroki)さんの「Perlメモ」に記載されていたものを参考にしています。
ソート(並べ替え)キーを連結して、ハッシュのキーとする
# ソート(並べ替え)キーとなる項目内容をタブをはさんで連結してキーとする foreach $sortno (@sortkey) { $key .= $item[$sortno] . "\t"; }
カンマで分解したCSVファイルから「@sortkey」で指定した項目の順番でキー項目を抜き出し、タブで連結しています。なお、一般のデータにない文字コードであれば、タブ以外の文字データを使って連結してもかまいません。
また、並べ替えるキーがアルファベットで大文字と小文字が混在している場合、そのままでは大文字と小文字で別々に並べ替えが行われるため、アルファベット順になりません。その場合は、「$key .= $item[$sortno] . "\t";」の部分を「$key .= lc($item[$sortno]) . "\t";」に変更します。lc関数を使用することでキーをすべて小文字に変換してから並べ替えるため、正しくアルファベット順にすることができます。
同じソート(並べ替え)キーが複数ある場合の対応
# 同じ値を持つキーが複数存在する場合に以下のように指定する。 $key .= sprintf("%08d",$recno);
CSVファイルのキー項目がすべて異なる場合は必要ないのですが、同一のキー項目がある場合、各レコードをハッシュにセットするため、そのままセットすると、異なるレコードでもキーが同じために後でセットした内容に上書きされて、データが少なくなってしまうことになります。したがって、同一のキー項目でもすべて異なる内容になるように8桁の連番をキー項目に連結するようにしています。これにより、結果として、同一のキー項目でもレコードの先頭からの順番は維持されるようになります。
レコードをハッシュの値としてセットする
# レコードをハッシュの値としてセットする。 $sort_record{$key} = $record[$recno];
各レコードをハッシュにセットします。その場合のキー項目は「@sortkey」で指定した項目と8桁のレコード連番を組み合わせたものになります。
ハッシュをソート(並べ替え)して、出力する
# ハッシュのキーをソート(並べ替え)し、キー順にレコードを出力する foreach $recno (sort keys %sort_record) { print OUT1 "$sort_record{$recno}\n"; }
ハッシュをキー項目でsortを使ってソート(並べ替え)した後、ハッシュの値となる各レコードを全件出力しています。
その他注意すべきこと
その他、注意すべき点は入力データがタブ区切りなどの場合です。
上記のスクリプトでは、どのようなCSVファイルにも対応できるようにしていますが、入力ファイルがCSV2形式(CSV2形式については、[3-1-1.固定長データとCSVデータ]を参照してください)のCSVファイルである場合には、
my $tmp = $line1; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @in1 = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g);
となっている箇所を以下のように変更することもできます(上記のままでもCSV2形式のCSVファイルに対応しています)。
入力データがCSV2形式のCSVファイルの場合、
@in1 = split(",",$line1,-1);
また、入力ファイルがタブ区切りの場合には以下のように変更します。
入力データがタブ区切りの場合、
@in1 = split("\t",$line1,-1);
1件ずつ読み込む方法でのソート(並べ替え)処理
上記の例では入力ファイルを一気に読み込む方法にしていますが、通常通り、1件ずつ読み込む方法をとる場合は、以下のようにします。
# csvsor_another.pl # 内容 : CSV形式データの昇順ソート # (対象ファイルをCSV形式としてソート(並べ替え)する) # Copyright (c) 2002-2011 Mitsuo Minagawa, All rights reserved. # (minagawa@fb3.so-net.ne.jp) # 使用方法 : c:\>perl csvsort_another.pl # open(IN1,"input.txt"); open(OUT1,">output.txt"); # ソート(並べ替え)キーの入っている項目番号(2番目と1番目の項目) @sortkey = (1,0); # ソート(並べ替え)キーをセットするハッシュのキー $key = undef; # ソート(並べ替え)用のキーと1行分レコードをセットするハッシュ %sort_record = (); # 入力件数を初期設定する。 $in1_ctr = 0; # ソート(並べ替え)キーとなる項目番号から # ソート(並べ替え)キーとなる文字列を作っておく while ($line1 = <IN1>) { # 対象ファイルをカンマで分解する chomp($line1); $tmp = $line1; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @in1 = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); # 入力ファイルをタブで分解する。 # @in1 = split("\t",$line1,-1); # ソート(並べ替え)キーとなる項目の内容を # タブで連結してハッシュのキーとする。 foreach $sortno (@sortkey) { $key .= $in1[$sortno] . "\t"; } # 同じ値を持つキーが複数存在する場合に以下のように指定する。 $key .= sprintf("%08d",$in1_ctr); # ソート(並べ替え)キーとソート(並べ替え)キーに対応するレコードを # ハッシュにセットする $sort_record{$key} = $line1; $in1_ctr++; $key = undef; } # ハッシュのキーをソート(並べ替え)し、キー順にレコードを出力する foreach $recno (sort keys %sort_record) { print OUT1 "$sort_record{$recno}\n"; } close(IN1); close(OUT1);
shift_jis以外の入力ファイルや見出しに全角文字を使う場合
また、shift_jis以外の入力ファイルを使用する場合に、ソート(並べ替え)結果の1行目に見出しを入れる場合などのようにスクリプトの中に出力する全角文字を直接指定する場合は、文字コードの指定を行う必要があります。
まず、openする前に入力ファイルの文字コードがutf-8であれば、「use encoding "utf-8"」とし、スクリプトの文字コードもutf-8にします。同様に入力ファイルの文字コードがeuc-jpであれば、「use encoding "euc-jp"」とし、スクリプトの文字コードもeuc-jpにします(ただし、出力ファイルの文字コードは[3-5.文字コードの変換]でよる方法で変換しない限り、Perlの内部コードであるutf-8のまま変わりません)
use encoding "utf-8"; open(IN1,"input.txt"); open(OUT1,">output.txt");
また、入力ファイルの文字コードが"utf-8"か"euc-jp"で、ソート(並べ替え)キーに全角文字をセットした場合、「Wide character in print at xxxx.pl line xx, <IN1> line xxxx.」というメッセージが出てきます(入力ファイルの文字コードが"shift_jis"の場合は、メッセージは表示されません)。
この場合、以下のように「STDERR」というファイルハンドルを追加すると、指定したファイルにエラーメッセージが出力されます。
use encoding "utf-8"; open(IN1,"input.txt"); open(OUT1,">output.txt"); open(STDERR,">error.txt");
ただし、入力ファイルの文字コードが"utf-8"でも出力ファイルの文字コードをshift_jisにする場合は、ソート(並べ替え)結果を出力する直前に「Encode::from_to()」関数を使って、shift_jisに変換すれば、上記のようなエラーメッセージは表示されなくなります。「Encode::from_to()」関数については、[3-5.文字コードの変換]を参照してください。
use Encode; open(IN1,"input.txt"); open(OUT1,">output.txt"); open(STDERR,">error.txt"); ・・・・・ foreach $recno (sort keys %sort_record) { Encode::from_to($sort_record{$recno},'utf8', 'cp932'); print OUT1 "$sort_record{$recno}\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