最初に,dll.txt を対象に script/pict0b.pl を実行し,結果を確認する。
---------------------------------------- % perl script/pict0b.pl dll.txt Node (regex): \bonly\b Context (2-5): 2 ---------------------------------------- ・Node (regex): 検索語を正規表現で指定 ・Context (2-5): 前後に表示する語の数を指定
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++; }'
複数の要素を一つにまとめるには,リストを用いる。要素同士はコンマで区切り,全体を ( ) で括る。(文脈によっては ( ) を付けなくとも大丈夫だが,一応付けるものとしておく。) 要素は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") と同じ
これまで扱ってきた変数は値を一つだけ持つもの(「スカラー変数」という)だったが,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)
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; ------------------------------------------------------------
既に見たように,インデックス(数字,変数)で指定する。
$var = $array[0]; #@array の最初の要素の値を$varに代入 $var = $array[$#array]; # 〃 最後 〃 $array[0] = 10; #@array の最初の要素の値を10とする
最後の例のように配列の要素 ($array[0]) に値を代入した場合,もし対応する配列 (@array) が存在しなければ,自動的に作られる。
要素の削除・取り出し:shift と popshift は配列の最初の要素を削除する。返り値として削除した要素を返す。pop は配列の最後の要素を削除し,返り値として削除した要素を返す。
$変数 = shift (@配列); #最初の要素が削除され,変数に代入される $変数 = pop (@配列); #最後の 〃 ,変数に 〃
unshift は配列の先頭に,push は配列の末尾に要素を追加する。「挿入要素」はリスト。コンマで区切って並べてもよいし,配列で指定してもよい。
unshift (@配列, 挿入要素) #先頭に挿入 push (@配列, 挿入要素) #末尾に挿入
リストの中にリストを入れると展開されて,一つのリストになる。
(0, (1, 2, 3)) #(0, 1, 2, 3) と同じ ((0, 1, 2), 3) # 〃
これを利用して,次のやり方でも配列に要素を代入することができる。
@配列 = (挿入要素, @配列) #先頭に挿入 @配列 = (@配列, 挿入要素) #末尾に挿入
shift ← unshift → |
@配列 ($配列[0], $配列[1], ... $配列[$#配列]) | → pop ← push |
インデックスをリストで指定すると,配列の一部をリストとして取り出すことができる。配列なので,$ ではなく @ を付ける点に注意。
@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; ----------------------------------------------------------------------
@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 とは無関係。勘違いしやすいので注意すること。
meibo1.txt の a) 各行の合計点を出すスクリプト,b) 縦の列それぞれの平均点を出すスクリプトを書きなさい。
コマンドラインから「検索語」「前後の語数」が指定できるように pict0b.pl を書き換えなさい。
pict0b.pl を書き換え,コマンドラインで「検索語」「前後の語数」が指定された場合にはその値を使い,指定されていない場合には,標準入力から入力できるようにしなさい。
下に示す 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文中の語数」になると考える。
配列の n 番目の要素が n 語の文の頻度を表わす (たとえば,2番目の要素 $freq[2] は2語文の頻度を表わす) と考える。
@freq の内容: ($freq[0], $freq[1], $freq[2], ... $freq[$#freq])
上の n-word_S.pl を参考にして,1語文・2語文...の頻度をグラフで表わすスクリプトを書きなさい。入力テキストは1文1行に整形されているものとする。
n-word_S.pl に手を加え,出力の最初の部分に,文の総数・総語数・1文当たりの平均語数・最小語数・最大語数を表示するようにしなさい。