技術ブログ

CopilotでExcel自動化はどこまでできる?Office Scriptsを使って実務データを加工してみた

・ 読了 約28分

CopilotでExcel自動化はどこまでできる?Office Scriptsを使って実務データを加工してみた

DX、AIとめざましく産業における情報処理の発展が進んでいますが、実態としてはまだまだExcelは業務や部署を横断するデータの中心となっており、切り離せない存在かと思います。

しかし、その業務への負荷は甘く見ることができず、可能であればExcelにおける業務を効率化・自動化したいというニーズは至る所にあると想像できます。

Excel業務の自動化と聞いて、まず「マクロ」を思い浮かべる人は多いはずです。そして、それを表現するためのVBAを使えば、Excel上のかなり複雑な操作まで自動化できるのは事実です。今でもVBAでなければ扱いにくい処理はありますし、Microsoftも、Excel機能のカバー範囲はOffice ScriptsよりVBAのほうが広いと説明しています。

なので、この記事で言いたいのは「VBAは古くAI時代にそぐわないから、使うべきではない」ということではありません。

問題として散見されるのは、誰かが作ったマクロに業務が依存し、その中身を誰も説明できない、または改修できなくなった状態です。

たとえば、こんなExcelを見たことはないでしょうか。

  • 毎月、決まったExcelを開いて「実行」ボタンを押す
  • 作った人はすでに異動・退職している
  • エラーが出ると業務が止まる
  • 列を一つ増やしただけで動かなくなる
  • VBAを読める人がおらず、怖くて触れない
  • それでも重要な月次業務なので使い続けるしかない

自動化した当初は便利だったはずなのに、数年後には誰も触れない「負の遺産」になってしまう。

いわゆる野良マクロの問題です。ところがCopilotをはじめとするAIの登場によって、この構造が少し変わり始めています。

コードを自分で書けなくても、

このExcelをこう加工したいので、Office Scriptsを書いてください

と依頼すれば、CopilotやChatGPTなどのAIが実現するためのコードを提示してくれるからです。

では、AIにOffice Scriptsを書かせれば、プログラミング経験のない人でも実務レベルのExcel自動化ができるのでしょうか。

今回、例として、売上明細と商品マスタを使った月次集計を題材に試してみます。

Office Scriptsとは

まずOffice Scriptsの説明です。普通にExcelを触っているだけだとなじみのないものかもしれません。Office Scriptsは、Excelの操作を自動化するためにMicrosoftが提供している仕組みです。

Excelの「自動化」から操作を記録したり、コードエディターでスクリプトを作成したりできます。Microsoft Learnでも、Office Scriptsは日常的なExcel作業を自動化するための機能として説明されています。

コードにはTypeScriptというプログラミング言語が使われており、ExcelScript.WorkbookWorksheetRangeTableなどのAPIを通してExcelを操作します。

ここまでの情報だと、なんだかVBAと似ているように感じるかもしれませんが、設計思想には違いがあります。

Microsoftは、VBAをデスクトップ中心の自動化、Office Scriptsをクロスプラットフォームかつクラウドベースの自動化として整理しています。Office ScriptsはExcel on the web、Windows版Excel、Mac版Excelで利用でき、Power Automateからも実行できます。

一方で、VBAのほうがExcelデスクトップ機能を広く扱えます。たとえばVBAでは、セルの値が変更されたとき、Excelファイルを開いたときなどといったExcel上の操作、イベントをきっかけに、自動でマクロを実行できます。

一方、Office Scriptsには、こうしたExcel内部の操作を直接きっかけにスクリプトを実行するイベント機能は現在ありません。基本的にはユーザーがスクリプトを実行するか、Power Automateなど別の仕組みから呼び出して実行します。

そのため、Office ScriptsはVBAをそのまま置き換えるものではなく、それぞれ得意な自動化の形が異なります。

ただ、個人的には、これから新しくExcel業務を自動化するのであれば、まずはVBAを使わずに実現できないかを検討することをおすすめしています。

現在はPower Automateを使ってExcel以外のサービスと処理をつなげたり、各種コネクタを利用してSharePointやTeamsなどと連携したりできます。さらにCopilotの登場によって、コードやフローを自分ですべて書けなくても、自動化の仕組みを作るハードルは以前よりかなり下がっています。

なお、Office Scriptsを利用できる環境はMicrosoft 365のライセンスや組織の管理者設定などによって異なります。実際に試す場合は、自分のExcelに「自動化」タブが表示されるか確認してください。

参考:

今回は、少し面倒な売上明細を加工してみる

単に「列幅を揃える」「セルに色をつける」だけなら、Office Scriptsでなくてもそれほど困りません。今回は、もう少し実務でありがちな処理を考えます。

基幹システムなどから、次のような売上明細がExcelで出力されるとします。これでもまだ簡易的な明細かもしれません。

売上明細シート

受注番号

明細番号

受注日

顧客コード

顧客名

商品コード

数量

単価

担当者

ステータス

A001

1

2026/07/01

C001

株式会社ABC

P001

10

12000

佐藤

受注

A001

2

2026/07/01

C001

株式会社ABC

P002

2

85000

佐藤

受注

A002

1

2026/07/01

C002

DEF工業株式会社

P002

3

85000

鈴木

キャンセル

A003

1

2026/07/02

C001

株式会社ABC

P001

5

12000

佐藤

受注

A003

1

2026/07/02

C001

株式会社ABC

P001

5

12000

佐藤

受注

別シートには商品マスタがあるとします。

商品マスタシート

商品コード

商品カテゴリ

P001

消耗品

P002

機器

P003

保守

この2つから、毎月次の処理をしているとします。

  1. キャンセルされた受注明細を除外する
  2. 二重登録された明細を除外する
  3. 商品コードを商品マスタと照合し、商品カテゴリを付与する
  4. 数量 × 単価で売上金額を計算する
  5. 顧客別の売上を集計する
  6. 担当者別の売上を集計する
  7. 売上金額の大きい順に並べる
  8. 結果を「月次集計」シートへ出力する

Excel関数やピボットテーブルを組み合わせればできる処理です。Power Queryを使う方法もあります。

ただ、毎月手作業で同じ加工をしているのであれば、一連の処理として実行できるようにしたくなります。

そこでAIにOffice Scriptsを書かせてみます。

まずは日本語で処理内容を伝えてみる

AIへの依頼は、たとえば次のようにプロンプトを投げます。シートをプロンプトの中で説明していますが、直接Excelファイルを添付してもいいかもしれません。

ExcelのOffice Scriptsを書いてください。

「売上明細」シートには以下の列があります。

受注番号
明細番号
受注日
顧客コード
顧客名
商品コード
数量
単価
担当者
ステータス

「商品マスタ」シートには、
商品コードと商品カテゴリがあります。

次の処理をしてください。

1. ステータスが「キャンセル」の行を除外
2. 受注番号と明細番号が両方同じ行を重複として除外
3. 商品コードを商品マスタと照合して商品カテゴリを追加
4. 数量 × 単価から売上金額を計算
5. 加工後の明細を「月次集計」シートへ出力
6. 顧客別売上と担当者別売上も同じシートに作成
7. 集計結果は売上金額の降順にする
8. 金額を3桁区切りで表示し、列幅を自動調整する

必要なシートや列が存在しない場合は、
途中まで書き換えずにエラーにしてください。

ここでポイントなのは、Office ScriptsのAPIをほとんど知らなくても依頼自体は問題なくできることです。普段行っている業務を、処理手順として説明しています。

AIにコードを書かせる場合、以前の「自分でVBAを書く」と比べて、この入口のハードルはかなり低くなっています。

生成されるOffice Scriptsはこんな形になる

実装方法はいくつもありますが、たとえば次のようなOffice Scriptsが作られるはずです。もちろん一発で動くものができる保証はありませんが、実行してみてエラーが出たらそれをまたAIに投げて何が問題かを確認してもらい、修正してもらうことが可能です。

type CellValue = string | number | boolean;

function main(workbook: ExcelScript.Workbook) {
  const salesSheet = workbook.getWorksheet("売上明細");
  const masterSheet = workbook.getWorksheet("商品マスタ");

  if (!salesSheet || !masterSheet) {
    throw new Error("「売上明細」または「商品マスタ」シートがありません。");
  }

  const salesRange = salesSheet.getUsedRange(true);
  const masterRange = masterSheet.getUsedRange(true);

  if (!salesRange || !masterRange) {
    throw new Error("売上明細または商品マスタにデータがありません。");
  }

  // Excelへの読み取りをループ内で繰り返さないよう、
  // 最初にまとめて配列として取得する。
  const salesValues = salesRange.getValues();
  const masterValues = masterRange.getValues();

  if (salesValues.length < 2 || masterValues.length < 2) {
    throw new Error("処理対象のデータ行がありません。");
  }

  const salesHeaders = salesValues[0].map(value => String(value).trim());
  const masterHeaders = masterValues[0].map(value => String(value).trim());

  const requiredSalesHeaders = [
    "受注番号",
    "明細番号",
    "受注日",
    "顧客コード",
    "顧客名",
    "商品コード",
    "数量",
    "単価",
    "担当者",
    "ステータス"
  ];

  for (const header of requiredSalesHeaders) {
    if (salesHeaders.indexOf(header) === -1) {
      throw new Error(`売上明細に「${header}」列がありません。`);
    }
  }

  const productCodeIndex = masterHeaders.indexOf("商品コード");
  const categoryIndex = masterHeaders.indexOf("商品カテゴリ");

  if (productCodeIndex === -1 || categoryIndex === -1) {
    throw new Error("商品マスタに必要な列がありません。");
  }

  const col = (name: string): number => salesHeaders.indexOf(name);

  // 商品マスタをMapにする。
  const productMaster = new Map<string, string>();

  for (let i = 1; i < masterValues.length; i++) {
    const productCode = String(masterValues[i][productCodeIndex]).trim();
    const category = String(masterValues[i][categoryIndex]).trim();

    if (productCode !== "") {
      productMaster.set(productCode, category);
    }
  }

  const detailRows: CellValue[][] = [[
    "受注番号",
    "明細番号",
    "受注日",
    "顧客コード",
    "顧客名",
    "商品コード",
    "商品カテゴリ",
    "数量",
    "単価",
    "売上金額",
    "担当者"
  ]];

  const seenDetails = new Set<string>();

  const customerTotals = new Map<
    string,
    { name: string; total: number }
  >();

  const staffTotals = new Map<string, number>();

  for (let i = 1; i < salesValues.length; i++) {
    const row = salesValues[i];

    const status = String(row[col("ステータス")]).trim();

    if (status === "キャンセル") {
      continue;
    }

    const orderNo = String(row[col("受注番号")]).trim();
    const detailNo = String(row[col("明細番号")]).trim();

    // 受注番号だけではなく、受注番号 + 明細番号で重複を判定。
    const detailKey = `${orderNo}::${detailNo}`;

    if (seenDetails.has(detailKey)) {
      continue;
    }

    seenDetails.add(detailKey);

    const customerCode = String(row[col("顧客コード")]).trim();
    const customerName = String(row[col("顧客名")]).trim();
    const productCode = String(row[col("商品コード")]).trim();
    const staff = String(row[col("担当者")]).trim();

    const quantity = Number(row[col("数量")]);
    const unitPrice = Number(row[col("単価")]);

    if (!Number.isFinite(quantity) || !Number.isFinite(unitPrice)) {
      throw new Error(
        `${i + 1}行目の数量または単価を数値として解釈できません。`
      );
    }

    const category = productMaster.get(productCode) ?? "マスタ未登録";
    const salesAmount = quantity * unitPrice;

    detailRows.push([
      orderNo,
      detailNo,
      row[col("受注日")],
      customerCode,
      customerName,
      productCode,
      category,
      quantity,
      unitPrice,
      salesAmount,
      staff
    ]);

    const currentCustomer = customerTotals.get(customerCode);

    customerTotals.set(customerCode, {
      name: customerName,
      total: (currentCustomer?.total ?? 0) + salesAmount
    });

    staffTotals.set(
      staff,
      (staffTotals.get(staff) ?? 0) + salesAmount
    );
  }

  const customerRows: CellValue[][] = [[
    "顧客コード",
    "顧客名",
    "売上金額"
  ]];

  const sortedCustomers = Array.from(customerTotals.entries())
    .sort((a, b) => b[1].total - a[1].total);

  for (const [customerCode, value] of sortedCustomers) {
    customerRows.push([
      customerCode,
      value.name,
      value.total
    ]);
  }

  const staffRows: CellValue[][] = [[
    "担当者",
    "売上金額"
  ]];

  const sortedStaff = Array.from(staffTotals.entries())
    .sort((a, b) => b[1] - a[1]);

  for (const [staff, total] of sortedStaff) {
    staffRows.push([staff, total]);
  }

  // 既存の月次集計があれば作り直す。
  workbook.getWorksheet("月次集計")?.delete();

  const reportSheet = workbook.addWorksheet("月次集計");

  reportSheet.getRange("A1").setValue("加工済み明細");
  reportSheet.getRangeByIndexes(
    1,
    0,
    detailRows.length,
    detailRows[0].length
  ).setValues(detailRows);

  reportSheet.getRange("M1").setValue("顧客別売上");
  reportSheet.getRangeByIndexes(
    1,
    12,
    customerRows.length,
    customerRows[0].length
  ).setValues(customerRows);

  reportSheet.getRange("Q1").setValue("担当者別売上");
  reportSheet.getRangeByIndexes(
    1,
    16,
    staffRows.length,
    staffRows[0].length
  ).setValues(staffRows);

  // 金額表示を整える。
  if (detailRows.length > 1) {
    reportSheet
      .getRangeByIndexes(2, 8, detailRows.length - 1, 2)
      .setNumberFormat("#,##0");
  }

  if (customerRows.length > 1) {
    reportSheet
      .getRangeByIndexes(2, 14, customerRows.length - 1, 1)
      .setNumberFormat("#,##0");
  }

  if (staffRows.length > 1) {
    reportSheet
      .getRangeByIndexes(2, 17, staffRows.length - 1, 1)
      .setNumberFormat("#,##0");
  }

  reportSheet.getUsedRange()?.getFormat().autofitColumns();
}

もちろん、コードだけ見ると、非エンジニア、プログラミングの知識があまりない人には少し難しく見えかもしれません。しかし、ここで重要なのは、これを最初から自分で書く必要がなくなったことです。

AIに要求を伝え、生成されたコードをOffice Scriptsのコードエディターへ貼り付けて動作確認する。エラーが出れば、そのエラーとコードまたAIに渡して修正させる。

仕様を変えたい場合も、今あるコードを貼り付けた上で、

キャンセルだけでなく「失注」も除外してください

マスタ未登録の商品があったら処理を中断してください

顧客別集計に受注件数も追加してください

などと追加で修正をすることもできます。コードを書く能力がなくても、Excel自動化へ入っていける範囲は確実に広がっています。

実は、AIより先に「業務ルール」で一度ハマる

ただ、この例にはかなり重要なポイントがあります。最初に以下の内容をAIへ依頼したとします。

受注番号が重複している行を削除してください

一見すると正しい依頼に見えます。しかし、今回のデータを見ると、受注番号 A001 には明細番号1と2があります。

つまり、1回の受注に複数の商品が含まれることがあると見ることができます。この前提を知らず、受注番号だけを見て重複削除すると、正常な明細まで消しかねません。

必要なのは、「受注番号が同じなら重複とみなす」ではなく、「受注番号と明細番号が両方同じなら二重登録とみなす」という業務ルールです。

ここはAIが勝手に決めることはできません。AIは、指示された内容からコードを書くことは得意です。

しかし、

  • 何を一意なレコードとみなすのか
  • キャンセルは除外するのか、売上をマイナスにするのか
  • マスタに存在しない商品はエラーにするのか
  • 顧客名が変わった場合にどちらを正とするのか
  • 数量や単価が空欄ならどう扱うのか

といった業務上の意味、ロジックまでは、当然把握することはできず、指示をする人間が情報として渡してあげなければなりません。もちろん、『ロジックを推測して』と指示することはできますが、何が正解かを判断できるのは、その業務を知っている人だけです。

業務ルールを正しく言語化することの重要性が上がる。実際にAIで業務自動化をやってみると、この業務にまつわる前提情報をまとめておくことが、かなり重要だと感じます。

これは、昔の「野良マクロ」と何が違うのか

ここで最初の話に戻します。昔であれば、こうしたExcel加工を自動化したいとき、「社内でExcelに詳しい人」に頼んでVBAを書いてもらうことがあったかもしれません。そして、その出来上がったマクロはその時点では便利になり、ある一定の期間の中ではトータルで効率がよくなったはずです。

しかし、その人が異動もしくは退職してしまうと、

この条件分岐は何のためにあるんだろう

なぜこのデータだけ除外されているんだろう

列を追加したらエラーになったけれど、どこを直せばいいんだろう

という状態が発生します。コードが読める人がいなければ、このマクロはそのままブラックボックスとして変化します。

しかし、AI+Office Scriptsではこの状況は少し変わります。

コードを生成させるだけではなく、

このOffice Scriptsが何をしているか日本語で説明してください

この重複判定はどの列を使っていますか

「商品マスタ」シートが存在しない場合はどうなりますか

この処理でデータが欠落する可能性を挙げてください

受注番号と明細番号の組み合わせで重複判定するように修正してください

といった形で、既存コードの理解や修正にもAIを使えるからです。

VBA時代には「コードを書ける人」と「業務を知っている人」の間に大きな壁がありました。生成AIによって、その壁はかなり低くなっています。

ただし、Office Scriptsなら負の遺産にならないわけではない

ここは勘違いしないほうがよいと思います。AIにコードを書かせれば、属人化が自動的になくなるわけではありません。

むしろ、コードを作るコストが下がったことで、謎のスクリプトを大量生産することも簡単になってしまいました。

  1. 誰かがAIに頼んでOffice Scriptsを作る。
  2. 便利なのでチーム内で使われる。
  3. 少しずつ条件が追加される。
  4. 数年後には、誰が生成したのか分からないスクリプトが実行され続けている

これでは、野良マクロが「野良Office Scripts」に変わっただけです。

MicrosoftのOffice Scriptsのベストプラクティスでも、処理前に必要なシートやテーブルが存在するか確認し、データやブックの状態を検証してから変更することが推奨されています。

AIに書かせる場合でも、

「動いたから終わり」ではなく、どんな前提で動いているのかという業務知識やロジックを残しておくことが重要です。

参考:

コードの書き方にも、生成AIへ伝えたほうがいいポイントがある

今回のコードでは、当然ながらExcelのセルを1行ずつ読みながら処理しているわけではありません。

const salesValues = salesRange.getValues();

最初に範囲内のデータをまとめて配列へ読み込み、その後はTypeScript側で繰り返しの処理をしています。

MicrosoftはOffice Scriptsの処理のパフォーマンスの対策として、ループの中で繰り返しブックを読み書きするのを避け、可能な限り最初にデータを一度取得してローカル変数で処理することを推奨しています。

生成AIはコードを書いてくれますが、常に最適なコードを返すとは限りません。

そのため、

セルを1件ずつ読み書きせず、getValues()でまとめて取得して処理してください

のような条件をプロンプトへ追加するのも有効です。

Office スクリプトのパフォーマンスの改善 - Microsoft Learn

Power Automateと組み合わせると「Excelを開いて実行する」から先へ進める

Office Scriptsの面白いところは、Excelの中だけで終わらないことです。

Power AutomateからもOffice Scriptsを実行できます。

たとえば、

  1. SharePointやOneDriveにExcelが配置される
  2. Power Automateが処理を開始する
  3. Office ScriptsでExcelを加工する
  4. 処理結果を別の場所へ保存する
  5. Teamsやメールで担当者へ通知する

といったワークフローなどを作れます。

Microsoft Learnでも、Power AutomateからOffice Scriptsを実行し、Excelを開かずにスケジュールやイベントを起点としてブックを更新できることが説明されています。

ここまで来ると、Office Scriptsは「マクロの代わり」というより、Microsoft 365上の業務フローを構成する部品の一つとして見たほうがわかりやすいです。

Power Automate を使用した Office スクリプトの実行 - Microsoft Learn

では、データ加工は全部Office Scriptsでやればいいのか

今回くらいの処理であればOffice Scriptsでも実装できます。ただし、データ加工だからといって何でもOffice Scriptsに寄せればいいわけではありません。

Microsoftも、Power QueryとOffice Scriptsの使い分けについて、以下のような方向性を示しています。

  • 大規模な外部データソースからの取得・変換・結合にはPower Query
  • Excel中心の処理、書式、可視化、細かなプログラム制御、Power Automate連携にはOffice Scripts

Power Queryはデータ取得・変換のための仕組みとして作られているのに対し、Office ScriptsはExcelそのものをプログラムで操作できることが強みです。

さらに、Office ScriptsやPower Automateにはデータ量や実行時間などの制限もあります。

そのため、数万、数十万行のデータを複数システムから定常的に集めて変換する

といった処理になると、そもそもExcelの中で完結させるべきなのかを考えたほうがよくなります。

AIによって「コードを書くこと」はボトルネックではなくなりつつある

高度なソフトウェア開発のような文脈ではやはり依然として専門知識が欠かせない状況ではありますが、今回のようなExcel加工を自動化するくらいであれば、とりあえずはトライアンドエラーを繰り返しながら実現できる世界になってきたかと思います。VBAを書ける人がいないというところで止まっていた会社も多かった思いますが、AIとOffice Scriptsを組み合わせることによって、その壁はかなり低くなってきています。

少なくとも、以下のいずれかに該当するようなものであれば、日本語で処理内容を説明しながらOffice Scriptsを作ることは十分現実的になっています。

  • 行の抽出
  • 重複データの処理
  • マスタとの突合
  • 新しい列の計算
  • 集計
  • 並び替え
  • シートの生成
  • 書式設定

ただ、実際に試してみると、難しいところも見えてきます。それはTypeScriptを書かせることではありません。

繰り返しとなりますが、「この業務では、何をどう処理するのが正しいのか」を定義することです。

  • 受注番号だけで重複を判断してよいのか。
  • マスタに存在しない値をどう扱うのか。
  • エラーが一件あったら全部止めるのか、それとも正常な行だけ処理するのか。

こうしたルールは、その業務を知っている人にしか決められません。

AIによってコードを書くコストが下がった結果、むしろ業務知識の重要性がはっきりしてきたとも言えます。

マクロの負の遺産から抜け出すために必要なのは、Office Scriptsそのものではない

Office Scriptsは便利です。AIによって、これまでコードを書けなかった人でも利用できる可能性が大きく広がっています。

ただし、VBAをOffice Scriptsへ置き換えれば、負の遺産がなくなるわけではありません。本当に避けたいのはVBAではなく、なぜこの処理をしているのかわからない、誰も仕様を説明できない、壊れたときに直せない、といった状態が発生してしまっていることです。

AIの優れたところは、コードを書くことだけでなく、コードを説明し、修正し、業務ルールとコードの間を行き来するためにも使えることです。

これまで一部の「Excelに詳しい人」に閉じていた自動化を、業務を知っている人自身が扱えるところまで持っていけるのか。

Office ScriptsとAIの組み合わせは、その可能性をかなり感じさせます。

一方で、対象が一つのExcelを超えて、

  • 複数ファイルをまとめて処理する
  • PDFやCSVも取り込む
  • SharePoint上のファイルを定期的に加工する
  • 複数のマスタと突合する
  • 大量データを処理する
  • 処理履歴やエラーを管理する

と広がっていけば、Excelのスクリプトだけで管理すること自体が新しい負債になる可能性もあります。

「AIでOffice Scriptsを書けるか」だけではなく、

どこまでExcelで自動化し、どこから業務フローやデータ処理の仕組みとして外に出すのか。

これからは、その境界を考えることのほうが重要になっていきそうです。

弊社では、Excelやデータを中心とした業務改善・自動化のご相談を幅広く承っています。「この作業も自動化できる?」くらいの段階でも構いません。まずはお気軽にご相談ください。


Resonalエンジニアリング部

Resonalエンジニアリング部

Google Cloudや生成AIの導入、プロダクト開発における役立つコンテンツをお届けします。

お問い合わせ

サービスに関するご質問や、取材・パートナーシップのご相談など、こちらからお気軽にご連絡ください。

お問い合わせはこちら