月末になると、ダウンロードフォルダが CSV だらけになります。App Store Connect のファイナンシャルレポート、AdMob の推定収益、それからメディエーションで相乗りしている Unity Ads・Liftoff・InMobi のレポート。通貨も期間の区切り方も列名も、見事なくらい揃っていません。
壁紙アプリで個人開発を始めたのは2014年の春で、いまは壁紙とヒーリング系を中心に4本を運営しています。おかげさまで累計ダウンロードは5,000万を超えましたが、その分だけ月末に突き合わせるレポートの種類も増えました。長らくこの作業はローカルの pandas スクリプトで回してきたものの、どこかのネットワークがレポート形式を変えるたびに列名参照が外れて落ちる、という小さな修理を繰り返す日々が続いていました。
今年に入ってから、この月次の締め作業を Claude API の Code Execution ツールに任せる形へ改めました。CSV の中身をトークンとして読ませるのではなく、サンドボックスへ「ファイルごと」渡して、向こう側で pandas を走らせる方式です。実装してみると、公式ドキュメントの最小例と実運用との間にはいくつか段差がありました。ファイルを添付しただけで実行時間の課金が始まる仕様や、長い処理の途中で返ってくる pause_turn の扱いはその代表です。動かしたコードと一緒に、この段差の越え方を順に書き残しておきます。
ローカル集計スクリプトの保守をやめた理由
誤解のないように書いておくと、毎朝の定型 KPI 集計までコード実行ツールに置き換えたわけではありません。日次の売上確認は以前から Claude Code で App Store Connect と AdMob の売上を毎朝集計するパイプライン — 壁紙アプリ 4 本で 8 週運用した実装ノート に書いたローカルの仕組みで安定して回っています。形式が安定していて、毎日同じ問いに答える処理は、普通のスクリプトの方が速くて安いからです。
壊れ続けていたのは月次の「締め」の方でした。理由ははっきりしていて、昨年メディエーションに Unity Ads・Liftoff・InMobi の3社を加えてから、取り込むレポートの種類が一気に増えたためです。支払いプロファイルや W-8BEN の整備を済ませて入金が始まると、今度は各社の CSV が相手になります。通貨が USD と JPY で混在し、締めの期間が暦日だったり太平洋時間だったり、同じ「収益」でも列名が revenue だったり estimated_earnings だったりします。列名参照のゆらぎを場当たり的に吸収するコードは、書くほどに読めなくなっていきました。
そこで線引きを変えました。形式が安定している定型集計はローカルに残し、形式が揺れる月次の探索的な突き合わせはサンドボックスに任せる 。スクリプトの保守コストはレポート形式の変更頻度に比例します。変更のたびに人間がコードを直すより、列名のゆらぎの吸収そのものを Claude にやらせる方が、私の規模では明らかに安くつきました。
もう一つの理由はコンテキストの節約です。CSV を会話の本文に貼り付けると、その全行がトークンとして課金され、コンテキストウィンドウも圧迫します。Code Execution ツールと Files API の組み合わせなら、ファイルはコンテナへ直接ロードされ、Claude はコードを書いてそれを読みます。トークンに乗るのは書いたコードと標準出力の要約だけです。数MBのレポート一式を扱う月次処理では、この差が効きます。
Code Execution ツールの現在地 — 2025年5月版の解説を読むと混乱します
このツールは更新が重ねられていて、検索で見つかる古い解説と現行仕様の差がかなり大きくなっています。実装前に整理しておくと迷いません。
code_execution_20250522(旧版) : 公開当初の Python 専用版です。code-execution-2025-05-22 というベータヘッダー前提の解説記事が今も多く残っています
code_execution_20250825(現行の標準) : Bash コマンドとファイル操作に対応し、対応モデル全てで使えます。応答ブロックの形式も旧版から変わりました
code_execution_20260120(新版) : REPL の状態維持とサンドボックス内からのツール呼び出し(programmatic tool calling)が加わった版で、対応は Opus 4.5 以降・Sonnet 4.5 以降などの新しい世代に限られます。Haiku 4.5 は 20250825 までの対応です
実行環境は執筆時点で Python 3.11 系の Linux コンテナ(メモリ 5GiB・ディスク 5GiB・1 CPU)で、pandas・numpy・matplotlib・openpyxl など集計に必要なライブラリはあらかじめ入っています。そして重要な制約として、コンテナからインターネットへは一切出られません 。pip install も外部 API への問い合わせもできない、という前提が後々の設計に効いてきます。詳細は公式ドキュメント に一覧があります。
なお、現行ドキュメントの最小例はコード実行についてはベータヘッダーなしで動く形になっています。ベータ指定が必要なのは Files API を併用するときの files-api-2025-04-14 です。SDK が古いと新しい応答型を知らずに戸惑うことがあるので、anthropic パッケージは最新化してから始めることをおすすめします。
最小構成で動かす — 応答ブロックの読み方から
最初の一歩は、ファイルなしの最小構成で応答の形を掴むことです。ここを飛ばして Files API 連携から入ると、応答の解析で必ず迷子になります。
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY は環境変数で渡す
response = client.messages.create(
model = "claude-sonnet-4-6" ,
max_tokens = 4096 ,
messages = [{
"role" : "user" ,
"content" : "1月から5月の売上 [412000, 389000, 455000, 401000, 478000] 円の平均と標準偏差を計算してください" ,
}],
tools = [{ "type" : "code_execution_20250825" , "name" : "code_execution" }],
)
# 応答は複数のコンテンツブロックの並びで返ってくる
for block in response.content:
if block.type == "server_tool_use" :
# Claude がサンドボックスで実行したコマンドやコード
print ( "実行内容:" , block.input)
elif block.type == "bash_code_execution_tool_result" :
result = block.content
print ( "stdout:" , result.stdout)
print ( "return_code:" , result.return_code)
elif block.type == "text" :
print ( "Claude の説明:" , block.text)
実行すると、server_tool_use ブロックに Claude が書いた計算コードが、続く bash_code_execution_tool_result ブロックの stdout に平均 427,000 円と標準偏差の計算結果が入り、最後の text ブロックで日本語の説明が締めくくられます。
ここで押さえておきたいのは、通常のツール呼び出しと違って実行が全てサーバー側で完結する ことです。自分で tool_result を組み立てて返す必要はなく、server_tool_use と *_tool_result のペアが応答の中に並びます。現行版ではこの結果ブロックが bash_code_execution_tool_result(シェル実行)と text_editor_code_execution_tool_result(ファイルの閲覧・作成・編集)の2系統に分かれているので、解析側は両方を想定しておきます。stderr と return_code を確認する癖をつけておくと、後で集計が静かに失敗していた、という事故を防げます。
Files API で売上 CSV をコンテナに渡す
応答の形が掴めたら本題です。月次レポート一式をアップロードし、container_upload ブロックでコンテナに載せます。
import anthropic
from pathlib import Path
client = anthropic.Anthropic()
# 月次レポート一式(App Store Connect / AdMob / メディエーション3社)
report_files = [
"asc_financial_2026_05.csv" ,
"admob_2026_05.csv" ,
"unity_ads_2026_05.csv" ,
"liftoff_2026_05.csv" ,
"inmobi_2026_05.csv" ,
]
uploaded = []
for path in report_files:
f = client.beta.files.upload( file = Path(path).open( "rb" ))
uploaded.append({ "type" : "container_upload" , "file_id" : f.id})
AGGREGATION_RULES = """あなたはアプリ収益の月次集計を行います。次のルールを厳守してください。
- 通貨は全て JPY に統一する。USD 建ての行は exchange_rate 列があればそれを使い、なければ 1USD = 155JPY で換算する
- 集計期間は 2026-05-01 から 2026-05-31 の暦日に揃える。範囲外の行は除外する
- アプリの識別は bundle_id / package_name / app_id のいずれかの列を正とし、表記ゆれの対応表を最初に作る
- 集計結果はネットワーク別×アプリ別に summary_2026_05.csv へ書き出す
- ネットワーク別×アプリ別の積み上げ棒グラフを revenue_2026_05.png に保存する
- 推測で数値を補完しない。判断に迷った行は excluded_rows.csv に理由付きで書き出す
"""
response = client.beta.messages.create(
model = "claude-sonnet-4-6" ,
betas = [ "files-api-2025-04-14" ],
max_tokens = 8192 ,
messages = [{
"role" : "user" ,
"content" : [{ "type" : "text" , "text" : AGGREGATION_RULES }] + uploaded,
}],
tools = [{ "type" : "code_execution_20250825" , "name" : "code_execution" }],
)
このコードの肝は、アップロード処理よりも AGGREGATION_RULES の方にあります。換算レート・期間の定義・除外基準といった会計上の約束事は、サンドボックスがどれだけ賢くても人間が決めて明文化するべき領域 です。特に効いているのが最後の一行で、「迷った行は excluded_rows.csv に理由付きで書き出す」と指示しておくと、集計から落ちた行を後から必ず追跡できます。この指示を入れる前は、欠損のある行が黙って平均値で補完されかけたことがあり、以来どの集計依頼にも除外ログの出力を必ず入れるようにしています。
為替レートをプロンプトで渡しているのは、先に書いた「コンテナはネットに出られない」制約のためです。最初の月はここを見落として、Claude がレート取得の API を叩こうとして失敗する出力を眺めることになりました。外から与えるべき情報と、中で計算できる情報の線引きは、最初に紙に書き出しておくと迷いません。
生成されたグラフと CSV を回収する
サンドボックス内で作られた summary_2026_05.csv や revenue_2026_05.png は、応答に含まれる file_id を拾って Files API からダウンロードします。公式例は bash 実行結果だけを走査する書き方ですが、私は結果ブロック全般を緩く走査する形にしています。
def extract_generated_file_ids (response) -> list[ str ]:
"""応答内の全ツール結果ブロックから生成ファイルの file_id を集める"""
file_ids = []
for block in response.content:
if not block.type.endswith( "_tool_result" ):
continue
content = getattr (block, "content" , None )
if content is None :
continue
# 実行結果の content リストに出力ファイルの参照が入る
for item in getattr (content, "content" , None ) or []:
file_id = getattr (item, "file_id" , None )
if file_id:
file_ids.append(file_id)
return file_ids
for file_id in extract_generated_file_ids(response):
meta = client.beta.files.retrieve_metadata(file_id)
client.beta.files.download(file_id).write_to_file(meta.filename)
print ( f "ダウンロード完了: { meta.filename } " )
getattr ベースの防御的な書き方にしているのは、ツール結果の型がバージョンアップで増減してきた経緯があるからです。型を固定して isinstance で書くより、file_id を持つものを拾うという意図だけをコードに残す方が、この種の進化中の API には合うというのが実装してみての感触です。
ダウンロードしたファイルのうち、私が毎月必ず開くのは excluded_rows.csv です。除外された行の理由を眺めていると、「Liftoff のレポートにテスト配信の行が混ざっていた」「日付列のタイムゾーン表記が変わった」といった変化に気づけます。集計の答えよりも、答えから外れたものの方に運用の情報量がある というのは、12年分の月締めを経た正直な実感です。
コンテナ再利用で「追い質問」のできる締め作業にする
月次の締めで一番ありがたかったのは、集計が一往復で終わらなくてよくなったことです。応答に含まれるコンテナ ID を次のリクエストに渡すと、同じサンドボックスを作業ファイルごと再利用できます。
container_id = response.container.id
followup = client.beta.messages.create(
container = container_id, # 同じサンドボックスを作業ファイルごと再利用する
model = "claude-sonnet-4-6" ,
betas = [ "files-api-2025-04-14" ],
max_tokens = 4096 ,
messages = [{
"role" : "user" ,
"content" : "summary_2026_05.csv を読み直し、前月比の下落が大きいアプリ×ネットワークの組を3つ抜き出してください" ,
}],
tools = [{ "type" : "code_execution_20250825" , "name" : "code_execution" }],
)
会話としては新規でも、コンテナ上の summary_2026_05.csv はそのまま残っているので、アップロードし直さずに追い質問ができます。「この下落は単価と表示回数のどちらが原因か」「このネットワークだけ日別で見せてほしい」と、締めの数字を眺めながらの対話が成立します。
注意点は、コンテナは作成から30日で失効する ことです。container_id をデータベースに保存して翌月も使い回す設計にすると、container_expired エラーで止まります。私は「締め作業のその日のうちだけ再利用し、月が変わったら必ず作り直す」と割り切りました。なお新しい code_execution_20260120 版ならファイルに加えて REPL の変数状態も維持されますが、月次締めの用途では 20250825 のファイル持ち越しで十分というのが私の感触です。
課金の仕組みを先に理解しておく — 5分最小と無料枠
Code Execution ツールはトークンとは別建てで、実行時間に対して課金される 仕組みです。執筆時点の公式仕様を整理すると次のようになります。
実行時間の課金は 1回あたり最低5分から カウントされます
組織ごとに月1,550時間の無料枠 があり、超過分は1コンテナ・1時間あたり $0.05 です
モデルのトークン料金(入出力)は通常どおり別にかかります
web_search_20260209 または web_fetch_20260209 を同じリクエストに含める場合、コード実行自体の追加課金はありません
応答の usage.server_tool_use.code_execution_requests で実行回数を確認できます
そして一番見落としやすい仕様がこれです。リクエストにファイルを含めると、ツールが一度も呼ばれなくても実行時間が課金されます 。ファイルをコンテナへプリロードする処理が走るためです。プロンプトの言い回しを試行錯誤する段階では、ファイルを外して文面だけを調整し、固まってからファイル付きで投げる、という順番にするだけで無駄な5分課金の積み上がりを避けられます。
実際のコスト感も書いておきます。私の月次締めはコンテナ1本で5〜15分、追い質問を入れても月1時間に届きません。つまり実行時間は無料枠の中に完全に収まり、支払いはトークン分の数十円〜百円程度だけです。比較のために書くと、もし5ファイル・合計8MBほどのレポートを本文に貼ってトークンとして読ませようとすれば、概算で200万トークン規模になり、そもそも標準のコンテキストウィンドウに収まりません。仮に分割して収めたとしても入力トークンだけで数ドルかかります。ファイルはコンテナへ、本文には判断ルールを — この分担が精度の面でもコストの面でも素直でした。
よくある間違いと落とし穴
運用を始めてから踏んだもの、踏みかけたものをまとめます。
1. 旧バージョンのサンプルコードを混ぜてしまう。 検索上位には Python 専用だった code_execution_20250522 時代の記事がまだ多く、応答の解析コードをそこから写すと現行版の bash_code_execution_tool_result / text_editor_code_execution_tool_result を取りこぼします。ツール型は 20250825 に統一し、応答の解析も現行の2系統で書き直してください。
2. 「ツールを呼ばなければ無料」だと思い込む。 前述のとおり、ファイル付きリクエストはプリロードだけで課金が始まります。ファイルを添付したままプロンプトだけ10回調整すれば、実行ゼロでも50分相当が無料枠から消えます。試行錯誤はファイルなしで行うのが安全です。
3. pause_turn を終端だと勘違いする。 5ファイルの突き合わせのような長い処理では、ターンの途中で stop_reason が pause_turn になって応答が返ることがあります。これはエラーではなく「続きがある」の合図で、返ってきた応答をそのまま積み直して再リクエストすると処理が継続します。
messages = [{
"role" : "user" ,
"content" : [{ "type" : "text" , "text" : AGGREGATION_RULES }] + uploaded,
}]
while True :
resp = client.beta.messages.create(
model = "claude-sonnet-4-6" ,
betas = [ "files-api-2025-04-14" ],
max_tokens = 8192 ,
messages = messages,
tools = [{ "type" : "code_execution_20250825" , "name" : "code_execution" }],
)
if resp.stop_reason != "pause_turn" :
break
# 中断されたターンをそのまま積み直すと、続きから実行される
messages.append({ "role" : "assistant" , "content" : resp.content})
最初にこれを知らずに「途中までの集計結果」を最終結果として読みかけたことがあります。stop_reason の確認はループの形で最初から組み込んでおくべきでした。
4. コンテナ ID を恒久的な ID として保存する。 30日で失効します。月をまたぐ再利用は設計に入れず、毎月作り直します。
5. サンドボックスの「ネットなし」を忘れる。 為替レートの取得も pip install もできません。外部から与える情報はプロンプトに明記し、ライブラリはプリインストール一覧で足りるかを先に確認します。集計用途なら pandas・numpy・matplotlib・openpyxl が揃っているので、私のケースでは不足はありませんでした。
6. 機微なデータをそのまま投げる。 この機能はゼロデータ保持(ZDR)の対象外で、コンテナ上のデータは最大30日保持されます。売上レポート自体に個人情報はほぼ含まれませんが、ユーザーレビューやサポートログを同じ仕組みに流すなら、ユーザーレビューやクラッシュログをClaude APIに渡す前に — 個人情報を可逆マスキングする前処理の設計メモ で書いたようなマスキング前処理を必ず挟んでください。アップロードしたファイルの管理全般は Claude API Files API — ドキュメントを永続化してAPIコストを大幅削減 も参考になるはずです。
まず AdMob の CSV を1本渡すところから
全部を一度に組む必要はありません。最初の一歩としておすすめなのは、今月の AdMob レポートを1本だけアップロードし、最小構成のコードに container_upload ブロックを1つ足して、「日別の推定収益を折れ線グラフにして png で保存してください」とだけ頼んでみることです。応答ブロックの並び方と file_id の回収まで一巡すれば、あとは集計ルールを1行ずつ育てていくだけで月次の締めまで届きます。
形式の揺れに振り回されてきた月末の作業が、判断ルールを言語化して渡す作業に変わると、締めの時間そのものが運用を見直す時間になります。私自身、いまも excluded_rows.csv を眺めては換算ルールを書き足している途中ですが、この往復は嫌いではありません。お読みいただきありがとうございました。