量産型エンジニアの憂鬱

きっと僕は何物にもなれない。

Perl6のコンストラクタと継承

Perl5にはクラス構文がありません。

モジュール名で bless された リファレンスをインスタンスとして扱うことができます。
ハッシュリファレンスまたは無名ハッシュを bless した場合、キー値ペアをメンバとして使えます。
Perl5 では new メソッドで 自身のモジュール名で bless した 無名ハッシュ を返すことでコンストラクタとして利用しています。

また、グローバル変数 @ISA にスーパークラスを指定することで、スーパークラスのメソッドが利用できるようになります(継承)。
上記スクリプト内では Bird インスタンスが Animal の name メソッドを利用しています。


さて、Perl6ではクラス構文が存在しています。

is で継承することができます。自然言語っぽくていいですね。 Bird is Animal。
メンバ変数が public な場合は上記のように簡単に記述することができます。
継承してもデフォルトコンストラクタがよしなにやってくれています。

メンバを private 変数にした場合はどうなるでしょうか。

全て Nil になってしまいました。
前回 の記事で書いた通り、 デフォルトコンストラクタでは private なメンバに値を渡してくれないようです。

今回は、継承した場合のコンストラクタについて考えましょう。

method new はオーバーライドされてしまう

サブクラスの new がスーパークラスの new でオーバーライドしてしまいます。

サブクラスで new メソッドを定義した場合に、スーパークラスの new が呼ばれず、値が設定できません。
また、サブクラスからスーパークラスの private メンバにアクセスすることもできません。
前回の記事のように無理やり private なメンバに値を設定してやろうとしていると、継承したときにスーパークラスの値が設定できなくなってしまいます。

submethod BUILDを使う

submethod BUILD の場合、スーパークラスのコンストラクタも呼ばれます。

スーパークラスの private メンバにも値を設定してやることができます。

method new と bless で書く方が Perl5 での記述と似ていてついそっちで書きたくなってしまうのですが、
基本 submethod BUILD 使う方がいいですね。

DBUnitでxmlからインサートするときに最初のレコードでnullを指定したい

DBUnitが便利ですね。
Spring Bootにおけるテストでは以下のように書けば毎回データセットをインサートしてくれます。

最初のレコードでnullを指定したい場合にちょっと戸惑いました。

二つ目以降のレコードに null を指定したい場合は、エレメントにカラムを書かなければ null になります。

f:id:duck8823:20161106001224p:plain

最初のレコードに null を指定して、二つ目以降のレコードには任意の値を指定したい場合、困ったことになります。
データセットを以下のように書いてインサートすると

f:id:duck8823:20161106001710p:plain

二つ目のレコードのnameカラムに値が入ってくれません。
どうやら一つ目のレコードに記述されているカラム名しか見ないようです。
二つ目以降のカラムに存在しないカラム名を指定してもエラーになりません。

ReplacementDataSet を利用することで null をインサートすることが可能になります。

文字列 {null} を null に変換しています。
データセットは次のように用意します。

f:id:duck8823:20161106002529p:plain

これで null がインサートされました。
普段はアノテーションでデータセットを指定できる TestExecutionListener を作ってこれを利用しています。
テスト毎にデータセットを指定するのに、以下のようにアノテーションで指定出来て便利です。

Perl6でprivateなメンバにコンストラクタで値を渡したい

※ 追記あり

Perl6でprivateなメンバを作ったときに、コンストラクタで値を渡せなくてちょっとハマったのでメモ。

publicなメンバを設定する場合、

メンバ変数の $ の後に . をつけることで、アクセッサが自動的にできます。
インスタンスからメンバ名でアクセスできるようになります。
また、デフォルトコンストラクタでは、名前付き引数としてpublicなメンバに値を渡してやることができます。

publicなフィールドのみ持つクラスを作る場合はめっちゃスッキリ。
Pythonの namedTuple とかそんな感じで使えますね。

さて、メンバをprivateにしたい場合はどうするのか。
変数の . を ! に変更することで、 private なメンバになります。

この場合、アクセッサは自分で記述する必要があります。
また、デフォルトコンストラクタでは値を渡すことができません。

じゃあ、コンストラクタをオーバライドしてみましょう。

これでもダメでした。
blessの時点で値が入ってこないみたい。

それなら、blessした後に private なメソッドを介して値を渡してやればいいんじゃないか。

イケました。
private なメソッドはメソッド名の前に ! をつけます。
インスタンスから実行しようとしてもちゃんと syntax error になりました。

アクセッサをオーバライドする

アクセッサをオーバーライドしてdieするなどして、アクセスできなくしちゃう方法もあります。
この場合、コンストラクタはデフォルトでいいので便利かも。
クラス内からメンバ変数にアクセスする場合に . でアクセスするとオーバライドしたメソッドが呼び出されてしまうので、 ! でアクセスするのが注意点。


[2016/11/06追記]

BUILDで書く

ご指摘いただきました。公式でちゃんと書かれてますね。

submethod BUILD で private なフィールドに値を設定できます。
ご指摘&URLのご紹介ありがとうございます。
newをオーバーライドするのは、どうしても固定長引数にしたい時など限定的な場合のみにした方がよさそうですね。

ウェブでログインが必要なルーチンワークをPythonスクリプト書いてJenkinsさんに実行してもらう

最近、ルーチンワークを少しずつ自動化しています。
ウェブをチェックするようなルーチンはスクレイピングを行うのですが、ログインなどのブラウザ操作が必要な場合もあります。
ブラウザ操作が必要な場合でも自動化するのが今回の目標。

Python + Seleniumで自動操作

ウェブブラウザの自動操作はSeleniumの得意とするところです。
JavaPythonPerlRubyJavaScript(Node.js)など様々な言語で利用できます。

やっぱりパパッと書いて実行するのはスクリプト言語の得意とするところですね。
普段はPerlスクリプトを書くことが多いのですが、今回はPythonで書きました。
理由は、 Selenium Server を実行するのがめんどくさかっただけ(これも後述するDockerfileに書いちゃえばあとは簡単)。

今回は、Facebook
f:id:duck8823:20160903215516p:plain
この部分の新着通知をSlackに通知したいと思います。

書いたPythonスクリプトはこんな感じ。


WebDriverのAPIこちらを見れば書いてあります。
引数に渡されたユーザ名とパスワードを利用してFacebookにログインし、ログイン後のページ要素から情報を取得しています。

Slackへの通知はWebHooksを利用。
WebHooksのURLは適宜変更してください。

このスクリプト


で実行できます。

f:id:duck8823:20160904015408p:plain
ローカルマシンで実行する場合はWebDriverをFirefoxで指定するといいです。

Dockerで実行できるようにする

上のスクリプトをJenkins上で実行できるようにするには、Dockerコンテナを作ってその中で実行するのが楽です。
Python3.5、Selenium、PhantomJSをインストールしたDockerfileはこちら。


これを実行するのは、


Jenkinsに実行させる

ジョブはフリースタイルで作成しました。

ソースコード管理

f:id:duck8823:20160904012339p:plain
スクリプトとDockerfileがあればよいので、gistにのせてそこからcloneするようにしました。

ビルド環境

f:id:duck8823:20160904012624p:plain
パスワードをビルドに直書きしたくなかったので、credentialsに登録したユーザ名とパスワードを利用できるようにします。
(コンソールには表示されてしまいます。)
Credentials Buinding Pluginを利用しました。
参考にしたのは、ここ

ビルド

f:id:duck8823:20160904013052p:plain
Dockerを実行させるスクリプトのユーザ名とパスワードを、ビルド環境のUse secret text(s) or file(s)で指定したものに変更します。

定期的に実行させる

f:id:duck8823:20160904013401p:plain
あとは定期的に実行させるだけです。
ビルド・トリガ定期的に実行にチェックを入れ、cron形式でスケジュールを登録します。
上記の例は毎朝9時に実行する例。



これを応用すれば内部サイトのアップデート確認、実行などいろんなことができます。
JenkinsもDockerがあれば簡単に実行できるいい時代です。
ローカルJenkinsさんを雇ってルーチンワークを肩代わりしてもらいましょう。

GmailをSlackに通知するPerlスクリプトを書いた

チームSlackを導入してしばらくたったのですが、 まだ一部のやり取りはメールで行っています。

この通知をまとめたい場合、SlackにはEmail連携が用意されています
Slack便利。
が、これは有料版でしか使えません。
メールクライアントソフト使えばいいかもしれないんですが、自分は今までメールを開きっぱなしだったモニタの一部分をSlackに置き換えたので、
Slackを開いているとついついメールが来ていることに気づくのが遅くなってしまいます。

メールはGmailなんやし、Gmail APIで取得してSlackのBotに通知させりゃええやん。

f:id:duck8823:20160828134749p:plain
f:id:duck8823:20160828135323p:plain
こんな感じで通知あるとわかりすいですよね。 ってことで、スクリプトを書きました。

作成したスクリプト

use strict;
use warnings FATAL => 'all';

use Google::API::Client;
use Google::API::OAuth2::Client;

use utf8;
use Encode;

use Slack::RTM::Bot;

my $user_id = 'Gmailアカウント(メールアドレス)';
my $client_id = 'Google APIs プロジェクトのクライアントID';
my $client_secret = 'Google APIs プロジェクトのクライアントシークレット';
my $label_regex = qr/Gmailのラベルの正規表現/;

my $slack_api_token = 'Slack Bot のトークン';
my $channel = '通知するSlackのチャンネル';

my $interval = 60;

my $bot = Slack::RTM::Bot->new(
    token => $slack_api_token
);
$bot->start_RTM;

my $auth = Google::API::OAuth2::Client->new({
        auth_uri => "https://accounts.google.com/o/oauth2/auth",
        token_uri => "https://accounts.google.com/o/oauth2/token",
        client_id => $client_id,
        client_secret => $client_secret,
        redirect_uri => "urn:ietf:wg:oauth:2.0:oob",
        auth_doc => {
            oauth2 => {
                scopes => {
                    "https://www.googleapis.com/auth/gmail.readonly"
                    => "gmail read"
                }
            }
        }
    });
print Encode::encode_utf8("以下のURLにアクセスし、表示されるコードを入力してエンターを押してください.\n");
print $auth->authorize_uri . "\ncode: ";
my $code = <stdin>;
$auth->exchange($code);

my $service = Google::API::Client->new()->build('gmail', 'v1');
my $labelIds = [];
my $res = $service->users->labels->list(body => {userId => $user_id})->execute({auth_driver => $auth});
for my $label (@{$res->{labels}}) {
    push @$labelIds, $label->{id} if $label->{name} =~ $label_regex;
}
$auth->refresh;

while(1) {
    my @new_messages;
    for my $labelId (@$labelIds) {
        my $body = {
            userId => $user_id,
            labelIds => $labelId,
            q => 'is:unread'
        };
        $res = $service->users->messages->list( body => $body )->execute( { auth_driver => $auth } );
        for my $message (@{$res->{"messages"}}) {
            $res = $service->users->messages->get( body => { userId => $user_id, id => $message->{id} } )->execute( {auth_driver => $auth } );
            if($res->{internalDate} / 1000 < time() - $interval) {
                last;
            }
            my %headers = ();
            for my $header (@{$res->{payload}->{headers}}) {
                $headers{$header->{name}} = $header->{value};
            }
            my $mes = <<"EOL";
From: $headers{From}
To: $headers{'Delivered-To'}
Date: $headers{Date}
Subject: $headers{Subject}
Snipet: $res->{snippet}
EOL
            push @new_messages, $mes;
        }
        $auth->refresh;
    }
    if (@new_messages > 0) {
        $bot->say(
            channel => $channel,
            text => Encode::encode_utf8(sprintf("%s 件の新規メッセージがあります.\n%s", scalar @new_messages, join("\n\n", @new_messages))))
    }
    sleep $interval;
}

Google APIs プロジェクトの作成

Gmail APIを利用するにはGoogle APIsでプロジェクトを作成する必要があります。
Gmailアカウントにログインしたら、Google APIs ライブラリにアクセスします。

f:id:duck8823:20160828113123p:plain
上部メニューのプロジェクト(すでにプロジェクトがある場合はその名前になっていると思います)をクリックし、プロジェクトを作成をクリックします。

下記のようなウィンドウが表示されるので、利用規約を読んで同意し、作成 をクリックします。
f:id:duck8823:20160828113535p:plain

しばらくしたら、ヘッダが作成したプロジェクトになっています。
f:id:duck8823:20160828113806p:plain

Gmail API の有効化

左メニュー > ダッシュボード
から、 [API を有効にする] をクリックします。
f:id:duck8823:20160828114209p:plain

すると検索フォームが現れるので、 Gmail と入力します。
リストにGmail APIが表示されるので、それをクリックします。 f:id:duck8823:20160828114316p:plain

ページ上部に 有効にする と表示されているので、クリックすることで有効化できます。
f:id:duck8823:20160828114512p:plain

認証情報を作成する

f:id:duck8823:20160828115455p:plain

Gmail APIは認証が必要ですので、このままでは使用できません。
以下のように表示されるので、認証情報を作成します。

左メニュー > 認証情報 > OAuth同意画面
をクリックし、
ユーザーに表示するサービス名を入力し 保存 をクリックします。
サービス名以外は任意です。
f:id:duck8823:20160828121459p:plain

続いて
左メニュー > 認証情報 > 認証情報
から、 認証情報を作成 をクリックし、 OAuth クライアントID をクリックします。
f:id:duck8823:20160828121750p:plain

クライアントIDの作成画面が表示されるのですが、
今回はスクリプトなので、 その他 にしました。
f:id:duck8823:20160828123219p:plain

作成をクリックすると、クライアントIDとクライアントシークレットが表示されるので、最初に紹介したスクリプトをこれで置き換えてください。
f:id:duck8823:20160828122053p:plain

使ったモジュール

Gmail APIGoogle::API::Clientを拝借しました。
こちらの記事を参考にさせていただきました。
Slackへの通知はSlack::RTM::Botを使っています。

使い方

上記スクリプトを実行すると、コンソールに認証URLが表示されます。

> perl script.pl
以下のURLにアクセスし、表示されるコードを入力してエンターを押してください.
https://accounts.google.com/o/oauth2/auth?client_id=Google APIs プロジェクトのクライアントID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/gmail.r
eadonly
code:

表示されるURLにアクセスすると、アプリケーション権限の許可画面になります。
この場合は表示のみのアクセス権です。
f:id:duck8823:20160828201809p:plain

許可をクリックするとコードが表示されるので、コンソールにコピペしてエンターキーを押します(上記スクリプトではコンソールには表示されないようしています)。
f:id:duck8823:20160828203054p:plain

この後は、Ctr + zを押した後に bgコマンドをうつとバックグラウンドに移せます。

Slack::RTM:BotGoogle::API::OAuth2::Clientを組み合わせれば、 SlackのBotにつぶやいて任意のタグの未読メールを取得するなんてことも簡単ですね!

参考

Google Gmail APIでメールを取得する - Qiita
Google::API::Clientを使う(perl) | @knok blog