PAYATHON 2026

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 导出之间的执行时序。

备注:内容仅供参考。