はじめに
クレジットカードの利用明細はPDFやCSVでダウンロードすることができますが、これらを手動で取得するのは手間がかかります。
うまく改造すればfreeeへの自動連携なども考えられます。
ビジネスカードのAPI非対応の履歴も取れますので、色々と利用できそうです。
そこで、Playwrightを使って自動化することで、手動での取得作業を自動化することができます。
以下は取得したcsvから月の支払金額をLine通知するサンプルです
https://github.com/natsumeaurlia/rakuten-csv
準備
適当な作業ディレクトリを作成。
必要なパッケージのインストール方法
以下のコマンドで、必要なパッケージをインストールします。
npm init playwright@latest
npm install playwright dotenv
ドライバーのダウンロードと設定方法
Playwrightでは、実行するブラウザに応じたドライバーを必要とします。本クローラーでは、Chromiumを利用するため、以下のコマンドでChromiumのドライバーをダウンロードします。
npx playwright install
dotenvを使った環境変数の設定方法
セキュリティ上の理由から、本クローラーではIDとパスワードを環境変数として設定します。.envファイルをプロジェクトのルートディレクトリに作成し、以下のように設定してください。
ID=<楽天カードのログインID>
PASS=<楽天カードのログインパスワード>
起動処理
main.ts,tsconfig.jsonを作成してください。
tsconfig.jsonはこのような形です。
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
main.tsはこのような雛形になります。
import * as dotenv from 'dotenv';
dotenv.config();
(async () => {
try {
} catch (err) {
console.error(err);
}
})();
まずはenvからIDとPASSがあるか確認します
tryブロックの中に追記してください
const ID = process.env.ID;
const PASS = process.env.PASS;
if (!ID || !PASS) throw new Error("ID or PASS is not set.");
これでID,PASSがなかったら実行されません。
次に以下のように追記してください。
const downloadDir = path.join(__dirname, 'storage');
const browser = await chromium.launch({
headless: true,
downloadsPath: downloadDir,
args: [
'--incognito',
'--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
]
});
const context = await browser.newContext({
acceptDownloads: true,
});
const page = await context.newPage();
csvの保存ディレクトリを選択。
chromium起動オプションで、シークレットモード、agent偽装、headless、ダウンロード許可状態で起動します。
ログイン処理
ログイン画面に移動し、3秒待機。
ログイン情報を入力欄に記載しログインボタンを押します。
少しスリープを入れてしっかり画面が開くようにします。
await page.goto("https://www.rakuten-card.co.jp/e-navi/index.xhtml");
await page.waitForTimeout(3000);
await page.fill('input[name="u"]', ID);
await page.fill('input[name="p"]', PASS);
await Promise.all([
page.click('input[id="loginButton"]'),
page.waitForLoadState('networkidle'),
]);
await page.waitForTimeout(2000);
明細取得処理
利用明細をcsvにする関数は下記のように表せます。
unixtimeなどをつけてcsv名の重複を防いでいます
// 利用明細CSVのダウンロード
const downloadFunc = async () => {
const downloadPromise = page.waitForEvent('download', {timeout: 10000});
await page.click('.stmt-c-btn-dl.stmt-csv-btn');
const download = await downloadPromise;
const unixtime = Math.floor(new Date().getTime() / 1000);
await download.saveAs(path.join(downloadDir, unixtime + 'rakuten-card.csv'));
})
これを利用してダウンロードは行います。
それでは利用明細に移動します。
移動したら、楽天カードの利用可能なものを取得します。
// 利用明細画面に遷移する
await Promise.all([
page.goto('https://www.rakuten-card.co.jp/e- navi/members/statement/index.xhtml?l-id=enavi_all_glonavi_statement'),
page.waitForLoadState('networkidle'),
]);
const availableOptionsTexts = await page.evaluate(() => {
const cardSelect = <HTMLInputElement>document.querySelector('.stmt-head-regist-card__select__box');
return Array.from(cardSelect.querySelectorAll('option'))
.filter((option) => !option.innerText.includes('利用不可'))
.filter((option) => option.selected === false)
.map((option) => option.innerText);
});
あとはダウンロードしましょう
// 初期の利用可能なカードの利用明細をダウンロード
await downloadFunc();
// その他利用可能なカードの利用明細をダウンロード
for (const option of availableOptionsTexts) {
// カードを選択
await page.locator('.stmt-head-regist-card__select__box').selectOption(option);
await page.waitForLoadState('networkidle');
await downloadFunc();
}
await browser.close();
最終コード
import { chromium } from 'playwright';
import * as path from 'path';
import * as dotenv from 'dotenv';
dotenv.config();
(async () => {
try {
const ID = process.env.ID;
const PASS = process.env.PASS;
if (!ID || !PASS) throw new Error("ID or PASS is not set.");
// storageフォルダに保存
const downloadDir = path.join(__dirname, 'storage');
const browser = await chromium.launch({
headless: true,
downloadsPath: downloadDir,
args: [
'--incognito',
'--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
]
});
const context = await browser.newContext({
acceptDownloads: true,
});
const page = await context.newPage();
await page.goto("https://www.rakuten-card.co.jp/e-navi/index.xhtml");
await page.waitForTimeout(3000);
await page.fill('input[name="u"]', ID);
await page.fill('input[name="p"]', PASS);
await Promise.all([
page.click('input[id="loginButton"]'),
page.waitForLoadState('networkidle'),
]);
await page.waitForTimeout(2000);
// 利用明細画面に遷移する
await Promise.all([
page.goto('https://www.rakuten-card.co.jp/e-navi/members/statement/index.xhtml?l-id=enavi_all_glonavi_statement'),
page.waitForLoadState('networkidle'),
]);
const availableOptionsTexts = await page.evaluate(() => {
const cardSelect = <HTMLInputElement>document.querySelector('.stmt-head-regist-card__select__box');
return Array.from(cardSelect.querySelectorAll('option'))
.filter((option) => !option.innerText.includes('利用不可'))
.filter((option) => option.selected === false)
.map((option) => option.innerText);
});
// 利用明細CSVのダウンロード
const downloadFunc = async () => {
const downloadPromise = page.waitForEvent('download', {timeout: 10000});
await page.click('.stmt-c-btn-dl.stmt-csv-btn');
const download = await downloadPromise;
// Wait for the download process to complete
console.log(await download.path());
// Save downloaded file somewhere
const unixtime = Math.floor(new Date().getTime() / 1000);
await download.saveAs(path.join(downloadDir, unixtime + 'rakuten-card.csv'));
}
// 初期の利用可能なカードの利用明細をダウンロード
await downloadFunc();
// その他利用可能なカードの利用明細をダウンロード
for (const option of availableOptionsTexts) {
// カードを選択
await page.locator('.stmt-head-regist-card__select__box').selectOption(option);
await page.waitForLoadState('networkidle');
await downloadFunc();
}
await browser.close();
} catch (err) {
console.error(err);
}
})();