この投稿について
PerlでTwitter API v1.1を使ったスクリプトを書こうとしたところ、思いの外、スンナリとは動かなかったので、やったことを簡単に記録しておこうと思います。
とはいえ、遭遇した問題の本質的なことや、もっと好ましい別解がないかなど、あまり掘り下げる余力がなかったので、私よりも能力・時間のある人が同様な状況に遭ったときに、さらに深く掘り下げられることに期待します。
経緯
Twitter APIのoembed(GET statuses/oembed | Twitter Developers)を使い、ツイートのidからhtmlを取得するということを定期的にやっています。
これまで、Twitter API v1を利用するようになったままでしたが、稼働していたので、放置していました。
しかし、先ごろより以下のようなレスポンスが返るようになってしまいました。
{"errors":[{"message":"The Twitter REST API v1 is no longer active. Please migrate to API v1.1. https://dev.twitter.com/docs/api/1.1/overview.","code":64}]}
そんなわけで、遅ればせながら、v1.1対応を行うことにしました。
環境
- OS
- FreeBSD 9.1-RELEASE-p13 i386
- perl
- v5.16.3
perlそのものやモジュール等は、FreeBSDのpkgngにて、随時インストール・アップグレードしたものを使用しています。
対応内容の概要
まずは、OAuth認証が必要ということで、Twitter Appsで、アプリケーションの登録を行い、API Keyなど取得しました。
また、従来はLWP::UserAgentモジュールを使用して、APIのURLを叩いていただけでしたが、同じやり方では、OAuth認証に必要なパラメータを生成するのも煩雑になるので、Net::Twitter::Lite::WithAPIv1_1モジュールを使用することにします。
さらに、httpsで接続する必要があるようなので、そのようにスクリプトを書きます。
APIを叩くテスト
まずは、次のようなスクリプトを書いてみました。
#!/usr/bin/perl
use strict;
use warnings;
use Net::Twitter::Lite::WithAPIv1_1;
use Mozilla::CA;
use Data::Dumper;
my $consumer_key = q(**********);
my $consumer_secret = q(**********);
my $access_token = q(**********);
my $access_token_secret = q(**********);
my $nt = Net::Twitter::Lite::WithAPIv1_1->new(
consumer_key => $consumer_key,
consumer_secret => $consumer_secret,
ssl => 1,
);
$nt->access_token($access_token);
$nt->access_token_secret($access_token_secret);
$nt->{ua}->ssl_opts(SSL_ca_file => Mozilla::CA::SSL_ca_file());
eval {
my $id = "489446951184654337";
my $res = $nt->get_oembed({id => $id});
print $res->{html} if exists $res->{html};
};
if ($@) {
print Dumper $@;
}
__END__
定期的な実行のためのプログラムに組み込むものというよりは、まずは、APIの使い方が正しいかどうかも含めて、確認するためだけのものです。
問題発生
これで完成も同然と思いましたが、実行してみると以下のような出力となりました。
$VAR1 = bless( {
'http_response' => bless( {
'_content' => 'Server closed connection without sending any data back at /usr/local/lib/perl5/site_perl/5.16/Net/HTTP/Methods.pm line 373.
',
'_rc' => 500,
[後略]
サーバからレスポンスを受け取ることもできなかったようです。
TCPか、あるいはさらに下層レベルのネットワークに問題があることを、まずは疑いましたが、wgetコマンドを使った場合など、別の手段でTwitter APIにhttps接続することはできているようです。
他に、存在もしないようなProxyサーバを経由しようとしているのではないかということも疑いましたが、そうしたこともなさそうです。
そんな紆余曲折があった末に、そもそもLWP::UserAgentモジュールを使って、httpsでリクエストしようとするだけでも、同じ問題が生じるというところに行き着きました。
Net::SSLモジュールを使う
あれこれウェブページを検索している中で、Net::SSLを使うようにしたらよろしいという記述がいくつか見られました。
具体的には、以下のようにしてみると良いということなので、先ほどのスクリプトの先頭近くに挿入してみます。
$Net::HTTPS::SSL_SOCKET_CLASS = "Net::SSL";
また、use Net::SSL
もします。
ホスト名検証ができない
上のコードを挿入して実行してみます。
まず、次のような警告が出るようになりましたが、ひとまず今回は無視します。
Name "Net::HTTPS::SSL_SOCKET_CLASS" used only once: possible typo at twitter_api_test.pl line 10.
そして、以下のような出力となりました。
$VAR1 = bless( {
'http_response' => bless( {
'_content' => 'Can\'t connect to api.twitter.com:443 (Crypt-SSLeay can\'t verify hostnames)
Net::SSL from Crypt-SSLeay can\'t verify hostnames; either install IO::Socket::SSL or turn off verification by setting the PERL_LWP_SSL_VERIFY_HOSTNAME environment variable to 0 at /usr/local/lib/perl5/site_perl/5.16/LWP/Protocol/http.pm line 41.
',
'_rc' => 500,
[後略]
せっかくのhttpsですが、ホスト名検証はできないから、ホスト名検証しないでも良いことにして動かすようにと言われているようです。
従来はhttpだったということもあるし、当分はホスト名検証はしないことでも良いかと思うことにして、以下のコードを挿入しました。
$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
さらに、Mozilla::CAモジュールを使っていた部分を削りました。
テスト成功
いくつか修正をして、以下のようなテストスクリプトとなりました。
#!/usr/bin/perl
use strict;
use warnings;
use Net::SSL;
use Net::Twitter::Lite::WithAPIv1_1;
use Data::Dumper;
$Net::HTTPS::SSL_SOCKET_CLASS = "Net::SSL";
$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
my $consumer_key = q(**********);
my $consumer_secret = q(**********);
my $access_token = q(**********);
my $access_token_secret = q(**********);
my $nt = Net::Twitter::Lite::WithAPIv1_1->new(
consumer_key => $consumer_key,
consumer_secret => $consumer_secret,
ssl => 1,
);
$nt->access_token($access_token);
$nt->access_token_secret($access_token_secret);
eval {
my $id = "489446951184654337";
my $res = $nt->get_oembed({id => $id});
print $res->{html} if exists $res->{html};
};
if ($@) {
print Dumper $@;
}
__END__
これで、次の出力を得ることができました。
<blockquote class="twitter-tweet"><p>500RT【こっちみんなw】水族館が公開した「エイの干物」が怖すぎる <a href="http://t.co/Lqk3xAfiKx">http://t.co/Lqk3xAfiKx</a> 「マリンワールド海の中道」(福岡)がFacebookに投稿した画像が「怖すぎる」と話題となっている。 <a href="http://t.co/Jjya0sfsmH">pic.twitter.com/Jjya0sfsmH</a></p>— ライブドアニュース (@livedoornews) <a href="https://twitter.com/livedoornews/statuses/489446951184654337">July 16, 2014</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
画像が怖すぎです。
追記:ホスト名検証
本投稿のTwitter API v1.1対応では、SSLのホスト名検証について、あっさりと優先度を下げてしまっていましたが、コメントをいただき、確かめてみたところ、$ENV{HTTPS_CA_DIR}
のセットをすればよく、割と容易に対応可能であることがわかりました。
下記の書き換えたコードでは、Mozilla::CAモジュール(Mozilla::CA::SSL_ca_file()
)を使おうとしたという理由だけで、$ENV{HTTPS_CA_DIR}
ではなく$ENV{HTTPS_CA_FILE}
をセットしていますが、これで動作はするようです。
#!/usr/bin/perl
use strict;
use warnings;
use Net::SSL;
use Net::Twitter::Lite::WithAPIv1_1;
use Data::Dumper;
use Mozilla::CA;
$ENV{HTTPS_CA_FILE} = Mozilla::CA::SSL_ca_file();
my $consumer_key = q(**********);
my $consumer_secret = q(**********);
my $access_token = q(**********);
my $access_token_secret = q(**********);
my $nt = Net::Twitter::Lite::WithAPIv1_1->new(
consumer_key => $consumer_key,
consumer_secret => $consumer_secret,
ssl => 1,
);
$nt->access_token($access_token);
$nt->access_token_secret($access_token_secret);
eval {
my $id = "489446951184654337";
my $res = $nt->get_oembed({id => $id});
print $res->{html} if exists $res->{html};
};
if ($@) {
print Dumper $@;
}
__END__
また、いただいた情報を踏まえて再確認したところ、
use Net::Twitter::Lite::WithAPIv1_1;
の前に
use Net::SSL;
をしておけば、
$Net::HTTPS::SSL_SOCKET_CLASS = "Net::SSL";
というのが無くても結果は変わらないようです。
なお、結果を print することが主目的ではないため、今回は無視していますが、"Wide character in print" という警告が出るかと思います。
もう少し汎用性を持たせて書くときに、Encode::encode_utf8() するなど、適宜対応すればよろしいかと思います。