書類の電子化とスプレッドシートを使った台帳テンプレート・GAS・Python
スプレッドシートのテンプレート(GASつき)
↓
/**
* ===============================================================
* logFolderToSheet —— 必須 5 列だけ確保して台帳へ書込(省略なし版)
* --------------------------------------------------------------
* 必須列 : ['ファイル名','URL','fileId','作成日時','folderId']
* 1) headerIndex で現在の列マップを取得
* 2) 無い列だけ sheet.appendRow() で右端に追加(既存列は上書きしない)
* 3) Drive 内ファイルを BATCH_SIZE (=50) 行ごとにバッチ書込
* 4) 重複 fileId は Set 判定でスキップ
* 5) onEdit/onOpen トリガーは禁止。時間主導や手動トリガーで実行
* ---------------------------------------------------------------
* フォルダ ID : 設定用!A2 または ScriptProperty 'folderId'
* MIME/拡張子 : 設定用!B2(".pdf" や "image/" 等)※空欄なら全ファイル
* ===============================================================
*/
function logFolderToSheet() {
/* ---------- 設定 ---------- */
const REQUIRED = ['ファイル名','URL','fileId','作成日時','folderId'];
const SHEET_NAME = 'スキャン台帳';
const BATCH_SIZE = 50;
/* ---------- シート取得 & 必須列確保 ---------- */
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME);
// 必須列チェック後は必ず最新ヘッダを取得
let colMap = headerIndex(SHEET_NAME);
const missing = REQUIRED.filter(c => colMap[c] === undefined);
if (missing.length) {
sheet.appendRow(missing);
}
// ここで強制再取得して確実に最新列順へ
colMap = headerIndex(SHEET_NAME, undefined, { forceRefresh: true });
/* ---------- フォルダ ID / フィルタ取得 ---------- */
const folderId = getFolderId();
if (!folderId) {
SpreadsheetApp.getUi().alert('folderId が未設定です(設定用!A2 または ScriptProperty)。');
return;
}
const filterStr = getFilterStr(); // 拡張子 / MIME 部分文字列
/* ---------- Drive 走査 ---------- */
const folder = DriveApp.getFolderById(folderId);
const files = folder.getFiles();
// 既存FileIdセット(重複排除)
const lastRow = sheet.getLastRow();
const idSet = new Set(
lastRow > 1
? sheet.getRange(2, colMap['fileId'] + 1, lastRow - 1).getValues().flat()
: []
);
/* ---------- バッチ書込 ---------- */
const buffer = [];
while (files.hasNext()) {
const f = files.next();
if (idSet.has(f.getId())) continue; // 重複スキップ
// フィルタ(拡張子 or MIME 部分一致)
if (filterStr) {
if (filterStr.startsWith('.')) {
if (!f.getName().toLowerCase().endsWith(filterStr.toLowerCase())) continue;
} else {
if (!f.getMimeType().includes(filterStr)) continue;
}
}
const row = new Array(Object.keys(colMap).length).fill('');
row[colMap['ファイル名']] = f.getName();
row[colMap['URL']] = f.getUrl();
row[colMap['fileId']] = f.getId();
row[colMap['作成日時']] = Utilities.formatDate(
f.getDateCreated(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
row[colMap['folderId']] = folderId;
buffer.push(row);
if (buffer.length >= BATCH_SIZE) {
sheet.getRange(sheet.getLastRow() + 1, 1, buffer.length, row.length)
.setValues(buffer);
buffer.length = 0; // バッファクリア
}
}
if (buffer.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, buffer.length, buffer[0].length)
.setValues(buffer);
}
}
/* ====================================================================
* サポート関数
* ==================================================================== */
/**
* headerIndex —— ヘッダー ⇔ 列番号ユーティリティ(キャッシュ付)
* - 引数・振る舞いは README コメントを参照
*/
function headerIndex(sheetName, key, opts) {
opts = Object.assign({ headerRow: 1, indexBase: 0, forceRefresh: false }, opts || {});
const cacheKey = `hdr:${sheetName}:${opts.headerRow}`;
// 1) 実行中メモリキャッシュ
headerIndex._mem = headerIndex._mem || {};
if (!opts.forceRefresh && headerIndex._mem[cacheKey]) {
return _dispatch(headerIndex._mem[cacheKey], key, opts.indexBase);
}
// 2) ScriptCache(TTL 6h)
const sc = CacheService.getScriptCache();
let headers = null;
if (!opts.forceRefresh) {
const cached = sc.get(cacheKey);
if (cached) headers = JSON.parse(cached);
}
// 3) シートから読み込み
if (!headers) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
if (!sheet) throw new Error(`Sheet '${sheetName}' not found`);
headers = sheet.getRange(opts.headerRow, 1, 1, sheet.getLastColumn()).getValues()[0];
headerIndex._mem[cacheKey] = headers;
sc.put(cacheKey, JSON.stringify(headers), 21600); // 6h
}
return _dispatch(headers, key, opts.indexBase);
/* --- 内部ヘルパ --- */
function _dispatch(arr, k, base) {
const off = base === 1 ? 1 : 0;
if (k === undefined) return arr.reduce((o, h, i) => (o[h] = i + off, o), {});
if (typeof k === 'number') return arr[k - 1] ?? null;
if (Array.isArray(k)) return k.reduce((o, h) => (o[h] = arr.indexOf(h) + off || null, o), {});
if (typeof k === 'string') {
const idx = arr.indexOf(k);
return idx === -1 ? null : idx + off;
}
throw new Error('key must be undefined/string/number/string[]');
}
}
/* ---------- フォルダ ID 取得 ---------- */
function getFolderId() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const s2 = ss.getSheetByName('設定用');
const idIn = s2 ? String(s2.getRange('A2').getValue()).trim() : '';
return idIn || PropertiesService.getScriptProperties().getProperty('folderId') || '';
}
/* ---------- フィルタ文字列取得(拡張子 or MIME 部分) ---------- */
function getFilterStr() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const s2 = ss.getSheetByName('設定用');
return s2 ? String(s2.getRange('B2').getValue()).trim() : '';
}
}
概要
大量の紙書類を電子化し、Googleスプレッドシートを用いた台帳管理からはじめたい方向け
対象: 業務でGoogle Workspace環境を使っており、現場の書類管理を効率化したい方。
制約
- 雇用契約書等の機密情報が対象
筆者の環境
- クラウド: Google Workspace(Google Drive, Google Spreadsheet, Google Apps Script)
- スキャナー: ScanSnap (流し込みスキャンができること)
- Adobe Acrobat Pro (ページの分割に使用)
書類電子化の流れ
-
紙書類のスキャン
- ホチキスやクリップの除去、付箋の剥離、A3書類はA4変換等
- スキャナーでPDF化
-
Google Driveにアップロード
- フォルダ構成(例:年度・部署・個人フォルダごとなど)
-
スプレッドシート台帳作成
-
Driveのファイル情報を一覧管理
-
カラム例:
ファイル名 URL fileId 作成日時 folderId
-
-
自動化(GAS)
- 指定したGoogle Drive内のフォルダからファイルリストを取得し、自動でスプレッドシートに書き出す
- カラムの順序が変わっても動くスクリプト
- 重複回避