Post

MonitokyoGas Backend 1 - Retrieve Data

MonitokyoGas Backend 1 - Retrieve Data

Project Overview

在這個篇中,將紀錄

  • 抓取東京ガス的歷史用電紀錄
  • 使用Github action設計自動抓取資料的流程
  • 將資料儲存到csv檔案中

抓取東京ガス的歷史用電紀錄

取得資料

為了抓取東京ガス的歷史用電紀錄,必須知道從登入到我們點擊獲取用電紀錄的過程是怎麼發生的。在我們登入並點擊「使用量」後,從開發者模式中可以看到網路中有向https://members.tokyo-gas.co.jp/graphql傳送POST,Query為

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
query DailyElectricityUsage(
  $contractIndexNumber: Int!
  $electricityContractNumber: String!
  $fromDate: String!
  $toDate: String
) {
  dailyElectricityUsage(
    contractIndexNumber: $contractIndexNumber
    electricityContractNumber: $electricityContractNumber
    fromDate: $fromDate
    toDate: $toDate
  ) {
    averageUsageForSameContract
    date
    usage
    __typename
  }
}

以及variables:contractIndexNumber 1electricityContractNumber "XXXXXXXXXX"fromDate "YYYY-MM-DD"toDate null 便可以獲得歷史用電紀錄:

1
2
3
4
5
6
7
8
9
{"data":
  {"dailyElectricityUsage":
    [{"averageUsageForSameContract":9.07,
      "date":"2025-08-13T15:00:00.000Z",
      "usage":4.1,
      "__typename":"DailyElectricityUsage"
      }, ...]
  }
}

其中,averageUsageForSameContract代表同一個用電方案下其他人的平均,usage代表自己的用電量。

因此我們可以透過下列程式碼抓取資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
async function fetchElectricityUsage(cookie: string): Promise<UsageData[]> {
  const fromDate = dayjs().subtract(14, "day").format("YYYY-MM-DD");
  const response = await axios.post(
    "https://members.tokyo-gas.co.jp/graphql",
    {
      operationName: "DailyElectricityUsage",
      variables: {
        contractIndexNumber: 1,
        electricityContractNumber: process.env.CONTRACT_NUMBER,
        fromDate: fromDate,
        toDate: null,
      },
      query: `
        query DailyElectricityUsage(
          $contractIndexNumber: Int!
          $electricityContractNumber: String!
          $fromDate: String!
          $toDate: String
        ) {
          dailyElectricityUsage(
            contractIndexNumber: $contractIndexNumber
            electricityContractNumber: $electricityContractNumber
            fromDate: $fromDate
            toDate: $toDate
          ) {
            averageUsageForSameContract
            date
            usage
            __typename
          }
        }
      `,
    },
    {
      headers: {
        "Content-Type": "application/json",
        Origin: "https://members.tokyo-gas.co.jp",
        Referer: "https://members.tokyo-gas.co.jp/usage?tab=electricity",
        Cookie: cookie,
        "User-Agent": "Mozilla/5.0",
      },
    }
  );
  console.log("Response status:", response.status);
  return response.data.data.dailyElectricityUsage.map((entry: any) => ({
    date: entry.date.slice(0, 10),
    usage: entry.usage,
    contract_number: process.env.CONTRACT_NUMBER,
  }));
}

仔細看資料,在N日時執行時,第一筆資料日期為N-1日並且usage為null,而第二筆資料雖然日期是N-2日,但才是N-1日的用電量。理由是因為他使用UTC + 0時間,所以Local Time N-1日的date(N-1T00:00:00.000)會變成N-2 的15時(N-2T15:00:00.000)。這個在資料抓取後必須做處理。

獲取Cookie

因為網站有Cookie來請求資料,因此在設計自動化時,必須獲取Cookie才能夠進行抓取。 在模擬登入中,使用Typescript中的puppeteer來進行網站抓取Cookie的Header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
export async function loginAndGetCookie(): Promise<string> {
  const email = process.env.TOKYOGAS_EMAIL!;
  const password = process.env.TOKYOGAS_PASSWORD!;
  if (!email || !password) {
    throw new Error("Please set TOKYOGAS_EMAIL and TOKYOGAS_PASSWORD in your .env file");
  }

const browser = await puppeteer.launch({
  headless: true,
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
  const page = await browser.newPage();

  // Visit the login page
  await page.goto("https://members.tokyo-gas.co.jp/", { waitUntil: "networkidle0" });

  // Click the login button
  const goToLoginButtonSelector = 'a.text-center.flex.justify-center.w-full.rounded-lg.py-5.px-6.text-labelLarge.font-normal.bg-primary.text-onPrimary.cursor-pointer[href="/api/mtg/v1/auth/login"]';

  await page.waitForSelector(goToLoginButtonSelector, { visible: true, timeout: 10000 });

  await Promise.all([
    page.waitForNavigation({ waitUntil: "networkidle0" }),
    page.click(goToLoginButtonSelector),
  ]);

  // Fill in email and password
  await page.type('input[name="loginId"]', email);
  await page.type('input[name="password"]', password);

  // Click the login button
  await Promise.all([
    page.click('button[type="submit"]'),
    page.waitForNavigation({ waitUntil: "networkidle0" }),
  ]);

  // After successful login, retrieve cookies
  const cookies = await page.cookies();

  const cookieHeader = cookies.map(c => `${c.name}=${c.value}`).join("; ");

  await browser.close();

  return cookieHeader;
}

使用Github action設計自動抓取資料的流程

因為Tokyogas 大概都是在13:00 JST時間更新前一天的用電量,在github action 設定上除了手動trigger外,也加上了cron:

1
2
3
4
on:
  schedule:
    - cron: '30 4 * * *'
  workflow_dispatch:

主要流程為

  1. checkout
  2. check the existence of data branch
  3. setup nodejs
  4. install dependencies
  5. Grep new data by fetchElectricity.ts
  6. Push the generated csv to data branch

在第5步Grep new data by fetchElectricity.ts中,我們會先檢查是否有cookie存放在backend/cookie_store,若沒有的話會先抓取Cookie。

在Github Repo裡的Screts 中設定自己的CONTRACT_NUMBERTOKYOGAS_EMAILTOKYOGAS_PASSWORD

將資料儲存到csv檔案中

在還沒想到更好的方法前,採取方法為將資料推送到data branch,避免每天抓取資料時,會使得main branch 受到CICD的影響,讓每次開發都要處理合併問題。

但這也造成幾個新問題是「該怎麼將main branch 新變動同步到data branch」以及「該怎麼push csv 到data branch」。

  • 將main branch 新變動同步到data branch,可以查看update_branch.yml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
        - name: Sync and push to data branch
          run: |
            git fetch --all
            # echo all branches
            git branch -a
            # Check if data branch exists and switch to it, otherwise exit
            if git show-ref --verify --quiet refs/remotes/origin/data; then
              git switch data
              git pull origin data
            else
              echo "Data branch does not exist. Exiting."
              exit 0
            fi
            git merge --no-ff origin/main -m "Merge main into data [skip ci]"
            git push origin data
    
  • push csv 到data branch,可以查看crawler.yml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
        - name: Ensure data branch exists and checkout
          run: |
            git fetch origin
            if git show-ref --verify --quiet refs/remotes/origin/data; then
              git checkout data
            else
              git checkout -b data
            fi
    
        - name: Push the csv file to the repository
          env:
            GITHUB_TOKEN: $
          run: |
            git config --global user.name "github-actions[bot]"
            git config --global user.email "github-actions[bot]@users.noreply.github.com"
            git add backend/csv_store/*.csv
            git commit -m "Update CSV files [skip ci]" || echo "No changes to commit"
            git push https://x-access-token:${GITHUB_TOKEN}@github.com/$.git data
    

Ref

This post is licensed under CC BY 4.0 by the author.