前の投稿で前は何ができて、今回何を実現するようにしたかを書いてみた。本文はn8nでZaimの自動記帳を強力化する話の細かい設定を記録します。

目次
重要な設定は主に下記があります
- TimeZone(一瞬で終わるものですが、ミスったらいろいろ苦労)
- 正規表現で各社の公式メールを同じ書式にする
- GASにある本来のコードを各種ペイに合わせて修正
- GASのコードに内容を追加
- HTTPリクエストでGAS関数発動のWorkFlowを作成
- HTTPリクエストでntfy通知を設定
1-TimeZone
私は最初これを忘れました。それでメールを整うことができたが、GASのトリガーをどうしても発動しませんでした。
下記画面で設定できます。WorkFlowの右上にある「・・・」です。


2-正規表現で各社の公式メールを同じ書式にする
ここはn8nとAIの強さに任せる。
実際ANAPAYのテストメールで拾った内容はこんな感じ、真ん中の部分は自分の設定内容。


正規表現はAIに聞けば一発で、私は知識なしでも難なく書けました。
ちなみに楽天ペイの場合はこんな感じです。
利用日時:{{ $json.date.match(/\d{4}-\d{2}-\d{2}/)[0] }}<br>
利用金額:{{parseFloat($json.text.match(/(?<=楽天キャッシュ)\s*([\d,]+)(?=\s*円)/)[0].replace(/,/g, ''))}}円<br>
利用店舗:{{ $json.text.match(/(?<=ご利用店舗\s*)[\s\S]+?(?=\s*電話番号)/)[0].trim().split('\n')[0] }}<br>ANA Payは
利用日時:{{ $json.date.match(/\d{4}-\d{2}-\d{2}/)[0] }}<br>
利用金額:{{parseFloat($json.text.match(/(?<=利用金額:)\s*([\d,]+)(?=\s*円)/)[0].replace(/,/g, ''))}}円<br>
利用店舗:{{ $json.text.match(/(?<=ご利用店舗:\s*)[\s\S]+?(?=\s*詳細)/)[0].trim() }}<br>メルペイは
利用日時:{{ $json.date.match(/\d{4}-\d{2}-\d{2}/)[0] }}<br>
利用金額:{{parseFloat($json.text.match(/(?<=¥)\s*([\d,]+)(?=\s*\n)/)[0].replace(/,/g, ''))}}円<br>
利用店舗:{{ $json.text.match(/(?<=店舗名 : \s*)[\s\S]+?(?=\s*\n)/)[0].trim() }}<br>一定程度の日本語能力があって、Gemini(Chat GPTもできるはず)にニーズを話せるのなら、書く仕事はAIがやってくれます。(私はなぜか母国語ではなく、日本語でもできてしまいました)。これで本来内容が複雑だった公式のメールは下記のような3行になります。

3-GASにある本来のコードを各種ペイに合わせて修正
ここは本来のコードを張り付けて、コメントに記入して説明します。
const CONSUMER_KEY = PropertiesService.getScriptProperties().getProperty("ZAIM_CONSUMER_ID");
const CONSUMER_SECRET = PropertiesService.getScriptProperties().getProperty("ZAIM_CONSUMER_SECRET")
// 使用する支払い方法に合わせてIDを変えてください。合計4か所あります。(変えなくても作動はしますが、複数ペイが存在すると管理がややこしくなります)
const RAKUTEN_PAYMENT_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("RAKUTEN_PAYMENT_ID"));
const DEFAULT_CATEGORY_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("DEFAULT_CATEGORY_ID"));
const DEFAULT_GENRE_ID = parseFloat(PropertiesService.getScriptProperties().getProperty("DEFAULT_GENRE_ID"));
// 今日の日付を取得
var date = new Date(); // 現在の日付と時刻を取得
var today = Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy-MM-dd");
// 1日前の日付を取得(ここは必要に応じて変えられます。)
var yesterday = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1);
yesterday = Utilities.formatDate(yesterday, Session.getScriptTimeZone(), "yyyy-MM-dd");
// 認証のリセット
function reset() {
var service = getService();
service.reset();
}
// 認証サービスの設定
function getService() {
return OAuth1.createService("Zaim")
// Set the endpoint URLs.
.setAccessTokenUrl("https://api.zaim.net/v2/auth/access")
.setRequestTokenUrl("https://api.zaim.net/v2/auth/request")
.setAuthorizationUrl("https://auth.zaim.net/users/auth")
// Set the consumer key and secret.
.setConsumerKey(CONSUMER_KEY)
.setConsumerSecret(CONSUMER_SECRET)
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction("authCallback")
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties());
}
// OAuth Callbackの設定
function authCallback(request) {
var service = getService();
var authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput("認証できました!このページを閉じて再びスクリプトを実行してください。");
} else {
return HtmlService.createHtmlOutput("認証に失敗");
}
}
// GETパラメーターを作成
function encodeParams(params) {
var encodedParams = [];
for (var key in params) {
encodedParams.push(encodeURIComponent(key) + "=" + encodeURIComponent(params[key]));
}
return encodedParams.join("&");
}
// 過去の支払いデータを取得
function getPastData(service) {
var url = "https://api.zaim.net/v2/home/money";
// 日付で検索(ここは必要に応じて変えられます。)
var params = {
"start_date": yesterday,
"end_date": today,
}
// 日付指定したい場合は日付を指定してコメントアウトを外す(ここは必要に応じて変えられます。)
// params = {
// "start_date": "2024-04-20",
// "end_date": "2024-04-23",
// }
// データの取得
var response = service.fetch(url + "?" + encodeParams(params));
var result = JSON.parse(response.getContentText());
// Logger.log(result); // 取得したデータを見たい場合コメントアウトを外す
// 楽天ペイのみの支払いでフィルタリング
var rakutenPayData = result.money.filter(function(item) {
return item.from_account_id === RAKUTEN_PAYMENT_ID;
});
return rakutenPayData
}
// 楽天ペイ情報をZaimに登録(メイン関数、名前も変えられるはず、私はしていませんでした。)
function rakutenPayToZaim() {
// Gmailで該当メールを検索(本来のコードは公式メールを識別するものだが、ここは自分が設定したメールのテーマにします)
var start = 0;
var max = 2; // 過去いくつまでメールを遡るか
var query = 'subject: ("自分で設定したメールのテーマ") ';
var threads = GmailApp.search(query, start, max);
// 既存のデータを取得
var service = getService();
if (service.hasAccess()) {
var existingData = getPastData(service)
} else {
var authorizationUrl = service.authorize();
Logger.log("次のURLを開いてZaimで認証したあと、再度スクリプトを実行してください。: %s",
authorizationUrl);
}
// 各楽天ペイの支払いについて登録
for (var i = 0; i < threads.length; i++) {
var messages = threads[i].getMessages();
for (var j = 0; j < messages.length; j++) {
var message = messages[j];
var subject = message.getSubject();
var body = message.getPlainBody();
// ご利用日時、ご利用店舗、楽天キャッシュの金額を抽出(本来のコードは複雑ですが、GASでスクリプトを発動させる時点でもうメールが3行になったので、正規表現は下記にします。
if (subject == "自分で設定したメールのテーマ") {
var usageDate = body.match(/\d{4}-\d{2}-\d{2}/);
var cash = body.match(/\s*([0-9,]+)円/);
var shop = body.match(/利用店舗:\s*([\s\S]*?)---/)[1].trim();
cash = parseInt(cash, 10)
}
var isExisting = false;
Logger.log("日付:" + usageDate + ", 支払金額:" + cash + ", お店:" + shop)
// ポイント払いの場合はスキップ(ここは保留、0円スキップは必要)
if (cash == 0) {
Logger.log("ポイント払いのためスキップ");
continue;
}
// 指定の日付範囲に入っていなければスキップ
if (usageDate != yesterday & usageDate != today) continue;
// 既存のデータと比較して、新しいデータのみを追加
for (var k = 0; k < existingData.length; k++) {
if (existingData[k]["date"] == usageDate && existingData[k]["amount"] == cash && existingData[k]["place"] == shop) {
isExisting = true;
Logger.log("既に入力済み")
break;
}
}
if (!isExisting) {
// 登録カテゴリーシートからデータを取得(下記DBの名前も変えてください。複数種類を同時に自動記帳させたいのならこれが必須です。)
var files = DriveApp.getFilesByName("ZAIM_DB");
var originalData = []
if (files.hasNext()) {
spreadsheet = SpreadsheetApp.open(files.next());
var originalSheet = spreadsheet.getSheetByName("登録カテゴリー");
originalData = originalSheet.getRange(2, 1, originalSheet.getLastRow() - 1, 4).getValues();
}
// 登録カテゴリーシートを検索して適切なカテゴリとジャンルを設定
var category_id = DEFAULT_CATEGORY_ID;
var genre_id = DEFAULT_GENRE_ID;
for (var k = 0; k < originalData.length; k++) {
var [storeName, exactMatch, categoryId, genreId] = originalData[k];
if ((exactMatch && storeName === shop) || (!exactMatch && shop.includes(storeName))) {
category_id = categoryId;
genre_id = genreId;
break;
}
}
// 支払い情報の登録、コメントは好みで変えられます
var url = "https://api.zaim.net/v2/home/money/payment";
var payload = {
"category_id": category_id,
"genre_id": genre_id,
"amount": cash,
"date": usageDate,
"place": shop,
"from_account_id": RAKUTEN_PAYMENT_ID,
"comment": "システムから登録"
};
var options = {
"method": "post",
"payload": payload
};
service.fetch(url, options);
Logger.log("支払い入力完了")
}
}
}
}
// Spreadsheetに書き込み
function writeCategoriesToSpreadsheet(categories, genres, accounts) {
const SPREADSHEET_NAME = "ZAIM_DB";
const CATEGORY_SHEET_NAME = "カテゴリと内訳";
const ACCOUNT_SHEET_NAME = "支払方法";
const ORIGINAL_SHEET_NAME = "登録カテゴリー";
// スプレッドシートを取得または作成
let spreadsheet;
var files = DriveApp.getFilesByName(SPREADSHEET_NAME);
if (files.hasNext()) {
spreadsheet = SpreadsheetApp.open(files.next());
} else {
spreadsheet = SpreadsheetApp.create(SPREADSHEET_NAME);
}
// カテゴリとジャンルのシートを作成
let categorySheet = spreadsheet.getSheetByName(CATEGORY_SHEET_NAME);
if (categorySheet) {
categorySheet.clear(); // 既存のシートをクリア
} else {
categorySheet = spreadsheet.insertSheet(CATEGORY_SHEET_NAME);
}
// アカウントのシートを作成
let accountSheet = spreadsheet.getSheetByName(ACCOUNT_SHEET_NAME);
if (accountSheet) {
accountSheet.clear(); // 既存のシートをクリア
} else {
accountSheet = spreadsheet.insertSheet(ACCOUNT_SHEET_NAME);
}
// オリジナルカテゴリーシートの作成
let originalSheet = spreadsheet.getSheetByName(ORIGINAL_SHEET_NAME);
if (!originalSheet) {
originalSheet = spreadsheet.insertSheet(ORIGINAL_SHEET_NAME);
var originalHeaders = ["店舗名", "完全一致", "カテゴリーID", "内訳ID"];
originalSheet.getRange(1, 1, 1, originalHeaders.length).setValues([originalHeaders]);
}
// 最初のシートが存在する場合、削除する
let defaultSheet = spreadsheet.getSheetByName("シート1");
if (defaultSheet) {
spreadsheet.deleteSheet(defaultSheet);
}
// カテゴリとジャンルのヘッダーを作成
var categoryHeaders = ["カテゴリーID", "カテゴリー名", "内訳ID", "内訳名"];
categorySheet.appendRow(categoryHeaders);
// アカウントのヘッダーを作成
var accountHeaders = ["支払ID", "支払方法"];
accountSheet.appendRow(accountHeaders);
// データ行を作成
var categoryRows = [];
categories.forEach(category => {
var categoryGenres = genres.filter(genre => genre.category_id === category.id);
if (categoryGenres.length > 0) {
categoryGenres.forEach(genre => {
categoryRows.push([
category.id,
category.name,
genre.id,
genre.name,
]);
});
} else {
categoryRows.push([
category.id,
category.name,
"",
"",
]);
}
});
var accountRows = accounts.map(account => [account.id, account.name]);
// 一括で書き込み
categorySheet.getRange(2, 1, categoryRows.length, categoryHeaders.length).setValues(categoryRows);
accountSheet.getRange(2, 1, accountRows.length, accountHeaders.length).setValues(accountRows);
Logger.log("カテゴリ情報、内訳、支払い方法一覧を取得しました: " + spreadsheet.getUrl());
}
// カテゴリと内訳の取得(スプレッドシート作成のメイン関数)
function getInfo() {
var service = getService();
if (!service.hasAccess()) {
var authorizationUrl = service.authorize();
Logger.log("次のURLを開いてZaimで認証したあと、再度スクリプトを実行してください。: %s",
authorizationUrl);
}
// カテゴリの取得
var categoryUrl = "https://api.zaim.net/v2/home/category";
var response = service.fetch(categoryUrl)
var category = JSON.parse(response.getContentText()).categories;
var filterCategory = category.filter(item => item.mode === "payment" && item.active === 1.0)
.sort((a, b) => a.sort - b.sort);
// ジャンルの取得
var genreUrl = "https://api.zaim.net/v2/home/genre";
response = service.fetch(genreUrl)
var genre = JSON.parse(response.getContentText()).genres;
var filterGenre = genre.filter(item => item.active === 1.0)
.sort((a, b) => a.sort - b.sort);
// アカウント一覧の取得
var accountUrl = "https://api.zaim.net/v2/home/account";
var response = service.fetch(accountUrl);
var account = JSON.parse(response.getContentText()).accounts;
account = account.filter(item => item.active === 1.0)
.sort((a, b) => a.sort - b.sort);
// スプレッドシートに書き込み
writeCategoriesToSpreadsheet(filterCategory, filterGenre, account);
}4-GASのコードに追加する内容
この部分の役割は「GASスクリプトの発動をn8nにゆだねる」ことで、上記コードの一番下に下記コードを追加
// n8nで動かすためのコード
function doPost(e) {
try {
// 下記関数を動かす(関数名をいじったらここも修正しなければいけない)
rakutenPayToZaim();
return ContentService.createTextOutput(JSON.stringify({
"status": "success",
"message": "rakutenPayToZaim executed successfully"
})).setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({
"status": "error",
"message": error.toString()
})).setMimeType(ContentService.MimeType.JSON);
}
}そしてGAS画面の右上にある「デプロイ」をクリックし、ウエブアプリをデプロイします。


クリックすると設定は三つしかありません
- 説明:要は名前です。自由に入れていいです。
- 次のユーザーとして実行:「自分」にします。(他の選択肢は状況によって選べますが、今回は自分のためのものなので)
- アクセスできるユーザー:一番簡単なのは「全員」にすることです。これでurlさえ分かれば、全員この関数を動かせることになるので、urlの管理は重要。
urlとは、デプロイされたウエブアプリのurlです

5-HTTPリクエストでGAS関数発動WorkFlowを作成
WorkFlowにHTTPリクエストを追加
設定は簡単、基本下記のような感じでやれば動く。GASのウエブアプリでGoogle認証とか追加した場合、相応に認証情報も変えることが必要です。
Follow RedirectsはONにする。

6-HTTPリクエストでntfyの通知を作成
ntfyの通知を作成、これはntfyにこだわらなくてもいいです。telegramでも、ほかの通知サービスでもいいです。

Bodyに変数も入れられるので、ANA PayのようなメールワンパターンしかないWorkFlowに金額も表示するようにしました。他のペイも全部個別で作りこめばできるが、私は固定文面でやることにしました。
これで実現できたこと
- 自動実行: 設定した時間間隔ではなく、ほぼ楽天ペイのメールが来ると即時記帳します。
- 対応メール:実店舗とオンラインだけではなく、楽天モバイル、らくらく投資、ネット注文(私の場合それがマックでのオンライン注文)をすべて対応できるようにします。
- 出金元:楽天ペイ以外、ANA Pay、メルペイなど、決済通知メールさえあれば、すべて自動記帳可能という仕様に。※残念ながらpaypayは今回対象外にせざるを得ない。(が、公式より楽な方法がある)
- 通知機能:記帳できたらntfyにて通知が来る
- テスト期間中、失敗時の救済:MarcoDroidで手動記帳ボタンを作って、失敗する場合はスマホでぽちっと記帳。
黄色い部分は全部できました。手動記帳はMarcoDroidのHTTPリクエストアクションで一発ですので、ここで書かなくても問題にはならないでしょう。
今後したいこと
- GASではなく、n8nですべて完結できれば。
- paypayはメール来ないから完全自動は難しいが、できる限り便利にしたい。
昨日できて今日書きました。問題があれば随時更新します。
「n8nでZaimの自動記帳を強力化する話②-詳細設定」への1件のフィードバック
コメントは受け付けていません。