😎

Puppeteerでfirebase認証のかかったサービスを自動操作する

2021/08/20に公開

きっかけ

https://cap-baseball.com
というサービスを運営しています。
 選手から提供してもらった写真をもとに「キャップ野球選手ガチャ」という機能を提供しているのですが、その発展版として、現状のカードを育成してペナントをシミュレーションするゲームの提案がありました。
 その実装をする上で、同じく自分が開発したサービスである
https://cap-scorebook.com

  • 試合を記録するスコアブックアプリ

をpuppeteer で自動操作すれば、成績も自動集計できて便利なのではと思いました。
ただし、CAP-SCOREBOOK には firebase 認証を使っています。

firebase 認証をどうするかという問題

firebase 認証も各プロバイダの認証を puppeteer で通せばいいので、
まず twitter から試してみることにします。

そうだ twitter にログインしよう

const puppeteer = require("puppeteer");
const ACCOUNT = "******";
const PASS = "*******";
const browser = await puppeteer.launch({
  headless: true,
  ignoreHTTPSErrors: true,
  executablePath: "/usr/bin/chromium-browser",
  args: ["--no-sandbox", "--no-zygote"],
});
try {
  const page = await browser.newPage();
  await page.setDefaultNavigationTimeout(0);
  //twitterログイン
  await page.goto("https://twitter.com/login");
  await page.waitForSelector('input[name="session[username_or_email]"]');
  await page.type('input[name="session[username_or_email]"]', ACCOUNT);
  await page.type('input[name="session[password]"]', PASS);
  await page.click("div[role=button]");
  await page.waitForNavigation();
} catch (error) {
  console.error(error);
} finally {
  await browser.close();
}
  • ログインはできるものの、毎回新規ログインだと見做されて通知が多い
  • 挙句、複数回試すと不正ログインと見做されて twitter の別の認証画面に飛ばされる

そうだ google にログインしよう

google は headless モードを bot 判定する

通常のpuppeteerでheadless:falseの場合は何ともないのですが、
headless:trueだとbotと判定されログインがブロックされる仕様になっています。

そこで。

  • puppeteer-extra
  • puppeteer-extra-plugin-stealth

を導入します。

      const puppeteer = require("puppeteer-extra");
      const SCOREBOOK_HOST = "http://localhost:3050";
      const email = "*****";
      const password = "*******";

      puppeteer.use(require("puppeteer-extra-plugin-stealth")());
      // Launch puppeteer browser.
      puppeteer.launch({ headless: false }).then(async (browser) => {
        try {
          console.log("Opening chromium browser...");
          const page = await browser.newPage();
          //スコアブックサインイン
          await page.goto(SCOREBOOK_HOST + "/register");
          await page.waitForSelector("img");
          //googleサインインボタンを押す
          await page.click("img");
          await page.waitForNavigation();
          await page.waitForSelector("#identifierId");
          await page.type("#identifierId", email);
          await page.waitFor(1000);
          await page.keyboard.press("Enter");
          await page.waitForNavigation();
          await page.waitFor(1000);
          await page.type('input[type="password"]', password);
          await page.waitFor(1000);
          await page.keyboard.press("Enter");
          await page.waitFor(15000);
        } catch (error) {
        console.error(error);
        } finally {
        await browser.close();
        }

ReactNativeWeb を puppeteer で操作する

testIDを付与する

  • ReactNativeWebのコンポーネントではclassNameが指定ができない
  • class名は自動生成される。デプロイし直すと別名になっているので固定でアクセスできない
  • testIDプロパティを付与できるのでそれを頼りに要素をセレクトする

RNwebに関する解釈が間違っているかもしれないのでこの方法がベストかどうかはわかりません。

SPAでは要素が描画されていない場合もあるので、

  • waitForSelectorで待つ
  • 該当の要素を操作する

が基本の組み合わせです。
以下はtestIDchange_input_typeを指定した場合の要素の探し方です。

await page.waitForSelector('div[data-testid="change_input_type"]';
await page.click('div[data-testid="change_input_type"]');

docker で puppeteer を使う

dockerfile

FROM node:14

# 最新のchromeをインストールする
RUN  apt-get update \
     && apt-get install -y wget gnupg ca-certificates \
     && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
     && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
     && apt-get update \
     && apt-get install -y --allow-unauthenticated google-chrome-stable \
     && rm -rf /var/lib/apt/lists/* \
     && wget --quiet https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh -O /usr/sbin/wait-for-it.sh \
     && chmod +x /usr/sbin/wait-for-it.sh

# puppeteer付属のchromiumを使わないと指示する
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

RUN npm --global config set user root && \
    npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth --unsafe-perm

# 必要であれば日本語フォント(スクショなどに必要)
RUN mkdir /noto
ADD https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip /noto
WORKDIR /noto
RUN unzip NotoSansCJKjp-hinted.zip && \
    mkdir -p /usr/share/fonts/noto && \
    cp *.otf /usr/share/fonts/noto && \
    chmod 644 -R /usr/share/fonts/noto/ && \
    /usr/bin/fc-cache -fv
WORKDIR /
RUN rm -rf /noto


WORKDIR /src

メモリが少ないので複数起動には向いてない

当初VPS上で実行しようとしていたのですが、

  • 3イニング10分で終わる
  • 試合数が増えると(総当たり)同時並行実行しないと1週間で終わらない
  • 同時並行実行するとおそらくメモリが足りなくなる

という問題があり、試合データの準備をcronで行い、
ローカルで未完了の試合だけをpuppeteerで実行する方法にしました。

Discussion