好きなことを、好きなだけ。

MTG, ゲーム, 漫画など、好きなことをつれづれなるままに。1記事15分で書く。

【MTG】WisdomGuildのカードデータをCSV化するスクリプト書いた

戦乱のゼンディカーのフルスポイラーが発売されましたね。 見るからに遅い環境のようですが、やってみないと全然実感が湧きません。

  • パワーとタフネスはどのラインがキーになるの?
  • マナコスト分布はどうなっているの? etc..

といった環境全体のことが気になるのですが、スポイラーから地道に数字拾うのも面倒です。
なので、カードデータをCSV出力するスクリプトを作りました。言語は Perl です。

watanabe-yoichi/MTG_card_list_getter · GitHubgithub.com

方針

Wisdom Guild のデータベースでは、特定エキスパンションのカード一覧は http://whisper.wisdom-guild.net/cardlist/{エキスパンション名} というURLになっています。カード一覧のカード名にはリンクが貼ってあり、リンク先にはカード単体のページ( 例) http://whisper.wisdom-guild.net/card/BFZ001/ )があります。

カード一覧の画面で情報は一望できますが、このテキストを解析するのはすごく面倒。。。 一方、カード単体のページであれば、情報がテーブルにまとまっているのでそこから取得してやればうまくいきそうです!

1. カード一覧ページからカード個別へのリンクを全取得
2. カード個別のページで前述のテーブルから情報を取得
3. CSV出力

という流れでいけそう。

作成

スクレイピングツール的なものを作るは初めてでしたが、どうやら

  • LWP::UserAgent でサイトの内容を取得
  • HTML::TreeBuilder で解析

というのがスタンダードみたい。

まずは必要そうなものを実装して、カード個別のページで試してみて様子を見てみることに。

sub parse_content {
    my $url  = shift;

    # LWPを使ってサイトにアクセスし、HTMLの内容を取得する
    my $ua = LWP::UserAgent->new('agent' => USER_AGENT);
    my $res = $ua->get($url);
    my $content = $res->content;

    # HTML::TreeBuilderで解析する
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($content);

    return +{
        tree    => $tree,
        encoder => $encoder,
    };
}
# カード情報を取得
my @rows = $tree->look_down(
    'class', 'wg-whisper-card-detail'
)->look_down(_tag => 'table')->look_down(_tag => 'td');

とりあえず、テーブル内の td タグを全取得して、あらかじめ用意していたハッシュのキーに対して順番に値を突っ込む、という乱暴な実装。 この時点で問題点は2つ。

1. 文字化け

TreeBuilder はUTF8で文字化けするので、一度デコードする必要があったようです。 取得して $contentguess_encoding でデコードしておきます。

my $encoder = guess_encoding($content, qw/ euc-jp shiftjis 7bit-jis /);
$content = decode($encoder->name, $content) unless (utf8::is_utf8($content));
2. カードの種類によって項目が違う

クリーチャーとスペルとプレインズウォーカーでは項目が違っていて、カードタイプ毎に固有の項目がある作りでした。(該当しない項目が空白になっているのではなくて、項目自体が無い) なので、出力結果を見てみるとキーとバリューがズレてしまいうまくいきませんでした。

項目の違いを吸収するためにテーブルの項目名も取得するようにします。
thtd で一時的にハッシュを作って、CSV出力前に共通のフォーマットに展開します。

  • 一時ハッシュの作成
sub extract_card_info {
    my @urls  = @_;

    my $count = 0;
    my @all_card_info;
    for my $url (@urls) {
        my $tree_info = parse_content($url);
        my $tree      = $tree_info->{tree};
        my $encoder   = $tree_info->{encoder};

        # カード情報テーブルを取得
        my @rows = $tree->look_down(
            'class', 'wg-whisper-card-detail'
        )->look_down(_tag => 'tr');

        # 各パラメータを取得
        my $card_info;
        for my $row (@rows) {
            my $type    = $row->look_down(_tag => 'th')->as_text;
            my $content = $row->look_down(_tag => 'td')->as_text;

            $card_info->{$type} = encode('utf-8', $content);
        }
        warn "extract: ".$card_info->{カード名};

        push @all_card_info, $card_info;
        $tree = $tree->delete;
    }

    return \@all_card_info;
}
  • フォーマットへの展開と出力
# カード情報の出力
my $content_fh = $file->open('a') or die $!;
my $card_num;
for my $card_info (@{ $all_card_info }) {
    my @row;
    for my $key (@{ &CONTENT_TYPES }) {
        if (exists $card_info->{$key}) {
            push @row, $card_info->{$key};
        }
        else {
            push @row, '-';
        }
    }
    $csv->print($content_fh, \@row) if @row;

    $card_num++;
}
$content_fh->close;
  • 全カードタイプの項目を網羅した共通フォーマット
use constant {
    # カード項目
    CONTENT_TYPES => [qw/
        カード名
        マナコスト
        タイプ
        テキスト
        オラクル
        P/T
        忠誠度
        フレーバ
        イラスト
        セット等
        再録
    /],
};

処理を分けたり変数名などを調整したりして完成。

出力結果 MTG_card_list_getter/BattleforZendikar.csv at master · watanabe-yoichi/MTG_card_list_getter · GitHub

使い方

  • mtg_card_list_getter.pl の固定値に特定のエキスパンションのURLを入力する
use constant {
  TARGET_URL => 'http://whisper.wisdom-guild.net/cardlist/BornoftheGods/',
}

今後はブラウザ上から実行してCSVをダウンロードできるようにしたいなぁと思っています。

参考URL

簡単!たった13行のコードでHTML取得&解析をするPerlスクリプト · DQNEO起業日記 Perlでブックオフの店舗を検索し、結果をハッシュの配列に格納する - As a Futurist...