整理試算表最怕「刪到手抖」:資料一多、空白列夾雜、狀態還有作廢與過期,滑一滑就迷路。
其實 Google Apps Script 的 deleteRows() 很適合處理「一段一段」的連續刪除,搭配先讀資料、篩出目標、再合併區段由下往上刪,就能又快又穩。
本篇文章用日常語氣帶你走過完整流程:從基礎語法到常見情境(空白列、作廢標記、日期過期)、效能心法、乾跑確認與記錄備份,全都給你。想把表整理到清清爽爽,從這裡開始。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
deleteRows() 是什麼?和 deleteRow() 有何不同?
在 Google Apps Script 裡,工作表(Sheet)物件提供兩種刪列方法:
deleteRow(rowPosition):刪除單一列。
deleteRows(rowPosition, howMany):從 rowPosition 起,連續刪除 howMany 列。
重點:Apps Script 的列、欄索引是從 1 開始(1-based)。第 1 列就是最上面那列。
基本語法:
const sheet = SpreadsheetApp.getActive().getSheetByName('工作表1');
// 從第 5 列開始,連續刪除 3 列(第 5、6、7 列會被刪掉)
sheet.deleteRows(5, 3);
參數限制與規則:
rowPosition ≥ 1,且不得超過目前總列數。
howMany ≥ 1,且 rowPosition + howMany - 1 不得超過總列數。
刪除會直接改變資料位置與索引;後續操作要小心行號位移。
什麼情境適合用 deleteRows()?
1. 清除中間的空白列:整理匯入資料、去除視覺噪音。
2. 刪除符合條件的資料列:例如狀態=「作廢」、金額=0、標記=「已處理」。
3. 依日期或版本清檔:只保留最近 N 天資料,舊的全刪。
4. 縮小檔案體積:刪除尾端大量空列(常見於拖拉填滿後留下幾萬列空白)。
5. 資料重建前的重整:管線跑完,先清掉暫存列再寫入。
開始前的準備與建議流程
1. 建立測試副本:避免在正式表上直接實驗。
2. 開啟 Apps Script 編輯器:
試算表 → 擴充功能 → Apps Script。
3. 加上自訂選單(可選):讓不懂程式的同事也能按鈕執行。
4. 權限:第一次執行會要求授權,一般情況僅需存取該試算表即可。
5. 資料備份:重要資料建議先複製到另一個工作表或檔案。
基礎範例:一次刪一段連續列
範例 1:從第 5 列起連刪 3 列
function demoDeleteRowsBasic() {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName('工作表1');
sheet.deleteRows(5, 3);
}
範例 2:刪除尾端的多餘空白列(常見清檔)
function trimTrailingEmptyRows() {
const sheet = SpreadsheetApp.getActiveSheet();
const lastRow = sheet.getLastRow(); // 最後一列「有資料」的位置
const maxRows = sheet.getMaxRows(); // 工作表實際持有的列數
const toDelete = maxRows - lastRow;
if (toDelete > 0) {
sheet.deleteRows(lastRow + 1, toDelete); // 從有資料的下一列開始刪
}
}
心法:getLastRow() 只看「有內容」的最後一列;很多人以為刪到最後一列 = 乾淨,實際上尾巴常殘留一大段空列,要搭配 getMaxRows() 才能一次砍掉。
條件刪除:先找出要刪的行,再由下而上刪
為何「由下而上」? 因為刪列會造成行號上移,若從上往下刪,下一個目標列號會改變,容易跳刪或誤刪。
範例 3:刪除「狀態」欄為「作廢」的列
假設第 1 列是標題,狀態 欄在第 3 欄(C 欄)。
function deleteRowsByStatus() {
const sheet = SpreadsheetApp.getActiveSheet();
const startRow = 2; // 跳過標題列
const lastRow = sheet.getLastRow();
if (lastRow < startRow) return;
const statusCol = 3; // C 欄
const range = sheet.getRange(startRow, 1, lastRow - startRow + 1, sheet.getLastColumn());
const values = range.getValues();
const toDelete = [];
for (let i = 0; i < values.length; i++) {
const row = values[i];
if ((row[statusCol - 1] + '').trim() === '作廢') {
// 真實行號 = startRow + i
toDelete.push(startRow + i);
}
}
// 由下而上刪
for (let i = toDelete.length - 1; i >= 0; i--) {
sheet.deleteRow(toDelete[i]); // 單列刪除更直覺
}
}
範例 4:刪除整列都「空白」的資料列
function deleteEmptyRows() {
const sheet = SpreadsheetApp.getActiveSheet();
const startRow = 2; // 保留標題
const lastRow = sheet.getLastRow();
if (lastRow < startRow) return;
const lastCol = sheet.getLastColumn();
const range = sheet.getRange(startRow, 1, lastRow - startRow + 1, lastCol);
const values = range.getValues();
const toDelete = [];
values.forEach((row, i) => {
const isEmpty = row.every(cell => (cell === '' || cell === null));
if (isEmpty) toDelete.push(startRow + i);
});
for (let i = toDelete.length - 1; i >= 0; i--) {
sheet.deleteRow(toDelete[i]);
}
}
範例 5:刪除「日期」早於指定日的列
假設日期在第 1 欄(A 欄),格式為可被 new Date() 解析的值。
function deleteRowsBeforeDate() {
const sheet = SpreadsheetApp.getActiveSheet();
const startRow = 2;
const lastRow = sheet.getLastRow();
if (lastRow < startRow) return;
const cutoff = new Date('2024-01-01'); // 門檻日
const dateCol = 1;
const range = sheet.getRange(startRow, 1, lastRow - startRow + 1, sheet.getLastColumn());
const values = range.getValues();
const toDelete = [];
values.forEach((row, i) => {
const v = row[dateCol - 1];
const d = (v instanceof Date) ? v : new Date(v);
if (!isNaN(d) && d < cutoff) toDelete.push(startRow + i);
});
for (let i = toDelete.length - 1; i >= 0; i--) {
sheet.deleteRow(toDelete[i]);
}
}
效能最佳實務:加速、安全、少踩坑
1. 一次讀整塊再判斷
使用 getRange(...).getValues() 取得矩陣,在記憶體決定要刪哪些列,比逐列讀取快很多。
2. 由下而上刪
列索引不會因上方刪除而變動,避免跳刪/誤刪。
3. 合併連續刪除
若預計刪除的行號是連續區段,優先選擇 deleteRows(start, len) 取代多次 deleteRow(),API 呼叫更少、耗時更短。
範例:把 [10,11,12, 20,21, 50] 轉為區段 10x3, 20x2, 50x1 來刪。
function deleteByRanges(sheet, rows) {
// rows 已排序的行號陣列,例如 [10,11,12,20,21,50]
if (rows.length === 0) return;
// 轉為連續區段
const ranges = [];
let start = rows[0], prev = rows[0];
for (let i = 1; i < rows.length; i++) {
if (rows[i] === prev + 1) {
prev = rows[i];
} else {
ranges.push([start, prev]);
start = prev = rows[i];
}
}
ranges.push([start, prev]);
// 由後往前刪(避免區段上移)
for (let i = ranges.length - 1; i >= 0; i--) {
const [s, e] = ranges[i];
sheet.deleteRows(s, e - s + 1);
}
}
4. 避免在迴圈中頻繁呼叫 Sheet API
先算好名單再刪;不要在每個 for 內都 deleteRow()(除非很少量)。
5. 迴避保護範圍
若你的檔有「受保護的範圍」或代理權限不夠,刪除可能失敗。必要時先檢查或暫時取消保護,或改只刪未受保護區段。
6. 與篩選器/資料檢視的互動
標準「篩選器」並不會擋你刪列,但視覺上容易誤判。建議在取消篩選或切到所有列顯示時執行,避免混淆。
7. 配額與長時間作業
如果要刪的列數很多(上萬),建議分批(例如一次處理 1000 列),中間可 Utilities.sleep(200) 休息一下,減少超時風險。
綜合案例:三段需求一次完成
需求:
1. 刪除 C 欄=「作廢」
2. 刪除整列空白
3. 只保留最近 90 天,舊的刪掉
4. 最後把尾端空列收乾淨
function cleanupSheet() {
const sheet = SpreadsheetApp.getActiveSheet();
const header = 1;
const startRow = header + 1;
const lastRow = sheet.getLastRow();
if (lastRow < startRow) {
trimTrailingEmptyRows();
return;
}
const lastCol = sheet.getLastColumn();
const values = sheet.getRange(startRow, 1, lastRow - startRow + 1, lastCol).getValues();
const STATUS_COL = 3; // C
const DATE_COL = 1; // A
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 90);
const toDelete = [];
for (let i = 0; i < values.length; i++) {
const row = values[i];
const rowNum = startRow + i;
// 1) 狀態=作廢
const status = (row[STATUS_COL - 1] + '').trim();
if (status === '作廢') { toDelete.push(rowNum); continue; }
// 2) 整列空白
const isEmpty = row.every(c => c === '' || c === null);
if (isEmpty) { toDelete.push(rowNum); continue; }
// 3) 日期早於 cutoff
const v = row[DATE_COL - 1];
const d = (v instanceof Date) ? v : new Date(v);
if (!isNaN(d) && d < cutoff) { toDelete.push(rowNum); }
}
// 轉為區段並刪
toDelete.sort((a, b) => a - b);
deleteByRanges(sheet, toDelete);
// 收尾:砍掉尾端空列
trimTrailingEmptyRows();
}
與其他方法的比較
| 方法 | 適用情境 | 優點 | 注意 |
|---|---|---|---|
deleteRow(n) |
少量、零散刪除 | 直覺 | 多次呼叫效率較差 |
deleteRows(start, len) |
連續大區段 | 呼叫少、較快 | 需要先把行號整併成區段 |
| 改寫資料而非刪列(寫回過濾後的新資料) | 大規模重整 | 可一次重建乾淨表 | 會重排資料,需小心格式/公式 |
加入自訂選單,讓同事可一鍵執行
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('清理工具')
.addItem('清掉作廢/空白/過期', 'cleanupSheet')
.addSeparator()
.addItem('修剪尾端空列', 'trimTrailingEmptyRows')
.addToUi();
}
常見錯誤與「雷點」總整理
1. 從上往下刪 → 行號位移
症狀:跳刪、漏刪、錯刪。
解法:由下往上,或先合併成區段用 deleteRows() 一次刪。
2. 參數越界
錯誤訊息常見為 Range not found、The number of rows in the range must be at least 1。
檢查:rowPosition >= 1、howMany >= 1、rowPosition + howMany - 1 <= maxRows。
3. 誤刪標題列
忘了 startRow = 2。習慣:把標題列當常數,或先檢查第一列後再篩選。
4. 日期判斷失敗(時區/字串格式)
new Date('2024-01-01') 在不同地區可能被解讀成 UTC 造成前一天或當天,或字串格式解析失敗。
建議:儘量用真正的日期物件(工作表儲存格為日期格式),或用 Utilities.formatDate 規格化。
5. 空白判斷只看一欄
以一欄為準容易誤判。整列 every() 判斷更穩。若有隱藏公式,請用 getDisplayValues() 或檢查 trim()。
6. 資料受保護
受保護範圍(你沒權限)會使刪除失敗。
解法:排除該區、調整權限、或先取消保護。
7. 濾鏡/資料檢視造成視覺混亂
看似只刪到可見列,實際底層刪到其他列。
習慣:執行前關閉篩選,或在乾淨視圖操作。
8. 迴圈中大量呼叫 API
每刪一列就呼叫一次,速度會很慢且容易超時。
解法:先收集行號 → 合併區段 → deleteRows() 批次刪。
9. 把「刪列」當成「清空」
想保留列格式卻用了刪除。
替代:僅清值用 clear() / clearContent();刪列會把該列整個結構拿掉。
10. 忽略 getMaxRows()
只刪到 getLastRow() 以為乾淨,尾端實際還有數千空列。
解法:搭配 trimTrailingEmptyRows()。
除錯與驗證清單
先在副本測試,並在每次刪除前 Logger.log(toDelete) 檢查名單。
加上乾跑(dry-run)模式:列出將被刪除的行號,不真的刪。
function dryRun(toDelete) {
SpreadsheetApp.getUi().alert('將刪除行號:\n' + toDelete.join(', '));
}
建議把「條件判斷」與「刪除動作」拆函式,單元測試更容易。
若遇超時:改成分批處理,每批處理完 SpreadsheetApp.flush()。
問題集
Q1:能只刪「可見的」列嗎?
Apps Script 沒有直接「只刪可見列」的 API。你需要自己依條件挑選行號,或先用公式/輔助欄標示,再由程式依標示刪除。
Q2:大規模(>5 萬列)要怎麼刪比較穩?
分批讀、分批刪,每批合併區段;中間 Utilities.sleep(200);必要時將資料分頁到多個工作表處理。
Q3:刪除會破壞參照公式嗎?
會影響相對位址的範圍,特別是整列被移除後,引用範圍可能縮短或錯位。建議:
將公式集中在標題列或固定區塊。
重要報表使用 命名範圍 降低風險。
Q4:想保留格式,只清掉資料?
用 clear()(清含格式)或 clearContent()(只清內容)。
