TL;DR
- 店舗物件ポータルサイトに公式APIはなく、マイソクPDFからの転記業務が人手で行われていた。これをPlaywrightによるブラウザ操作とClaude Visionによる情報抽出で自動化した。
- 非公式UI操作を本番運用に耐えるレベルまで持っていく鍵は、「登録」と「AI埋め」を分離する2段フローと、「書けた」ではなく「残っている」を成功の定義にするKPI設計だった。
- React制御フォーム・仮想スクロール・アコーディオンといった、非公式UI操作特有のハマりどころとその対策を、実装例つきで紹介する。
この記事について
対象読者
- 業務SaaSの自動化やLLM×RPAの実装に興味がある人
- 公式APIがないサービスをどう自動化するか悩んでいる人
- ブラウザ操作の自動化で「動いたはずなのに保存されていない」問題に心当たりがある人
前提環境
- 言語: Python 3
- Webフレームワーク: FastAPI + Uvicorn
- ブラウザ自動化: Playwright 1.61 + Chromium(公式Dockerイメージ)
- LLM: Claude Sonnet 4.5(Vision)、Prompt Cachingあり
- PDF処理: PyMuPDF
- スプレッドシート連携: gspread + Google Sheets API
- ストレージ: Google Drive API
- デプロイ: Railway(実行基盤)、Netlify(起動UI)
- 通知: LINE Notify / Messaging API
扱うこと / 扱わないこと
- 扱うこと: 全体アーキテクチャ、設計判断(2段フロー・KPI設計・物件特定方式)、非公式UI操作でのハマりどころと対策、実測コスト・時間
- 扱わないこと: 個別のクライアント情報、セレクタ一覧やフィールドカタログの全列挙(再現性を高めるための詳細は意図的に省略する)
問題(現象・課題)
店舗物件の仲介業務では、レインズなどから取得したマイソクPDFを、物件ポータルサイトの登録画面に人手で転記している。1ファイルにはだいたい20〜25物件が含まれ、項目は住所・賃料・面積だけでなく、敷金礼金・設備・契約形態・出店可能業種など約50項目に及ぶ。
ボトルネックは大きく3つあった。
| 課題 | 内容 |
|---|---|
| 転記コスト | 物件あたり数十項目。毎日複数ファイルを処理すると半日仕事になる |
| 既存物件との重複 | 同じ物件がすでに登録済みの場合、差分更新が必要になる |
| 公開品質 | 登録カードのバリデーションが厳しく、誤入力すると公開ボタンが機能しなくなる |
原因(なぜ単純な「LLMでPDFを読んでフォームに貼る」では足りないか)
ポータルサイト側は公式APIを提供していない。加えて、UI自体が単純なフォームではなく、以下のような複雑さを持っていた。
- ログイン必須のSPA(シングルページアプリケーション)
- 仮想スクロールで描画されるカード一覧(画面外のカードはDOMに存在しない)
- Reactの状態管理下にあるselect/radio(値を直接書き換えても反映されない)
- 折りたたみ式のアコーディオンUI(開かずに値を書くと保存時に消える)
つまり「LLMにPDFを読ませてフォームに貼り付ける」だけでは対応できず、ブラウザ自動化そのものが実装の本丸になった。
解決策・実装
1. 全体アーキテクチャ
処理の流れは次の通り。
Google Drive(未処理PDF)
│
▼
Netlify(起動ボタン) ──POST /trigger──► Railway
│
Playwright + Chromium │
Claude Vision で抽出 │
ポータルサイトのUI操作 │
Google Sheetsに実行ログ │
▼
Drive「処理済み」へ移動
LINE通知
各層の役割は以下の通り。
| 層 | 役割 |
|---|---|
| Netlify | 起動用の静的UI。APIのURLと秘密鍵はビルド時に埋め込む |
| Railway | FastAPI + Playwrightの実行基盤。ジョブはバックグラウンド1本のみ(同時実行はエラーで拒否) |
| Google Drive | 未処理・処理済み・失敗の3フォルダで管理 |
| Google Sheets | 実行ログ・要確認リストの管理 |
| Claude(Anthropic) | ページ画像とテキストからJSON形式で項目を抽出 |
| LINE | 完了通知・開発者向けアラート |
利用者から見た操作フローはシンプルで、Driveに「未処理」としてPDFを置き、Netlifyの起動ページで実行ボタンを押すだけ。クライアント側にPythonやPlaywrightの知識は不要にした。
2. 設計判断①: 「登録」と「AI埋め」を分離する2段フロー
最初に検討したのは「登録カードにAIで値を入れてから登録する」という一体型のフローだった。だが実際には採用しなかった。
段A 登録(台帳に載せる)
PDFアップロード → OCRによる登録カードはそのまま
→ メモ印を書く → 公開ONを実測できたページだけ登録
→ メモ検索で台帳を検証
段B 編集AI(値を入れる)※段Aの成功分だけ
PDFからVisionで抽出 → 編集画面で1項目ずつ書く
→ 保存 → 読み戻して検証
分離した理由は次の通り。
- 登録カードはバリデーションが厳しく、AIで値を触ると公開ボタンが無効化されやすい
- 編集画面の方が制約が緩く、値入れの主戦場として向いている
- 先にOCRでとにかく台帳に載せてから、あとでAIが上書きする方が、物件を取りこぼしにくい
Bad / Good の比較
Bad(登録とAI反映を一体化する):
PDFアップロード
→ 登録カードにAIで抽出した値を直接入力しながら登録
→ バリデーションエラーで公開ボタンが押せず、物件ごと取りこぼす
Good(登録とAI反映を分離する):
PDFアップロード
→ まずOCRの標準登録で台帳に載せる(バリデーションを通しやすい)
→ 登録が成功したものだけ、編集画面でAIが値を上書きする
→ 保存後に読み戻して、値が本当に残っているか検証する
3. 設計判断②: KPIは「書けた」ではなく「残っている」
自動化の成果測定でありがちな失敗は、「ボタンを押して入力できた」ことを成功と見なしてしまうことだ。今回は、保存後に画面を読み戻し、値が実際に残っていることだけを成功(AI反映)としてカウントする設計にした。部分的に一部フィールドだけ残っている場合も、その分だけカウントする。
| 区分 | 意味 |
|---|---|
| AI反映 | 台帳確認済み、かつAIによる値が1項目以上残存している |
| OCRのみ | 台帳には載ったが、AIによる値の残存が確認できない |
| 保留 | 未登録、または台帳未確認 |
| 失敗 | マイソクの読み取り自体ができなかった |
「書いたつもり」をKPIにしてしまうと、費用・築年月・契約形態といった項目が保存後に初期値へ戻ってしまう問題を見逃す。実際にこれがフェーズ1で最も苦労したバグだった。
4. 設計判断③: 物件特定はメモ印(一意トークン)で行う
住所での物件特定は曖昧になりやすい。同一住所に複数物件が存在するケースや、OCRによる表記ゆれがあるためだ。代わりに、登録カードのメモ欄に一意なトークンを書き込む方式にした。
Agent_yyyymmddHHMM_<正規化したPDF名>_pN
例: Agent_202607231017_zmn_list_20260721192513_1_p1
_p1 が _p19 に誤マッチしないよう、数字の境界を意識した検索にしている。この方式なら、人間が見ても「どのPDFの何ページ由来の物件か」を追跡できる。
編集画面で1項目以上書けた場合は、別レイヤーとして [Agent反映] YYYY-MM-DD という印を先頭に追記する。これは「全項目が入った」という意味ではなく、「AIによる編集を試みた」ことを示す印として扱っている。
5. 技術的にキツかったところ
ここからは非公式UI操作特有の実装の泥臭い部分を紹介する。
React制御フォーム
// これでは動かない
element.value = "新しい値";
Reactが管理するinputやselectは、DOMのvalueを直接書き換えるだけでは内部のstateが更新されない。ネイティブのvalue setterを使い、inputイベントとchangeイベントを発火させ、React内部の_valueTrackerをリセットする必要があった。selectやradioも同様の対応が必要で、保存前の読み戻しと保存後の検証、という二重チェックを入れている。
仮想スクロール
登録画面のカード一覧は仮想スクロールで描画されており、画面外のカードはDOM上に存在しない。存在しないカードにはメモも書けず、公開ボタンも押せない。対策として、
- 3ページ単位でチャンクに分けてスタンプ処理する
- カードが見つからない場合はチャンク内でfail-fastし、無駄な待機をしない
- 失敗したページだけ、全体で90秒(1ページ最大12秒)の猶予で再挑戦する
という設計にした。それでも掴めないページは登録せず、「保留」として扱う。これが保留の主な発生要因の一つになっている。
費用ペア(あり/なし + 単位 + 金額のセット)
敷金・礼金などの費用項目は「あり/なし」「単位(ヶ月/円/%)」「金額」の3点セットで構成されている。ラベルに近いselectを誤って選択すると、隣接する別の項目(礼金の欄に敷金の値が入る、など)を壊してしまう。単位の推定、行の特定、描画待ちの調整を積み重ねて精度を上げた。
アコーディオン
費用・建物・設備・契約に関する項目は折りたたみ式のアコーディオンUIになっている。閉じたまま値を書き込むと、保存後に消えてしまう。再反映処理でも、必ず展開してから値を書き込むようにしている。
面積の単位変換(㎡⇔坪)
ポータルサイト側の面積入力は連動しており、AIが㎡で出力しても画面側が坪表示になっている(あるいはその逆)というケースがあった。比率が約3.305であれば同一の値とみなす検証を入れることで、誤検知(偽陰性)を減らした。
6. 運用設計の進化: 承認ワークフローを捨てた話
当初は、差分をスプレッドシートに出力して人間が承認する「scan → apply」という2段階の運用だった。しかし、人間がすべての項目を承認する運用では、ボトルネックがスプレッドシート側の確認作業に移ってしまう。
現在の本番運用では、承認を挟まず一気通貫で処理する「ingest」方式に切り替え、失敗したものだけを人間が確認する運用にしている。運用が安定して回るようになった後、「失敗だけ人が見る」という設計に振り切った、という経緯だ。
数字で見る結果(2026年7月時点の実測)
コスト
- 1ファイル(平均20〜25ページ)あたり、Claude Visionのコストがおよそ60円
- 自動で価値が出る物件が10〜15件だとすると、1物件あたり約4〜6円
- 毎日10ファイル×30日の運用で、インフラ費用別でおよそ月1.8万円
- ページ単位でVisionが走るため、成功件数にかかわらずファイル単価はほぼ固定になる
処理時間
- 10PDFの一気通貫処理(ingest): 約3時間27分(1ファイル平均約21分)
- 保留分の再反映(公開56件前後): 約43〜45分、物件間隔の中央値は約47秒
モデル比較(16ページ、Sonnet 4.5 vs Haiku 4.5)
- 埋まった項目数の平均はどちらも26項目
- 処理速度はHaikuの方が速い(10.2秒 vs 15.5秒)
- 一致率は約53%で、住所・賃料・内装・駅名の項目でずれやすい
- 精度を優先し、本番ではSonnetを採用
まとめ
- 公式APIのない業務SaaSでも、Playwrightによるブラウザ操作とClaude Visionによる抽出を組み合わせることで、本番運用に耐える自動化が実現できた
- 鍵となったのは「登録」と「AI埋め」の分離、そして「書けた」ではなく「残っている」を基準にしたKPI設計。RPA×LLMの自動化では、保存後の読み戻し検証を組み込まないと「書いたつもり」の失敗を見逃しやすい
- React制御フォーム・仮想スクロール・アコーディオンといったUIの複雑さは、それぞれ個別の対策が必要だった
今後の課題
| 優先度 | 内容 |
|---|---|
| 1 | 編集AIの残存率向上(費用・築年月・契約形態が保存後に消える問題への対処) |
| 2 | 登録画面でのカード検出漏れの削減 |
| 3 | OCRで十分な情報が取れているページはVision抽出をスキップし、コストを削減 |
| 4 | 保留分の再反映時に、検証NGのフィールドだけを再適用する |
| 5 | 物件あたりの処理待ち時間(中央値47秒)の短縮 |
なお画像の自動登録(フェーズ2)は、テキスト項目のKPIが安定してから着手する予定にしている。
このプロジェクトを通じて改めて感じたのは、非公式UI操作を前提にした自動化は、対象サイトのUI実装(セッション管理・仮想スクロール・Reactの状態管理)に強く依存するため、大量一括処理の耐性を自動化側だけで完全に担保するのは難しい、ということだ。だからこそ、一定件数(今回は10ファイル)ごとに区切って継続処理する運用設計にしている。