Perl を使おう! 第5回


[ 目次 / 前頁 / 次頁 ]

前回の宿題

  1. カウンタースクリプトをログ(アクセス時刻、アクセスした相手のホスト名とブラウザ名)をカンマ区切りで記録し、引数としてファイル名を指定するように修正。 (例えば、index.html のカウンターに対しては実行時引数として index を与え、カウンターファイル名を .num-index ロックファイル名を .lock-index ログファイル名を log-index とする。)
    回答例 by yama

前回の宿題でカウンタープログラムにログを付ける機能を追加しましたが、 そこにブラウザの種類を記述する項目を設けました。 どのような、ブラウザがあるのでしょうか? カウンターのログファイルを表示してみましょう。(ここではファイルの最後の10件)
% /usr/bin/tail log-index
ns4010117.ip-192-99-6.net,Tue Sep 30 19:47:49 JST 2014,Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
62.4.2.222,Tue Sep 30 20:31:22 JST 2014,Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
188.143.232.111,Tue Sep 30 21:38:43 JST 2014,Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1),
188.143.232.111,Tue Sep 30 21:38:49 JST 2014,Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1),
37.58.100.236-static.reverse.softlayer.com,Tue Sep 30 21:47:45 JST 2014,Mozilla/5.0 (compatible; AhrefsBot/5.0; +http://ahrefs.com/robot/),
ns4010117.ip-192-99-6.net,Wed Oct  1 02:41:36 JST 2014,Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
62.4.2.222,Wed Oct  1 07:56:36 JST 2014,Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0
37.203.211.70,Wed Oct  1 11:39:54 JST 2014,Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
101.72.156.27.broad.fz.fj.dynamic.163data.com.cn,Wed Oct  1 11:53:39 JST 2014,Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0
104.128.29.248,Wed Oct  1 15:28:51 JST 2014,Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
このように、あなたのホームページを訪れた人に関する情報が集まりだすと、 いろいろとまとめてみたくなるものです。 (どんな所から来ているかとか、どんなブラウザを使っているかなどの情報がすでに集まってるのですから。) また、ブラウザの種類によって異なった処理を行なうようなことについても考えてみましょう。 ファイルの中から文字列のマッチを行なうような場合には、正規表現が有効になります。 今回は、perlにおける正規表現とパターンマッチ、置換について、またログファイルを扱うのでファイルハンドルについても詳しく扱います。

Perl 言語 その4

ファイルハンドル

  1. ファイルハンドル?

    すでに簡単なファイルハンドルについては了解しており、カウンターファイルやログファイルの為にファイルからの読み込みや書き出しを利用していますが、 ここではPerlにおけるファイルハンドルについてもう一度詳しくまとめます。
    ファイルハンドルとはPerlのプロセスと外部とのI/Oコネクションに対して Perl プログラム内でつけた名前のことであり、STDINはPerlプロセスと標準入力とのコネクションの、STDOUTは標準出力、STDERRは標準エラーとのコネクションのファイルハンドルです。ファイルハンドル名もまた、独自の名前空間によって管理されていますが、ファイルハンドルの先頭には特別な文字をつけません。 そのため、予約語とぶつかる可能性があるためファイルハンドルには大文字だけを使うことが推奨されています。

  2. ファイルハンドルのオープンとクローズ

    Perlにはあらかじめ3つのファイルハンドルSTDIN, STDOUT, STDERRが用意されており、 これら以外もファイルハンドルを利用するためには、open() 演算子を利用し、ファイルハンドルの使用後 close() 演算子によりファイルハンドルをクローズします。
        open(FILEHANDLE,"filename")   (または "<filename")
          filenameをファイルハンドルFILEHANDLEに結びつけ読み込み用にオープン
        open(FILEHANDLE,">filename")
          filenameをファイルハンドルFILEHANDLEに結びつけ書き出し用にオープン
        open(FILEHANDLE,">>filename")
          filenameをファイルハンドルFILEHANDLEに結びつけ追加書き出し用にオープン
    
    いずれの場合も open() は成功すれば真を、失敗すれば偽を返します。 (ファイルがない、パーミッションがないなどの場合が考えられます。) すでにオープンされているファイルを再びオープンしたり、プログラムの終了時にはファイルは自動的にクローズされるので、close()は必ずしも利用しなければならない訳ではありません。

  3. ファイルハンドルのエラー処理(die()演算子)

    通常ファイルハンドルを使用する際に起こるエラーに対する処理として、 「成功したら続行し、失敗したらエラーを出力し終る」といった流れがあります。 このような時に利用されるのが、 die() 演算子です。 die() 演算子はリストを受け取りそのリストを標準エラーへと出力し、 このプログラムを動かしているPerl のプロセスを終了させ、何か失敗したことを示す終了ステータスを返します。
        open(FILE,"filename") || die "I can't open this file\n" 
    
    || 演算子を利用し、open() 演算子が偽の場合に die() が実行されます。die() のメッセージにPerlのプログラム名と行番号を付加したい場合にはメッセージの最後に改行を入れず、付加したくない場合には改行を入れます。
        die "I can't open this file"    #表示する 
        die "I can't open this file\n"  #表示しない
    


  4. ファイルハンドルの利用

    入力用にオープンしたファイルハンドルはSTDINの場合同様ブラケットで囲んで使用します。 前回の宿題でログファイルを作ったので、これを使用しましょう。 まずは単純に眺めてみましょう。
        open(LOG,"log-index");
        while ($a=<LOG>){
          print $a;
        }
    
    これでファイルの内容をずらっと表示することは可能ですが、前回やったように、 これは次のように$_ を利用して簡単に書くことができます。
        open(LOG,"log-index");
        while (<LOG>){
          print;
        }
    
    引数を省略した場合には $_ に対して処理が行なわれる性質を利用したものでしたね。 このくらいの処理のためにエディタを利用するのも面倒なので、コマンドラインから実行してみましょう。
        % perl -e 'open(LOG,"log-index"); while(<LOG>){ print; }'
        150.46.103.154,Mon Jan 20 16:50:01 JST 1997,Mozilla/3.01 (Win95; I)
        tcur1ds29.iba.mesh.ad.jp,Mon Jan 20 17:11:02 JST 1997,Mozilla/2.02 [ja] (Win95;I)
        (中略)
        owari.nmiri.city.nagoya.jp,Mon Jan 20 18:44:11 JST 1997,Mozilla/3.01 (X11; I; SunOS 5.4 sun4m)
        210.133.202.2,Mon Jan 20 21:51:04 JST 1997,Mozilla/2.0 (compatible; MSIE 3.01; Windows 95)
    
    どっと出力されてしまいました。 これだけの処理をするなら、cat を使った方が簡単ですね。 では、行番号をつけるなどの付加的な処理を行なってみましょう。 行番号は $. という変数に入っているのでこれを利用します。
        open(LOG,"log-index");
        while (<LOG>){
          print "$. : $_";
        }
    のように変更しました。
        % perl -e 'open(LOG,"log-index"); while(<LOG>){ print "$. : $_"; }'
        1 : 150.46.103.154,Fri Nov 22 22:11:48 JST 1996,Mozilla/3.01 (Win95; I)
        2 : kyot1du07.kyt.mesh.ad.jp,Sat Nov 23 01:34:08 JST 1996,Mozilla/2.02 [ja] (Win95; I)
       (中略)
        440 : owari.nmiri.city.nagoya.jp,Mon Jan 20 18:44:11 JST 1997,Mozilla/3.01 (X11; I; SunOS 5.4 sun4m)
        441 : 210.133.202.2,Mon Jan 20 21:50:58 JST 1997,Mozilla/2.0 (compatible; MSIE 3.01; Windows 95)
        442 : 210.133.202.2,Mon Jan 20 21:51:04 JST 1997,Mozilla/2.0 (compatible; MSIE 3.01; Windows 95)
    
    これらは起動時パラメータを使用してもっと簡単に
        % perl -e 'while(<>){ print "$. : $_"; }' log-index
    
    として得ることもできます。

    書き込み用、追加書き込み用にオープンしたファイルハンドルは print キーワードと引数のリストの間にファイルハンドルを置きます。

        print LOG "date: $date\n";
        print STDOUT "Hello, world!\n";  # print "Hello, world!\n"と同じ
    

  5. ファイルテスト演算子

    ファイルが存在しているかなどファイルのテストを行なう演算子が用意されている。 ファイルテスト演算子によりファイルをテストするにはファイル演算子に対してファイル名を与えます。 この演算子に対するオペランドとしては文字列リテラルのみならず、評価すると文字列値になるスカラー式も利用できます。
        $log="log-index"
        if (-e $log){
            open(LOG,"log-index");
            ......                 #処理
        } else {
            print "Not exist!";
        }
    とか
       if (-e ".num" && -e ".lock"){
       ...
       }
    とか
       if (-r "log-index" && -w "log-index"){
       ...
       }
    
    のように利用できます。

    ほとんどのファイルテスト演算子は真か偽かを返しますが、 -s 演算子はファイルが空でなければ、バイト単位でファイルの大きさを返すことにより真を返し、-M, -A, -C は日数を返します。
    またオペランドとしてファイルハンドルを利用することもできます。 オペランドを省いた時には、いつものように $_ 変数により指定されている名前のファイルをテストします。

    ファイル演算子一覧

    演算子 テスト
    -e ファイルやディレクトリが存在している
    -z ファイルが存在していてかつ大きさが0
    -s ファイルやディレクトリが存在していてかつ大きさが0でない
    -r ファイルやディレクトリが読みだし可能
    -w ファイルやディレクトリが書き込み可能
    -x ファイルやディレクトリが実行可能
    -o ファイルやディレクトリをユーザが所有
    -R ファイルやディレクトリが実効ユーザでなく実ユーザにより読みだし可能<
    -W ファイルやディレクトリが実効ユーザでなく実ユーザにより書き込み可能
    -X ファイルやディレクトリが実効ユーザでなく実ユーザにより実効可能
    -O ファイルやディレクトリが実効ユーザでなく実ユーザにより所有
    -f 普通のファイルである
    -d ディレクトリである
    -l シンボリックリンクである
    -S ソケットである
    -p 名前つきパイプである
    -b ブロック特殊デバイスである
    -c キャラクタ特殊デバイスである
    -u ファイルやディレクトリがsetuidされている
    -g ファイルやディレクトリがsetgidされている
    -k ファイルやディレクトリがstickyビットがセットされている
    -t このファイルハンドルに対して isatty()が真である
    -T テキストファイルである
    -B バイナリファイルである
    -M 最終更新からの日数
    -A 最終アクセスからの日数
    -C inodeの最終変更からの日数

正規表現

  1. マッチと置換

    文字列に対して、マッチや置換を行なう際には =~ 演算子を用い、マッチの場合はマッチ演算子 / / (スラッシュとスラッシュの間にパターンを記述)を、置換には置換演算子 s/ / / を利用します。
    マッチ
    $a =~ /abc/ # $a に"abc"という文字列があれば真、そうでなければ偽を返す
    ($a =~ /abc/option #オプションについては後述)

    置換
    $a =~ s/abc/def/ # $a に"abc"という文字列を見つけたらそれを"def"に置換
    ($a =~ s/abc/def/option #いろいろなオプションについては後述)

    grep と同じ動作をperlにさせるのも簡単であり、さきほどのログファイルについてブラウザの種類がいつくかみられましたがそれに関して探してみましょう。 ブラウザはネットスケープまたはインターネットエクスプローラのときには Mozilla と記されていました。 それにマッチする行を出力するには
        open(LOG,"log-index");
        while(<LOG>){
          if($_=~/Mozilla/){
             print "$_";
          }
        }
    
    とすればよいですが、$_ はここでも省略可能ですから、パターンマッチに使用する変数が$_の場合には単に if(/Mozilla/){ .. で十分です。 ついでにMozilla がある行から、ブラウザ情報を抜き出し、行番号と共に表示し、最後に出現トータルを表示するスクリプトを記述しましょう。
        $ARGV[0]="log-index";    #実行時パラメータとして扱われる。
        $cnt=0;
        while(<>){
          if(/Mozilla/){
             $cnt++;
             @log=split(/,/);
             print "$.: $log[2]\n";
          }
        }
        print "Total: $cnt\n"
    
    ここで、$ARGV[0]="log-index"; としているのは、実行時の引数としてファイル名をダイヤモンド演算に渡す方法については、すでに扱いましたが実際にはダイヤモンド演算しは @ARGV を見に行くので、実行時パラメータを利用するスクリプトを書く際に、 実行の確認でいちいち実行時パラメータを入れるのが面倒な時 @ARGV を利用します。 また、コマンドラインからは
    % perl -e '$cnt=0;while(<>){ if(/Mozilla/){ $cnt++; @log=split(/,/);print "$.: $log[2]\n";} }print "Total: $cnt\n"' log-index
    とすればいいでしょう。 しかし、実際にはマッチや置換はパターンにより処理することが多いですからPerlで利用できるパターンについて知らないと効力は半減です。

  2. パターン
    1文字のパターン
  3. 文字1つ:その文字自身にマッチ
        . (ドット):改行文字以外の任意の1文字にマッチ
        
  4. 文字クラス:[]で囲まれた文字のどれか1つにマッチ (A-Zの表記可) [abcd] : abcdのいずれかにマッチ [a-dA-D] : [abcdABCD]と同等 [0-9+\-] : 数字かプラス(+)マイナス(-)にマッチ(マイナス自身は\を前に置く)
  5. 否定された文字クラス:[^]内に含まれない1文字にマッチ
        [^0-9] : 数字以外の1文字にマッチ
        [^\^] : ^(キャレット)以外の1文字にマッチ
    
  6. バックスラッシュ付の文字(位置指定文字)
        \n  改行文字
        \r  復帰文字
        \t  タブ
        \f  改ページ
        \b  単語境界
        \B  単語境界以外
        \nnn 8進値nnnのASCII文字
        \xnn 16進値 nnのASCII文字
        \cX  ASCIIコントロール文字
    
        ^   行頭  (^自身を文字列の先頭で指定する場合には\^で指定)
        $   行末  (パターンの末尾以外ではスカラー変数名として解釈される)
    
  7. あらかじめ定義されている文字クラス
        \d (数字)     : [0-9]
        \w (単語)     : [a-zA-Z0-9_]
        \s (空白文字) : [ \r\t\n\f]
        \D (数字以外)     : [^0-9]
        \W (単語以外)     : [^a-zA-Z0-9_]
        \S (空白文字以外) : [^ \r\t\n\f]
    
    パターンのまとまり
  8. 並び
        abc  : aの次がbでその次がcと並んでいるパターン
    
  9. 繰り返し
        * : 直前の文字の0回以上の繰り返し
        + : 直前の文字の1回以上の繰り返し
        ? : 直前の文字の0回または1回の繰り返し
        {s,e} : 直前の文字のs回からe回までの繰り返し       
            a{3,5} aが3個から5個
            a{5,}  aが5個以上
            a{5}   aが5個
            a{,5}  aが5個以下
    
  10. 選択
        a|b : a または b
            /Blue|Red/ のように並びに利用できる(一文字の選択は/[ab]/を利用)
    
  11. ()による記憶
        (パターン) : ()内で指定したパターンにマッチした内容をインデックスを付けて保存
          \整数 (バックスラッシュ+整数n) : n番目の()にマッチした内容
             /a(.)b\1/  は axbx にはマッチするがaxby にはマッチしない
             /a(.)b(.)c\2d\1/  はaxbycydx などにマッチする
    
  12. マッチ演算子

    マッチ演算子に関するいくつかのオプションがあります。

    大文字と小文字を同一視する
        /regexp/i  
    
    i オプションは大文字と小文字を同一視します。パターン /sub/ はsub, SUB, Subなどにマッチします。

    スラッシュ以外の区切り文字を使う
        mdelimiter regexp delimiter
    
    パターンの中でスラッシュを指定する場合には、バックスラッシュを前におきますが、このかわりにスラッシュ以外のデリミタ(区切り記号)を使うことができます。 /\/usr\/local/ とするかわりに m%/usr/local% とした方が見やすいですね。

  13. 置換演算子

    保存されたパターンの為の特殊変数
    パターンマッチで()内に指定したパターンが \1 \2 .. で正規表現の中で利用できることについてすでに触れましたが、マッチさせたあとでこれらのパターンを使用する為に $1, $2 の特殊変数が用意されています。 また、$& はマッチした文字列全体を返し、$`, $' はそれぞれマッチした部分より前および後ろにある全ての文字列を返します。
    置換演算子に関するオプションとしていくつか用意されているものについて
    グローバルな置換
        s/regexp/string/g
    
    g オプションを指定しない場合、最初に見つかったものに対して置換を行ない置換を終了します。 g オプションを指定した場合、マッチ可能な全ての部分について置換を行ないます。

    大文字と小文字を同一視する
        s/regexp/string/i
    
    置き換え後に評価する
        s/regexp/string/e
           $_='ab12cd';
           s/\d+/$&*2/e;      # $_ は 'ab24cd'に
           s/\w/$& x 2/eg;    # $_ は 'aabb2244ccdd'に
    

    スラッシュ以外の区切り文字を使う
        s#regexp#string#
    
    sのあとにおいた文字がデリミタになります。

  14. 変換演算子

    指定されたパターンの置換ではなく、文字単位で変換する演算子が tr 演算子です。
        tr/abc/ABC/
    
    tr 演算子は元の文字列のリストにある文字を、新しいリストの対応する文字に置き換えます。
    削除オプション d
    末尾にdをつけると新しい文字列のリストが元の文字列のリストより短い場合に、対応する文字がない文字を削除します。
    補集合オプション c
    指定した文字の補集合に対して変換を行ないます。
    圧縮オプション s
    同じ文字が連続する場合にそれらを1つに圧縮します。

  15. split(), join()

    すでに split(), join() については扱ってきていますが、これまでは//に囲まれた文字で文字列を切る(結合する)という使用方法をとってきましたが、実は//で指定した正規表現に対して行なわれます。
さて、準備はだいぶ整ったようなので次回からは本格的に Perl を使いこなしていきましょう。

宿題!

  1. 前の宿題にあった日付を表示するSSIプログラムの修正
    実は、前回作った日付を表示するプログラムは不完全で、その日が1日から9日の場合(一桁の日)にはエラーとなります。この問題は、このときには、空白が2つあるために失敗します。これを、修正して下さい。
  2. 自分ページのログファイルを解析し Netscape と MSIE その他の数を集計する。
  3. (おまけ)カウンタープログラムでファイルのロックを行ないましたが、flock()はjperlなどの場合にはエラーとなります。 perl4を利用できない人はロックをしないで作っていたことと思います。 他の方法を使って、同時アクセスの問題を解消してみましょう。

[ 目次 / 前頁 / 次頁]
最終更新時刻 03/06/17 21:06
By Yoshiro Yamamoto