doodle-on-web

自分で調べたことや、仕事の中で質問されたことなどをまとめています。

Excelのセルに =CLAUDE_ASK と書くだけ|xlwingsでAIを表計算に組み込む

スポンサーリンク

Excelの列に並んだ数百件のテキストを、1件ずつコピーしてAIに貼り付け、戻ってきた答えをまた貼り付ける——この往復作業、そろそろ終わりにしませんか。

「Pythonスクリプトで一括処理すればいい」という声はもっともです。それでもExcelでやる理由は2つ。データが最初からExcelにあること、そしてPythonを書かない同僚がそのまま使えることです。セルに数式を1つ書けばAIが動く——この形にしておくと、仕組みが自分の手を離れて回り始めます。

Excelのセルに =CLAUDE_ASK と書くだけ|xlwingsでAIを表計算に組み込むのサンプル ▲ Excel+Claude サンプル

準備:2つのインストールと1つの設定

pip install xlwings anthropic
xlwings addin install
setx ANTHROPIC_API_KEY "sk-ant-..."

APIキーはブックやコードに直接書かず、環境変数に置きます。ここで必ず詰まるのが setx の挙動で、実行中のプロセスには反映されません。設定後はターミナルとExcelを一度閉じて開き直してください。

さらにExcel側で「ファイル → オプション → トラストセンター → トラストセンターの設定 → マクロの設定」から 「VBAプロジェクトオブジェクトモデルへのアクセスを信頼する」 にチェックを入れます。

まず選ぶ:セル関数型かボタン実行型か

セル関数型(UDF) ボタン実行型
書き方 =CLAUDE_ASK(...) ボタンを押す
実行タイミング 再計算のたび 自分で握れる
対応環境 Windows版Excel限定 Windows / Mac
向く場面 対話的な試行錯誤 大量処理・共有ブック

Macを使う人がいる、あるいは共有ブックで誤爆を防ぎたいなら、迷わずボタン実行型です。以下はセル関数型を軸に説明しますが、中身の関数はそのまま流用できます。

最小構成:セルからClaudeを呼ぶ

xlwings quickstart claude_excel でひな形を作り、生成された .py に書きます。ポイントは、API呼び出しを普通の関数 _ask() として先に定義し、UDFはそれを呼ぶだけにすることです。後でキャッシュもバッチも、この土台に載せられます。

import xlwings as xw
from anthropic import Anthropic

client = Anthropic()  # 環境変数からキーを読む

def _call(model, prompt, max_tokens):
    resp = client.messages.create(
        model=model, max_tokens=max_tokens,
        messages=[{"role": "user", "content": prompt}])
    return resp.content[0].text.strip()

def _ask(instruction, text):
    return _call("claude-sonnet-5",
        f"{instruction}\n\n---\n{text}\n---\n前置きは不要。結果だけを返してください。", 300)

@xw.func(async_mode="threading")
def CLAUDE_ASK(instruction, text):
    try:
        return _ask(instruction, text)
    except Exception as e:
        return f"#ERR: {e}"

同名の .xlsm を開き、xlwingsリボンの Import Functions を押せば完了です。

=CLAUDE_ASK("この問い合わせを「請求」「不具合」「その他」で分類して", A2)

async_mode="threading" は忘れずに。これがないとAPI応答を待つ間Excelのウィンドウごと固まります。付けるだけで体感が変わる一行です。

コストと速度の壁を越える

再計算が課金される、という落とし穴

Excelは再計算のたびに関数を実行します。つまり F9を押すたびに全行ぶんの課金が発生する。規模感を掴んでおきましょう。200行 × 平均500トークン ≒ 入力10万トークン。うっかりF9を10回叩けば100万トークン、出力も同程度です。現在の単価を掛ければ、笑えない額になる場合があるとすぐ分かります(単価は公式の料金ページで確認を)。

対策はキャッシュ。同じ入力なら二度と課金させません。処理が終わった列は値貼り付けで確定させておくとさらに安全です。

1行1リクエストをやめる

200行を1行ずつ処理すれば200往復。ネットワーク待ちだけで数分です。まとめて投げましょう。ただしバッチには2つの必須の作法があります。

max_tokens を行数から見積もる。 30字要約×200行は日本語で軽く8,000トークンを超え、出力が途中で切れて json.loads が失敗します。目安は「行数 × 想定文字数 × 2」。JSON構造とマルチバイトの余裕を見て2倍です。そもそも1回に200行を詰め込まず、20〜30行のチャンクに割るのが確実です。

② 返ってきた要素数を必ず検証する。 これが最大の落とし穴で、1行抜けただけで以降のセルがすべて1行ずつズレたまま「それらしく」埋まります。目視では気づけません。件数が合わなければエラーにして止めるべきです。

使い分けの基準

分類・タグ付け・定型要約のようなブレの少ない処理はバッチ+Haiku 4.5で十分。金額や固有名詞が絡み、行ズレが許されない処理は1行1リクエストasync_mode="threading" で並列に流れます)。この線引きを最初に決めておくと迷いません。

統合版:キャッシュ付きバッチ

3つの改善を1つにまとめた実用形です。キャッシュ済みの行はAPIに送らず、未処理分だけをチャンクに分けて投げます。

import hashlib, json, pathlib, re

CACHE = pathlib.Path(__file__).with_name("claude_cache.json")
_cache = json.loads(CACHE.read_text("utf-8")) if CACHE.exists() else {}
CHUNK = 25

def _key(instruction, text):
    return hashlib.sha256(f"{instruction}||{text}".encode()).hexdigest()

def _batch(instruction, cells, chars=40):
    todo = [str(c) for c in cells if _key(instruction, str(c)) not in _cache]
    for i in range(0, len(todo), CHUNK):
        part = todo[i:i + CHUNK]
        numbered = "\n".join(f"{n+1}. {v}" for n, v in enumerate(part))
        raw = _call("claude-haiku-4-5-20251001",
            f'{instruction}\n\n各行を処理し {{"results": ["1行目", ...]}} '
            f'形式のJSONのみを返してください。\n\n{numbered}',
            max_tokens=len(part) * chars * 2)          # 行数 × 想定文字数 × 2
        results = json.loads(re.search(r"\{.*\}", raw, re.S).group())["results"]
        if len(results) != len(part):                  # 行ズレを絶対に通さない
            raise ValueError(f"行数不一致 入力{len(part)}/応答{len(results)}")
        _cache.update({_key(instruction, v): r for v, r in zip(part, results)})
    CACHE.write_text(json.dumps(_cache, ensure_ascii=False), "utf-8")
    return [_cache[_key(instruction, str(c))] for c in cells]

@xw.func(async_mode="threading")
@xw.arg("cells", ndim=1)
def CLAUDE_BATCH(instruction, cells):
    try:
        return [[r] for r in _batch(instruction, cells)]
    except Exception as e:
        return [[f"#ERR: {e}"] for _ in cells]

=CLAUDE_BATCH("30字で要約", A2:A201) の1式で全行が縦にスピルします。行数不一致で落ちたときは CHUNK を10程度に下げると通ることがほとんどです。

ボタン実行型と、その先の選択肢

def fill_summaries():
    sht = xw.Book.caller().sheets["データ"]
    rows = sht.range("A2").expand("down").value
    if not isinstance(rows, list):   # 1行だけのときスカラーが返る
        rows = [rows]
    sht.range("B2").value = [[r] for r in _batch("100字で要約して", rows)]
Sub FillSummaries()
    RunPython "import claude_excel; claude_excel.fill_summaries()"
End Sub

本気で数千行を回すなら、この方式からMessage Batches API(非同期処理向けで、通常より割安な単価が設定されています)に繋ぐのが正攻法です。セルからの同期呼び出しでは使えませんが、ボタン実行型なら素直に載ります。また、同じ長い指示文を何百回も送る構造なので、プロンプトキャッシュも効きます。

つまずきやすいポイント

  • 関数が出てこない:Import Functionsの押し忘れか、.py.xlsm のファイル名不一致がほぼ原因
  • キーが読まれないsetx の後にExcelを再起動したか確認
  • JSONのパース失敗:応答が ```json で囲まれることがあるため re.search(r"\{.*\}", text, re.S) で中身を抜く
  • レート制限(429):数秒待ってリトライを挟む
  • キーの流出.xlsm を共有するなら、キーは各自の環境変数に置く前提で設計する

まとめ

xlwingsが提供するのは「ExcelとPythonの橋渡し」だけですが、その先にClaude APIを繋いだ瞬間、表計算ソフトが自然言語を処理できる集計ツールに変わります。

ポイントは3つ。キャッシュで再計算を殺すバッチでリクエストを減らす大量処理は軽量モデルに任せる。そこに行数の検証を足せば、業務で使える水準になります。

まずは10行程度のサンプルで CLAUDE_ASK を動かしてみてください。セルに答えが返ってきた瞬間、手作業に戻る気はなくなるはずです。

※記事中のモデルIDは2026年8月時点のものです。


関連記事

あわせてチェック