見出し画像

kintoneのデータを毎朝スプレッドシートに自動転送する仕組みをGASで作った話【コード全文公開】

どうも、拓海です。

突然ですが、こんな経験ありませんか。

「kintoneのデータをExcelで集計したい。でも毎回CSVでダウンロードしてインポートするのがめんどくさい。誰かが自動化してくれないかな……」

これ、kintone導入企業あるあるです。kintoneは入力・管理はめちゃくちゃ便利なんですが、「数字の集計や分析はスプレッドシートでやりたい」というニーズが現場には根強くあります。そのたびにCSVダウンロード → インポートの手作業が発生している現場を、僕はこれまでいくつも見てきました。

今回は GAS(Google Apps Script)を使ってkintoneのレコードをGoogleスプレッドシートへ自動エクスポートする仕組み を作ったので、設計の考え方からコードの中身まで全部公開します。


なぜCSVダウンロードではダメなのか

「手動でダウンロードすればいいじゃん」と思う人もいるかもしれません。でも実際に運用してみると、これが地味にきつい。

毎日集計レポートを作っているチームだと、朝イチにkintoneを開いてCSVダウンロードして、スプレッドシートにインポートして、フォーマットを整えて……という作業が毎日発生します。1回5分でも月20営業日で1時間40分。年間で20時間以上、この作業だけで消えていく計算になります。

さらに厄介なのが「やり忘れ」と「人依存」です。担当者が休んだら集計が止まる。引き継ぎが発生したら手順書から作らなければならない。手作業が多いほど、こういうリスクが積み重なっていきます。

自動化すれば、これが全部ゼロになります。

GASを選んだ理由

kintoneとスプレッドシートを連携する方法は複数あります。

連携ツール(YoomやMakeなど) を使う方法もあります。ノーコードで設定できて便利ですが、月額コストがかかります。件数や頻度によっては料金がそれなりに膨らむので、「そこまで複雑な連携ではないのに……」と感じるケースも多い。

kintoneプラグイン を使う方法もありますが、プラグインも年間費用がかかるものがほとんどです。

その点、GASは 完全無料 です。Googleアカウントがあれば今日から使えて、サーバーも不要。kintoneのREST APIはGASのUrlFetchAppと相性がよく、実装もシンプルに書けます。

「コードを書けるなら、GASが最強コスパ」というのが僕の結論です。

作ったものの概要

完成した仕組みはこうです。

毎朝7時
  ↓
GASが自動実行
  ↓
kintone REST API(カーソルAPI)でレコードを全件取得
  ↓
Googleスプレッドシートの指定シートに書き出し
  ↓
翌朝、同じ処理を繰り返す

特徴をまとめると:

  • 件数制限なし:kintoneのAPIには「1万件を超えるoffset指定は禁止」というルールがあります(2020年7月から)。これを知らずに実装すると1万件超えたタイミングで突然エラーになります。今回はカーソルAPIを使って、件数無制限で確実に取得できるようにしています。

  • スクリプトプロパティで設定管理:APIトークンをコードに直書きしない設計です。コードを誰かに見せても秘密情報が漏れない。

  • フィールドタイプ自動変換:kintoneのAPIレスポンスには様々なフィールドタイプが混在します(チェックボックス、ユーザー選択、サブテーブルなど)。これを適切に文字列変換してスプレッドシートに書き込む処理を実装しています。

  • エラーメッセージが日本語:HTTPステータスと一緒にkintone側のエラーコードとメッセージを出力するので、何が問題なのかすぐわかります。

実装でハマったポイント3つ

実際に作ってみて引っかかった部分を正直に書いておきます。同じところで詰まる人が減れば嬉しい。

1. offsetに10,000件の上限がある

kintoneの records.json(レコード一括取得API)には、offset パラメータに指定できる値が 最大10,000件 という制限があります。これは2020年7月のアップデートで導入された制限で、これを知らずにoffsetをループで増やしていくコードを書くと、1万件を超えたところでいきなり HTTP 400 エラーが返ってきます。

解決策はカーソルAPIの使用です。/k/v1/records/cursor.json というエンドポイントを使う方法で、こちらはoffset制限を受けません。公式推奨の全件取得方法です。

カーソルAPIの流れはシンプルで、

  1. POST でカーソルを作成(取得条件を渡す)

  2. GET でカーソルIDを指定してレコードを500件ずつ取得

  3. next: false が返ってきたら全件取得完了(カーソルは自動削除)

ポイントは、GETリクエストに Content-Type: application/json ヘッダーを付けてはいけない ということ。これを付けると CB_IL02(不正なリクエスト) エラーになります。POSTには必要、GETには不要です。実際にこれでハマって30分溶かしました。

// NG: GETにContent-Typeを付けるとエラーになる
const getOpts = {
  method: 'get',
  headers: {
    'X-Cybozu-API-Token': apiToken,
    'Content-Type': 'application/json'  // ← これが原因
  }
};

// OK: GETはAPIトークンだけでいい
const getOpts = {
  method: 'get',
  headers: {
    'X-Cybozu-API-Token': apiToken
  }
};

2. SUBDOMAINにURLを貼ってしまう問題

スクリプトプロパティの SUBDOMAIN に、kintoneのURLをそのままコピペしてしまう人が続出します(自分を含む)。

NG: https://mycompany.cybozu.com/k/28/
OK: mycompany

「サブドメインだけ入力してください」と書いても、「サブドメインって何?」となる人もいます。なのでコードの中でこの間違いを自動補正する関数を入れました。

function extractSubdomain_(input) {
  const s = input.trim();
  // URLが貼られていてもサブドメインだけ取り出す
  const match = s.match(/^(?:https?:\/\/)?([^./]+)\.cybozu\.com/);
  if (match) return match[1];
  return s.replace(/[/\\]/g, '');
}

これで https://mycompany.cybozu.com/k/28/ を渡されても mycompany だけ取り出せます。ユーザーの入力ミスをコードで吸収するのは、使いやすいツールを作る上で地味に大切なことだと思っています。

3. APIトークン発行後に「アプリを更新」が必要

kintoneでAPIトークンを発行したあと、「アプリを更新」ボタンをクリックしないと設定が反映されません。これを知らずに「トークン発行したのにHTTP 401エラーになる!」と焦るパターンがあります。

操作の流れは: 設定 → APIトークン → 生成する → 保存 → アプリを更新 ← ここを忘れない

この「アプリを更新」は、kintoneあるあるの落とし穴です。フォームの変更と同様、設定変更は必ずアプリ更新して本番に反映させる必要があります。

コードの全体構成

実装したスクリプトの関数一覧です。

関数名 役割 exportKintoneToSheets() メイン処理。シートを全消去して全件書き直し appendNewRecords() 差分追記。前回以降の変更分だけ末尾に追加 fetchAllRecords_() カーソルAPIで全件取得するコア処理 buildSheetData_() レコードをシート行データに変換 extractFieldValue_() フィールドタイプ別の値変換 setDailyTrigger() 毎日7時の自動実行トリガーを設定 setWeeklyTrigger() 毎週月曜8時のトリガーを設定 connectionTest() 接続テスト。総レコード数とフィールド一覧を確認 getProperties_() スクリプトプロパティ取得 extractSubdomain_() URL混入のサニタイズ handleHttpError_() HTTPエラー処理(日本語メッセージ)

コアとなるカーソルAPIの全件取得処理を抜粋します。

function fetchAllRecords_(props) {
  const { subdomain, appId, apiToken, query, fields } = props;
  const baseUrl = `https://${subdomain}.cybozu.com/k/v1/records/cursor.json`;

  // POST/DELETE用(Content-Typeが必要)
  const postOpts = {
    headers: {
      'X-Cybozu-API-Token': apiToken,
      'Content-Type': 'application/json'
    },
    muteHttpExceptions: true
  };

  // GET用(Content-Typeは不要)
  const getOpts = {
    method: 'get',
    headers: { 'X-Cybozu-API-Token': apiToken },
    muteHttpExceptions: true
  };

  // Step1: カーソル作成
  const cursorBody = { app: appId, size: 500 };
  if (query)                       cursorBody.query  = query;
  if (fields && fields.length > 0) cursorBody.fields = fields;

  const createRes = UrlFetchApp.fetch(baseUrl, Object.assign({}, postOpts, {
    method:  'post',
    payload: JSON.stringify(cursorBody)
  }));
  handleHttpError_(createRes, 'カーソル作成');

  const { id: cursorId, totalCount } = JSON.parse(createRes.getContentText());
  Logger.log(`カーソル作成完了 / 対象件数: ${totalCount}件`);

  // Step2: 500件ずつ取得ループ
  let allRecords = [];
  try {
    while (true) {
      const getRes = UrlFetchApp.fetch(
        baseUrl + '?id=' + cursorId,  // IDはそのまま連結(encodeURIComponentは不要)
        getOpts
      );
      handleHttpError_(getRes, 'カーソル取得');

      const page = JSON.parse(getRes.getContentText());
      allRecords = allRecords.concat(page.records);
      Logger.log(`取得済み: ${allRecords.length} / ${totalCount}件`);

      if (!page.next) break; // next=false で全件取得完了
    }
  } catch (e) {
    // エラー時はカーソルを明示的に削除してリソースを解放
    try {
      UrlFetchApp.fetch(baseUrl, Object.assign({}, postOpts, {
        method:  'delete',
        payload: JSON.stringify({ id: cursorId })
      }));
    } catch (_) {}
    throw e;
  }

  return allRecords;
}

encodeURIComponent でカーソルIDをエンコードしてしまうと400エラーになるので注意です。カーソルIDはURLにそのまま連結するだけでOKです。

設定方法(スクリプトプロパティ)

スクリプトプロパティに以下を設定するだけで動きます。コードを一切書き換える必要はありません。

プロパティ名 値の例 必須 SUBDOMAIN mycompany 必須 APP_ID 28 必須 API_TOKEN xxxxxxxxxx 必須 SHEET_NAME kintoneデータ 任意 QUERY ステータス = "受注" 任意 FIELDS 会社名,担当者,金額 任意 ADD_TIMESTAMP true 任意

設定後は connectionTest 関数を実行して接続確認してください。実行ログに「接続成功!総レコード数: ○件」と出れば準備完了です。

上書きと差分追記、どちらを使うべきか

実装した関数は2種類あります。

exportKintoneToSheets(全件上書き) 毎回シートを全消去して書き直します。「常に最新の状態をスプレッドシートで確認したい」というニーズに最適です。毎朝7時のトリガーで実行すれば、出社したときに昨日までのデータが整理されたシートが用意されています。

appendNewRecords(差分追記) 前回実行以降に追加・更新されたレコードだけを末尾に追加します。履歴を蓄積したい場合に使います。この機能を使う場合は ADD_TIMESTAMP を true にしておく必要があります。最終列に実行日時が記録され、次回の基準時刻として使われます。

どちらが正解かはユースケース次第です。「レポートのたびに最新データで分析したい」なら全件上書き、「時系列でデータを追いたい」なら差分追記という選び方になります。

実際の活用シーン

kintone×GASのエクスポート自動化が特に刺さる使い方をいくつか紹介します。

週次の営業レポート自動化 案件管理アプリのデータを毎週月曜に自動取得して、スプレッドシートのピボットテーブルで集計。月曜の朝に全員のスプレッドシートが更新された状態で週次MTGに臨める。

経営ダッシュボードへの連携 Looker Studioと組み合わせると、kintoneのデータをリアルタイムに近い形でダッシュボード表示できます。GASで毎朝エクスポートしたスプレッドシートをLooker Studioのデータソースにするだけ。

バックアップ kintoneのデータをスプレッドシートに定期的にコピーしておくことで、万が一のときのバックアップになります。完全な代替にはなりませんが、軽量なデータ保全として有効です。

経理・会計システムへの連携 freeeやMoneyForwardなど、Excelインポートに対応している会計ソフトとの連携に使えます。kintoneで管理している経費データや請求データをスプレッドシート経由で会計ソフトに取り込む、という使い方です。

まとめ

今回作ったkintone×GASの自動エクスポートをまとめます。

  • 完全無料:GASはGoogleアカウントがあれば無料で使える

  • 1万件超えに対応:カーソルAPIを使って件数無制限で取得

  • 設定はスクリプトプロパティだけ:コードを触らずに設定変更できる

  • ハマりポイントをコードで解決:URL混入の自動補正、エラーメッセージの日本語化

kintoneを使っている企業の多くがスプレッドシートも併用しています。「毎回手動でCSVダウンロードしている」というのは、ほぼ確実に自動化の余地があります。

僕はこれをCoconalaで販売しています。「自分で実装する時間はないけどこの仕組みが欲しい」という方はご相談ください。設定から動作確認まで対応しています。

次回はスプレッドシートからkintoneへの逆方向、データ自動登録の仕組みについて書く予定です。


拓海(Takumi) kintoneカスタマイズ・GAS開発 / Coconalaにて出品中


いいなと思ったら応援しよう!