先日, ある販売管理システムの画面を覗いていた時のことだ。20年もののクライアント/サーバー型で, 公式のAPIは存在しない。担当者は毎朝, 3画面を切り替えながらCSVを手で吐き出し, Excelに貼り付けていた。「これ, LLMで自動化できませんか」と聞かれて, 私はしばらく画面を眺めていた。
この手のシステムからデータを取り出す方法は, 大きく三つある。定期CSVエクスポート, ODBCによる直接DB参照, そして画面自動操作だ。どれも一長一短で, 選び方を誤ると半年後に手戻りする。3回シリーズの1回目として, まずはこの"入口"を扱う。次回はExcel, 3回目は書き戻しの話をする予定だ。

三つの取り出し方 — それぞれの壊れ方
まず結論から言うと, 「機能でどれを選ぶか」ではなく「どう壊れるかで選ぶ」のが実務的だ。以下, 順に見ていく。
1. 定期CSVエクスポート
最も単純で, 最も見落とされている選択肢。多くの基幹·販売管理システムには, 夜間バッチや手動操作でCSVを吐く機能が最初から付いている。これを共有フォルダ(SMBのマウント先を想定)に落とし, 取り込み側で拾う。
コードは驚くほど短い。
import pandas as pd
from pathlib import Path
CSV_DIR = Path("/mnt/legacy_export")
def load_latest_orders():
files = sorted(CSV_DIR.glob("orders_*.csv"))
if not files:
raise FileNotFoundError("エクスポートが存在しない")
latest = files[-1]
# 基幹側が Shift_JIS で吐くケースが今もある
df = pd.read_csv(latest, encoding="cp932")
return df
壊れ方は主に三つ。文字コード(Shift_JIS/CP932が残る), スキーマ変更(列が増える·順序が入れ替わる), エクスポート漏れ(夜間バッチが失敗しても翌朝まで気づかない)。特に最後は厄介で, ファイルが「存在しない」ではなく「昨日と同じファイルが残っている」形で壊れる。私は一度これで一日分のデータを取りこぼしたことがあって, 以来, ファイル名のタイムスタンプと中身の最終更新日の両方をヘルスチェックに入れている。
2. ODBC · 直接DB参照
基幹システムがSQL ServerやOracleで動いていて, かつ運用側がDBへの読み取り権限をくれる場合。データの鮮度は最も良いし, SQLで自由に切れる。ただし現場で見かける実装の多くは, 「読めるようになった時点」で満足していて, 副作用の話が抜け落ちている。
import pyodbc
import pandas as pd
DSN = "YOUR_DSN"
UID = "reader"
PWD = "YOUR_PASSWORD"
def fetch_orders_since(ts):
conn = pyodbc.connect(
f"DSN={DSN};UID={UID};PWD={PWD};"
# 読み取り用途は明示的に READ COMMITTED SNAPSHOT を検討
)
sql = """
SELECT order_id, customer_id, order_date, amount
FROM dbo.orders WITH (NOLOCK)
WHERE updated_at >= ?
"""
return pd.read_sql(sql, conn, params=[ts])
WITH (NOLOCK)(あるいはREAD UNCOMMITTED)を安易に入れているコードを現場でよく見る。読み取り側の負荷は下がるが, 読み取り中に書き込まれた「まだ確定していない行」まで拾ってしまう。基幹の業務トランザクションと衝突させない設計にするなら, スナップショット分離レベルを使うか, 更新頻度の低い夜間ウィンドウでの読み取りにする。厳密には話が長くなるので, ここでは「NOLOCKは万能ではない」とだけ書いておく。
もう一つの落とし穴は, DB側のスキーマ変更に取得側が気づけないこと。ベンダーがマイナーアップデートで列を増やしただけで, 事前予告なく SELECT * のクエリが崩れる。カラム名を明示し, 想定外の列を検出したらSlackに投げる程度の防御は入れておいて損はない。
3. 画面自動操作
APIもDBアクセスもない場合の最後の手。人がキーボードとマウスで操作している画面を, PyAutoGUIやPlaywright, あるいは商用RPAで自動化する。緑画面のメインフレーム(TN3270やVT100)相手ならOCRを組み合わせる。
擬似コードだが, 画面操作の基本形はこうなる。
import pyautogui
import time
def export_daily_report(date_str):
# 座標ではなく画像で位置を特定するのが基本
btn = pyautogui.locateOnScreen("assets/daily_report_btn.png", confidence=0.9)
if btn is None:
raise RuntimeError("ボタンが見つからない — UIが変わった可能性")
pyautogui.click(pyautogui.center(btn))
time.sleep(1.5)
pyautogui.typewrite(date_str, interval=0.05)
pyautogui.press("enter")
time.sleep(3)
# 各ステップ後に「今どの画面にいるか」を検証
if pyautogui.locateOnScreen("assets/export_dialog.png", confidence=0.9) is None:
raise RuntimeError("エクスポートダイアログが開いていない")
見た目通り, これは脆い。日立ソリューションズの記事にもあった通り, RPAはAIと違って画面の変化を自ら学習しない。ボタンが5ピクセルずれただけで, あるいはSAPをバージョンアップしただけでウィンドウ名が変わっただけで, 全スクリプトが黙って失敗する。
現実的な運用は, ①座標ではなく画像マッチングや要素属性(Playwrightのrole·label)で位置を特定, ②各ステップ後に「今どの画面にいるか」の検証を挟む, ③失敗時のスクリーンショット保存を必須にする, ④ヘルスチェックを別プロセスで回す, あたりに落ち着く。
どう壊れるかで選ぶ
三つの方式を並べると, 壊れ方の性質が全く違うことに気づく。
CSV: 静かに古くなる(バッチ失敗を検知しないと昨日のデータを今日として使う)
ODBC: スキーマ変更で突然壊れる(でも壊れたことは即分かる)
画面操作: UI改修で突然壊れる(かつ, どこで壊れたかが分かりにくい)
LLMを載せる側から見ると, これは「LLMが誤答した時, 上流のデータ層が原因かどうかを切り分けられるか」に直結する。以前, 疎結合とhuman-in-the-loopの重要性について書いたことがあるが, ここでも同じ話になる。取得層が壊れる時, 業務ロジック層まで一緒に落ちる作りにしてはいけない。
取得層を業務ロジックから隔離する
具体的には, 取得層(fetch)と正規化層(normalize)と業務層(analyze/generate)を明確に分ける。取得層は「その日その時点の生データを, 素朴なDataFrameかJSONで返す」以上のことをしない。正規化層で列名·文字コード·欠損·型を吸収する。LLMを呼ぶのは業務層で, 正規化された安定した形式に対してだけ。
こうしておくと, 三つの取り出し方のどれを選んでも, あるいは途中で「CSVからODBCに切り替える」判断をしても, 業務ロジックは書き換えなくて済む。日本語ドキュメントの前処理を扱った回でも似た構造をとったが, 「前処理層で吸収する」設計はレガシー相手ではほぼ必ず必要になる。
エージェントに開発をさせる場合も同じで, LLM側から見えるインターフェースが安定していれば, 中の実装が画面操作でもODBCでも構わない。取得層は"入口"であって, LLM本体の仕事ではない。
まとめ
APIがない, という前提から始めると, 三つの選択肢の間で悩む時間はそう長くない。「その組織の, その運用担当の, その基幹システムが, どう壊れる時に誰が気づけるか」を軸に置くと自然と決まる。技術的な優劣ではなく, 検知経路と復旧経路で選ぶ。
次回は, 取り出したデータの多くがExcelという"画面"に閉じ込められている問題を扱う予定だ。結合セル·シート跨ぎ·暗黙ルールで動く「仕様書としてのExcel」を, LLMにどう渡すか。
筆者は 5years+ で 韓国·日本の中小企業向け AI/LLM の業務応用を担当している。