GoogleスプレッドシートのドライブのCSVを、スプレッドシートへ取り込む

GAS

※本記事はプロモーションを含みます

目次

業務システムや基幹データベースは定期的にCSVファイルを吐き出します。なので、Googleドライブ経由でスプレッドシートに取り込む処理は、GASによる業務自動化で非常に遭遇頻度が高いタスクです。「GAS ドライブ CSV 取り込み」といった条件で検索してコードを組みます。ただし、単にファイルを開いて中身を貼り付けるだけのように見えて、実際には型情報の消失や文字コードの相違が起きます。空行の処理や同名ファイルの重複取得も起きます。なので、稼働後に発覚する地雷が数多く潜んでいます。基幹システムの仕様変更や予期せぬデータ混入は、処理を突然ストップさせます。なので、裏側で何が起きているのかを把握した上で、取り込み処理を構築する必要があります。

Utilities.parseCsv でハマる型変換と空行の挙動

Google Apps ScriptでCSVを取り扱う際、中核となるツールは Utilities.parseCsv() メソッドです。このメソッドはテキスト形式のCSVデータを2次元配列に一括変換してくれる便利なツールです。ただし、内部処理の仕様を正しく理解していないと、思わぬバグを引き起こします。最大の落とし穴は、CSV内に含まれるデータがすべて文字列(String)としてパースされる点です。というのは、区切り文字や改行コードの判定ルールに依存する挙動があるからです。

先頭のゼロが意味を持つ商品コードや顧客ID(「001234」など)をそのまま取り込むとします。ただし、後続のスプレッドシートへの書き込み処理やSpreadsheetAppの型推論は、データを数値の「1234」に変換します。なので、桁落ちが発生するケースがあります。Utilities.parseCsv() 自体はCSVテキストを解釈して純粋な2次元文字列配列(String[][])を生成します。なので、スプレッドシートの setValues() にそのまま渡すと、スプレッドシート側が自動的に「数値」と判断して表示形式を変更してしまいます。というのは、スプレッドシートが型を自動判定するからです。

また、末尾に空行が含まれているCSVを取り込む場合も確認しましょう。基幹システムが出力したファイルは、最終行の末尾に改行コード(CRLFまたはLF)が付与されていることが多々あります。これを Utilities.parseCsv() に渡すと、最後の要素として空の1次元配列(要素が1つの空文字)が生成されることがあります。この状態で getRange(1, 1, csv.length, csv[0].length) のように配列の要素数をそのまま指定してセル範囲を取得しようとすると、最終行だけ列数が合いません。なので、「The number of columns in the range must match the number of columns in the data.」という実行時エラーが発生して処理が中断されます。

さらに、カンマ(,)や改行が含まれるフィールドはダブルクォーテーション(”)で囲まれます。Utilities.parseCsv() はエスケープ処理のデータを正しくパースします。ただし、データ内のダブルクォーテーション自体がエスケープ違反を起こしていると、パース全体が失敗します。なので、標準メソッドの癖やデータの崩れによるエラーのリスクをあらかじめ考慮しておきましょう。

スプレッドシートへ取り込むスクリプトの全容

GoogleスプレッドシートのドライブのCSVを、スプレッドシートへ取り込む処理を実装します。ここでは、基本的なコード例を示します。定期実行のバッチ処理や、ドライブ上に置かれた最新のCSVを現在のシートへ同期させたいタイミングで使用しましょう。

function importCsvFromDrive() {
  // Googleドライブから対象のCSVファイルを取得
  const file = DriveApp.getFilesByName("import.csv").next();
  
  // Blobを経由してUTF-8としてテキストを取得し、2次元配列に変換
  const csv = Utilities.parseCsv(file.getBlob().getDataAsString("UTF-8"));
  
  // 書き込み対象のアクティブシートを取得
  const sheet = SpreadsheetApp.getActiveSheet();
  
  // 既存のシートデータを全消去(値と数式のみクリア)
  sheet.clearContents();
  
  // 配列の行数と列数を元に範囲を指定し、一括で書き込み
  sheet.getRange(1, 1, csv.length, csv[0].length).setValues(csv);
}

このコードを動かす際、いくつかのズレや想定外の動作が発生します。1つ目は DriveApp.getFilesByName(“import.csv”).next() の部分です。Googleドライブは同名のファイルを同じフォルダ内に複数保存できる仕組みを持っています。なので、もし「import.csv」という名前のファイルがドライブ内に複数存在している場合、next() で取得されるファイルが最新のファイルである保証はありません。というのは、過去のバックアップやゴミ箱に入っている同名ファイルを拾ってしまうからです。なので、古いデータがシートに上書きされる原因になります。

2つ目は、sheet.clearContents() と setValues(csv) の関係です。clearContents() はセルのデータのみを消去します。ただし、罫線や背景色などの書式設定(フォーマット)を残します。なので、以前取り込んだデータの行数が今回のCSVデータの行数より多かった場合、下部に古い書式だけが残ってしまいます。あるいは、以前設定した数値フォーマットが原因で、新しいCSVデータの表示が狂ったりします。これを回避したいなら、完全にシートをクリーンにする処理を入れましょう。なので、取り込み領域のフォーマットを明示的にクリアするロジックが必要です。

Shift_JIS混在やヘッダーずれによる書き込み失敗の回避

前述の基本コードで最も事故が起きやすい要素は、文字コードの問題です。コード内では getDataAsString(“UTF-8”) と指定しています。ただし、日本のビジネス環境で出力されるCSVファイル(特にWindows環境の基幹システムやExcelから出力されたファイル)は、Shift_JIS(Windows-31J)でエンコードされているケースが少なくありません。文字コードがShift_JISのファイルを UTF-8 で読み込むと、日本語部分がすべて文字化けを起こします。なので、最悪の場合はパース処理自体が異常終了します。

この文字化けと読み込みエラーを回避したいなら、取得するBlobに対して正しい文字コードを指定しましょう。Shift_JISのファイルを取り扱う場合は、file.getBlob().getDataAsString(“Shift_JIS”) または “MS932” と明示的に指定してテキスト化します。外部システムから取り込むCSVの文字コードが固定でない場合や、外部拠点が手動でアップロードする運用の場合は、文字コードの不一致による崩れを想定しておきましょう。なので、あらかじめエンコードを統一させるルールを決めておくか、GAS側で読み込み時のエンコード指定を切り替えられる設計にしておきます。

また、CSVデータのヘッダー(1行目)の列数が、データ行(2行目以降)の列数と異なる場合も確認しましょう。この不一致は、書き込みエラーの直接的な原因になります。例えば、注記コメントがファイル末尾に入っていたり、途中の行で区切り文字のカンマが抜けていたりします。なので、Utilities.parseCsv() が出力する2次元配列の要素数が各行によって揃わなくなります。JavaScriptの配列としては成立します。ただし、SpreadsheetAppの setValues() は「各行の配列の長さ(列数)が完全に一致している矩形データ」でなければ受け付けません。というのは、1行でも列数が異なる行が存在するとエラーになる仕様だからです。なので、処理は即座に例外を発行して停止します。

これを回避したいなら、setValues() を呼び出す前に2次元配列をループ処理で走査しましょう。すべての行の列数を最大の列数に合わせて整形するパディング処理(足りない列に空文字 “” を補完する)を挟みます。または、不整合な行をログに出力してスキップするバリデーションロジックを実装します。特に複数人が編集するCSVや手動作成のCSVを取り込む運用では、この列数の揺らぎに対する回避策を確認しましょう。

同名ファイル取得エラーと大容量化に伴うタイムアウトの回避

実務で「GoogleスプレッドシートのドライブのCSVを、スプレッドシートへ取り込む」運用を長く続けていると、ファイル検索の重複とデータ量の増大という2つの壁に必ずぶつかります。コードの安定性を高めたいなら、これらの問題に対する防御手段を組み込んでおきましょう。

まず、Googleドライブ上のファイル取得ロジックを改善します。DriveApp.getFilesByName() はドライブ全体(ルートフォルダおよび配下の全フォルダ)から該当するファイル名のものを検索して取得しようとします。なので、ドライブ内に同一名称のCSVが存在する場合や、ゴミ箱の中にファイルが残っている場合、意図しないファイルが取得されるリスクが高まります。これを回避したいなら、特定のフォルダIDを直接指定して FolderApp.getFolderById(FOLDER_ID).getFilesByName(FILE_NAME) のように限定したフォルダ内から検索しましょう。あるいは、検索結果のファイル一覧をループして更新日時(getLastUpdated())を比較します。なので、最も新しいファイル1つだけを特定して処理する安全な構造に切り替えます。

次に、データ量が数万行クラスに膨らんだ場合のタイムアウトとメモリオーバーを回避します。GASには1回の実行時間制限(標準アカウントで6分)があります。数MBを超える巨大なCSVファイルを Utilities.parseCsv() で一括変換し、それを setValues() でシート全体に一度に書き込もうとすると、スクリプトの実行時間が急激に延びます。なので、タイムアウトエラーになることがあります。また、メモリ上限に達して Exceeded memory limit エラーが発生することもあります。

この大容量化に伴う処理失敗を回避したいなら、一度にすべてのデータをシートに書き込もうとするのはやめましょう。一定の行数(例えば1000行単位)ごとに分割して getRange().setValues() を実行するチャンク書き込みのロジックを取り入れます。さらに、SpreadsheetAppの操作自体が重い処理です。なので、不要な再計算やイベントキックを抑止する工夫も必要です。処理対象のCSVが日々増大していく業務フローでは、処理時間の監視とデータ量の制限値を設けておくかを確認しましょう。これが、長期間トラブルなく自動化を運用し続けるための鍵です。

タイトルとURLをコピーしました