🐡

ポケモンカードでプログラミング ~カードデータスクレイピング編~

2023/01/01に公開約5,200字1件のコメント

整理ページ

前書き

ポケモンカードってご存知でしょうか。
数年前に突如人気が復活した、いわゆるトレーディングカードゲームです。
プレイヤーとしては、夜行とかナイトマーチと呼ばれていたギミックが猛威を振るっていた時期が最もハマっていました。
https://www.pokemon-card.com/card-search/details.php/card/30514
ゾロアークGXでデッキを回して、
https://www.pokemon-card.com/card-search/details.php/card/36120/regu/all
マーシャドーGXで殴るようなコンセプトだったと記憶してます。
https://www.pokemon-card.com/card-search/details.php/card/35656/regu/all

...と思いで話は書き始めると止まらず、昔話老人会になってしまうのでこの辺りにします。

カードデータを集めようと思ったきっかけは、どんなデッキを作ったか、そのデッキの勝率がどうだったか、初期手札や序盤のカード回しの確率はどれくらいか、相性の良いカードはないかなど、デジタル上で管理探索したい事柄があったが、それができる都合の良いアプリケーションがなかったことでした。
英語版ポケモンカードのカードデータベースは非公式ですが素敵なサイトがありWeb APIも公開されています。
https://pokemontcg.io/

公式が多言語化対応すればそれまでなんですが、日本語版で同様のカードデータがなく、また海外と日本でカード収録のタイムラグがあるため公式の多言語化は難しいのではないかと考え、作成しようと思いました。

※一時期さくさんという方が作成されていました。
https://note.com/ch3cooh/n/n299f8066a1a9

そんなこんなで、昔の記憶を思い出しながら記事を書いていきます。

ゴール

  • 公式サイトカードリストのデータを、弾毎にJSON形式のデータにする
  • JSONの中身はpokemontcg.ioを参考に、詰められるものを詰めていく

注意事項

対象サイトへの断りのないクローリングなので、迷惑のかからないようにしましょう。(アクセスに wait を挟むなど)
ポケモンカード公式サイトの引用は、テキストリンクがルールとなっているため、読みづらい箇所がございます。
https://www.pokemon-card.com/policy.html
また、スクレイピングプログラムは様々な様を考慮し、現状は公開できませんのでご了承ください。

取得したデータ

最初に取得したデータの例を載せます。
2022年12月発売のVSTARユニバース収録、リーフィアVSTARはこんな形で取得されています。

[
    {
        "cardId": 42273,
        "multiId": "869_012",
        "nameJp": "リーフィアVSTAR",
        "nameUs": "",
        "imageUrlOfficial": "https://www.pokemon-card.com/assets/images/card_images/large/S12a/042273_P_RIFUIAVSTAR.jpg",
        "imageUrl": "",
        "cardText": "",
        "supertype": "pokemon",
        "subtype": "vstar",
        "trainerText": "",
        "energyText": "",
        "hp": "260",
        "color": [
            "grass"
        ],
        "optionValue": {
            "isV": false,
            "isVStar": true,
            "isVMAX": false,
            "isFossil": false,
            "isV-UNION": false
        },
        "author": "PLANETA Hiiragi",
        "rarity": "RARYTY_UNDEFINED",
        "productNo": "869",
        "set": "ハイクラスパック「VSTARユニバース」",
        "setCode": "S12a",
        "generation": "SA",
        "setNo": "012",
        "ability": {
            "name": "アイビースター",
            "text": "自分の番に使える。相手のベンチポケモンを1匹選び、バトルポケモンと入れ替える。[対戦中、自分はVSTARパワーを1回しか使えない。]"
        },
        "attacks": [
            {
                "name": "リーフガード",
                "damage": "180",
                "text": "次の相手の番、このポケモンが受けるワザのダメージは「-30」される。",
                "costs": [
                    "grass",
                    "grass",
                    "colorless"
                ],
                "convertedCost": 3
            }
        ],
        "addRule": "ポケモンVSTARがきぜつしたとき、相手はサイドを2枚とる。",
        "weaknesses": {
            "color": "fire",
            "calc": "multiply",
            "value": "2"
        },
        "retreats": {
            "costs": [
                "colorless",
                "colorless"
            ],
            "convertedCost": 2
        },
        "resistances": {},
        "nationalPokedexNumbers": null,
        "evolves": null
    }
]

進化、ICHIGEKI/RENGEKI等は未実装です。VSTARパワーを持ったグッズもちょっとフォーマットが追いついてないです。
ですが、今回のゴールはこれです。

コード

環境

今回はGoogleが公開しているHeadless Chromeを操作するためのNodeライブラリ、puppeteerを使っています。
https://github.com/puppeteer/puppeteer

  • node: 16.15.0
  • puppeteer: 16.1.0

大まかな流れ

カードリストページのヘッダ下メニュー右にある「条件を追加する」を開くと商品名一覧があると思います。
これら1つ1つに数字が割り当てられていて、例えばVSTARユニバースでは869という数字がそれに当たります。(devコンソールから該当ラジオボタンのvalueを見ると割り当てられている数字が分かります。)
その数字をpg=869というクエリストリングとしてカードリストURLにつけると、その弾に収録されているカード一覧が表示されます。
このような形で弾毎のカードリストURLを生成して、後は個別のカードページに対してループしてデータを収集していきます。

実装例

puppeteerの細かい使い方は割愛します。
個別カードのページを見てみると
https://www.pokemon-card.com/card-search/details.php/card/42273/regu/all

  • 上ブロックはカード名
  • 左ブロックはカード画像と弾、レアリティ(リーフィアVSTARはありませんが...)、イラストレーター情報
  • 右ブロックはカードの具体的な内容

となっていることが分かります。
これらそれぞれに class が割り当てられているため、CSSセレクタの要領で取得していきます。
上ブロックカード名は

await page.goto(detailUrl, {'https://www.pokemon-card.com/card-search/details.php/card/42273/regu/all': 'domcontentloaded'})
sleep(500) // waitする関数
const nameNode = await page.$('section.Section h1.Heading1.mt20')
const name = await nameNode.getProperty('innerText')

といったような形で取得できます。プロパティに指定する文字列はdevコンソールで地道に調べ、当てはめてみて良さげな文字列を探し出すの繰り替えしです。

右ブロックの内容は、ポケモン・トレーナー・エネルギーによって変わる(と思っている)のですが、これらは画像のURLで判断が可能でした。

  • ポケモンの場合、https://www.pokemon-card.com/xxxxxx/yyyy_P_POKEMONNAME.jpg
  • トレーナーの場合、https://www.pokemon-card.com/xxxxxx/yyyy_T_TRAINERNAME.jpg
  • エネルギーの場合、https://www.pokemon-card.com/xxxxxx/yyyy_E_ENERGYNAME.jpg

みたいになっているので、この値を元に条件分岐させたりします。

const supertypeOne = imgUrl.slice(imgUrl.lastIndexOf('/') + 1).split('_')[1]

こんな感じで

あとがき

実装パートほぼすっ飛ばしているので何いってるんだ感ありますが、興味があれば試してみてください。
実装を再開した際にしんどかったこととしては、どこかでh2なのかh4なのかフォーマットが変わったこと、VSTARパワーというGXワザと異なる新しいフォーマットが登場したこと、グッズにもVSTARパワーがつくことがある(未解決)などがありました。

弾情報で世代(SM・USUM・剣盾)管理をしているのですが、2023年1月末には早くも新世代のSVカードが収録されます。
そのため、また修正を加える必要がありそうですが、三日坊主にならなければカードデータだけでもちゃんと集めていきます。

何かあれば続編を、なければ次は試しにアプリを作ってみた話かデータ分析or機械学習に手を出していこうと思います。

Discussion

スクレイピングは面白いね!よく参考になりました。ありがとうございます。

ログインするとコメントできます