皆さんこんにちは。
Perl、使ってますか?
私は今の案件で初めて触ったのですが、半年以上触った今でさえ、使いやすいシェルスクリプト程度にしか使えてないです。
そんなperlを使ってたらchomp
とsplit
のあわせ技でとんでもないことになったので、それを書きなぐってみようと思います。
split関数と言うもの
perlにはsplit関数というものがあります。PHPとかにもありますが、これは文字列を区切り文字に従って配列に分割する関数です。
use strict;
use warnings;
use utf8;
my $str = 'a,b,c,d,e';
my @arr = split(/,/, $str);
print $_ . "\n" for @arr;
$ perl split.pl
a
b
c
d
e
この関数には実は第3引数があって、文字列をいくつの配列に分割するかを指定するものです。
例えばsplit(/,/, $str, 2)
に書き換えてみると、実行結果は以下のようになります。
$perl split.pl
a
b,c,d,e
この第3引数の数の配列に分けるようです。
第3引数に負の値を入れると、出来る限り分割するようになります。
省略した場合は負の値を入れたのと同じなります。
通常は。
CSVを処理してみよう
CSVデータをこのsplitで処理してみましょう。
以下のようなデータを用意します。
a,b,,d,e
f,g,h,,
i,,k,,
ちょっと足りないデータがありますが、よくあることです。
これを以下のファイルで処理してみます。
use strict;
use warnings;
use utf8;
open(FH, 'data.csv');
while(<FH>) {
chomp($_);
my @data = split(/,/, $_);
print $_ . "\n" for @data;
}
ファイルの各行を取り出し、まず改行コードを取り除いた(chomp
)あと、,
を区切り文字として配列に分解し、各要素をプリントするという簡単なコードです。
実際に処理してみましょう。
$ perl read.pl
a
b
d
e
f
g
h
i
k
一見しておかしなところはどこにもなさそうですね。
しかし、よく考えてみましょう。
bとdの間に一行空行がありますが、これはその部分のデータがないためにこのようになっています。
一方、元データ上ではhとiの間に2つのデータが存在していなければなりません。
ということは、hとiの間の少なくとも2つの(空の)配列要素が作られなかったことになります。
何が起こっているのか
CSVでは各要素が個別に意味を持っている場合がほとんどのため、例え空の要素であろうとそれがなんの脈絡もなく消し飛んでしまうと非常に困ります。
実際困りましたし。
これの原因はperldocにあります
If LIMIT is omitted (or, equivalently, zero), then it is usually
treated as if it were instead negative but with the exception that
trailing empty fields are stripped (empty leading fields are
always preserved); if all fields are empty, then all fields are
considered to be trailing (and are thus stripped in this case).
Thus, the following:
print join(':', split(/,/, 'a,b,c,,,')), "\n";
produces the output
"a:b:c"
, but the following:
print join(':', split(/,/, 'a,b,c,,,', -1)), "\n";
produces the output
"a:b:c:::"
.
これを拙いながらも訳してみますと
LIMIT(第3引数)が省略された場合(もしくはゼロの場合)、大体負の値を指定した場合と同じ挙動を示すのだけど、末尾の空の要素は削除されます(戦闘の空の要素は常に保持されます。)。もし、すべての要素が空の場合、全ての要素が末尾として扱われます(そして削除されます)
つまり、以下のようになります
print join(':', split(/,/, 'a,b,c,,,')), "\n";
> これを実行すると`"a:b:c"`となりますが、
>```
print join(':', split(/,/, 'a,b,c,,,', -1)), "\n";
これを実行すると"a:b:c:::"
が得られます
どういうことやねん。。。
末尾の空の要素は削除されると。。。
それはどんどん連鎖的に適用されていくと。。。
この謎仕様を知らないと、splitした際に要素が少なくなったことに気づかず、不気味なバグを生む可能性がありますのでご注意を
対応策
末尾に何らかの文字列があった場合は、これは発生しません。
実は、chomp関数を使わず、改行コードをとらなかった場合は改行コードが末尾の文字列となるため削除されません。が、普通は行末の改行コードは取り除いておきたいので、chomp関数を使わないというのは対応としてはよろしくありません。
というわけで、split関数の第3引数に-1を指定してあげましょう。
use strict;
use warnings;
use utf8;
open(FH, 'data.csv');
while(<FH>) {
chomp($_);
my @data = split(/,/, $_, -1);
print $_ . "\n" for @data;
}
こいつを実行してあげると
$ perl read.pl
a
b
d
e
f
g
h
i
k
空データの部分も省略せずにプリントされるようになりました。
まとめ
何でこんな変な仕様なのか。。。
perlの深淵はまだまだ深いのかもしれません。
今回は短いですが、こんなところで失礼します。
参考
http://perldoc.perl.org/functions/split.html
http://d.hatena.ne.jp/dayflower/20101105/1288942018