Zaim の CSV インポートのブラウザ操作を Seleniumでちょっと自動化する
概要
- ヨドバシのクレジットカードを愛用しているが、Zaimの自動連携がないため毎月CSVインポートで記録している
- Windows では Power Automate Desktop でちょっと楽をしていたが、macOS でも同様の自動化をしたいと思ったので試してみた
前提
作業環境
macOS Tahoe 26.0.1(25A362)
使うもの
- Firefox 147.0.1 (aarch64)
- Python
- Selenium 4
- WebDriver BiDi
作るもの
- 所定の形式で作成しているCSVファイルをZaimにインポートする作業を、部分的に自動化するもの
- 実行すると、Zaimの入出力ページを開いて特定のプルダウンの選択肢を一気に設定してくれる
- 以下の動作は手動で対応する
- ログインする
- アップロードするCSVファイルを選択する
- アップロードをテスト
- 本番の家計簿にアップロード
- スクリプトを終了する
作業ログ
作業ディレクトリを用意
ディレクトリの作成
任意の場所でディレクトリを作成
mkdir zaim-automation
作成したディレクトリに移動
cd zaim-automation
.editorconfig の作成
あとでいろんなファイルを用意するのであらかじめ作っておく
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
Python 環境を用意する
Python のバージョンを指定
Python 3.12系の最新バージョンである 3.12.12 を利用することにした
インストール済みのバージョン一覧確認
pyenv versions
該当バージョンがなければ、以下コマンドでインストール
pyenv install 3.12.12
作業ディレクトリ配下の Python バージョンを固定
pyenv local 3.12.12
該当バージョンに設定されたことを確認
python --version
% python --version
Python 3.12.12
Python 環境を構築
.venv ディレクトリに専用の Python 環境を作成
python -m venv .venv
この時点で which python と which pip を実行すると、ユーザーディレクトリ以下のコマンドが参照されていることがわかる
% which python
/Users/hi3103/.pyenv/shims/python
% which pip
/Users/hi3103/.pyenv/shims/pip
.venv ディレクトリの環境が参照されるように、以下を実行
source .venv/bin/activate
which コマンドを再度実行し、パスが変わったことを確認
% which python
/Users/hi3103/Sites/zaim-automation/.venv/bin/python
% which pip
/Users/hi3103/Sites/zaim-automation/.venv/bin/pip
.venv 内の pip を最新に更新
以下のコマンドを実行
pip install -U pip
Selenium をインストール
以下のコマンドを実行
pip install selenium
インストールされたことを確認
pip show selenium
実行結果から、Seleniumのバージョンと、指定のPythonバージョン配下に格納されたことを確認
% pip show selenium
Name: selenium
Version: 4.39.0
Summary: Official Python bindings for Selenium WebDriver
Home-page: https://www.selenium.dev
Author:
Author-email:
License: Apache-2.0
Location: /Users/hi3103/.pyenv/versions/3.12.12/lib/python3.12/site-packages
Requires: certifi, trio, trio-websocket, typing_extensions, urllib3, websocket-client
Required-by:
.envファイルを用意
python-dotenv をインストール
Python で .env ファイルを扱えるようにする
pip install python-dotenv
.env を作成
ブラウザのプロファイルのパスや、遷移する画面のURLなどを .env ファイルで管理する
# Firefox automation profile directory
PROFILE_DIR=/Users/yourname/Library/Application Support/Firefox/Profiles/xxxxxxxx.xxxxxxxx
# Zaim URLs
LOGIN_URL=https://zaim.net/user_session/new
IMPORT_EXPORT_URL=https://zaim.net/home/money_upload
# Login wait (seconds)
LOGIN_TIMEOUT_SEC=180
チェック用コードを用意して確認
env_check.py というファイル名で以下を保存
# env_check.py
import os
from dotenv import load_dotenv
load_dotenv()
KEYS = [
"PROFILE_DIR",
"LOGIN_URL",
"IMPORT_EXPORT_URL",
"LOGIN_TIMEOUT_SEC",
]
for key in KEYS:
value = os.getenv(key)
if value is None:
print(f"[NG] {key} is not loaded")
else:
print(f"[OK] {key} = {value}")
以下を実行
python env_check.py
.env に記載した値が出力されることを確認
% python env_check.py
[OK] PROFILE_DIR = /Users/yourname/Library/Application Support/Firefox/Profiles/xxxxxxxx.xxxxxxxx
[OK] LOGIN_URL = https://zaim.net/user_session/new
[OK] IMPORT_EXPORT_URL = https://zaim.net/home/money_upload
[OK] LOGIN_TIMEOUT_SEC = 180
Git 管理のためのもろもろ
最終的に Github のリポジトリにあげておきたいので、必要なファイルを諸々用意する
Git 初期化
git init
README.md を作成
touch README.md
requirements.txt を作成
現状の構成を書き出しておく
pip freeze > requirements.txt
.env.sample を作成
.env ファイルは Git 管理外としたいので、 .env.sample という名称で複製し、ダミーデータを流し込んでおく
.gitignore を作成
# Python
__pycache__/
*.py[cod]
# Virtualenv
.venv/
# System
.DS_Store
# Logs
*.log
# Environment
.env
テストスクリプトを作成
Python も Selenium も初めて触るので、細かい単位で色々試してみる
Selenium で Firefox を起動して任意のURLを表示する
smoke.py として保存
from selenium import webdriver
driver = webdriver.Firefox()
driver.get("https://hi3103.net")
input("表示できたら Enter で終了: ")
driver.quit()
実行
python smoke.py
Selenium で Firefox を任意のプロファイルで起動する
profile_smoke.py として保存
from selenium import webdriver
PROFILE_DIR = "/Users/yourname/Library/Application Support/Firefox/Profiles/xxxxxxxx.xxxxxxxx"
options = webdriver.FirefoxOptions()
options.profile = PROFILE_DIR
driver = webdriver.Firefox(options=options)
driver.get("about:profiles")
input(
"\nabout:profiles を開いています。\n"
"「このプロファイルは現在使用中です」が\n"
"指定した PROFILE_DIR になっていることを確認したら Enter を押してください..."
)
driver.quit()
実行
python profile_smoke.py
Selenium で Firefox を .env で指定した任意のプロファイルで起動する
profile_env_smoke.py として保存
import os
from dotenv import load_dotenv
from selenium import webdriver
load_dotenv()
PROFILE_DIR = os.getenv("PROFILE_DIR")
if not PROFILE_DIR:
raise RuntimeError("PROFILE_DIR is not set in .env")
options = webdriver.FirefoxOptions()
options.profile = PROFILE_DIR
driver = webdriver.Firefox(options=options)
driver.get("about:profiles")
input(
"\nabout:profiles を開いています。\n"
"「このプロファイルは現在使用中です」が\n"
"指定した PROFILE_DIR になっていることを確認したら Enter を押してください..."
)
driver.quit()
実行
python profile_env_smoke.py
成果物
作成したスクリプト(zaim_flow.py)
あとはひたすらChatGPTとラリーして実行してを繰り返した
自分が月イチローカルで動かすだけなので、作り甘いところあっても許容
from __future__ import annotations
import os
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from dotenv import load_dotenv
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
@dataclass(frozen=True)
class Config:
profile_dir: Path
login_url: str
import_export_url: str
login_email: str
login_password: str
login_timeout_sec: int = 180
SEL_LOGIN_EMAIL = (By.NAME, "email")
SEL_LOGIN_PASSWORD = (By.NAME, "password")
SEL_LOGOUT = (By.CSS_SELECTOR, 'a[href="/user_session"][data-method="delete"]')
SEL_IMPORT_FILE = (By.CSS_SELECTOR, '#GeneralUploadFile[name="general_upload_file"][type="file"]')
SEL_UPLOAD_COLLAPSE_TOGGLE = (By.CSS_SELECTOR, 'h3.title[data-bs-toggle="collapse"][href="#collapseGeneralUpload"]')
SEL_LINES = [
(By.NAME, "date_line"),
(By.NAME, "category_line"),
(By.NAME, "genre_line"),
(By.NAME, "comment_line"),
(By.NAME, "place_line"),
(By.NAME, "from_account_line"),
(By.NAME, "to_account_line"),
(By.NAME, "name_line"),
(By.NAME, "payment_line"),
(By.NAME, "income_line"),
(By.NAME, "transfer_line"),
(By.NAME, "transfer_flag"),
(By.NAME, "calc_line"),
]
SEL_FLASH_MESSAGE = (By.ID, "flashMessage")
def load_config() -> Config:
load_dotenv()
def must(name: str) -> str:
v = os.getenv(name)
if not v:
raise RuntimeError(f"Missing required env var: {name}")
return v
profile_dir_raw = must("PROFILE_DIR")
login_url = must("LOGIN_URL")
import_export_url = must("IMPORT_EXPORT_URL")
login_email = must("LOGIN_EMAIL")
login_password = must("LOGIN_PASSWORD")
login_timeout_sec = int(os.getenv("LOGIN_TIMEOUT_SEC", "180"))
profile_dir = Path(profile_dir_raw).expanduser()
if not profile_dir.exists():
raise RuntimeError(f"PROFILE_DIR does not exist: {profile_dir}")
if not profile_dir.is_dir():
raise RuntimeError(f"PROFILE_DIR is not a directory: {profile_dir}")
return Config(
profile_dir=profile_dir,
login_url=login_url,
import_export_url=import_export_url,
login_email=login_email,
login_password=login_password,
login_timeout_sec=login_timeout_sec,
)
def is_logged_in(driver: webdriver.Firefox) -> bool:
return bool(driver.find_elements(*SEL_LOGOUT))
def fill_login_inputs(driver: webdriver.Firefox, wait: WebDriverWait, email: str, password: str) -> None:
email_el = wait.until(EC.presence_of_element_located(SEL_LOGIN_EMAIL))
password_el = wait.until(EC.presence_of_element_located(SEL_LOGIN_PASSWORD))
email_el.clear()
email_el.send_keys(email)
password_el.clear()
password_el.send_keys(password)
def wait_for_manual_login(driver: webdriver.Firefox, timeout_sec: int) -> None:
end = time.time() + timeout_sec
while time.time() < end:
if is_logged_in(driver):
return
time.sleep(0.5)
raise TimeoutException(f"manual login timeout: {timeout_sec}s")
def select_by_value_selenium(
driver: webdriver.Firefox,
wait: WebDriverWait,
locator: tuple,
value: str,
) -> None:
el = wait.until(EC.visibility_of_element_located(locator))
wait.until(EC.element_to_be_clickable(locator))
Select(el).select_by_value(value)
wait.until(lambda d: d.find_element(*locator).get_attribute("value") == value)
def main() -> int:
try:
cfg = load_config()
except Exception as e:
print(f"[FATAL] config error: {e}", file=sys.stderr)
return 10
options = webdriver.FirefoxOptions()
options.profile = str(cfg.profile_dir)
driver = webdriver.Firefox(options=options)
wait = WebDriverWait(driver, 30)
try:
# 1) ログインURLを開く
driver.get(cfg.login_url)
# 1.5) 未ログインならメール/パスワードを入力だけしておく(submitしない)
if not is_logged_in(driver):
fill_login_inputs(driver, wait, cfg.login_email, cfg.login_password)
print("[INFO] メール/パスワードを入力済み(ログインボタンは手動)", file=sys.stderr)
# 2-3) 手動ログイン待機 & ログイン完了判定
if not is_logged_in(driver):
print(
f"[INFO] 手動でログインしてください({cfg.login_timeout_sec}秒でタイムアウト)",
file=sys.stderr,
)
wait_for_manual_login(driver, cfg.login_timeout_sec)
print("[INFO] ログイン完了を検知。5秒待機", file=sys.stderr)
time.sleep(5
# 4) ファイル入出力ページへ遷移
print("[INFO] ファイル入出力ページ遷移", file=sys.stderr)
driver.get(cfg.import_export_url)
wait.until(EC.presence_of_element_located(SEL_IMPORT_FILE))
print("[INFO] ファイル入出力ページ到達を確認。5秒待機", file=sys.stderr)
time.sleep(5)
# 5) アコーディオン展開(select群を表示させる)
print("[INFO] アコーディオンを展開", file=sys.stderr)
toggle = wait.until(EC.element_to_be_clickable(SEL_UPLOAD_COLLAPSE_TOGGLE))
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", toggle)
toggle.click()
# 6) 13個のselectを value=1..13 で順に選択
for i, locator in enumerate(SEL_LINES, start=1):
value = str(i)
print(f"[INFO] select {locator} -> value={value}", file=sys.stderr)
select_by_value_selenium(driver, wait, locator, value)
print("[INFO] 全selectの選択完了。手動アップロード待機中...", file=sys.stderr)
input("作業が終わったら return キーを押してください")
driver.quit()
return 0
except TimeoutException as e:
print(f"[ERROR] timeout: {e}", file=sys.stderr)
return 3
if __name__ == "__main__":
raise SystemExit(main())
ディレクトリ構成
├── .editorconfig
├── .env
├── .env.sample
├── .git
├── README.md
├── requirements.txt
└── scripts
├── test
│ ├── env_check.py
│ ├── profile_env_smoke.py
│ ├── profile_smoke.py
│ └── smoke.py
└── zaim_flow.py
セットアップ手順
git clone 直後に初回だけ行う作業として以下を想定
Python環境の構築
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
環境変数の設定
cp .env.sample .env
vim .en
.env.sample から .env を作成し、中身を書き換える
- PROFILE_DIR: Firefoxのプロファイルディレクトリを指定
- LOGIN_EMAIL: Zaimのログインメールアドレス
- LOGIN_PASSWORD: Zaimのログインパスワード
使い方
python scripts/zaim_flow.py
- 🤖 Firefoxが指定のプロファイルで起動し、Zaimログイン画面が表示される
- 🤖 メールアドレス・パスワードが自動入力される
- 👨 手動でログイン実行する
- 🤖 5秒待機後、入出力ページに自動遷移
- 🤖 5秒待機後、「一般的な CSV ファイルをアップロードする」のアコーディオン展開と、1〜13列目のプルダウン選択が自動実行される
- 👨 任意のCSVファイルをアップロードし、[アップロードをテスト]後、[本番の家計簿にアップロード]する
- 👨 return キーを押下で終了