MP0

情報技術のこととか

モダーンなPerlの書き方

この記事は OIT Advent Calendar 2016 - Adventar 8日目の記事です。

軽く自己紹介

  • 冷雨 (@_ybrliiu) | Twitter
  • IN科3年
  • Perl大好きなことで有名かもしれません。
  • それより遅刻が多いことで有名ですね
  • 十国志NETっていうオンラインシュミレーションゲームを運営しています!

さて、、

後期の前半にPerlを使った演習があったのですが、例のごとく教えている内容が非常に古く、クソコードを生み出しやすくなるようなことが教えられていたので
昔の汚いPerlスクリプトにひどい目に合わされた*1Perl Monger*2 としては、クソコードを生み出しにくい現代的なPerlを書く方法を周知する必要があると強く感じ、この記事を書くことにしました。
全部書こうとすると長くなりすぎるので、今回は僕が特に重要だと思うもの数点を紹介します。
では早速本題に入っていきます!

Perlのコードを書きはじめる時に必須の記述

Perlのコードを書きはじめるときは、先頭にかならず下記の2行を記述するようにしましょう。

use strict;
use warnings;

何をしているのかというと、 strict プラグマ*3と warnings プラグマというものをロードしています。

strict プラグマとは?

Perlでは、何も宣言せずに使用した変数はなんとグローバル変数になってしまいます。
strictプラグマを使うと変数を必ずmyで宣言*4しなければならないようになります。
myで宣言した変数の有効範囲はブロック内のみなので、サブルーチン、if文、for文の中などでmyで宣言した変数は外部から参照できないようになります。
具体的には以下のような感じになります。

sub hello {
  my $name = 'りーう';
  $global = 'taint';
}
# グローバル変数を宣言するサブルーチンを呼び出し
hello();
# $global はここでも見える
print "HELLO, $global !!\n";
# $name は my で宣言されているので見えない
print "HELLO, $name !!\n";

また、変数を宣言しなければエラーがでるようになるので、変数のタイプミスにも気付きやすくなります。
つまり、strictプラグマを使うことでバグが減ります!!

warnings プラグマとは?

warnings プラグマは様々な警告を出してくれるようになります。
例えば、初期化されていない変数を使用した時や、数値として解釈できない文字列で計算をした時などに警告を出してくれるようになります。
要するに、プログラムが変な動作をしていたら警告して教えてくれるようになるってことです!

というわけで、Perlでコードを書くときは必ず先頭にuse strictuse warningsを書きましょう!

open関数は3引数で呼び出し、ファイルハンドルは変数に格納する

昔の方法でファイルを開いて読み込む処理を書くと、以下のようになります。

open FH, "< log.text";
my @log = <FH>;
close FH;

これには幾つか問題があります。 1つはファイル名とファイルオープンモードを指定するところが同じ文字列であることにより、脆弱性が生まれることです。
2つめはファイルハンドルがグローバル変数のような扱いになることです。*5
このように上記のコードはヤバイので、以下のように書くようにしましょう。

open my $fh, '<', 'log.text';
my @log = <$fh>;
close $fh;

ファイル名とファイルオープンモードが別ですし、ファイルハンドルがローカル変数なので安全です!

& でサブルーチンを呼び出さない

実は&を使わずにサブルーチンを呼び出すこともできます。

sub func { print "test function" }
&func;    # サブルーチン呼び出し
func();   # こう呼び出すこともできる

むしろ、&を使って他の人が作ったサブルーチンを呼び出したりすると、意図しない挙動になったりする場合があるのでやめましょう。 *6 他言語をメインで使っていた人も、&をつけないほうがわかりやすくていいんじゃないでしょうか。

時刻処理には Time::Piece などのモジュールを使う

Perlで時刻を取得するにはlocaltime関数を使用しますが、現在ではCPAN*7に楽に時刻を扱うことができるモジュールが何種類かあるので、それを活用するようにしましょう。
また、Perl5.9.5以降の場合は時刻をオブジェクトとして扱うことができるモジュール、Time::Pieceが標準で添付されています。
つまり、 use するだけで使えるのでPerl5.9.5以降を使っている方は是非Time::Pieceを使うようにしましょう。
Time::Pieceを使うと、localtime関数で書いていた処理が以下のように簡単書くことができます。

my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime;
$year += 1900;
$mon += 1;

print "$year$mon\n";

use Time::Piece;
# localtime 関数を上書きし、 localtime関数を呼び出すとオブジェクトが返えるようになります
my $time = localtime;
print $time->year . " 年 " . $time->mon . " 月 \n";

変数をたくさん宣言しなくていいですし、年や月なども足したりせずに取得できるのでいいですね!
なお、学校にある古いサーバーの幾つかでは動いているPerlのバージョンも低いので、Time::Pieceモジュールがない場合があります。 :(

間接オブジェクト記法でメソッドを呼び出さない

例えば、以下のようなコードです。

use CGI;
my $q = new CGI;  # ここ

これは、CGIモジュールのnewというメソッド(もといコンストラクタ)を、間接オブジェクト記法という方法で呼び出しています。
JavaC++と違って、newは演算子ではありません。
ちなみに、通常のメソッド呼び出しは次のようにします。

my $q = CGI->new;

Perlではメソッド呼び出しは->でやるんですよ。
Java.->になったと考えてください。
さて、引数をとるメソッドを最初の間接オブジェクト記法で書くとどうなるでしょうか。

$object->method($arg1, $arg2);   # これが
method $object $arg1, $arg2;     # こうなる

読みにくいですね...。
更に、最新のPerlでは間接オブジェクト記法で書くと警告を出すようにしようとする流れがあるようです。
将来警告がでるかもしれないことを考えると、やっぱり間接オブジェクト記法はやめておいたほうがいいです。

番外編: use utf8 する

utf8プラグマというものがあります。
これはPerlスクリプト内でunicode文字列を扱えるようにするプラグマです。
これを使うと、length関数でunicode文字列がちゃんと文字数を返すようになったり、変数名などに日本語が使えるようになったりします。
本当はuse strict, use warnings同様先頭に書いたほうが良いのですが、
使いこなすのに少しコツがいるので今回は説明しません。

さいごに

以上ー、現代的なPerlの書き方のうち、重要だと思うものを何点か紹介しました。
大学の環境や教えていることの中には古くて今ではやばいものがたくさんあるので、自分でいろいろ情報収集しましょう :(
また、Perlシェルスクリプト的な使い方もできますし、CPANを利用することでほんとにいろいろできるので、よければ授業以外でも使ってみてください。
最後まで読んでいただいた方、ありがとうございました!

*1: 十国志NETのベースとなったゲームのコードのこと

*2: 訳するとPerl屋とか、Perlの伝道師みたいな Perlを使っている人はよくこう呼ばれます。

*3: プラグマとはロードするとロードしたコード側に何らかの影響を及ぼすモジュールのことです。

*4: 他にもour, stateなどの宣言方法があります

*5: グロブとして扱われます。

*6: 具体的にはサブルーチンプロトタイプが無効になります。

*7: Perlの便利なモジュールがたくさん置かれている、素晴らしい場所です。

Exception::Tinyを継承したクラスで例外を搬送する時、スタックバックトレースを表示させるにはどうしたらいいのか

Perlでスタックバックトレースを伴った例外を発生させるにはCarp::confessを使えばいいわけですが、 下のコードのようにException::Tinyを継承したクラスで例外を発生させる時にconfessを使ってもスタックバックトレースが表示されません。

package MyApp::Exception;

use strict;
use warnings;
use utf8;
use Carp qw/confess/;
use parent qw/Exception::Tiny/;

sub throw {
    my $class = shift;

    my %args;
    if (@_ == 1) {
        $args{message} = shift;
    } else {
        %args = @_;
    }
    $args{message} = $class unless defined $args{message} && $args{message} ne '';

    ($args{package}, $args{file}, $args{line}) = caller(0);
    $args{subroutine} = (caller(1))[3];

    # die -> confess
    confess $class->new(%args);
}

原因はわかりません!!
他の例外搬送クラスはどうなんでしょうかね。
とりあえず当分evalの中でconfessを呼び出してそれをインスタンス変数に格納する、という強引な方法で対処することにします(

sub throw {
    my $class = shift;

    my %args;
    if (@_ == 1) {
        $args{message} = shift;
    } else {
        %args = @_;
    }
    $args{message} = $class unless defined $args{message} && $args{message} ne '';

    ($args{package}, $args{file}, $args{line}) = caller(0);
    $args{subroutine} = (caller(1))[3];
    
    # confessで例外を発生させてその内容を引数に追加
    eval { confess 'start trace.' };
    $args{trace} = $@;

    die $class->new(%args);
}

追記:

perldoc.jp

ドキュメントはちゃんと読みましょう!! Carpのドキュメント、バグのところにオブジェクトは受けつかないと書いてありますね。 とりあえず上記の方法かlongmessを格納する方法しかなさそうです。 誰かいいモジュールを知っている方がいらっしゃればぜひ教えていただきたいです。

PerlからYoutube Data APIを利用して動画を検索する

PerlからYoutubeの動画検索しようとした時のメモ、Youtube Data APIの利用の仕方というよりはWeb APIの使い方って感じですが。

準備

Youtube Data API v3はGoogleのアカウントを持っている上で認証しないと使用できないようなので、ドキュメントを参考に準備をします。
https://developers.google.com/youtube/v3/getting-started?hl=ja
途中で作成するAPIキーを後で使うことになるのでメモっておきましょう。

PerlからWeb APIを叩く方法

基本的な流れは、

リクエストURI作成

Web APIをリクエスト

JSONが返ってくるのでそれをPerlのデータ構造に変換

ってかんじです。

コードはこんな感じになりました。

use v5.22;
use warnings;
use utf8;

use URI;
use LWP::UserAgent;
use JSON::PP;

my $uri = URI->new('https://www.googleapis.com/youtube/v3/search');
$uri->query_form(
  key        => 'APIキー',
  q          => 'GUMI',
  part       => 'id,snippet',
  maxResults => 10,
  type       => 'video',
);

my $ua = LWP::UserAgent->new();
$ua->timeout(10);
my $response = $ua->get($uri);
my $response_json = $response->content;

my $json = JSON::PP->new();
my $response_data = $json->decode($response_json);

my @video_list = map { $_->{id}{videoId} } @{ $response_data->{items} };
say "https://www.youtube.com/watch?v=$_" for @video_list;

これを保存して実行すると

$ perl youtube.pl 
https://www.youtube.com/watch?v=w0dgGYAQVao
https://www.youtube.com/watch?v=XgNG68swi3k
https://www.youtube.com/watch?v=TUGJLqV5hfY
https://www.youtube.com/watch?v=qn0OtcV7NRA
https://www.youtube.com/watch?v=y7p5BQqAgoE
https://www.youtube.com/watch?v=l0YdN5eY11I
https://www.youtube.com/watch?v=vUQoWRAM15Y
https://www.youtube.com/watch?v=rqg90hkSJ7s
https://www.youtube.com/watch?v=MVoZ_P45v7Y
https://www.youtube.com/watch?v=dNIg2oEhgyg

こんな感じに条件に一致する動画のURIのリストが取れます!

参考(Youtube APIで学ぶWeb APIのキホン)

Amazon CAPTCHA

Ubuntu14.04にncursesをインストール

ncursesのインストール

$ sudo apt-get install libncurses5-dev

ncurseswのインストール(ncursesのUTF-8対応版らしい?)

$ sudo apt-get install libncursesw5-dev