• ベストアンサー

2次元ハッシュ または 2次元配列をソートしたい

2次元ハッシュのソートをしたいです。 ハッシュは2つのキーを使用していて、 1つ目のキーは文字列、2つ目のキーは数字(0からの連番)です。 ハッシュの中身は文字列が入っています。 これを次のような表に見立てて、特定の列でソートしたいのです。 hash['a']['0'], hash['a']['1'], ..., hash['a']['50'], hash['q']['0'], hash['q']['1'], ..., hash['q']['50'], hash['c']['0'], hash['c']['1'], ..., hash['c']['50'], ... hash['d']['0'], hash['d']['1'], ..., hash['d']['50'], 例えば 6列目の値によってソートするということです。 以下のようにソートしようとしましたが、うまくいきません。 my @sorthash = sort { $a->[6] <=> $b->[6] } @hash; 何かヒントがあれば教えてください。

質問者が選んだベストアンサー

  • ベストアンサー
  • Werner
  • ベストアンサー率53% (395/735)
回答No.4

> ただ、perl の文法の誤りはどこにあるのか分からないので、教えていただけないでしょうか。 必ずしも文法エラーにはならないかもしれませんが、最初の > hash['a']['0'], hash['a']['1'], ..., hash['a']['50'], で、[ ] の中に文字列が入っています。 [ ] は配列要素のアクセスに使うので整数しか使えません。 (ついでに頭に$がついてない。) もしhashが名前の通りハッシュ(%hash)であるなら、 [ ] でアクセスするのはおかしいし、@hashもおかしいです。 前半の方はPerlコードではなくイメージを図示したものと考えることもできますが、 @hashの方は誤りと見なされても仕方ないですね。 (hashが配列であるなら正しいですが、あなたはハッシュと言っていましたから。 また、hashという名前の配列を作るのは文法上正しくても混乱を招くのであまりよろしくないですね。) > さて、実際のところは、配列を格納したハッシュにするのが一番適切かと思い、修正してみました。 要するにやりたいことは、 7列目(indexが6なので1から数えるなら7列目だよね?)をキーにして行をソートすることですよね。 それなら配列の配列(二次元配列)がもっとも適当だと思います。 二次元配列を使ったやり方はNo.3で書かれていますので試してみてください。 (疑問点があればまた聞いてください。) CSVから配列のハッシュに格納している部分ですが、 > @{$content{$values[6]}} = @values; ここで $values[6] をハッシュのキーにしてますから、 $values[6] (7列目) に同じ値が出現したときは 先に代入されたデータが消えてしまいます。 これは意図通りの動作ですか? ここの部分は、正直なぜ $values[6] をキーにしたハッシュにしているのかよく分からないです。 別のところで、7列目の値をキーに行全体を一発で取得したいというニーズが有ったのなら この構造にするのもうなずけるのですが、 そうでないなら二次元配列で良くありませんか? > for (my $i = 0; $i < $#sorted_keys; $i++) { $#sorted_keys は配列@sorted_keysの最後のインデックス番号を示します。 配列@sorted_keysの要素数ではありません。 よって、 $i < $#sorted_keys ではなく $i <= $#sorted_keys が正しいと思います。

palayo
質問者

お礼

> $#sorted_keys は配列@sorted_keysの最後のインデックス番号を示します。 > 配列@sorted_keysの要素数ではありません。 > よって、 $i < $#sorted_keys ではなく $i <= $#sorted_keys が正しいと思います。 おわっ。こんな間違いをしていたのですね。気づかなかった。 $i < @sorted_keys で解決しました。 ありがとうございました。

その他の回答 (5)

  • taknak08
  • ベストアンサー率50% (8/16)
回答No.6

おっと、  print OUT join(',', map { s/"/""/g ? qq{"$_"} : $_ } @$_), "\n"; ではなく  print OUT join(',', map { (s/"/""/g or /,/) ? qq{"$_"} : $_ } @$_), "\n"; でしたね、失礼しました・・・苦笑

  • taknak08
  • ベストアンサー率50% (8/16)
回答No.5

こんばんは。No.2のtaknak08です。  > ただ、perl の文法の誤りはどこにあるのか分からないので、教えていただけないでしょうか。 最初のご質問中の「hash['a']」は、Perlとしては文法の誤りとなります。 「$hash{'a'}」の誤りか、かなり無理な解釈をしても「$hash['a']」の誤りなように思われます。 (またPerlの場合、「ハッシュ」は $x{...}、「配列」は $x[...]、と、かなり気を遣って?区別して呼ばれています) しかし少なくともコードが動作しているのであれば、実際には文法の誤りは無いのだと思います。 補足で添付いただいたコードを試したわけではないのですが、  > # ソートしない場合には、カラム名の行も出力されますが、  > # ソートするとカラム名の行が出力されません。 の理由が、ぱっと見では私にはちょっと分かりませんでした。 純粋に「CSVデータのうち6列目の値でソートし、その結果を再びCSVで出力したい」ということでしたら、以下のようなコードではいかがでしょうか。 ハッシュは一切用いずに、CSVデータを純粋な二次元配列(=@values)へ格納し、6列目(=$values[...][5])でソートして、出力しています。 一度おためしいただければと思います。(インデントが取れてしまうので行頭の空白を全角に置換しています ご注意ください) #!/usr/bin/perl use strict; open DATA, '< src.csv' or die $!; my @values; while (<DATA>) {  s/[\r\n]*$/,/s;  my @v;  while (s/^("(?:[^"]|"")*"|[^",]*),//) {   my $v = $1;   $v =~ s/^"(.*)"$/$1/ and $v =~ s/""/"/g;   push @v, $v;  }  die 'Invalid format' if length;  push @values, [@v]; } close DATA; # Sort in 6th column. @values = sort { $a->[5] cmp $b->[5] } @values; open OUT, '> dest.csv' or die $!; for (@values) {  print OUT join(',', map { s/"/""/g ? qq{"$_"} : $_ } @$_), "\n"; } close OUT;

palayo
質問者

お礼

そもそも最初に配列の配列に格納していないのは、 ただ単にcsvを並び替えるだけでなく、csvの編集も行っているためです。 特定のセルを編集するに当たり、 セルの位置を ${$content{$name}}[24] のようにハッシュを使って取得したいからです。 最初のお礼のソースで ####### # ハッシュ %csv_content の編集 ###### と書いていた部分です。 でないと、毎度走査しなくてはならず、非常に非効率だからです。 まさに該当しそうな記事がありました。 http://codezine.jp/article/detail/1020?p=1 以上、ありがとうございました。

  • kumoz
  • ベストアンサー率64% (120/185)
回答No.3

> $csv_content{$values[6]}{$i} = $values[$i]; > ... > my @sortvals = sort { $a->[6] <=> $b->[6] } @vals; 上の sort 文自体は、対象が数値ならば問題がないように思います。%csv_content の作成時に $values[6] を使っていて、それが 'a' や 'q' に該当すると思うのですが? <=> の代わりに cmp を使うと結果はどうなるでしょうか? csv の内容がわからないので細かいことはわかりませんが、一度 %csv_content を作ってから @vals を作っているのは少し無駄なように思います。 my @vals; while (my $line =<DATA>) { ... push @vals, [@values]; } my @sortvals = sort { $a->[6] cmp $b->[6] } @vals; # csv にして出力

palayo
質問者

お礼

お返事ありがとうございます!! kumoz様のおっしゃるとおり、ハッシュを配列に置きなおすのは かなり無駄ですので、taknak08様の回答への補足、お礼に書いたように、 ハッシュ値に配列を使ってみました。 依然、問題がおきていますので、そちらの方にもご回答いただけたら、 ありがたいです。

  • taknak08
  • ベストアンサー率50% (8/16)
回答No.2

Wernerさんが指摘されているとおり、Perlの文法に誤りがあるようですので、まずは入門書から紐解かれるとよいと思います。 (PHPご出身でしょうか? PHPとPerlはどちらも「$」や「[...]」を用いており、ぱっと見は似てはいますが、実際はかなり異なる言語です。) 「1つ目のキーは文字列、2つ目のキーは数字(0からの連番)です。  ハッシュの中身は文字列が入っています。」 をそのままPerlで書くと、下記のようになるでしょうか。 my %hash = ( 'a' => ['aiueo', 'kakiku', 'sasisu'], 'q' => ['zzzzz', 'yyyyyy', 'xxxxxx'], 'd' => ['a0123', 'b98765', 'c77777'], ); Perlでは、ハッシュに順番はありませんので、「%hashの中身を直接並び替える」ということはできません。 しかし「$hash{'...'}[1]='xxx' のように2列目(←1列目を[0]と数えています)の値(=この場合は'xxx')の大小で並び替えた%hashのキー一覧(=この場合は'...'の一覧リスト)を取り出す」ということはできます。 一度下記のコードをお試しください。 use strict; my %hash = ( 'a' => ['aiueo', 'kakiku', 'sasisu'], 'q' => ['zzzzz', 'yyyyyy', 'xxxxxx'], 'd' => ['a0123', 'b98765', 'c77777'], ); print "hash{'q'}[1] is '", $hash{'q'}[1], "'.\n\n"; # yyyyy print "Sorted by the string of 2nd column:\n"; my @sorted_key = sort { $hash{$a}[1] cmp $hash{$b}[1] } keys %hash; for my $sorted_key (@sorted_key) { print "hash{'", $sorted_key, "'}: "; for (my $i = 0; $i < 3; ++$i) { print "[", $hash{$sorted_key}[$i], "]"; } print "\n"; }

palayo
質問者

お礼

最初の質問で、ハッシュと書きながら配列表記にしてしまったのは 混乱を招いたようで、失礼しました。 ただ、perl の文法の誤りはどこにあるのか分からないので、教えていただけないでしょうか。 $i が0,1,2,3,... のような整数値の場合にも、$hash{$i} のようなハッシュって使えますよね? ですから、前のソースで書いている表記 $csv_data{$name}{$i} のような 書き方も文法上の誤りはないと思うのですが・・・。 実際のところ、ソート以外の部分はちゃんと動作してましたし。 2変数のハッシュとして書けば $csv_data{$name}{$i} となり、 2次元のハッシュ配列(なんていうの?)として書けば、 taknak08様の指摘するような $csv_data{$name}[$i] となるだけの違いかと・・・。 確かに、今回の条件では taknak08様の方法の方が適切ではありますが、 文法上はどちらも正しいのでは?? 私の認識違いでしたら、教えてください。 さて、実際のところは、配列を格納したハッシュにするのが一番適切かと思い、修正してみました。 補足のソースで一応動作しています。 ただ、ソートするとデータが1件(1行)だけ失われます。 元のcsvでカラム名に使われていた行なのですが・・・。 ソートせずに出力すると、ちゃんと出力されます。 (当然、先頭ではない行に出力されてしまいますが) なんとか修正したいのですが、原因が分かりません。 何か分かれば教えていただけないでしょうか?

palayo
質問者

補足

open(DATA, '<', $csv_file) or die "csv file does not exist."; while (my $line = <DATA>) { $line .= <DATA> while ($line =~ tr/"// % 2 and !eof(DATA)); $line =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; my @values = map {/^"(.*)"$/s ? scalar($_ = $1, s/""/"/g, $_) : $_} ($line =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); @{$content{$values[6]}} = @values; } close(DATA); # ソートしない場合には、カラム名の行も出力されますが、 # ソートするとカラム名の行が出力されません。 #my @sorted_keys = keys( %content ); my @sorted_keys = sort { $a cmp $b } keys( %content ); open(OUT, '>'. "$new_csv_file"); for (my $i = 0; $i < $#sorted_keys; $i++) { my $line = join ',', map {(s/"/""/g or /[\r\n,]/) ? qq("$_") : $_} @{$content{$sorted_keys[$i]}}; print OUT $line,"\n"; } close(OUT);

  • Werner
  • ベストアンサー率53% (395/735)
回答No.1

ハッシュのはずなのに書き方が配列になってますよ? ハッシュのプレフィクスは@じゃなくて%ですし、 値の参照も [ ] ではなく { } です。 もしhashが本当にハッシュならperlとしておかしいので 修正して補足してもらえますか? あとできればハッシュにテストデータを入れるコードも提示してくれると 分かりやすくて答えやすいです。 なお、文字列の比較に <=> 演算子を使うのは間違いです。 文字列の比較は cmp 演算子。 #おまけ use strict; use Data::Dumper; my %hash = ( 'a'=> [0,1,2,3,4,5], 'b'=> [12,13,14,15,16,17], 'c'=> [6,7,8,9,10,11], ); print Dumper(\%hash); my @ary = sort {$a->[3] <=> $b->[3]} values %hash; print Dumper(\@ary);

palayo
質問者

お礼

お返事ありがとうございます。 補足にUPした通り、CSVを読み込んで処理した後、 再びCSVで出力しようとしています。 ハッシュのままだと出力がよく分からなかったので、配列に変換しました。 ソートと出力がハッシュのままできるのであれば、ハッシュのままの方が 配列に変換する作業が省けて早いでしょうから、望ましいのでしょうが・・。 どちらでもいいので、どうしたらいいか教えていただけないでしょうか?

palayo
質問者

補足

my %csv_content = (); my @values; my $ncsvCols=0; open(DATA, '<', "src.csv") or die "csv not exist."; #正規表現によるCSVの読み込み while (my $line = <DATA>) { $line .= <DATA> while ($line =~ tr/"// % 2 and !eof(DATA)); $line =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @values = map {/^"(.*)"$/s ? scalar($_ = $1, s/""/"/g, $_) : $_} ($line =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); for (my $i = 0; $i< $#values; $i++) { $csv_content{$values[6]}{$i} = $values[$i]; } if ($#values > $ncsvCols) { $ncsvCols = $#values; } } close(DATA); ####### # ハッシュ %csv_content の編集 ###### # CSV の出力用配列作成 open(OUT, '>'. "dest.csv"); my @vals; my @csv_key = keys(%csv_content); for (my $i = 0; $i < $#csv_key; $i++) { for (my $j = 0; $j < $ncsvCols; $j++) { if ( exists $csv_content{$csv_key[$i]}{$j} ){ $vals[$i][$j] = $csv_content{$csv_key[$i]}{$j}; } } } ## ここでソートしたいが??? #my @sortvals = sort { $a->[6] <=> $b->[6] } @vals; # CSV にして出力 for (my $i = 0; $i < $#csv_key; $i++) { my $line = join ',', map {(s/"/""/g or /[\r\n,]/) ? qq("$_") : $_} @{$vals[$i]}; print OUT $line."\n"; } close(OUT); 1;

関連するQ&A