每天會收到一份對帳檔,貼進「匯入」分頁,再由程式併進「對帳明細」。檔案有時重複寄,有時對方改過某一筆的金額。
如果程式每次都把「匯入」整份接到「對帳明細」後面,重複寄一次,明細就多一批重複的列。這篇的做法是:同一份檔案不管跑幾次,結果都一樣。
先看流程
重點是用「交易編號」認出已經收過的列。認得出來,才知道哪些該新增、哪些該跳過。
先準備
- 試算表建三個分頁:「匯入」(貼每天的檔案)、「對帳明細」(累積的結果)、「待確認」(放內容對不上的列)。
- 「匯入」與「對帳明細」第 1 列的表頭,依序是
交易編號、日期、金額、摘要;「待確認」的表頭是交易編號、既有、匯入。 - 三個分頁的交易編號欄,都先設成純文字再貼資料。這篇的編號是文字(例如
00123),純文字才不會被轉成數字 123。貼上後如果發現前導 0 不見了(00123變123),代表欄格式不是純文字,請清掉重貼,否則程式會把它當成另一個編號。 - 在「擴充功能 → Apps Script」把四個區塊依序貼進去並儲存,函式選單選
runImport按「執行」。若 Google 要求授權,先看清楚列出的權限再同意。
區塊一:把編號整理成可比對的樣子
/**
* @OnlyCurrentDoc
*/
const HEADER = ["交易編號", "日期", "金額", "摘要"];
// 鍵:轉成文字、去頭尾空白。數字 123 與文字 "00123" 不是同一個鍵,所以交易編號欄要設成純文字
function normalizeKey(raw) {
return raw === null || raw === undefined ? "" : String(raw).trim();
}normalizeKey把編號轉成文字並去掉頭尾空白,A004與A004會被當成同一個。- 數字
123與文字"00123"不是同一個鍵,所以才要求編號欄先設純文字。 - 空白或空儲存格會得到空字串,後面當作「沒有編號」處理。
HEADER是表頭要有的欄名,區塊三用來檢查。
區塊二:先分類,不寫任何東西
function isDate(value) {
return Object.prototype.toString.call(value) === "[object Date]";
}
// 比對其他欄位用:日期比時間,其他比文字
function cellText(value) {
return isDate(value) ? String(value.getTime()) : normalizeKey(value);
}
// 規劃:只分類,不寫任何東西
function planImport(existing, incoming) {
const known = new Map();
existing.forEach((row) => {
const key = normalizeKey(row[0]);
if (key !== "" && !known.has(key)) {
known.set(key, row);
}
});
const seen = new Set();
const result = { toAdd: [], same: 0, conflicts: [], dupInFile: 0, emptyKey: 0 };
incoming.forEach((row) => {
const key = normalizeKey(row[0]);
if (key === "") {
result.emptyKey++;
return;
}
if (seen.has(key)) {
result.dupInFile++;
return;
}
seen.add(key);
const old = known.get(key);
if (!old) {
result.toAdd.push([key].concat(row.slice(1)));
} else if (old.slice(1).every((value, i) => cellText(value) === cellText(row[i + 1]))) {
result.same++;
} else {
result.conflicts.push({ key: key, existing: old, incoming: row });
}
});
return result;
}planImport只負責分類,不碰試算表,輸入是兩份二維陣列,輸出是每一類的列與數量。- 同一份檔裡編號重複,只取第一筆,其餘算
dupInFile;沒有編號的列算emptyKey。 - 編號已存在、其他欄位也一樣,算
same;編號已存在但其他欄位不同,放進conflicts,不進toAdd。 - 新增的列會把編號去掉空白再寫入。日期用時間比,其他欄位比文字。
區塊三:讀資料、檢查分頁與表頭
function getSheet(book, name) {
const sheet = book.getSheetByName(name);
if (!sheet) {
throw new Error("找不到分頁「" + name + "」");
}
return sheet;
}
function checkHeader(sheet, name) {
const actual = sheet.getRange(1, 1, 1, HEADER.length).getValues()[0];
if (HEADER.some((title, i) => normalizeKey(actual[i]) !== title)) {
throw new Error("「" + name + "」的表頭要依序是:" + HEADER.join("、"));
}
}
function readRows(sheet, cols) {
const last = sheet.getLastRow();
return last < 2 ? [] : sheet.getRange(2, 1, last - 1, cols).getValues();
}
function describeRow(row, timeZone) {
return row
.slice(1)
.map((value) => (isDate(value) ? Utilities.formatDate(value, timeZone, "yyyy-MM-dd") : String(value)))
.join(" / ");
}getSheet找不到分頁就丟錯,不會默默跳過。checkHeader表頭不對就丟錯。兩個分頁欄位順序錯了,資料會寫到錯的欄,所以寧可停下來。readRows從第 2 列讀到最後一列,一次getValues,沒有資料就回傳空陣列。describeRow把一列整理成「日期 / 金額 / 摘要」的文字,放進「待確認」給人看。
區塊四:只寫新增的,並記下待確認
// 接在最後一列之後;第 1 欄先設成純文字,編號才不會被轉成數字
function appendRows(sheet, rows, cols) {
if (rows.length === 0) {
return;
}
const start = sheet.getLastRow() + 1;
sheet.getRange(start, 1, rows.length, 1).setNumberFormat("@");
sheet.getRange(start, 1, rows.length, cols).setValues(rows);
}
function runImport() {
const book = SpreadsheetApp.getActiveSpreadsheet();
const source = getSheet(book, "匯入");
const detail = getSheet(book, "對帳明細");
const pending = getSheet(book, "待確認");
checkHeader(source, "匯入");
checkHeader(detail, "對帳明細");
const plan = planImport(readRows(detail, 4), readRows(source, 4));
const timeZone = book.getSpreadsheetTimeZone();
const listed = new Set(readRows(pending, 3).map((row) => JSON.stringify(row)));
const fresh = plan.conflicts
.map((c) => [c.key, describeRow(c.existing, timeZone), describeRow(c.incoming, timeZone)])
.filter((row) => !listed.has(JSON.stringify(row)));
appendRows(detail, plan.toAdd, 4);
appendRows(pending, fresh, 3);
Logger.log(
"新增 " + plan.toAdd.length +
"、相同 " + plan.same +
"、待確認 " + plan.conflicts.length + "(新列入 " + fresh.length + ")" +
"、檔內重複 " + plan.dupInFile +
"、空鍵 " + plan.emptyKey
);
}appendRows接在最後一列之後,一次setValues。寫入前先把第 1 欄設成純文字,編號才不會被轉成數字。runImport全程只讀「匯入」,不改它。- 「待確認」會先讀出已經列過的內容,同一筆衝突不會因為重跑而一直增加。
- 執行記錄會顯示這次新增、相同、待確認、檔內重複、空編號各幾筆。
執行結果
下面是一組範例資料:「對帳明細」原本有 A001、A002(金額 250)、A003 三列,「匯入」有 12 列,最右欄的比對結果是方便閱讀加上去的:
| 列 / 欄 | A | B | C | D | E |
|---|---|---|---|---|---|
| 1 | 交易編號 | 日期 | 金額 | 摘要 | 比對結果 |
| 2 | A001 | 2026-10-01 | 100 | 文具 | 相同,略過 |
| 3 | A002 | 2026-10-02 | 280 | 運費 | 金額不同,進待確認 |
| 4 | A003 | 2026-10-03 | 80 | 郵資 | 相同,略過 |
| 5 | A004 | 2026-10-04 | 500 | 海報 | 新增(編號去空白) |
| 6 | A005 | 2026-10-05 | 60 | 貼紙 | 新增 |
| 7 | A005 | 2026-10-05 | 60 | 貼紙 | 檔內重複,略過 |
| 8 | 2026-10-06 | 10 | 沒有編號 | 空編號,略過 | |
| 9 | 00123 | 2026-10-07 | 330 | 包材 | 新增 |
| 10 | A006 | 2026-10-08 | 45 | 膠帶 | 新增 |
| 11 | A007 | 2026-10-09 | 90 | 紙箱 | 新增 |
| 12 | A008 | 2026-10-10 | 120 | 標籤 | 新增 |
| 13 | A001 | 2026-10-01 | 999 | 改過的重複列 | 檔內重複,略過 |
示範試算表
第一次執行,執行記錄是「新增 6、相同 2、待確認 1(新列入 1)、檔內重複 2、空鍵 1」。「對帳明細」變成:
| 列 / 欄 | A | B | C | D |
|---|---|---|---|---|
| 1 | 交易編號 | 日期 | 金額 | 摘要 |
| 2 | A001 | 2026-10-01 | 100 | 文具 |
| 3 | A002 | 2026-10-02 | 250 | 運費 |
| 4 | A003 | 2026-10-03 | 80 | 郵資 |
| 5 | A004 | 2026-10-04 | 500 | 海報 |
| 6 | A005 | 2026-10-05 | 60 | 貼紙 |
| 7 | 00123 | 2026-10-07 | 330 | 包材 |
| 8 | A006 | 2026-10-08 | 45 | 膠帶 |
| 9 | A007 | 2026-10-09 | 90 | 紙箱 |
| 10 | A008 | 2026-10-10 | 120 | 標籤 |
示範試算表
A002 還是 250,沒有被改成 280。「待確認」多了一列:
| 列 / 欄 | A | B | C |
|---|---|---|---|
| 1 | 交易編號 | 既有 | 匯入 |
| 2 | A002 | 2026-10-02 / 250 / 運費 | 2026-10-02 / 280 / 運費 |
示範試算表
立刻再執行一次:新增 0、相同 8、待確認 1(新列入 0)。兩個分頁的列數都沒變。把「匯入」清成空表再執行,全部是 0,也沒有寫入。這幾個表是預先寫好的結果,不會自己執行程式。
為什麼衝突不覆蓋
編號相同、金額不同的列,程式不替你決定哪邊對,而是放進「待確認」,由人看過再處理。
| 拒絕重複(這篇的做法) | 覆蓋更新 | |
|---|---|---|
| 同編號、內容不同 | 保留舊的,列出來給人看 | 用新的蓋掉舊的 |
| 對方誤改金額 | 舊資料不受影響 | 已核對的數字被悄悄改掉 |
| 對方更正金額 | 要人手動改一次 | 自動更新 |
| 適合 | 對帳、財務、已經核過的資料 | 商品價格這類永遠以最新為準的資料 |
兩種都對,差別在「錯誤蓋掉舊資料」的代價有多大。對帳資料通常寧可多一道人工確認。
換成你的情境
- 鍵欄:這篇用第 1 欄的交易編號;你的資料可以是訂單編號、Email,或兩欄合起來。
- 比對欄:
planImport裡比對其他欄位的範圍(old.slice(1)),可以改成只比金額。 - 衝突處理:放進待確認、直接略過、或覆蓋更新,依上表的取捨決定。
- 分頁名稱與欄數:
HEADER、各分頁名稱、4與3這兩個欄數。
金額變了就自動更新,要改哪裡?
如果規則改成「同一個交易編號的金額變了,就用匯入的新金額更新舊的那一列」,要改哪個區塊?這個改法有什麼風險?
延伸閱讀
多人同時執行可能重複新增,需要另外加鎖。單次執行有 6 分鐘的時間上限,資料量大到超過上限時會被中斷。編號欄請先設成純文字,否則 00123 這類編號會被轉成數字。
接下來可以
相關文章
查看所有文章選單就能整理資料:分割文字、移除重複內容、修剪空白
不寫公式也能整理資料:「資料」選單裡的「將文字分割成多欄」、「資料清理」下的「移除重複內容」與「修剪空白」怎麼用、會改到哪裡,以及什麼時候該改用 SPLIT、UNIQUE、TRIM 公式。附一份可照做的小資料。
XLOOKUP 入門:用品名查價格,也對照 VLOOKUP
XLOOKUP 的三個必填參數、找不到時自訂顯示的內容,以及可以往左查的寫法,最後和 VLOOKUP 的寫法逐項對照。
儲存格的值與顯示格式:看起來一樣,其實不一樣
儲存格存的值和畫面上看到的樣子是兩件事。用圖看日期、百分比、小數位數、帶單位的數字,學會用 N、ISNUMBER 查出真相,也知道格式該設在哪裡。
留言討論
共 0 則留言
