複雑なデータ型
次はもう少し複雑な例です。「customer_data.csv」を、都道府県と市区ごとのメンバーが分かるように、以下のフォーマットでまとめてみましょう。
神奈川県 横浜市 50893,本間雅洋,29 30876,三井武,18 川崎市 63487,堀井裕太,45 02746,能美孝志,37 北海道 苗穂町 34249,合田由美,23 : :
この集計を行うためには、Perlで多重のデータ構造を可能とするのに利用される「リファレンス」の知識が不可欠となります。
リファレンス、無名配列、無名ハッシュ
PerlにはリファレンスというC言語のポインタと似た概念があり、これを利用することで配列とハッシュを自由に入れ子にできます。例えば、次のような表現が可能です。
$data{'千葉県'}{'松戸市'} = ['小林憲司', '高田美鈴', '荒田正人'];
これは、千葉県の松戸市には、小林さんと高田さんと荒田さんが住んでいるというデータを表現したものです。あたかも、多重ハッシュの値が配列であるかのような構造です。リファレンスに関して理解することで、この用なデータ構造を扱うことができるようになります。
リファレンスはC言語で言うポインタで、データを指し示すスカラ値です。配列、ハッシュに\
を付けて取得することができます。
@arr = (1, 2, 3);
%hash = (key1 => val1, key2 => val2);
# 以下の二つが、リファレンスとなる
$ref_arr = \@arr;
$ref_hash = \%hash;
リファレンスを元の配列・ハッシュと同等に扱うためには、次のように記述します。ちょうど配列の名前の部分に、{$ref}
というように中括弧でくくったリファレンスを入れたかのような記述になります。
# デリファレンスして配列に代入する @arr2 = @{$ref_arr}; # デリファレンスして値を取り出す print ${$ref_arr}[1], "\n"; print ${$ref_hash}{key2}, "\n";
ただ、${$ref}[0]
のようにリファレンスの先にある配列の要素を取り出す書式は非常によく使われるため、短縮系が用意されています。それが、アロー演算子「->」です。
print $ref_arr->[1], "\n"; print $ref_hash->{key2}, "\n";
ここまで来ると、先ほどの松戸市の例のデータ構造を作ることができます。実際に作ってみましょう。
# 人が含まれた配列 @people = ('小林憲司', '高田美鈴', '荒田正人'); # 市区名をキーに、人を集めた配列を紐づける %chiba = ('松戸市' => \@people); # 県名をキーに、市区名と人の配列を含むデータを紐づける %data = ('千葉県' => \%chiba);
なんとかデータ構造を定義できましたが、ちょっと面倒くさいです。Perlではもっと直感的な定義が可能です。先ほどのコードでは、クッションとして@people
と%chiba
という変数があるため、全体として、どのようなツリー構造が形成されているのか理解するのは大変です。そこで、今度は「無名リファレンス」と呼ばれる物でこれらの変数を使わないように書き換えます。
無名リファレンスとは、配列名やハッシュ名をつけずにリファレンスを定義するというもので、[1, 2, 3]
と書けば、(1, 2, 3)
という配列を指し示すリファレンス値となり、{key => val}
と中括弧で書くと、(key => val)
というハッシュを指し示すリファレンス値となります。これらを使い、%data
を書き直すと以下のようになります。
%data = ( '千葉県' => { '松戸市' => ['小林憲司', '高田美鈴', '荒田正人'] } );
$data{'千葉'}->{'松戸'}
とたどると、3人を含んだ配列のリファレンスにたどり着きそうだ、ということが分かりやすくなりました。なお、perlでは中括弧や大括弧間のアロー演算子が省略できるので、これを省略すると冒頭の式となります。
このように、perlでは無名配列や無名ハッシュを入れ子にすることで、かなり幅広く直感的なデータ表現をすることが可能です。%data
にもう少しデータを詰め込めば、その威力が分かるでしょう。
%data = ( '千葉県' => { '松戸市' => ['小林憲司', '高田美鈴', '荒田正人'], '柏市' => ['●●●●', '△△△△'], '船橋市' => ['●●●●', '△△△△'], }, '神奈川県' => { '川崎市' => ['●●●●', '△△△△'], '横浜市' => ['●●●●', '△△△△'], }, );
ただ、これらのデータを本当に使いこなすには、先ほどのデリファレンスの知識が必須ですので、多少練習をしておく必要があります。習うより慣れろ、だと思うので、いろいろなデータ構造を自分の手で作ってみると早く習得できるでしょう。
県・市ごとに、データを分割する
では、冒頭の集計に戻ります。「customer_data.csv」を、都道府県・市区ごとに集計するというものでした。この集計を実現するには、県と市区をキーとする多次元ハッシュの値として、人の情報を含む配列を指定できると、なんとかなりそうです。表示に関しては後から考えるとして、まずはデータ構造を構築する部分を書いてみましょう。
my %data = (); open(IN, 'customer_data.csv'); while(<IN>){ chomp; my ($id, $name, $age, $ken, $shi) = split(/,/, $_); # 人データを作る my $people = [$id, $name, $age]; # 結果配列に保存する push(@{$data{$ken}{$shi}}, $people); } close(IN);
人の情報は、$people
として無名配列を使いました。これを、$ken
と$shi
をキーとする要素を配列と見なし、push
で追加しています。
表示させる
%data
に蓄積されたデータを表示する部分を作りましょう。%data
は2次元のハッシュなので、まずはkeys
によるループが二つ続きます。さらにその値は配列ですので、内側に配列のループが来ることになり、計3つのループ構造になりそうです。取り出した$people
は、カンマで繋いで出力します。
# 出力ファイルを開く open(OUT, ">customer_output.txt"); foreach my $ken (keys %data){ # 県名の表示 print OUT $ken, "\n"; # 県名に対する値(市区がキーの連想配列)を取り出す my $ken_val = $data{$ken}; foreach my $shi (keys %{$ken_val}){ # 市区を表示 print OUT "\t", $shi, "\n"; # 県名・市区に対する値(人を含んだ配列)を取り出す my $shi_val = $data{$ken}{$shi}; foreach my $people (@$shi_val){ # カンマで区切って表示する print OUT "\t\t", join(',', @$people), "\n"; } } } # ファイルを閉じる close(OUT);
出力結果は長くなるので、外部のファイルに吐き出しました。print文にファイルハンドル(OUT
)を付け忘れないように気をつけてください。構築されたデータを読み込むループを作るときは、データを構築する時と同じ変数名を使うと、構造の理解に役に立ちます。ここでは、$ken
、$shi
、$people
に同じ命名を使っています。
これだけ複雑なデータを扱っても、ステップ数は20以下を保っています。多次元のデータ構造をこれほど簡潔に記述できる言語は少なく、この点に置いてもPerlはデータ処理に向いていると言えます。初めのうちは取り扱いに苦労するかもしれませんが、慣れるとどんな集計でもこなせるようなります。ぜひともチャレンジしてみてください。
まとめ
Perlを使ったデータ処理に関して、以下の事柄を説明しました。
- perlを使えば、テキストエディタとコマンドラインだけでCSVを高速に処理できる。
- 基本は
open
、while
ループ、close
の構造で書くことができる。 split
関数で、CSVの1行を配列に分けることができる。- 連想配列を利用して、グループ集計が可能である。
- 連想配列を利用して、ユニークチェックが可能である。
- リファレンスを用いて多次元のデータ構造を扱うことができる。
Perlを使ったCSV処理で何より大事にしたいのは、手軽さをきちんと確保しておくことです。いくら強力な言語でも、コーディングや実行に手間がかかるようでは使う気にはなれません。いつも使うエディタのショートカットや、DOSプロンプトのショートカットなどを起動しやすい位置に集めておき、素早くスクリプトを実行することができる環境と手順を作っておきましょう。