[ ↑ INDEX ] [ ← PREV ] [ NEXT → ]

Lesson 7 配列の並べ替え, foreach


■0. 準備

script/kwic3.pl を実行し,ソートキーとして 1, 2, 3 を指定すると,コンコーダンスラインが,それぞれ,左の文脈 (1),キーワード (2),右の文脈 (3) でソートされることを確認する。

   ----------------------------------------
   % perl script/kwic3.pl dll.txt
   SEARCH  STRING: \bjust\b
   SORT KEY (1-3): 3
   ----------------------------------------

■1. 配列の要素の並べ替え(1): reverse

reverse はリストの要素を逆順に並び替えたリストを返す関数。引数のリストそのものを並び替えるわけではない。

   @reversed = reverse (0, 1, 2)   #@reversed の値: (2, 1, 0)

   @new = reverse @old             #@old の値を逆順にし @new の値とする。
                                   #@old 自体には変化なし。

   #$pre の内容を空白類で分割,逆順にし,スペースを挟んで連結
   $pre_revd = join (' ', reverse (split (/\s+/, $pre)))

代入演算子の左辺がリストだと,右辺にもリストが来ることが要求される。リストが来ることが要求されるコンテキストをリストコンテキストという。これに対して,左辺にスカラー変数が来ると,代入演算子の右辺にもスカラー値 (一つのみからなる値) が要求される。このようなコンテキストをスカラーコンテキストという。

上の例のように reverse をリストコンテキストで使うと配列を返り値として返すが,スカラーコンテキストで用いると,配列の値を結合し一つの値としたものを返す。

   @array = reverse (1, 2, 3)       #@array の内容:(3, 2, 1)
   $var   = reverse (1, 2, 3)       #$var    〃   :'321'

cmp. などの演算子の左右の項,length などの関数の引数の位置などもスカラーコンテキストである。これらの位置では,自動的に一つの文字列に変換されるので,join を使って一つの文字列に結合しなくともよい。

ある文字列を逆順に並び替えた文字列を得るには,次のようにすればよい。「元の文字列を分割し,逆順に並び替えて,結合する」という作業を自動的に行ってくれる。

   $new = reverse 'abc'             #$new の内容:'cba'

◇ スクリプトの実行:各行の単語を逆順に並び替える

次のスクリプトを実行しなさい。入力ファイルは適当なものを選ぶこと。出力の結果が確認できたら,スクリプトの各行の意味を確認しなさい。

   script/reverse.pl
   --------------------------------------------------
   while (<>) {
      print join (' ', reverse split (' ', $_));
      print "\n";
   }
   --------------------------------------------------

コマンドラインから次のように入力して実行しても,同じ結果が得られる。

   % perl -ne 'print join (" ", reverse split (" ", $_)), "\n";'

■2. foreach

リストの要素それぞれに対して,ある処理をする場合には,foreach を使う。リストは ( ) で括る。配列等で指定する場合も ( ) を付けること。次のように { } の中で変数を指定すれば,リスト中のすべての要素に対して一定の処理を行うことができる。

   foreach $変数 (リスト) { ... $変数 ... }

   #$i に 0〜3 を順番に代入し,改行付きで出力
   foreach $i (0, 1, 2, 3) { print "$i\n"; }

   --------------------------------------------------
   @array = (0, 1, 2, 3);
   foreach $i (@array) {
      print "$i\n";
   }
   --------------------------------------------------

「リスト」部分は,範囲演算子 .. を使い,次のように書くこともできる。

   foreach $i (0 .. 3) { print "$i\n"; }

配列の要素の処理には forwhile を使うことも可能だが,while を使った処理の場合には,注意が必要である。

   --------------------------------------------------
   @array = (0, 1, 2, 3);
   for ($i = 0; $i <= $#array; $i++) {
      print "$array[$i]\n";
   }
   --------------------------------------------------

   --------------------------------------------------
   @array = (0, 1, 2, 3);
   while (defined ($i = shift @array)) {
      print "$i\n";
   }
   --------------------------------------------------

while を使った処理では,shift により @array から要素を取り出し,defined (新出) により,値が定義されているかどうかを確認している。defined を省略すると,$i に第一の要素 0 が代入されると,偽と判断され,処理が終わってしまう。また,defined を使った場合でも,未定義値があればそこで処理が終わってしまうので,while を使うのは適当でない。(Perl では,未定義・数値の 0・文字列の "0"・空の文字列 "" は,偽と判断される。)


■3. 配列の要素の並べ替え(2): sort

sort はリストを引数に取り,並べ替えたリストを値として返す。並べ替えの方法を指定しないと,文字列として比較し,文字コード上の順番で並べ替える。

   #@new の値:('ape', 'cat', 'dog')
   @old = ('dog', 'cat', 'ape'); @new = sort @old;

並べ替えの順番を指定するには,ブロック { } を使う。

   sort { 定義 } リスト

定義部分が返す値 1, 0, -1 により,リストの要素が並べ替えられる。要素の比較には,演算子 <=>, cmp がよく使われる。(Cf. Lesson 5 「2. 比較の演算子」)

定義部分では,リスト中の要素一つ一つを指定する代わりに,$a$b の2つの変数を使って,二つの要素を比較する。

   @new = sort { $a cmp $b } @old    #文字列として比較しソート
   @new = sort { $a <=> $b } @old    #数値として比較しソート

$a$b の順序を入れ換えると逆順になる。

   @new = sort { $b cmp $a } @old
   @new = sort { $b <=> $a } @old

次のようにすると,すべて小文字に変換してから比較するので,大文字小文字を区別せずにソートすることができる。

   @new = sort { lc $a cmp lc $b } @old 

次のようにすると,後ろから文字を比較して,文字コード順で並べることができる。(何が何の引数・項かを明示するために,( ) を付けることが必要。)

   @new = sort { (reverse $a) cmp (reverse $b) } @old 

[さらに詳しいソート方法]


◇ スクリプトの実行:いろいろな順番で並び替える

ソートの定義を変えて出力し,結果を確認しなさい。

   % perl -e 'foreach $i (sort {$a cmp $b} (qw/1 01 10.0 10 1.5/)) { print "$i\n"};'

       1. {$a <=> $b}
       2. {$b <=> $a}

   % perl -e 'foreach $i (sort {$a cmp $b} (qw/cat turtle CAT Zebra/)) { print "$i\n"};'

       1. {$b cmp $a}
       2. {lc $a cmp lc $b}
       3. {lc (reverse $a) cmp lc (reverse $b)}
       4. {length $a <=> length $b}

◇ スクリプトの実行:単語をソートして出力する

次のスクリプトを実行しなさい。入力ファイルは適当なものを選ぶこと。出力の結果が確認できたら,スクリプトの各行の意味を確認しなさい。

   script/sort1.pl
   --------------------------------------------------
   while (<>) {
      push (@words, split);
   }

   foreach $i (sort { $a cmp $b } @words) {
      print $i . "\n";
   }

   exit;
   --------------------------------------------------

■4. リストの要素の指定

配列と同様,インデックスでリスト中の特定の要素を取り出すことができる。

   (リスト)[インデックス]

この方法を使うと,次のように一度配列に代入してから要素を取り出すようにしなくとも,

   @bugs = ("ant", "bee", "cricket");
   $first = $bugs[0];

リストにインデックスを振ることで,直接,要素を取りだすことができる。

   $first = ("ant", "bee", "cricket")[0];

リストを値として返す関数などを「リスト」部分に書くこともできる。

   $string = "ant bee cricket";
   $first  = (split " ", $string)[0];

例えば,各レコードの最後のフィールドを出力したいのなら,次のように書いてもよいが,

   while (<>) { @words = split; print $words[-1] . "\n"; }

次のように書くことも可能。

   while (<>) { print ((split)[-1] . "\n"); }
外側の ( )print 関数の引数を表わすもの,内側の ( ) はインデックスが振られたリストのもの。普通,print 関数の引数に付ける括弧は省略可能だが,引数の先頭にリストがあるため,この括弧が print 関数の引数と解釈されることを防ぐために,print の引数全体を括る括弧が必要になる。

◇ スクリプトの実行

次のスクリプトを実行し,結果を確認しなさい。(meibo1.txt の内容) 次にインデックスの数字を変え,指定したフィールドが抜き出せているか確認しなさい。

   % perl -ne 'chomp; print ((split ",", $_)[-1] . "\n")' data/meibo1.txt

◇ スクリプトの実行

script/kwic3.pl を実行し,結果を確認しなさい。なぜそのような結果になるのか,スクリプトの各行の意味を確認しなさい。また,前の文脈でソートする場合,単語を逆順に並べ替えて比較するようにするには,どうスクリプトを書き換えたらよいか考えなさい。

   script/kwic3.pl
   -----------------------------------------------------------------
   print STDERR "SEARCH  STRING: ";
   $string = <STDIN>;
   chomp $string;
   
   print STDERR "SORT KEY (1-3): ";
   $sortkey = <STDIN>;
   chomp $sortkey;
   
   $/ = "";
   
   while(<>){
   
      s/ *\n */ /g;
   
      while(/$string/ig){
   
         $key  =  $&;
         $pre  =  substr ($`, -25);
         $post =  substr ($', 0, 25);
   
         $file =  $ARGV;
         $file =~ s#.*/([^/]+)$#$1#;
   
         push (@lines, (join "\t", $file, $pre, $key, $post));
      }
   }
   
   foreach $line (sort { (split "\t", $a)[$sortkey]
                            cmp
                         (split "\t", $b)[$sortkey] } @lines) {
      $count++;
      printf "%5d | %-12.12s | %25s|%s|%-25.25s\n",
             $count, split ("\t", $line);
   }
   
   exit;
   -----------------------------------------------------------------

■5. 条件に合う要素の抜き出し・リストからリストへのマッピング

grep

grep は,リスト中の条件に合った要素のみからなるリストを返す関数である。条件は「式」または「ブロック」で指定する。リスト中の各要素は $_ で参照できる。

   grep , リスト
   grep { ブロック } リスト

例えば,ファイル名のリスト @files があったとして,その中から拡張子 .txt のものだけを取り出すには次のようにすればよい。

   @texts = grep  $_ =~ /\.txt$/, @files    #式の場合 ($_ =~ は省略可)
   @texts = grep {$_ =~ /\.txt$/} @files    #ブロックの場合 ($_ =~ は省略可)

「式」「ブロック」で $_ の値を変更すると,元のリストの値も変化する。次のスクリプトを実行すると,'a, b, c.dat' と出力する。

   --------------------------------------------------
   @files = ('a.txt', 'b.txt', 'c.dat'); 
   grep {$_ =~ s/\.txt$//} @files;
   print join (", ", @files);
   --------------------------------------------------

その他の例

   #'34' と出力
   print grep $_ > 2, (1, 2, 3, 4);
   #@i の内容: ("bee", "cricket")
   @i = grep {$_ gt "b"} ("ant", "bee", "cricket");

map

map は,リスト中の各要素を「式」または「ブロック」で評価した結果からなるリストを返す関数である。grep 同様,各要素は $_ で参照できる。

   map , リスト
   map { ブロック } リスト
   #@doubled の内容: (2, 4, 6)
   @doubled = map $_ * 2, (1, 2, 3);
   #[改行] 付きで 1 2 3 を出力。
   #foreach $i (1, 2, 3) { print $i . "\n" } と同じ
   print map {$_ . "\n"} (1, 2, 3);

「式」「ブロック」で $_ の値を変更すると,元のリストの値も変化する。

   #@texts の各要素の拡張子.txt を削除
   map {$_ =~ s/\.txt$//} @texts;

元のリストの値に手を加えたくないのであれば,例えば次のようにして処理する。

   @basenames = @files; map {$_ =~ s/\.txt$//} @basenames;

   @basenames = map {$i = $_; $i =~ s/\.txt$//; $i} @texts;
ブロックは最後に評価した式を返り値として返す。上の例なら $i が返り値となる。最後の $i がないと,ブロックは置換した回数を返す。

◇ スクリプトの実行:条件に合った要素の出力

次のスクリプトを実行しなさい。出力の結果が確認できたら,スクリプトの各行の意味を確認しなさい。

   $ perl -e 'print grep {/\bworth\b/i} <>' dll.txt
   $ perl -e 'print grep {s/\bworth\b/**$&**/ig} <>' dll.txt
ダイヤモンド演算子 <> がリストコンテキストで使われている点に注意。<> はリストコンテキストに置かれると,全レコードを要素とするリストを返す。while ループの処理の場合と違って,一度全テキストをリストとして読み込むため,テキストのサイズが大きいと処理に問題が出ることもある。

■6. 配列を使ったスクリプトの例

これまでに学んだことを組み合わせて使えばかなり複雑な処理ができる。しかし,どう組み合わせるかは,例を多く見ないとなかなか身に付かない。ここでは,配列を使った例を3つ示す。

例1:コンマ区切りテキストをソートして出力する

下のスクリプトは,コンマ区切りテキスト (例. meibo1.txt) を,指定したフィールド (1, 2 は除く) をキーとしてソートするスクリプトである。

   % perl script/sort_csv.pl data/meibo1.txt

   script/sort_csv.pl
   --------------------------------------------------
   #ソートキーを指定する
   print STDERR "SORT KEY: ";
   $key = <STDIN>;
   chomp $key;

   #全レコードを配列に入れる
   while (<>) { chomp; push @records, $_ }

   #指定されたキー (数値) でソートする
   @records_sorted = sort
                     {   (split ",", $a)[$key]
                                <=>
                         (split ",", $b)[$key]
                     }
                     @records;

   #ソートされたものを出力する
   foreach $i (@records_sorted) { print "$i\n" }

   exit;
   --------------------------------------------------

例2:語彙リストを作る

sort1.pl では,テキスト中のすべての語を指定した順番で出力した。語彙リストを作るには,複数回出現するものは一度だけ出力するようにスクリプトを書き換えればよい。sort1.pl に手を加え,同じ語は一度だけ出力するようにするにはどうしたらよいだろうか。

重複しない要素だけ出力するのなら,前の要素と比較し,内容が異なる場合だけ出力するようにすればよい。例えば,次のようにする。

   script/sort2.pl
   --------------------------------------------------
   while (<>) {
      push (@words, split);
   }

   foreach $i (sort { $a cmp $b } @words) {
      if ($prev ne $i) {     #前の語と異なるなら,
         print $i . "\n";    #改行付きで出力,
         $prev = $i;         #$iの値を$prevの新たな値とする。
      }
   }

   exit;
   --------------------------------------------------

例3:各語の頻度を出す

テキスト中の各単語の頻度を出力するには,ソートされた配列の要素について,前後を比較し,同じなら頻度を数えるカウンターの数を大きくし,異なれば頻度とともに出力すればよい。

   script/wordfreq1.pl
   --------------------------------------------------
   while (<>) {
      push (@words, split);
   }

   foreach $i (sort { $a cmp $b } @words) {
      if ($i eq $prev) {          #$iが前の単語と同じなら,
         $count++;                #カウンターの値を一つ大きくする。
      } else {                    #違う単語なら,
         print "$count\t$prev\n"; #「頻度+タブ+単語」の形式で出力,
         $prev  = $i;             #新しい単語を$prevの値とし,
         $count = 1;              #カウンターを1に戻す。
      }
   }

   print "$count\t$prev\n";       #最後の単語と頻度を出力

   exit;
   --------------------------------------------------

上のスクリプトでは,foreach ブロックの後に $prev を出力しているが,これがないと,最後の単語のデータが出力されない。出力結果だけを見ると問題がないように見えるので,ミスに気付きにくい。スクリプトを書くときは,小さなサイズのデータを処理し,処理結果が正しいかどうかを目で直接確認し,問題があれば修正してから,実際のデータを処理するようにした方が安全である。


□ 練習問題

練習問題A:必ずやること。

  1. sort1.pl を参考にして,英文テキストに現れる単語を次の順番で並べ替えるスクリプトを書きなさい。

    1. 大文字小文字を区別せずにアルファベット順でソート
    2. a. の逆順 (Z/z → A/a) でソート
    3. 単語を語尾から比較し,アルファベット順でソート
      (-a で終わる単語が -b で終わる単語よりも先に来る。)
    4. 各単語の文字数でソート
  2. dll.txt に含まれる文を,文字数の大きいものから小さいものへと並べ替えなさい。以前作成した1行1文に整形したテキストを利用すること。

  3. 適当なファイルを対象に sort2.plwordfreq1.pl を実行しなさい。出力結果を確認し,なぜそうなるのか,スクリプトの内容を確認しなさい。

練習問題B:余裕があったらやってみよう。

  1. wordfreq1.pl では,大文字と小文字を区別しソートしているが,これを,表示順では大文字と小文字を区別しないが,頻度を出すときには大文字と小文字を区別して出力するように,スクリプトを書き換えなさい。

    ヒント:{ lc $a cmp lc $b }{ lc $a cmp lc $b } に変えてソートしただけではうまくいかない。{ lc $a cmp lc $b || ... } のように,さらに比較の条件を付ける必要がある。... 部にどんな比較の条件を付ければよいか考える。[ソート方法について]

  2. wordfreq1.pl では,最初に不要な1行 ("\t\n") を出力してしまう。この不要な行を出力しないようにスクリプトを書き換えなさい。

  3. sort_csv.pl はフィールドを数値としてしか比較しないので,数値として比較できない文字列の場合,ソートは行えず,元の順序で出力される。ソートの定義に手を加え,数値として比較できない場合には,文字列として比較しソートできるようにしなさい。(||, or を使う必要がある。[ソート方法について])


[ ↑ INDEX ] [ ← PREV ] [ NEXT → ]