AnyEvent::HTTP::CookiesではなくAnyEvent::HTTP::Cookies::Unit の話をしてきました #niigatapm

Niigata.pm tech talk #1では AnyEvent::HTTPで使ったCookie情報 あるいは他のクライントで使ったCookie情報を相互に再利用できたらいいよね っていう話をしてきました。

      • -

外部WebAPIを利用してゴニョゴニョというのは、よくある(?)ケースだと思うけど、APIがないサイトは沢山あります。
その際、よく使われるのが ハンディブラウザ + スクレイピングツール で、WWW::Mechanize + Web::Scraper(Web::Query) の組み合わせが多いんじゃないでしょうか?

ただ、最近は AnyEvent 文脈で HTTP通信 を行うケースも多いと思います。
AnyEvent 文脈で WWW::Mechanize 相当の機能を持ったモジュールを僕は知りませんので、ログインなどセッション情報が必要なサイトではクッキーや他のヘッダー情報の管理は自前の処理機構上でやってます。

さて、比較的長い有効期限を持ったクッキーを使っているサイトもあります。その場合、実直にログインページからスタートする事なく、クッキーを使ってログイン状態のまま、目的のページへ移動することが可能な場合があります。
このCookie 情報をお気に入りのAnyEvent文脈、そうでない文脈で共有できると面白そうです。

Cookies情報の共有する際に
  • AnyEvent::HTTP は単なるハッシュリファレンスでCookie情報をプールしてAnyEvent::HTTP自体に情報を操作させるのに対し、WWW::Mechanize は HTTP::Cookiesオブジェクトの内部で情報をプールし操作は HTTP::Cookiesのメソッドを使う
  • 内部データの構造は似た構造
  • キー&バリューの構造が異なる。AnyEvent::HTTP では ハッシュリファレンス。HTTP::Cookies では 配列リファレンス
  • AnyEvent::HTTP は ネットスケープ準拠の Cookieに対応するのに対し、 HTTP::Cookies は Set-Cookies2 まで対応
  • 他のクライアントで利用できる保存形式が必要
「他のクライント」を考えてみると
  • WWW::Mechanize (isa LWP::UserAgent)
  • 実際のブラウザ
  • Ruby などの他の言語のクライアント

今回、HTTP::Cookies(::*) のsaveメソッドを使って保存する戦略を取ることにしました。2012.04.28現在
HTTP::Cookies の場合、デフォルトの保存形式は Set-Cookies3 で保存します。(サブクラスの場合は頃なる場合もあります。HTTP::Cookies::Safariでは plist形式のXML文書になります)


前置きが長くなりましたが本題

  • AnyEvent::HTTP が扱った Cookie 情報を HTTP::Cookies にセットする関数
  • (WWW::Mechanize, etc上で) HTTP::Cookies が扱った Cookie 情報を AnyEvent::HTTP で扱えるフォーマットに変換する関数
  • AnyEvent::HTTP が扱った Cookie 情報を HTTP::Cookies の 内部で扱うフォーマットに変換する関数(使いどころがないけど)

の関数を提供するモジュール Ginger::Cookies を書いてみました(α版。細かいところは見なおさないといけないと)。

デモコード(一部ふせております)

#!/usr/bin/env perl
use strict;
use warnings;
use HTTP::Cookies::Safari;
use Ginger::Cookies qw(set_cookies anyevent_style);
use AnyEvent;
use AnyEvent::HTTP;
use JSON;
use YAML;

my $WWW_PXXXX_NET = 'http://www.pxxxx.net';
my $stacc         = "${WWW_PXXXX_NET}/stacc/my/home/all/all";
my $file          = "$ENV{HOME}/Library/Cookies/Cookies.plist";
#my $file          = 'Cookies.plist';

my $cookie_jar = HTTP::Cookies::Safari->new;

$cookie_jar->load( $file );

my $jar = Ginger::Cookies::anyevent_style $cookie_jar;

my $cv = AE::cv;

http_request GET => $stacc,
    cookie_jar => $jar, sub {
        my($content, $hdr) = @_;
        if ($hdr->{Status} ne '200') {
            warn "failed: $hdr->{Status} $hdr->{Reason} $hdr->{URL}";
            return $cv->send;
        }
        my $tt    = ($content =~ /pxxxx\.context\.token\s=\s\'([^']+)\'/)[0];
        my $login = ($content =~ /pxxxx\.user\.loggedIn\s=\s([^;]+);/)[0];
        if ($login) {
            http_request GET => "${stacc}.json?tt=${tt}",
                cookie_jar => $jar, sub {
                my($content, $hdr) = @_;
                if ($hdr->{Status} ne '200') {
                    warn "failed: $hdr->{Status} $hdr->{Reason} $hdr->{URL}";
                    return $cv->send;
                }
                my $table = decode_json $content;
                warn Dump $table;

                $cv->send;
            };
        }
};

$cv->recv;

set_cookies $cookie_jar => $jar;

$cookie_jar->save( $file );


オチ
質疑応答で「関数ではなくクラスで提供しては」と言う指摘があったので、実装方法考えます