😠

教習所の授業の予約がクソだるい

2023/06/03に公開

初めに

  • puppeteerを用い、予約確認を自動で行うようにしました。
  • node-notifierとタスクスケジューラを用い、デスクトップに定期的に通知が出るようにしました。

背景

みなさんは、教習所に通ったことがありますか?免許の取り方には二種類存在し、合宿で取るパターンと、教習所に通って取るパターンの二種類があります。通って取るパターンだと、課金して教習所にスケジュールを組んでもらう方が多いです。私はお金が無いので課金はしませんでした。
ところが、課金勢のスケジュールで教習所の予約は全て埋まっており、課金勢がキャンセルした時間帯でしか無課金勢は予約できません。キャンセル料がほぼかからないのもあり、無課金勢は前日、あるいは当日にキャンセルされた予約を取り合うという構図になっています。つまり、定期的に予約を確認する必要があるということです。
これはかなり消耗します。しかも、授業の予約のサイトはかなり昔に作られたものであり、決して体験が良いサイトではありません。そのため、自動で確認するプログラムを書きました。

使用技術

  • Node.js
  • TypeScript
  • Puppeteer
  • node-notifier

予約情報を取得する

まずはブラウザを初期化しましょう。

main.ts
const browser = await puppeteer.launch();
const page = await browser.newPage();

const url = "url";
const response = await page.goto(url, {
  waitUntil: ["load"],
  timeout: 30000,
});

if (!response) {
  throw new Error(`Cannot access to url ${url}`);
}

if (response.status() !== 200) {
  throw new Error(
    `Failed to access to url ${url} with status = ${response.status()}`
  );
}

現時点でどのようなブラウザがシミュレートされているのか気になるので、確認しましょう。

main.ts
const content = await page.content();

console.log(content);

想定しているHTMLが吐き出されているかを確認してください。

続いて、予約サイトの外観を除いていきましょう。まずはログイン画面があります。

それらのhtml要素をDevToolsで確認し、

main.html
<input type="password" name="b.password" size="18" maxlength="16" id="p01aForm_b_password">

<input type="text" name="b.studentId" size="18" maxlength="7" value="" id="p01aForm_b_studentId" style="ime-mode:disabled">

このように、名前が b.password, b.studentIdで名前がついていることが分かります。そのため、

main.ts
const [studentIdInput] = await page.$x('//input[@name="b.studentId"]');
const [passwordInput] = await page.$x('//input[@name="b.password"]');

puppeteerでこのように指定し、要素を取得した後、必要な情報を入力し、ログインを行います。

main.ts
await studentIdInput.type("hoge");
await passwordInput.type("fuga");

await Promise.all([
  page.waitForNavigation(),
  page.click("#p01aForm_login"),
]);

ログインを行うと、予約のページに遷移します。予約のページはこのようになっています。

DevToolsで眺めると、テーブルのセルには、

  • 予約可能である場合 className = status1
  • 予約済である場合 className = status3

のようにclassが振られています。また、テーブルのセルには1~13時限、一週間分の予定があるため、それらすべてに対してチェックを行いましょう。

main.ts
const availableReservation = [];
 for (let d = 0; d < 7; d++) {
      for (let t = 0; t < 13; t++) {
        const status = await page.$(
          `#formId > table > tbody > tr > td.center > table:nth-child(4) > tbody > tr:nth-child(${
            d + 2
          }) > td:nth-child(${t + 2})`
        );
        const className = await (
          await status.getProperty("className")
        ).jsonValue();
        if (className === "status1") {
          availableReservation.push(
            `${days[d]} ${timeTable[t]} : ${className}`
          );
        } else if (className === "status3") {
          console.log(`予約済み ${days[d]} ${timeTable[t]} : ${className}`);
        }
      }
    }

最後に、それらの通知をデスクトップで行います。

main.ts
notifier.notify({
  title: "教習所予約可",
  message: availableReservation.join("\n"),
  wait: false,
});

定期実行する

そして、これを実行するようなmain.batを作成しましょう。

main.bat
@echo off
cd /d %~dp0 
npm run start

申し遅れましたが、package.jsonはこのようになっています。

package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "start": "ts-node --esm main.ts"
  },
  "dependencies": {
    "node-notifier": "^10.0.1",
    "puppeteer": "^20.1.1"
  },
  "devDependencies": {
    "@types/node": "^20.1.0",
    "@types/node-notifier": "^8.0.2",
    "typescript": "^4.4.4"
  },
  "type": "module"
}

コマンドにはts-nodeを使用しています。TypeScriptだとJavaScriptにトランスパイルしてから実行する必要がありますが、ts-nodeを使うことにより直接実行しています。また、--esmをつけることにより、ESMの形式(import / export構文)を使用する事が可能になります。

さて、定期実行ですが、今回はタスクスケジューラを使用しましょう。
タスクスケジューラを起動した後、タスクの作成をクリックします。名前は適当に決めましょう。

トリガーにはこのように周期や開始時間を設定できます。

操作には、プログラム/スクリプトの部分に先ほど作成したbatファイルへのファイルパスを指定してください。

以上で終了です。あとはデスクトップの通知を待ちましょう。

感想

最初はLINEに通知を送ろうとしましたが、色々面倒なのと、大掛かりになりそうなので、なるべく最小限のコストで実装できるようにしました。結構他のところでも応用が効きそうなので、自動化したい操作があればやってみてください。

Discussion