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

Lesson 6 リスト,配列,for


■0. 準備

最初に,dll.txt を対象に script/pict0b.pl を実行し,結果を確認する。

   ----------------------------------------
   % perl script/pict0b.pl dll.txt
   Node  (regex): \bonly\b
   Context (2-5): 2
   ----------------------------------------
      ・Node  (regex): 検索語を正規表現で指定
      ・Context (2-5): 前後に表示する語の数を指定

■1. for によるループ処理

   for (初期設定; 条件; ) { 処理 }

まず,「初期設定」で指定されている処理を行い,「条件」が真のあいだ「処理」を行う。「処理」終了後,毎回「式」を評価し,「条件」が真かどうかチェックする。


 「初期設定」を実行
    │
    │ ←─────────── 「式」を実行
    │                ↑
    ↓                │
 「条件」をチェック ─→ 真 ─→ 「処理」を行う 
    ↓
    偽
    ↓
  ループ処理の終了

例えば,次のように使う。

   #1〜10を改行付きで出力
   for ($i = 1; $i <= 10; $i++) { print "$i\n"; }

for と同じことは while を使ってもできるので,使いやすい方を使えばよい。

   初期設定; while ( 条件 ) { 処理; ; }

   $i = 1; while ( $i <= 10 ) { print "$i\n"; $i++; }

while 同様,無限ループに陥らないように気をつけること。


◇ スクリプトの実行

次のスクリプトを実行して,結果が同じになることを確認しなさい。次に,数字部分の値を変え実行し,予想通りの結果になるか確認しなさい。

   1. % perl -e 'for ($i = 1; $i <= 10; $i++) { print "$i\n"; }'
   2. % perl -e '$i = 1; while ( $i <= 10 ) { print "$i\n"; $i++; }'

■2. リスト:要素のグループ化

複数の要素を一つにまとめるには,リストを用いる。要素同士はコンマで区切り,全体を ( ) で括る。(文脈によっては ( ) を付けなくとも大丈夫だが,一応付けるものとしておく。) 要素は0番目から数える。

   (要素0, 要素1, 要素2, ..., 要素n)

代入演算子の右辺にリストを使うと,一度に複数の変数に値を代入できる。

   #3つの変数にまとめて値を代入
   ($count, $freq, $times) = (0, 0, 0);

   #2つの変数の値を入れ換える。
   ($y, $x) = ($x, $y);

右辺のリストの要素の数が多いと,その部分は無視される。逆に,左辺のリストの要素の方が多いと,対応しない要素には値が代入されない。

   #$a に $x の値が,$b に $y の値が代入される。
   ($a, $b) = ($x, $y, $z);

   #$a, $b に $x, $y の値が代入され,$c はそのまま。
   ($a, $b, $c) = ($x, $y);

(スペースを含まない) 文字列を要素としたリストを作るには,クォート風演算子 qw を使うと,引用符とコンマを省略できるので便利である。

   qw/Sun Mon Tue Wed Thu Fri Sat/

   ← "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" に相当

範囲演算子 .. を使うと,値が連続したリストが簡単に作れる。

   (0 .. 4)       #(0, 1, 2, 3, 4) と同じ
   (0 .. 4, 6)    #(0, 1, 2, 3, 4, 6) と同じ
   ("A" .. "D")   #("A", "B", "C", "D") と同じ

■3. 配列:リストに名前を付ける

これまで扱ってきた変数は値を一つだけ持つもの(「スカラー変数」という)だったが,0個以上の値を保持するには,配列(配列変数)を用いる。名前付きのリストのようなもの。(以後,特に断らないかぎり,リストと言えば配列のことを含むものとする。)

配列名には @ を付ける。代入演算子を使って,リストの内容を配列に代入することができる。

   @wdays = ("Sun.", "Mon.", "Tue.", "Wed.", "Thur.", "Fri.", "Sat.");

個々の要素は「$配列名[インデックス]」で表わす。要素を表わす変数は値を一つしか持たないスカラー変数なので,@ ではなく $ が付く。「インデックス」には数字を使う。

   $配列名[インデックス]

要素は0番目から数えるため,インデックスは 0 から「要素の数 -1」までとなる。負の数を指定すると,末尾からの位置を表わす。

   $wdays[0]  $wdays[1]  $wdays[2]  $wdays[3]  $wdays[4]  $wdays[5]  $wdays[6]
      ↓         ↓         ↓         ↓         ↓         ↓         ↓
   ( "Sun.",    "Mon.",    "Tue.",    "Wed.",    "Thu.",    "Fri.",    "Sat." )
      ↑         ↑         ↑         ↑         ↑         ↑         ↑
   $wdays[-7] $wdays[-6] $wdays[-5] $wdays[-4] $wdays[-3] $wdays[-2] $wdays[-1]

最後の要素のインデックスは「$#配列名」という変数に収められる。(上の例では,$#wdays の値は 6。) 配列が作成されたり,配列の要素の数に変更を加える操作をすると,「$#配列名」の値は自動的に変更される。

                  インデックス
             0    1    2     ...    n (= $#配列名)
   @配列名 ─────────────→
           (値0, 値1, 値2,   ...   値n) 

■4. 文字列の分割,文字列への結合:split, join

split を使うと,文字列を複数の部分に分割できる。split は文字列を指定された区切り文字で分割したリストを返す。

   split (区切り文字, 文字列)

引数を省略すると,区切り文字は /\s+/,対象は $_ となる。空の文字列を指定すると,文字単位で分割される。

   #$_ を空白類の連続で分割し,配列 @words に代入する
   @words = split (/\s+/, $_);

   #上と同じ
   @words = split;

   #文字単位に分割し,配列に代入
   @letters = split (//, "cat");   #@letters の内容:("c", "a", "t")
   ------------------------------------------------------------
   #語の数を数える

   while(<>){
      s/^\s+//;                    #行頭の空白類を削除
      s/\s+$//;                    #行末の 〃
      @words = split (/\s+/, $_);  #空白類で分割し,配列に代入
      $count += ($#words + 1);     #配列の要素数を$countに追加
   }

   print $count . "\n";            #$count の値を出力

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

join はリストの要素を指定された文字列を挟んで1つの文字列に結合する。

   join (区切り文字, リスト)
   #@words の要素をスペースで連結した結果を出力
   print join (' ', @words);

   #コンマ区切りテキストをタブ区切りに直し出力
   print (join ("\t", (split (",", $_))));

◇ スクリプトの実行

1. 次のスクリプトを実行し,結果を確認しなさい。

   % perl script/wc.pl dll.txt

   script/wc.pl
   ------------------------------------------------------------
   #語の数を数える

   while(<>){
      s/^\s+//;                    #行頭の空白類を削除
      s/\s+$//;                    #行末の 〃
      @words = split (/\s+/, $_);  #空白類で分割し,配列に代入
      $count += ($#words + 1);     #配列の要素数を$countに追加
   }

   print $count . "\n";            #$count の値を出力

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

2. 次のスクリプトを実行し,結果を確認しなさい。

   % perl script/pict0a.pl dll.txt

   script/pict0a.pl
   ------------------------------------------------------------
   $/ = "";

   while (<>) {
      tr/A-Z/a-z/;
      @words = ("", (split /\W+/, $_), "");
      for ($i=1; $i<=$#words-1; $i++) {
         if ($words[$i] =~ /\bjust\b/i) {
            printf "%s\t%s\t%s\n",
                   $words[$i-1], $words[$i], $words[$i+1];
         }
      }
   }

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

■5. 配列の要素の挿入・削除

要素の指定,値の取り出し・代入

既に見たように,インデックス(数字,変数)で指定する。

   $var = $array[0];         #@array の最初の要素の値を$varに代入
   $var = $array[$#array];   # 〃   最後   〃
   $array[0] = 10;           #@array の最初の要素の値を10とする

最後の例のように配列の要素 ($array[0]) に値を代入した場合,もし対応する配列 (@array) が存在しなければ,自動的に作られる。

要素の削除・取り出し:shiftpop

shift は配列の最初の要素を削除する。返り値として削除した要素を返す。pop は配列の最後の要素を削除し,返り値として削除した要素を返す。

   $変数 = shift (@配列);   #最初の要素が削除され,変数に代入される
   $変数 = pop   (@配列);   #最後の   〃   ,変数に 〃

要素の挿入:unshiftpush

unshift は配列の先頭に,push は配列の末尾に要素を追加する。「挿入要素」はリスト。コンマで区切って並べてもよいし,配列で指定してもよい。

   unshift (@配列, 挿入要素)       #先頭に挿入
   push    (@配列, 挿入要素)       #末尾に挿入

リストの中にリストを入れると展開されて,一つのリストになる。

   (0, (1, 2, 3))                  #(0, 1, 2, 3) と同じ
   ((0, 1, 2), 3)                  #   〃

これを利用して,次のやり方でも配列に要素を代入することができる。

   @配列 = (挿入要素, @配列)       #先頭に挿入
   @配列 = (@配列, 挿入要素)       #末尾に挿入
shift ←
 unshift →
@配列
($配列[0], $配列[1], ... $配列[$#配列])
 → pop
 ← push


■6. 配列スライス

インデックスをリストで指定すると,配列の一部をリストとして取り出すことができる。配列なので,$ ではなく @ を付ける点に注意。

   @alphabet = ("A" .. "Z");
   @alphabet[0,1,2];          #("A", "B", "C")
   @alphabet[2..4];           #("C", "D", "E") 

◇ スクリプトの実行

script/pict0b.pl を実行し,結果を確認しなさい。なぜそのような結果になるのか,スクリプトの内容を確認しなさい。

   % perl script/pict0b.pl dll.txt

   script/pict0b.pl
   ----------------------------------------------------------------------
   #検索語の指定
   print STDERR "Node  (regex): ";
   $node = <STDIN>;
   chomp $node;
   $node =~ tr/A-Z/a-z/;

   #表示する前後の語数の指定
   print STDERR "Context (2-5): ";
   $num = <STDIN>;
   chomp $num;
   $num = 2 if ($num < 2);
   $num = 5 if ($num > 5);

   #$num 個からなる配列を作成
   $#emptySlots = $num-1;

   #段落単位で読み込む
   $/ = "";                               

   while (<>) {

      #レコードをすべて小文字に
      tr/A-Z/a-z/;

      #記号類で分割 (don't→don t),前後に空の要素を加え配列に
      @words = (@emptySlots, (split /\W+/, $_), @emptySlots);

      #先頭の単語 ($words[$num]) から末尾の単語 ($words[$#words-$num]) までを処理
      for ($i=$num; $i<=$#words-$num; $i++) {

         #正規表現にマッチしたら
         if ($words[$i] =~ /$node/) {

           #前後の語と一緒にタブ区切りで出力
           print join ("\t", @words[$i-$num .. $i+$num]), "\n";

         }
      }
   }

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

■7. 特殊配列 @ARGV

@ARGV にはコマンドラインでスクリプトを実行したときの,スクリプトより後ろの引数のリストが収められている。配列 @ARGV を使うと,値をコマンドラインから直接スクリプトに渡すことができる。例えば次のようにコマンドラインから Perl を起動すると,

   % perl  script/merge.pl  data/nihongoA.cha  data/nihongoB.cha

実行中の merge.pl@ARGV の値は ("data/nihongoA.cha", "data/nihongoB.cha") となっている。各要素には $ARGV[0], $ARGV[1] でアクセスできるので,次のようにスクリプト中でファイルを指定していたものも,

   script/merge.pl
   --------------------------------------------------
   $fileA = "data/nihongoA.cha";
   $fileB = "data/nihongoB.cha";

   <<以下略>>
   --------------------------------------------------

次のように書き換えることで,スクリプト自体は変更せずに,処理するファイルをコマンドラインから指定できるようになる。

   --------------------------------------------------
   $fileA = $ARGV[0];
   $fileB = $ARGV[1];

   <<以下略>>
   --------------------------------------------------

コマンドラインの引数にワイルドカードが含まれている場合には,シェルによって展開された結果が @ARGV に入る。例えば,以下のように voa/* を引数に指定した場合,voa/* という文字列ではなく,展開された結果が @ARGV に入る。

   --------------------------------------------------
   % perl -e 'print join ("\n", @ARGV), "\n"' voa/*
   voa/2-257701.txt
   voa/2-257702.txt
   voa/2-257703.txt
   voa/2-257705.txt
   voa/2-257706.txt
   voa/2-257707.txt
   ...
   voa/2-257849.txt
   voa/2-257850.txt
   --------------------------------------------------
注意 $ARGV[0], $ARGV[1] などは @ARGV の要素であるが,インデックスの付かない $ARGV@ARGV とは無関係。勘違いしやすいので注意すること。

□ 練習問題

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

  1. meibo1.txt の a) 各行の合計点を出すスクリプト,b) 縦の列それぞれの平均点を出すスクリプトを書きなさい。

  2. コマンドラインから「検索語」「前後の語数」が指定できるように pict0b.pl を書き換えなさい。

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

  1. pict0b.pl を書き換え,コマンドラインで「検索語」「前後の語数」が指定された場合にはその値を使い,指定されていない場合には,標準入力から入力できるようにしなさい。

  2. 下に示す n-word_S.pl は,1行1文に整形されたテキストを基に,1語文・2語文・3語文 ... それぞれの出現頻度を出すスクリプトである。適当なファイルを対象にスクリプトを実行し,結果を確認しなさい。なぜそのような結果が得られるのか考えなさい。

       script/n-word_S.pl
       --------------------------------------------------
       @freq = ();                         #配列を初期化
    
       while (<>) {
          next if (/^\s*$/);               #空行は処理せず次の行へ
          s/^\s+//;                        #行頭の空白類を削除
          s/\s+$//;                        #行末の 〃
          @words = split (/\s+/, $_);      #空白類で分割,配列へ代入
          $number = $#words + 1;           #語数≒配列の要素数
          $freq[$number]++;                #該当語数の文の数を1大きくする
       }
    
       for ($i = 1; $i <= $#freq; $i++) {  #1から最大語数まで,
          printf "%3d: %3d\n",             #この形式で
                  $i,  $freq[$i];          #語数($i)と頻度($freq[$i])を出力
       }
    
       exit;
       --------------------------------------------------
           0語文は存在しないので,$freq[1] 以降を出力している
    

    基本的な考え方:

    1. 空白類で行を分割し配列を作ば,「配列の最後のインデックス+1」が「要素の数」,即ち「1文中の語数」になると考える。

    2. 配列の n 番目の要素が n 語の文の頻度を表わす (たとえば,2番目の要素 $freq[2] は2語文の頻度を表わす) と考える。

      @freq の内容: ($freq[0], $freq[1], $freq[2], ... $freq[$#freq])
      
  3. 上の n-word_S.pl を参考にして,1語文・2語文...の頻度をグラフで表わすスクリプトを書きなさい。入力テキストは1文1行に整形されているものとする。

  4. n-word_S.pl に手を加え,出力の最初の部分に,文の総数・総語数・1文当たりの平均語数・最小語数・最大語数を表示するようにしなさい。


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