ReoGrid ReoGrid Web

AIでスプレッドシートを一括処理: xlsx → JSON → LLM

· unvell team
AIでスプレッドシートを一括処理: xlsx → JSON → LLM

数千行のスプレッドシート — サポートチケット、商品リスト、アンケート回答 — があり、各行に LLM で何かをしたい。分類する、クレンジングする、フィールドを抽出する、翻訳する。手作業では無理ですし、きちんとやるには本物のパイプラインが要ります。

これがそのパイプラインです。4段階: ブラウザで xlsx を JSON に変換し、行を JSON として LLM に送り、構造化された JSON を受け取り、結果を新しい列に書き戻す。 スプレッドシート側に ReoGrid、AI 側に Claude を使い、最後に数千行を安く処理する Batch API で締めます。

なぜここでは JSON が正しいインターフェースなのか。 LLM は .xlsx バイナリではなくテキストを扱います。シートをコンパクトな行オブジェクトの配列に変換すれば、モデルにきれいでトークン効率の良い入力を与えられ、グリッドにそのまま書き戻せる構造化された結果が得られます。最初のホップはブラウザで XLSX を JSON に変換を参照。


アーキテクチャ

2つの半分を分けます。

  1. ブラウザ — ユーザーが xlsx を選び、ReoGrid がローカルで解析して素のデータ行を抽出します。ファイルは、あなたが(より小さく構造化された)JSON を送るまで端末から出ません。
  2. バックエンド — JSON の行を受け取り、LLM を呼びます。API キーはサーバー側にあり、ブラウザには決して置きません。
[ xlsx ファイル ] --ブラウザ--> [ JSON 行 ] --POST--> [ あなたのAPI ] --> [ Claude ] --> [ 結果 ]
                                                                                       |
[ グリッド + 新しい"AI"列 ] <-------------------------- 書き戻し ----------------------+

段階1 — xlsx → データ行(ブラウザ)

ファイルを読み込み、ヘッダーをキーにしたオブジェクトの配列として読みます(フルヘルパーは xlsx-to-data-rows レシピ)。

import { createReogrid, type ReogridInstance } from '@reogrid/pro';

const grid = createReogrid('#grid', { licenseKey: 'YOUR-LICENSE-KEY' });
await grid.loadFromFile(file);

function sheetToRows(grid: ReogridInstance): Record<string, string>[] {
  const ws = grid.worksheet;
  const snap = ws.getExportSnapshot();
  let lastRow = 0, lastCol = 0;
  for (const cell of snap.cells) {
    lastRow = Math.max(lastRow, cell.row);
    lastCol = Math.max(lastCol, cell.column);
  }
  const headers: string[] = [];
  for (let c = 0; c <= lastCol; c++) headers[c] = (ws.getDisplayText(0, c) || `col${c}`).trim();
  const rows: Record<string, string>[] = [];
  for (let r = 1; r <= lastRow; r++) {
    const row: Record<string, string> = {};
    for (let c = 0; c <= lastCol; c++) row[headers[c]] = ws.getDisplayText(r, c) ?? '';
    rows.push(row);
  }
  return rows;
}

const rows = sheetToRows(grid);
// → [{ Ticket: 'App crashes on login', Priority: '' }, …]

段階2 — 行を Claude に送る(バックエンド)

サーバー側で、行のチャンクをモデルに送り、構造化された JSON を返してもらいます。ここでは各サポートチケットを分類し、優先度を提案します。Anthropic SDK で json_schema 出力フォーマットを使えば、応答が必ずパースできます。

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic(); // 環境変数 ANTHROPIC_API_KEY を読む

const schema = {
  type: 'object',
  additionalProperties: false,
  properties: {
    results: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        properties: {
          index: { type: 'integer' },
          category: { type: 'string', enum: ['bug', 'billing', 'feature', 'other'] },
          priority: { type: 'string', enum: ['low', 'medium', 'high'] },
        },
        required: ['index', 'category', 'priority'],
      },
    },
  },
  required: ['results'],
};

async function classifyChunk(rows: Record<string, string>[]) {
  const response = await client.messages.create({
    model: 'claude-opus-4-8',
    max_tokens: 16000,
    thinking: { type: 'adaptive' },
    output_config: { format: { type: 'json_schema', schema } },
    messages: [{
      role: 'user',
      content:
        '各サポートチケットをカテゴリと優先度で分類してください。' +
        '行ごとに1つの結果を、配列のindexをキーにして返してください。\n\n' +
        JSON.stringify(rows.map((r, index) => ({ index, ticket: r.Ticket }))),
    }],
  });

  const text = response.content.find((b) => b.type === 'text');
  return JSON.parse(text!.text).results as {
    index: number; category: string; priority: string;
  }[];
}

JSON が入り、構造化された JSON が出る — まさに行に戻してマージできる形です。


段階3 — Batch API でスケール

チャンクごとに1回モデルを呼ぶのは数百行なら十分です。数千行なら Message Batches API を使います。全チャンクを一度に投入し、非同期で処理し、コストは50%安くなります。

// チャンクごとに1リクエストを投入
const batch = await client.messages.batches.create({
  requests: chunks.map((chunk, i) => ({
    custom_id: `chunk-${i}`,
    params: {
      model: 'claude-opus-4-8',
      max_tokens: 16000,
      output_config: { format: { type: 'json_schema', schema } },
      messages: [{ role: 'user', content: buildPrompt(chunk) }],
    },
  })),
});

// 完了までポーリング(多くのバッチは1時間以内に終わる)
let status = await client.messages.batches.retrieve(batch.id);
while (status.processing_status !== 'ended') {
  await new Promise((r) => setTimeout(r, 60_000));
  status = await client.messages.batches.retrieve(batch.id);
}

// 結果を集める — 順不同で届くので custom_id をキーにする
const byChunk: Record<string, unknown> = {};
for await (const result of await client.messages.batches.results(batch.id)) {
  if (result.result.type === 'succeeded') {
    const text = result.result.message.content.find((b) => b.type === 'text');
    byChunk[result.custom_id] = JSON.parse(text!.text);
  }
}

各リクエストがコンテキストウィンドウに無理なく収まるようチャンク分割し(1リクエストあたり数百行が良い出発点)、結果は必ず custom_id で照合します — バッチ結果は順番通りには返りません。


段階4 — 結果をグリッドに書き戻す

マージした結果をブラウザに返し、新しい列に書き込みます。ユーザーは自分のデータと並べて AI の出力を見られ、エクスポートもできます。

const ws = grid.worksheet;

// 新しい列のヘッダー
const aiCol = headers.length;
ws.cell(0, aiCol).setValue('AI Priority').setStyle({ bold: true });

// 行ごとに1値
results.forEach((res) => {
  ws.cell(res.index + 1, aiCol).setValue(res.priority);
});

// 任意: エンリッチしたシートを xlsx に書き出す(Pro)
grid.saveAsXlsx({ filename: 'tickets-classified.xlsx' });

ユーザーはスプレッドシートをエンリッチされて取り戻します — 同じファイル、新しい列が1つ、ダウンロード可能。


このパターンが向いていること

  • 分類 / タグ付け — カテゴリ、感情、優先度、振り分け。
  • クレンジング / 正規化 — 不揃いな書式の修正、氏名の分割、住所の標準化。
  • 抽出 — 自由記述列から構造化フィールドを取り出す。
  • 翻訳 / 書き換え — 列のローカライズ、商品説明の書き換え。

手作業で行ごとにやることは、LLM がシート全体にわたってこなせます。JSON の往復が両端で構造を保ちます。


コスト・プライバシー・精度のメモ

  • プライバシー。 xlsx はブラウザで解析され、処理対象に選んだ行だけがバックエンドに送られます。機密の列はクライアントを出る前に除去・マスクしましょう。
  • コスト。 Batch API は急がないジョブのトークンコストを半減します。タスクに必要な列だけ(行全体ではなく)を送れば、さらにトークンを削れます。
  • 精度。 json_schema 出力フォーマットは応答のを保証しますが、判断は保証しません。サンプルをスポットチェックし、結果は自動適用せずグリッドに書き込んでレビューに回すことで、ユーザーをループに保ちましょう。

関連

ReoGrid Web を試してみる

React/Vue 向けの Canvas ベース Excel 互換スプレッドシートコンポーネント。 Lite は無料 — npm install 一発で始められます。

関連記事

ニュースレター

開発の最新情報をお届けします

新しいリリース・機能追加・お知らせをいち早く受け取るには、
メーリングリストにご登録ください。