Eksport Sheet To PDF:同名文件存在时更新
结论
先通过 Google Sheets 的导出接口,将指定工作表生成 PDF,再按文件名到目标文件夹中查找:
- 如果没有同名文件,调用
destinationFolder.createFile(pdfBlob)创建文件。 - 如果已有同名文件,调用高级 Drive 服务的
Drive.Files.update()替换文件内容。
Drive.Files.update() 只更新 PDF 内容,不会改变原文件的 ID 和共享链接,因此比删除后重新创建更合适。
完整代码
function printSpreadsheetAsPDF() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const spreadsheetId = spreadsheet.getId();
const dataSheet = spreadsheet.getSheetByName('DATA KONSUMEN');
const invoiceSheet = spreadsheet.getSheetByName('CETAK TAGIHAN');
if (!dataSheet || !invoiceSheet) {
throw new Error('找不到 DATA KONSUMEN 或 CETAK TAGIHAN 工作表。');
}
const baseName = invoiceSheet.getRange('C4').getDisplayValue().trim();
const folderId = dataSheet.getRange('E4').getDisplayValue().trim();
if (!baseName) {
throw new Error('CETAK TAGIHAN!C4 中没有有效的文件名。');
}
if (!folderId) {
throw new Error('DATA KONSUMEN!E4 中没有目标文件夹 ID。');
}
const fileName = baseName + '.pdf';
const destinationFolder = DriveApp.getFolderById(folderId);
// 确保公式计算和单元格更新已提交。
SpreadsheetApp.flush();
const parameters = {
format: 'pdf',
size: 'letter',
portrait: 'true',
fitw: 'true',
sheetnames: 'false',
printtitle: 'false',
pagenumbers: 'false',
gridlines: 'true',
fzr: 'true',
gid: String(invoiceSheet.getSheetId())
};
const queryString = Object.keys(parameters)
.map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(parameters[key]);
})
.join('&');
const exportUrl =
'https://docs.google.com/spreadsheets/d/' +
spreadsheetId +
'/export?' +
queryString;
const response = UrlFetchApp.fetch(exportUrl, {
method: 'get',
headers: {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
},
muteHttpExceptions: true
});
const responseCode = response.getResponseCode();
if (responseCode !== 200) {
throw new Error(
'PDF 导出失败,HTTP 状态码:' +
responseCode +
';响应内容:' +
response.getContentText().slice(0, 500)
);
}
const pdfBlob = response
.getBlob()
.getAs(MimeType.PDF)
.setName(fileName);
const existingFiles = destinationFolder.getFilesByName(fileName);
if (!existingFiles.hasNext()) {
const newFile = destinationFolder.createFile(pdfBlob);
console.log('已创建 PDF:' + newFile.getUrl());
return newFile.getId();
}
// 更新找到的第一个同名文件,保留其文件 ID 和共享链接。
const existingFile = existingFiles.next();
Drive.Files.update(
{ mimeType: MimeType.PDF },
existingFile.getId(),
pdfBlob
);
console.log('已更新 PDF:' + existingFile.getUrl());
return existingFile.getId();
}
使用前的设置
Drive.Files.update() 属于 Apps Script 的高级 Drive 服务,仅启用内置的 DriveApp 还不够。
请在 Apps Script 编辑器中打开“服务”,添加并启用 Drive API。首次运行脚本时,还需要授予 Google Drive 和外部请求所需的权限。
如果没有启用高级 Drive 服务,通常会看到以下错误:
ReferenceError: Drive is not defined
实现原理
导出地址中的 gid 用于指定要导出的工作表:
gid: String(invoiceSheet.getSheetId())
即使 Spreadsheet 中有多个 sheet,生成的 PDF 也只包含 CETAK TAGIHAN。
这行代码只在指定的目标文件夹中查找同名文件:
const existingFiles = destinationFolder.getFilesByName(fileName);
如果没有找到,就创建新文件:
destinationFolder.createFile(pdfBlob);
如果已经存在,则更新原文件:
Drive.Files.update(
{ mimeType: MimeType.PDF },
existingFile.getId(),
pdfBlob
);
更新后文件 ID 不变,依赖该 ID 的共享链接或其他引用通常仍可继续使用。
如果文件夹中可能存在多个同名文件
Google Drive 允许同一文件夹中保存多个同名文件。上面的代码只更新查到的第一个文件,以免误改其他文件。
如果业务规则要求更新所有同名 PDF,可以改成:
let updatedCount = 0;
while (existingFiles.hasNext()) {
const existingFile = existingFiles.next();
Drive.Files.update(
{ mimeType: MimeType.PDF },
existingFile.getId(),
pdfBlob
);
updatedCount++;
}
console.log('已更新同名文件数量:' + updatedCount);
更稳妥的方法是,在首次创建文件后保存它的文件 ID,以后直接按 ID 更新。文件名不是唯一标识,重名、改名或移动文件都可能影响按名称查询的结果。
表单触发器注意事项
如果需要在 Google Form 提交后自动运行脚本,请使用能够授权的“提交表单时”安装型触发器。简单触发器受授权限制,不适合直接执行需要 Drive 和 UrlFetchApp 权限的操作。
两个表单可能在很短的时间内同时提交。此时,两个脚本实例可能都会判断文件不存在,然后分别创建 PDF。可以使用脚本锁避免这种情况:
function printSpreadsheetAsPDFWithLock() {
const lock = LockService.getScriptLock();
try {
lock.waitLock(30000);
return printSpreadsheetAsPDF();
} finally {
lock.releaseLock();
}
}
将安装型触发器绑定到 printSpreadsheetAsPDFWithLock。
SpreadsheetApp.flush() 可以提交当前脚本对表格所做的更新,但不能保证所有外部数据或复杂公式都能在固定时间内完成计算。如果导出的 PDF 偶尔仍是上一笔数据,需要检查表单响应、合并公式与 PDF 导出之间的执行时序。
备注:内容仅供参考。
