{"id":1634,"date":"2026-01-18T21:56:32","date_gmt":"2026-01-18T12:56:32","guid":{"rendered":"https:\/\/hi3103.net\/notes\/?p=1634"},"modified":"2026-01-18T21:58:20","modified_gmt":"2026-01-18T12:58:20","slug":"zaim-%e3%81%ae-csv-%e3%82%a4%e3%83%b3%e3%83%9d%e3%83%bc%e3%83%88%e3%81%ae%e3%83%96%e3%83%a9%e3%82%a6%e3%82%b6%e6%93%8d%e4%bd%9c%e3%82%92-selenium%e3%81%a7%e3%81%a1%e3%82%87%e3%81%a3%e3%81%a8%e8%87%aa","status":"publish","type":"post","link":"https:\/\/hi3103.net\/notes\/mac\/1634","title":{"rendered":"Zaim \u306e CSV \u30a4\u30f3\u30dd\u30fc\u30c8\u306e\u30d6\u30e9\u30a6\u30b6\u64cd\u4f5c\u3092 Selenium\u3067\u3061\u3087\u3063\u3068\u81ea\u52d5\u5316\u3059\u308b"},"content":{"rendered":"<h3>\u6982\u8981<\/h3>\n<ul>\n<li>\u30e8\u30c9\u30d0\u30b7\u306e\u30af\u30ec\u30b8\u30c3\u30c8\u30ab\u30fc\u30c9\u3092\u611b\u7528\u3057\u3066\u3044\u308b\u304c\u3001Zaim\u306e\u81ea\u52d5\u9023\u643a\u304c\u306a\u3044\u305f\u3081\u6bce\u6708CSV\u30a4\u30f3\u30dd\u30fc\u30c8\u3067\u8a18\u9332\u3057\u3066\u3044\u308b<\/li>\n<li>Windows \u3067\u306f Power Automate Desktop \u3067\u3061\u3087\u3063\u3068\u697d\u3092\u3057\u3066\u3044\u305f\u304c\u3001macOS \u3067\u3082\u540c\u69d8\u306e\u81ea\u52d5\u5316\u3092\u3057\u305f\u3044\u3068\u601d\u3063\u305f\u306e\u3067\u8a66\u3057\u3066\u307f\u305f<\/li>\n<\/ul>\n<h3>\u524d\u63d0<\/h3>\n<h4>\u4f5c\u696d\u74b0\u5883<\/h4>\n<p>macOS Tahoe 26.0.1\uff0825A362\uff09<\/p>\n<h4>\u4f7f\u3046\u3082\u306e<\/h4>\n<ul>\n<li>Firefox 147.0.1 (aarch64)\n<ul>\n<li>\u95a2\u9023\u30a8\u30f3\u30c8\u30ea\u30fc\uff1a<a href=\"https:\/\/hi3103.net\/notes\/mac\/1624\">macOS \u3067 Firefox \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u4f5c\u6210\u3059\u308b &#8211; hi3103\u306e\u5099\u5fd8\u9332<\/a><\/li>\n<\/ul>\n<\/li>\n<li>Python\n<ul>\n<li>\u95a2\u9023\u30a8\u30f3\u30c8\u30ea\u30fc\uff1a<a href=\"https:\/\/hi3103.net\/notes\/dev\/1621\">pyenv \u3067 python \u3092\u30d0\u30fc\u30b8\u30e7\u30f3\u7ba1\u7406\u3059\u308b &#8211; hi3103\u306e\u5099\u5fd8\u9332<\/a><\/li>\n<\/ul>\n<\/li>\n<li>Selenium 4\n<ul>\n<li><a href=\"https:\/\/www.selenium.dev\/\">https:\/\/www.selenium.dev\/<\/a><\/li>\n<\/ul>\n<\/li>\n<li>WebDriver BiDi\n<ul>\n<li><a href=\"https:\/\/www.w3.org\/TR\/webdriver-bidi\/\">https:\/\/www.w3.org\/TR\/webdriver-bidi\/<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><!--more--><\/p>\n<h3>\u4f5c\u308b\u3082\u306e<\/h3>\n<ul>\n<li>\u6240\u5b9a\u306e\u5f62\u5f0f\u3067\u4f5c\u6210\u3057\u3066\u3044\u308bCSV\u30d5\u30a1\u30a4\u30eb\u3092Zaim\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3059\u308b\u4f5c\u696d\u3092\u3001\u90e8\u5206\u7684\u306b\u81ea\u52d5\u5316\u3059\u308b\u3082\u306e<\/li>\n<li>\u5b9f\u884c\u3059\u308b\u3068\u3001Zaim\u306e\u5165\u51fa\u529b\u30da\u30fc\u30b8\u3092\u958b\u3044\u3066\u7279\u5b9a\u306e\u30d7\u30eb\u30c0\u30a6\u30f3\u306e\u9078\u629e\u80a2\u3092\u4e00\u6c17\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u308c\u308b<\/li>\n<li>\u4ee5\u4e0b\u306e\u52d5\u4f5c\u306f\u624b\u52d5\u3067\u5bfe\u5fdc\u3059\u308b\n<ul>\n<li>\u30ed\u30b0\u30a4\u30f3\u3059\u308b<\/li>\n<li>\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308bCSV\u30d5\u30a1\u30a4\u30eb\u3092\u9078\u629e\u3059\u308b<\/li>\n<li>\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3092\u30c6\u30b9\u30c8<\/li>\n<li>\u672c\u756a\u306e\u5bb6\u8a08\u7c3f\u306b\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9<\/li>\n<li>\u30b9\u30af\u30ea\u30d7\u30c8\u3092\u7d42\u4e86\u3059\u308b<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h3>\u4f5c\u696d\u30ed\u30b0<\/h3>\n<h4>\u4f5c\u696d\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u7528\u610f<\/h4>\n<h5>\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u306e\u4f5c\u6210<\/h5>\n<p>\u4efb\u610f\u306e\u5834\u6240\u3067\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u4f5c\u6210<\/p>\n<pre><code class=\"language-bash\">mkdir zaim-automation<\/code><\/pre>\n<p>\u4f5c\u6210\u3057\u305f\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u306b\u79fb\u52d5<\/p>\n<pre><code class=\"language-bash\">cd zaim-automation<\/code><\/pre>\n<h5>.editorconfig \u306e\u4f5c\u6210<\/h5>\n<p>\u3042\u3068\u3067\u3044\u308d\u3093\u306a\u30d5\u30a1\u30a4\u30eb\u3092\u7528\u610f\u3059\u308b\u306e\u3067\u3042\u3089\u304b\u3058\u3081\u4f5c\u3063\u3066\u304a\u304f<\/p>\n<pre><code class=\"language-bash\">root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n<\/code><\/pre>\n<h4>Python \u74b0\u5883\u3092\u7528\u610f\u3059\u308b<\/h4>\n<h5>Python \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u6307\u5b9a<\/h5>\n<p>Python 3.12\u7cfb\u306e\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3067\u3042\u308b 3.12.12 \u3092\u5229\u7528\u3059\u308b\u3053\u3068\u306b\u3057\u305f<\/p>\n<p>\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u6e08\u307f\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u4e00\u89a7\u78ba\u8a8d<\/p>\n<pre><code class=\"language-bash\">pyenv versions<\/code><\/pre>\n<p>\u8a72\u5f53\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u306a\u3051\u308c\u3070\u3001\u4ee5\u4e0b\u30b3\u30de\u30f3\u30c9\u3067\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb<\/p>\n<pre><code class=\"language-bash\">pyenv install 3.12.12<\/code><\/pre>\n<p>\u4f5c\u696d\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u914d\u4e0b\u306e Python \u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u56fa\u5b9a<\/p>\n<pre><code class=\"language-bash\">pyenv local 3.12.12<\/code><\/pre>\n<p>\u8a72\u5f53\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u8a2d\u5b9a\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d<\/p>\n<pre><code class=\"language-bash\">python --version<\/code><\/pre>\n<pre><code class=\"language-bash\">% python --version\nPython 3.12.12<\/code><\/pre>\n<h5>Python \u74b0\u5883\u3092\u69cb\u7bc9<\/h5>\n<p>.venv \u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u306b\u5c02\u7528\u306e Python \u74b0\u5883\u3092\u4f5c\u6210<\/p>\n<pre><code class=\"language-bash\">python -m venv .venv<\/code><\/pre>\n<p>\u3053\u306e\u6642\u70b9\u3067 <code>which python<\/code> \u3068 <code>which pip<\/code> \u3092\u5b9f\u884c\u3059\u308b\u3068\u3001\u30e6\u30fc\u30b6\u30fc\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u4ee5\u4e0b\u306e\u30b3\u30de\u30f3\u30c9\u304c\u53c2\u7167\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u304c\u308f\u304b\u308b<\/p>\n<pre><code class=\"language-bash\">% which python\n\/Users\/hi3103\/.pyenv\/shims\/python\n% which pip\n\/Users\/hi3103\/.pyenv\/shims\/pip<\/code><\/pre>\n<p>.venv \u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u306e\u74b0\u5883\u304c\u53c2\u7167\u3055\u308c\u308b\u3088\u3046\u306b\u3001\u4ee5\u4e0b\u3092\u5b9f\u884c<\/p>\n<pre><code class=\"language-bash\">source .venv\/bin\/activate<\/code><\/pre>\n<p>which \u30b3\u30de\u30f3\u30c9\u3092\u518d\u5ea6\u5b9f\u884c\u3057\u3001\u30d1\u30b9\u304c\u5909\u308f\u3063\u305f\u3053\u3068\u3092\u78ba\u8a8d<\/p>\n<pre><code class=\"language-bash\">% which python             \n\/Users\/hi3103\/Sites\/zaim-automation\/.venv\/bin\/python\n% which pip                \n\/Users\/hi3103\/Sites\/zaim-automation\/.venv\/bin\/pip<\/code><\/pre>\n<h5>.venv \u5185\u306e pip \u3092\u6700\u65b0\u306b\u66f4\u65b0<\/h5>\n<p>\u4ee5\u4e0b\u306e\u30b3\u30de\u30f3\u30c9\u3092\u5b9f\u884c<\/p>\n<pre><code class=\"language-bash\">pip install -U pip<\/code><\/pre>\n<h4>Selenium \u3092\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb<\/h4>\n<p>\u4ee5\u4e0b\u306e\u30b3\u30de\u30f3\u30c9\u3092\u5b9f\u884c<\/p>\n<pre><code class=\"language-bash\">pip install selenium<\/code><\/pre>\n<p>\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d<\/p>\n<pre><code class=\"language-bash\">pip show selenium<\/code><\/pre>\n<p>\u5b9f\u884c\u7d50\u679c\u304b\u3089\u3001Selenium\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u3068\u3001\u6307\u5b9a\u306ePython\u30d0\u30fc\u30b8\u30e7\u30f3\u914d\u4e0b\u306b\u683c\u7d0d\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d<\/p>\n<pre><code class=\"language-bash\">% pip show selenium \nName: selenium\nVersion: 4.39.0\nSummary: Official Python bindings for Selenium WebDriver\nHome-page: https:\/\/www.selenium.dev\nAuthor: \nAuthor-email: \nLicense: Apache-2.0\nLocation: \/Users\/hi3103\/.pyenv\/versions\/3.12.12\/lib\/python3.12\/site-packages\nRequires: certifi, trio, trio-websocket, typing_extensions, urllib3, websocket-client\nRequired-by: <\/code><\/pre>\n<h4>.env\u30d5\u30a1\u30a4\u30eb\u3092\u7528\u610f<\/h4>\n<h5>python-dotenv \u3092\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb<\/h5>\n<p>Python \u3067 .env \u30d5\u30a1\u30a4\u30eb\u3092\u6271\u3048\u308b\u3088\u3046\u306b\u3059\u308b<\/p>\n<pre><code class=\"language-bash\">pip install python-dotenv<\/code><\/pre>\n<h5>.env \u3092\u4f5c\u6210<\/h5>\n<p>\u30d6\u30e9\u30a6\u30b6\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u30d1\u30b9\u3084\u3001\u9077\u79fb\u3059\u308b\u753b\u9762\u306eURL\u306a\u3069\u3092 .env \u30d5\u30a1\u30a4\u30eb\u3067\u7ba1\u7406\u3059\u308b<\/p>\n<pre><code class=\"language-bash\"># Firefox automation profile directory\nPROFILE_DIR=\/Users\/yourname\/Library\/Application Support\/Firefox\/Profiles\/xxxxxxxx.xxxxxxxx\n\n# Zaim URLs\nLOGIN_URL=https:\/\/zaim.net\/user_session\/new\nIMPORT_EXPORT_URL=https:\/\/zaim.net\/home\/money_upload\n\n# Login wait (seconds)\nLOGIN_TIMEOUT_SEC=180<\/code><\/pre>\n<h5>\u30c1\u30a7\u30c3\u30af\u7528\u30b3\u30fc\u30c9\u3092\u7528\u610f\u3057\u3066\u78ba\u8a8d<\/h5>\n<p><code>env_check.py<\/code> \u3068\u3044\u3046\u30d5\u30a1\u30a4\u30eb\u540d\u3067\u4ee5\u4e0b\u3092\u4fdd\u5b58<\/p>\n<pre><code class=\"language-bash\"># env_check.py\nimport os\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nKEYS = [\n  \"PROFILE_DIR\",\n  \"LOGIN_URL\",\n  \"IMPORT_EXPORT_URL\",\n  \"LOGIN_TIMEOUT_SEC\",\n]\n\nfor key in KEYS:\n  value = os.getenv(key)\n  if value is None:\n    print(f\"[NG] {key} is not loaded\")\n  else:\n    print(f\"[OK] {key} = {value}\")\n<\/code><\/pre>\n<p>\u4ee5\u4e0b\u3092\u5b9f\u884c<\/p>\n<pre><code class=\"language-bash\">python env_check.py<\/code><\/pre>\n<p>.env \u306b\u8a18\u8f09\u3057\u305f\u5024\u304c\u51fa\u529b\u3055\u308c\u308b\u3053\u3068\u3092\u78ba\u8a8d<\/p>\n<pre><code class=\"language-bash\">% python env_check.py\n[OK] PROFILE_DIR = \/Users\/yourname\/Library\/Application Support\/Firefox\/Profiles\/xxxxxxxx.xxxxxxxx\n[OK] LOGIN_URL = https:\/\/zaim.net\/user_session\/new\n[OK] IMPORT_EXPORT_URL = https:\/\/zaim.net\/home\/money_upload\n[OK] LOGIN_TIMEOUT_SEC = 180<\/code><\/pre>\n<h4>Git \u7ba1\u7406\u306e\u305f\u3081\u306e\u3082\u308d\u3082\u308d<\/h4>\n<p>\u6700\u7d42\u7684\u306b Github \u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u3042\u3052\u3066\u304a\u304d\u305f\u3044\u306e\u3067\u3001\u5fc5\u8981\u306a\u30d5\u30a1\u30a4\u30eb\u3092\u8af8\u3005\u7528\u610f\u3059\u308b<\/p>\n<h5>Git \u521d\u671f\u5316<\/h5>\n<pre><code class=\"language-bash\">git init<\/code><\/pre>\n<h5>README.md \u3092\u4f5c\u6210<\/h5>\n<pre><code class=\"language-bash\">touch README.md<\/code><\/pre>\n<h5>requirements.txt \u3092\u4f5c\u6210<\/h5>\n<p>\u73fe\u72b6\u306e\u69cb\u6210\u3092\u66f8\u304d\u51fa\u3057\u3066\u304a\u304f<\/p>\n<pre><code class=\"language-bash\">pip freeze &gt; requirements.txt<\/code><\/pre>\n<h5>.env.sample \u3092\u4f5c\u6210<\/h5>\n<p>.env \u30d5\u30a1\u30a4\u30eb\u306f Git \u7ba1\u7406\u5916\u3068\u3057\u305f\u3044\u306e\u3067\u3001 <code>.env.sample<\/code> \u3068\u3044\u3046\u540d\u79f0\u3067\u8907\u88fd\u3057\u3001\u30c0\u30df\u30fc\u30c7\u30fc\u30bf\u3092\u6d41\u3057\u8fbc\u3093\u3067\u304a\u304f<\/p>\n<h5>.gitignore \u3092\u4f5c\u6210<\/h5>\n<pre><code class=\"language-bash\"># Python\n__pycache__\/\n*.py[cod]\n\n# Virtualenv\n.venv\/\n\n# System\n.DS_Store\n\n# Logs\n*.log\n\n# Environment\n.env\n<\/code><\/pre>\n<h4>\u30c6\u30b9\u30c8\u30b9\u30af\u30ea\u30d7\u30c8\u3092\u4f5c\u6210<\/h4>\n<p>Python \u3082 Selenium \u3082\u521d\u3081\u3066\u89e6\u308b\u306e\u3067\u3001\u7d30\u304b\u3044\u5358\u4f4d\u3067\u8272\u3005\u8a66\u3057\u3066\u307f\u308b<\/p>\n<h5>Selenium \u3067 Firefox \u3092\u8d77\u52d5\u3057\u3066\u4efb\u610f\u306eURL\u3092\u8868\u793a\u3059\u308b<\/h5>\n<p><code>smoke.py<\/code>  \u3068\u3057\u3066\u4fdd\u5b58<\/p>\n<pre><code class=\"language-python\">from selenium import webdriver\n\ndriver = webdriver.Firefox()\ndriver.get(\"https:\/\/hi3103.net\")\ninput(\"\u8868\u793a\u3067\u304d\u305f\u3089 Enter \u3067\u7d42\u4e86: \")\ndriver.quit()\n<\/code><\/pre>\n<p>\u5b9f\u884c<\/p>\n<pre><code class=\"language-python\">python smoke.py<\/code><\/pre>\n<h5>Selenium \u3067 Firefox \u3092\u4efb\u610f\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3067\u8d77\u52d5\u3059\u308b<\/h5>\n<p><code>profile_smoke.py<\/code> \u3068\u3057\u3066\u4fdd\u5b58<\/p>\n<pre><code class=\"language-python\">from selenium import webdriver\n\nPROFILE_DIR = \"\/Users\/yourname\/Library\/Application Support\/Firefox\/Profiles\/xxxxxxxx.xxxxxxxx\"\n\noptions = webdriver.FirefoxOptions()\noptions.profile = PROFILE_DIR\n\ndriver = webdriver.Firefox(options=options)\n\ndriver.get(\"about:profiles\")\n\ninput(\n  \"\\nabout:profiles \u3092\u958b\u3044\u3066\u3044\u307e\u3059\u3002\\n\"\n  \"\u300c\u3053\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u73fe\u5728\u4f7f\u7528\u4e2d\u3067\u3059\u300d\u304c\\n\"\n  \"\u6307\u5b9a\u3057\u305f PROFILE_DIR \u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u305f\u3089 Enter \u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044...\"\n)\n\ndriver.quit()\n<\/code><\/pre>\n<p>\u5b9f\u884c<\/p>\n<pre><code class=\"language-python\">python profile_smoke.py<\/code><\/pre>\n<h5>Selenium \u3067 Firefox \u3092 .env \u3067\u6307\u5b9a\u3057\u305f\u4efb\u610f\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3067\u8d77\u52d5\u3059\u308b<\/h5>\n<p><code>profile_env_smoke.py<\/code> \u3068\u3057\u3066\u4fdd\u5b58<\/p>\n<pre><code class=\"language-python\">import os\nfrom dotenv import load_dotenv\nfrom selenium import webdriver\n\nload_dotenv()\n\nPROFILE_DIR = os.getenv(\"PROFILE_DIR\")\nif not PROFILE_DIR:\n  raise RuntimeError(\"PROFILE_DIR is not set in .env\")\n\noptions = webdriver.FirefoxOptions()\noptions.profile = PROFILE_DIR\n\ndriver = webdriver.Firefox(options=options)\n\ndriver.get(\"about:profiles\")\n\ninput(\n  \"\\nabout:profiles \u3092\u958b\u3044\u3066\u3044\u307e\u3059\u3002\\n\"\n  \"\u300c\u3053\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u73fe\u5728\u4f7f\u7528\u4e2d\u3067\u3059\u300d\u304c\\n\"\n  \"\u6307\u5b9a\u3057\u305f PROFILE_DIR \u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u305f\u3089 Enter \u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044...\"\n)\n\ndriver.quit()\n<\/code><\/pre>\n<p>\u5b9f\u884c<\/p>\n<pre><code class=\"language-python\">python profile_env_smoke.py<\/code><\/pre>\n<h3>\u6210\u679c\u7269<\/h3>\n<h4>\u4f5c\u6210\u3057\u305f\u30b9\u30af\u30ea\u30d7\u30c8\uff08zaim_flow.py\uff09<\/h4>\n<p>\u3042\u3068\u306f\u3072\u305f\u3059\u3089ChatGPT\u3068\u30e9\u30ea\u30fc\u3057\u3066\u5b9f\u884c\u3057\u3066\u3092\u7e70\u308a\u8fd4\u3057\u305f<br \/>\n\u81ea\u5206\u304c\u6708\u30a4\u30c1\u30ed\u30fc\u30ab\u30eb\u3067\u52d5\u304b\u3059\u3060\u3051\u306a\u306e\u3067\u3001\u4f5c\u308a\u7518\u3044\u3068\u3053\u308d\u3042\u3063\u3066\u3082\u8a31\u5bb9<\/p>\n<pre><code class=\"language-python\">from __future__ import annotations\n\nimport os\nimport sys\nimport time\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom dotenv import load_dotenv\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support.ui import WebDriverWait, Select\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom selenium.common.exceptions import TimeoutException\n\n@dataclass(frozen=True)\nclass Config:\n  profile_dir: Path\n  login_url: str\n  import_export_url: str\n  login_email: str\n  login_password: str\n  login_timeout_sec: int = 180\n\nSEL_LOGIN_EMAIL = (By.NAME, \"email\")\nSEL_LOGIN_PASSWORD = (By.NAME, \"password\")\nSEL_LOGOUT = (By.CSS_SELECTOR, 'a[href=\"\/user_session\"][data-method=\"delete\"]')\nSEL_IMPORT_FILE = (By.CSS_SELECTOR, '#GeneralUploadFile[name=\"general_upload_file\"][type=\"file\"]')\nSEL_UPLOAD_COLLAPSE_TOGGLE = (By.CSS_SELECTOR, 'h3.title[data-bs-toggle=\"collapse\"][href=\"#collapseGeneralUpload\"]')\nSEL_LINES = [\n  (By.NAME, \"date_line\"),\n  (By.NAME, \"category_line\"),\n  (By.NAME, \"genre_line\"),\n  (By.NAME, \"comment_line\"),\n  (By.NAME, \"place_line\"),\n  (By.NAME, \"from_account_line\"),\n  (By.NAME, \"to_account_line\"),\n  (By.NAME, \"name_line\"),\n  (By.NAME, \"payment_line\"),\n  (By.NAME, \"income_line\"),\n  (By.NAME, \"transfer_line\"),\n  (By.NAME, \"transfer_flag\"),\n  (By.NAME, \"calc_line\"),\n]\nSEL_FLASH_MESSAGE = (By.ID, \"flashMessage\")\n\ndef load_config() -&gt; Config:\n  load_dotenv()\n\n  def must(name: str) -&gt; str:\n    v = os.getenv(name)\n    if not v:\n      raise RuntimeError(f\"Missing required env var: {name}\")\n    return v\n\n  profile_dir_raw = must(\"PROFILE_DIR\")\n  login_url = must(\"LOGIN_URL\")\n  import_export_url = must(\"IMPORT_EXPORT_URL\")\n  login_email = must(\"LOGIN_EMAIL\")\n  login_password = must(\"LOGIN_PASSWORD\")\n  login_timeout_sec = int(os.getenv(\"LOGIN_TIMEOUT_SEC\", \"180\"))\n\n  profile_dir = Path(profile_dir_raw).expanduser()\n\n  if not profile_dir.exists():\n    raise RuntimeError(f\"PROFILE_DIR does not exist: {profile_dir}\")\n  if not profile_dir.is_dir():\n    raise RuntimeError(f\"PROFILE_DIR is not a directory: {profile_dir}\")\n\n  return Config(\n    profile_dir=profile_dir,\n    login_url=login_url,\n    import_export_url=import_export_url,\n    login_email=login_email,\n    login_password=login_password,\n    login_timeout_sec=login_timeout_sec,\n  )\n\ndef is_logged_in(driver: webdriver.Firefox) -&gt; bool:\n  return bool(driver.find_elements(*SEL_LOGOUT))\n\ndef fill_login_inputs(driver: webdriver.Firefox, wait: WebDriverWait, email: str, password: str) -&gt; None:\n  email_el = wait.until(EC.presence_of_element_located(SEL_LOGIN_EMAIL))\n  password_el = wait.until(EC.presence_of_element_located(SEL_LOGIN_PASSWORD))\n\n  email_el.clear()\n  email_el.send_keys(email)\n\n  password_el.clear()\n  password_el.send_keys(password)\n\ndef wait_for_manual_login(driver: webdriver.Firefox, timeout_sec: int) -&gt; None:\n  end = time.time() + timeout_sec\n  while time.time() &lt; end:\n    if is_logged_in(driver):\n      return\n    time.sleep(0.5)\n  raise TimeoutException(f\"manual login timeout: {timeout_sec}s\")\n\ndef select_by_value_selenium(\n  driver: webdriver.Firefox,\n  wait: WebDriverWait,\n  locator: tuple,\n  value: str,\n) -&gt; None:\n  el = wait.until(EC.visibility_of_element_located(locator))\n  wait.until(EC.element_to_be_clickable(locator))\n  Select(el).select_by_value(value)\n  wait.until(lambda d: d.find_element(*locator).get_attribute(\"value\") == value)\n\ndef main() -&gt; int:\n  try:\n    cfg = load_config()\n  except Exception as e:\n    print(f\"[FATAL] config error: {e}\", file=sys.stderr)\n    return 10\n\n  options = webdriver.FirefoxOptions()\n  options.profile = str(cfg.profile_dir)\n\n  driver = webdriver.Firefox(options=options)\n  wait = WebDriverWait(driver, 30)\n\n  try:\n    # 1) \u30ed\u30b0\u30a4\u30f3URL\u3092\u958b\u304f\n    driver.get(cfg.login_url)\n\n    # 1.5) \u672a\u30ed\u30b0\u30a4\u30f3\u306a\u3089\u30e1\u30fc\u30eb\/\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3060\u3051\u3057\u3066\u304a\u304f\uff08submit\u3057\u306a\u3044\uff09\n    if not is_logged_in(driver):\n      fill_login_inputs(driver, wait, cfg.login_email, cfg.login_password)\n      print(\"[INFO] \u30e1\u30fc\u30eb\/\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u6e08\u307f\uff08\u30ed\u30b0\u30a4\u30f3\u30dc\u30bf\u30f3\u306f\u624b\u52d5\uff09\", file=sys.stderr)\n\n    # 2-3) \u624b\u52d5\u30ed\u30b0\u30a4\u30f3\u5f85\u6a5f &amp; \u30ed\u30b0\u30a4\u30f3\u5b8c\u4e86\u5224\u5b9a\n    if not is_logged_in(driver):\n      print(\n        f\"[INFO] \u624b\u52d5\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u304f\u3060\u3055\u3044\uff08{cfg.login_timeout_sec}\u79d2\u3067\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\uff09\",\n        file=sys.stderr,\n      )\n      wait_for_manual_login(driver, cfg.login_timeout_sec)\n\n    print(\"[INFO] \u30ed\u30b0\u30a4\u30f3\u5b8c\u4e86\u3092\u691c\u77e5\u30025\u79d2\u5f85\u6a5f\", file=sys.stderr)\n    time.sleep(5\n\n    # 4) \u30d5\u30a1\u30a4\u30eb\u5165\u51fa\u529b\u30da\u30fc\u30b8\u3078\u9077\u79fb\n    print(\"[INFO] \u30d5\u30a1\u30a4\u30eb\u5165\u51fa\u529b\u30da\u30fc\u30b8\u9077\u79fb\", file=sys.stderr)\n    driver.get(cfg.import_export_url)\n\n    wait.until(EC.presence_of_element_located(SEL_IMPORT_FILE))\n    print(\"[INFO] \u30d5\u30a1\u30a4\u30eb\u5165\u51fa\u529b\u30da\u30fc\u30b8\u5230\u9054\u3092\u78ba\u8a8d\u30025\u79d2\u5f85\u6a5f\", file=sys.stderr)\n    time.sleep(5)\n\n    # 5) \u30a2\u30b3\u30fc\u30c7\u30a3\u30aa\u30f3\u5c55\u958b\uff08select\u7fa4\u3092\u8868\u793a\u3055\u305b\u308b\uff09\n    print(\"[INFO] \u30a2\u30b3\u30fc\u30c7\u30a3\u30aa\u30f3\u3092\u5c55\u958b\", file=sys.stderr)\n    toggle = wait.until(EC.element_to_be_clickable(SEL_UPLOAD_COLLAPSE_TOGGLE))\n    driver.execute_script(\"arguments[0].scrollIntoView({block: 'center'});\", toggle)\n    toggle.click()\n\n    # 6) 13\u500b\u306eselect\u3092 value=1..13 \u3067\u9806\u306b\u9078\u629e\n    for i, locator in enumerate(SEL_LINES, start=1):\n      value = str(i)\n      print(f\"[INFO] select {locator} -&gt; value={value}\", file=sys.stderr)\n      select_by_value_selenium(driver, wait, locator, value)\n\n    print(\"[INFO] \u5168select\u306e\u9078\u629e\u5b8c\u4e86\u3002\u624b\u52d5\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u5f85\u6a5f\u4e2d...\", file=sys.stderr)\n    input(\"\u4f5c\u696d\u304c\u7d42\u308f\u3063\u305f\u3089 return \u30ad\u30fc\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\")\n    driver.quit()\n\n    return 0\n\n  except TimeoutException as e:\n    print(f\"[ERROR] timeout: {e}\", file=sys.stderr)\n    return 3\n\nif __name__ == \"__main__\":\n  raise SystemExit(main())\n<\/code><\/pre>\n<h4>\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u69cb\u6210<\/h4>\n<pre><code class=\"language-python\">\u251c\u2500\u2500 .editorconfig\n\u251c\u2500\u2500 .env\n\u251c\u2500\u2500 .env.sample\n\u251c\u2500\u2500 .git\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 requirements.txt\n\u2514\u2500\u2500 scripts\n    \u251c\u2500\u2500 test\n    \u2502     \u251c\u2500\u2500 env_check.py\n    \u2502     \u251c\u2500\u2500 profile_env_smoke.py\n    \u2502     \u251c\u2500\u2500 profile_smoke.py\n    \u2502     \u2514\u2500\u2500 smoke.py\n    \u2514\u2500\u2500 zaim_flow.py<\/code><\/pre>\n<h4>\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u624b\u9806<\/h4>\n<p>git clone \u76f4\u5f8c\u306b\u521d\u56de\u3060\u3051\u884c\u3046\u4f5c\u696d\u3068\u3057\u3066\u4ee5\u4e0b\u3092\u60f3\u5b9a<\/p>\n<h5>Python\u74b0\u5883\u306e\u69cb\u7bc9<\/h5>\n<pre><code class=\"language-bash\">python3 -m venv .venv<\/code><\/pre>\n<pre><code class=\"language-bash\">source .venv\/bin\/activate<\/code><\/pre>\n<pre><code class=\"language-bash\">pip install -r requirements.txt<\/code><\/pre>\n<h5>\u74b0\u5883\u5909\u6570\u306e\u8a2d\u5b9a<\/h5>\n<pre><code class=\"language-bash\">cp .env.sample .env<\/code><\/pre>\n<pre><code class=\"language-bash\">vim .en<\/code><\/pre>\n<p>.env.sample \u304b\u3089 .env \u3092\u4f5c\u6210\u3057\u3001\u4e2d\u8eab\u3092\u66f8\u304d\u63db\u3048\u308b<\/p>\n<ul>\n<li>PROFILE_DIR: Firefox\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u6307\u5b9a<\/li>\n<li>LOGIN_EMAIL: Zaim\u306e\u30ed\u30b0\u30a4\u30f3\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9<\/li>\n<li>LOGIN_PASSWORD: Zaim\u306e\u30ed\u30b0\u30a4\u30f3\u30d1\u30b9\u30ef\u30fc\u30c9<\/li>\n<\/ul>\n<h4>\u4f7f\u3044\u65b9<\/h4>\n<pre><code class=\"language-python\">python scripts\/zaim_flow.py<\/code><\/pre>\n<ol>\n<li>\ud83e\udd16 Firefox\u304c\u6307\u5b9a\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3067\u8d77\u52d5\u3057\u3001Zaim\u30ed\u30b0\u30a4\u30f3\u753b\u9762\u304c\u8868\u793a\u3055\u308c\u308b<\/li>\n<li>\ud83e\udd16 \u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u30fb\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u81ea\u52d5\u5165\u529b\u3055\u308c\u308b<\/li>\n<li>\ud83d\udc68 \u624b\u52d5\u3067\u30ed\u30b0\u30a4\u30f3\u5b9f\u884c\u3059\u308b<\/li>\n<li>\ud83e\udd16 5\u79d2\u5f85\u6a5f\u5f8c\u3001\u5165\u51fa\u529b\u30da\u30fc\u30b8\u306b\u81ea\u52d5\u9077\u79fb<\/li>\n<li>\ud83e\udd16 5\u79d2\u5f85\u6a5f\u5f8c\u3001\u300c\u4e00\u822c\u7684\u306a CSV \u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b\u300d\u306e\u30a2\u30b3\u30fc\u30c7\u30a3\u30aa\u30f3\u5c55\u958b\u3068\u30011\u301c13\u5217\u76ee\u306e\u30d7\u30eb\u30c0\u30a6\u30f3\u9078\u629e\u304c\u81ea\u52d5\u5b9f\u884c\u3055\u308c\u308b<\/li>\n<li>\ud83d\udc68 \u4efb\u610f\u306eCSV\u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3057\u3001[\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3092\u30c6\u30b9\u30c8]\u5f8c\u3001[\u672c\u756a\u306e\u5bb6\u8a08\u7c3f\u306b\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9]\u3059\u308b<\/li>\n<li>\ud83d\udc68 return \u30ad\u30fc\u3092\u62bc\u4e0b\u3067\u7d42\u4e86<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>\u6982\u8981 \u30e8\u30c9\u30d0\u30b7\u306e\u30af\u30ec\u30b8\u30c3\u30c8\u30ab\u30fc\u30c9\u3092\u611b\u7528\u3057\u3066\u3044\u308b\u304c\u3001Zaim\u306e\u81ea\u52d5\u9023\u643a\u304c\u306a\u3044\u305f\u3081\u6bce\u6708CSV\u30a4\u30f3\u30dd\u30fc\u30c8\u3067\u8a18\u9332\u3057\u3066\u3044\u308b Windows \u3067\u306f Power Automate Desktop \u3067\u3061\u3087\u3063\u3068\u697d\u3092\u3057\u3066\u3044\u305f\u304c\u3001macOS  [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[57],"tags":[61,110,40,111,15],"class_list":["post-1634","post","type-post","status-publish","format-standard","hentry","category-mac","tag-firefox","tag-mac-os-tahoe","tag-python","tag-selenium","tag-terminal"],"_links":{"self":[{"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/posts\/1634","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/comments?post=1634"}],"version-history":[{"count":0,"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/posts\/1634\/revisions"}],"wp:attachment":[{"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/media?parent=1634"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/categories?post=1634"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hi3103.net\/notes\/wp-json\/wp\/v2\/tags?post=1634"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}