使用Google Apps Script将所有工作表转换为PDF

6
我正在尝试将一个包含多个表格的谷歌电子表格转换为PDF文件。下面的脚本可以工作,但它只会创建一个包含电子表格最后一页的PDF文件。
function savePDFs() {
    SpreadsheetApp.flush();

    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheets = ss.getSheets();
    var url = ss.getUrl();

    //remove the trailing 'edit' from the url
    url = url.replace(/edit$/,'');

    //additional parameters for exporting the sheet as a pdf
    var url_ext = 'export?exportFormat=pdf&format=pdf' + //export as pdf

    //below parameters are optional...
    '&size=letter' + //paper size
    '&portrait=false' + //orientation, false for landscape
    '&fitw=true' + //fit to width, false for actual size
    '&sheetnames=false&printtitle=false&pagenumbers=false' + //hide optional
    '&gridlines=false' + //false = hide gridlines
    '&fzr=false' + //do not repeat row headers (frozen rows) on each page
    '&gid='; //leave ID empty for now, this will be populated in the FOR loop

    var token = ScriptApp.getOAuthToken();

    //make an empty array to hold your fetched blobs
    var blobs = [];

    //.fetch is called for each sheet, the response is stored in var blobs[]
    for(var i = 0; i < sheets.length; i++) {
        var sheetname = sheets[i].getName();

        //if the sheet is one that you don't want to process,
        //continue' tells the for loop to skip this iteration of the loop
        if(sheetname == "Team Member Numbers")
            continue;

        //grab the blob for the sheet
        var response = UrlFetchApp.fetch(url + url_ext + sheets[i].getSheetId(), {
            headers: {
                'Authorization': 'Bearer ' +  token
            }
        });

        //convert the response to a blob and store in our array
        blobs.push(response.getBlob().setName(sheets[i].getName() + '.pdf'));
        var array_blob = response.getBlob().setName(sheets[i].getName() + '.pdf');
    }

    //from here you should be able to use and manipulate the blob to send and
    //email or create a file per usual.

    // send email
    var subject = "Enter Subject"
    var message = "See attached PDF"
    MailApp.sendEmail("email addy here", subject, message,{attachments:[array_blob]});
}

@NathanHughes - 这个问题涉及电子表格中的所有工作表,而之前的问题只涉及单个工作表。细微的差别,但我认为不是重复的。此外,自其他问题的被接受答案以来,OAuth的故事变得更加简单 - 虽然这并不改变问题,但人们不太可能从关闭的问题中适应代码,因此我们关闭这个问题可能会对他们造成不便。(无论如何,我在两个问题上都贴了答案,以便其他人可以轻松找到。) - Mogsdad
1
@Mogsdad:感谢您的澄清,我撤回了我的关闭投票。 - Nathan Hughes
注意:如果工作表被隐藏,此方法将不起作用。请使用activate()方法取消隐藏工作表。 - Andrew Roberts
4个回答

8

我稍微修改了@Mogsdad的代码,使整个电子表格打印成一个PDF文件。关键在于调整导出参数。基本上只需将原代码中的

替换为:

'&gid=' + sheet.getSheetId()   //the sheet's Id

使用

(optSheetId ? ('&gid=' + sheet.getSheetId()) : ('&id=' + ss.getId()))  // Print either the entire Spreadsheet or the specified sheet if optSheetId is provided

所以上面的代码减去循环部分如下:
function savePDFs( optSSId, optSheetId ) {

  // If a sheet ID was provided, open that sheet, otherwise assume script is
  // sheet-bound, and open the active spreadsheet.
  var ss = (optSSId) ? SpreadsheetApp.openById(optSSId) : SpreadsheetApp.getActiveSpreadsheet();

  // Get folder containing spreadsheet, for later export
  var parents = DriveApp.getFileById(ss.getId()).getParents();
  if (parents.hasNext()) {
    var folder = parents.next();
  }
  else {
    folder = DriveApp.getRootFolder();
  }

  //additional parameters for exporting the sheet as a pdf
  var url_ext = 'export?exportFormat=pdf&format=pdf'   //export as pdf

      // Print either the entire Spreadsheet or the specified sheet if optSheetId is provided
      + (optSheetId ? ('&gid=' + sheet.getSheetId()) : ('&id=' + ss.getId()))

      // following parameters are optional...
      + '&size=letter'      // paper size
      + '&portrait=true'    // orientation, false for landscape
      + '&fitw=true'        // fit to width, false for actual size
      + '&sheetnames=false&printtitle=false&pagenumbers=false'  //hide optional headers and footers
      + '&gridlines=false'  // hide gridlines
      + '&fzr=false';       // do not repeat row headers (frozen rows) on each page

  var options = {
    headers: {
      'Authorization': 'Bearer ' +  ScriptApp.getOAuthToken()
    }
  }
  var response = UrlFetchApp.fetch("https://docs.google.com/spreadsheets/" + url_ext, options);
  var blob = response.getBlob().setName(ss.getName() + '.pdf');

  //from here you should be able to use and manipulate the blob to send and email or create a file per usual.
  //In this example, I save the pdf to drive
  folder.createFile(blob);

}

顺便说一下,谢谢 - 我一直在寻找这个问题的解决方案!


1
哦!太酷了!我不知道你可以这样做!(使用&id=来获取多个工作表。) - Mogsdad
1
如果您的脚本在导出 PDF 之前更改了表格的内容,您可能会发现 PDF 中缺少数据更改。如果是这种情况,请在生成 PDF 之前调用 ss.waitForAllDataExecutionsCompletion(timeout) - Walter Stabosz
1
@WalterStabosz 感谢您的分享,我因此而苦恼了一段时间。 - Amit Singh

6
该函数是根据“ianshedd...”提供的脚本进行改编的,链接在这里
它可以:
  • 生成包含表格所有工作表的PDF文件,并将它们存储在包含该表格的同一文件夹中。(它假定只有一个文件夹执行此操作,尽管Drive允许多个容器。)

  • 使用表格和工作表的名称命名pdf文件。

  • 使用Drive服务(DocsList已过时)。

  • 可以使用可选的电子表格ID来操作任何工作表。默认情况下,它希望在包含脚本的“活动电子表格”上运行。

  • 仅需要“普通”授权即可操作;无需激活高级服务(好吧...您确实需要一些,请参见此处),也不需要修改oAuthConfig

    用于检索电子表格PDF的fetch()调用的OAuth2授权通过ScriptApp.getOAuthToken()授予,它为当前用户提供OAuth 2.0访问令牌。

通过一些研究和努力,您可以连接到在线PDF合并API,以生成单个PDF文件。除此之外,在Google提供一种导出所有表格的PDF的方法之前,您只能使用单独的文件。请参见Gilbert的调整方式以获取多个工作表!

脚本:

/**
 * Export one or all sheets in a spreadsheet as PDF files on user's Google Drive,
 * in same folder that contained original spreadsheet.
 *
 * Adapted from https://code.google.com/p/google-apps-script-issues/issues/detail?id=3579#c25
 *
 * @param {String}  optSSId       (optional) ID of spreadsheet to export.
 *                                If not provided, script assumes it is
 *                                sheet-bound and opens the active spreadsheet.
 * @param {String}  optSheetId    (optional) ID of single sheet to export.
 *                                If not provided, all sheets will export.
 */
function savePDFs( optSSId, optSheetId ) {

  // If a sheet ID was provided, open that sheet, otherwise assume script is
  // sheet-bound, and open the active spreadsheet.
  var ss = (optSSId) ? SpreadsheetApp.openById(optSSId) : SpreadsheetApp.getActiveSpreadsheet();
  
  // Get URL of spreadsheet, and remove the trailing 'edit'
  var url = ss.getUrl().replace(/edit$/,'');

  // Get folder containing spreadsheet, for later export
  var parents = DriveApp.getFileById(ss.getId()).getParents();
  if (parents.hasNext()) {
    var folder = parents.next();
  }
  else {
    folder = DriveApp.getRootFolder();
  }
  
  // Get array of all sheets in spreadsheet
  var sheets = ss.getSheets();
  
  // Loop through all sheets, generating PDF files.
  for (var i=0; i<sheets.length; i++) {
    var sheet = sheets[i];
    
    // If provided a optSheetId, only save it.
    if (optSheetId && optSheetId !== sheet.getSheetId()) continue; 
    
    //additional parameters for exporting the sheet as a pdf
    var url_ext = 'export?exportFormat=pdf&format=pdf'   //export as pdf
        + '&gid=' + sheet.getSheetId()   //the sheet's Id
        // following parameters are optional...
        + '&size=letter'      // paper size
        + '&portrait=true'    // orientation, false for landscape
        + '&fitw=true'        // fit to width, false for actual size
        + '&sheetnames=false&printtitle=false&pagenumbers=false'  //hide optional headers and footers
        + '&gridlines=false'  // hide gridlines
        + '&fzr=false';       // do not repeat row headers (frozen rows) on each page

    var options = {
      headers: {
        'Authorization': 'Bearer ' +  ScriptApp.getOAuthToken()
      }
    }

    var response = UrlFetchApp.fetch(url + url_ext, options);
    
    var blob = response.getBlob().setName(ss.getName() + ' - ' + sheet.getName() + '.pdf');

    //from here you should be able to use and manipulate the blob to send and email or create a file per usual.
    //In this example, I save the pdf to drive
    folder.createFile(blob);
  }
}

/**
 * Dummy function for API authorization only.
 * From: https://stackoverflow.com/a/37172203/1677912
 */
function forAuth_() {
  DriveApp.getFileById("Just for authorization"); // https://code.google.com/p/google-apps-script-issues/issues/detail?id=3579#c36
}

虽然Drive允许多重包含,但是你的意思是一个文件可以存储在多个文件夹中? - Sujay Phadke
@SujayPhadke - 没错。(因为文件夹实际上并不是容器。)这类似于 Gmail 如何使用标签。 - Mogsdad
我明白了。虽然那样很不错,但你怎么做呢?右键单击会出现“移动”或“复制”等选项,这些似乎与此无关。 - Sujay Phadke
可以通过Drive API或Google Apps脚本中的Drive服务以编程方式完成。(Drive Web UI不再允许您添加多个文件夹,但底层服务可行。) - Mogsdad
很酷,我会试一下。但是,如果在Web界面中查看它,是否有办法通过标签或其他方式知道特定文件包含在多个文件夹中? - Sujay Phadke
显示剩余2条评论

3
我还没有足够的声望来评论,但似乎有一个小问题与上面Gilbert W提交的答案有关...虽然我也有可能是理解错了什么。
该解决方案包括以下行:
  + (optSheetId ? ('&gid=' + sheet.getSheetId()) : ('&id=' + ss.getId()))

然而,在此之前的代码中没有定义“sheet”。在Mogsdad的代码中,“sheet”是在被移除的循环内定义的。
for (var i=0; i<sheets.length; i++) {
var sheet = sheets[i];

“sheets”被定义为

var sheets = ss.getSheets();

该解决方案适用于想要打印整个电子表格的人,这是提出的问题。然而,该代码不再适用于想要打印单个页面的人。
另一个问题是Gilbert更新的代码HTML请求包含对sheet ID的引用,但没有包含对电子表格本身的引用。如果提供了特定的sheet ID,则响应会失败,但如果未提供sheet ID,则正常工作。通过将URL基础恢复到Mogsdad的方式,我使其再次工作。
另一个微调是:Gilbert的代码自动将新的.PDF命名为电子表格的名称。同时,Mogsdad的代码逐个打印每个工作表,并将每个.PDF以电子表格名称加上当前工作表的名称命名。如果适用,我希望使用单个工作表的名称打印PDF,并提供用户指定输出名称的能力。
由于不存在"getSheetById"方法,根据您代码的上下文,将"optSheetName"作为函数的参数可能更有意义,而不是"optSheetID"。如果需要,可以从"getSheetByName"获取sheet ID,我认为用户通常更可能拥有sheet的名称而不是sheet的ID。两者都可以从绑定脚本中以编程的方式获取名称和ID,但只有名称可以用于获取特定的现有sheet。
我还添加了一个可选的电子邮件参数,以便您可以同时打印并发送PDF。以下是我的版本:
function savePDFs( optSSId , optSheetName , optOutputName, optEmail) {

  // If a sheet ID was provided, open that sheet, otherwise assume script is
  // sheet-bound, and open the active spreadsheet.
  var ss = (optSSId) ? SpreadsheetApp.openById(optSSId) : SpreadsheetApp.getActiveSpreadsheet();
  var optSheetId = ss.getSheetByName(optSheetName).getSheetId();
  var outputName = (optOutputName ? optOutputName : (optSheetName ? optSheetName : ss.getName())) 

  // Get folder containing spreadsheet, for later export
  var parents = DriveApp.getFileById(ss.getId()).getParents();
  if (parents.hasNext()) {
    var folder = parents.next();
  }
  else {
    folder = DriveApp.getRootFolder();
  }

  var url_base = ss.getUrl().replace(/edit$/,'');

  //additional parameters for exporting the sheet as a pdf
  var url_ext = 'export?exportFormat=pdf&format=pdf'   //export as pdf

      // Print either the entire Spreadsheet or the specified sheet if optSheetId is provided
      + (optSheetId ? ('&gid=' + optSheetId) : ('&id=' + ss.getId()))      // Print either the entire Spreadsheet or the specified sheet if optSheetId is provided
      // following parameters are optional...
      + '&size=letter'      // paper size
      + '&portrait=true'    // orientation, false for landscape
      + '&fitw=true'        // fit to width, false for actual size
      + '&sheetnames=false&printtitle=false&pagenumbers=false'  //hide optional headers and footers
      + '&gridlines=false'  // hide gridlines
      + '&fzr=false';       // do not repeat row headers (frozen rows) on each page

  var options = {
    headers: {
      'Authorization': 'Bearer ' +  ScriptApp.getOAuthToken(),
    }
  }
  var response = UrlFetchApp.fetch(url_base + url_ext, options);
  var blob = response.getBlob().setName((outputName)+ '.pdf');
  folder.createFile(blob);

  GmailApp.sendEmail(optEmail, "Here is a file named " + outputName, "Please let me know if you have any questions or comments.", {attachments:blob});
} 

谢谢这个。它被证明非常有用。有一件事:optSheetName实际上并不是可选的。如果您没有指定它,那么“var optSheetId = ss.getSheetByName(optSheetName).getSheetId();”这行代码将会失败,因为getSheetByName()返回null。 - Andrew Roberts

2

以下是我根据Dr Queso的答案改编的主题变化。

所有参数(在代码中描述)都是可选的,如果没有指定,则使用活动电子表格,将所有选项卡转换为一个名为电子表格的PDF,并且不会通过电子邮件发送PDF文件。

function test() {

  // Create a PDF containing all the tabs in the active spreadsheet, name it
  // after the spreadsheet, and email it
  convertSpreadsheetToPdf('user@email.com')

  // Create a PDF containing all the tabs in the spreadsheet specified, name it
  // after the spreadsheet, and email it
  convertSpreadsheetToPdf('user@email.com', '1r9INcnsyvSQmeduJWVYAvznOOYei9jeAjsy0acA3G1k')

  // Create a PDF just containing the tab 'Sheet2' in the active spreadsheet, specify a name, and email it
  convertSpreadsheetToPdf('user@email.com', null, 'Sheet2', 'PDF 3')
}

/*
 * Save spreadsheet as a PDF
 *     
 * @param {String} email Where to send the PDF [OPTIONAL] 
 * @param {String} spreadsheetId Or the active spreadsheet[OPTIONAL]
 * @param {String} sheetName The tab to output [OPTIONAL]
 * @param {String} PdfName [OPTIONAL]
 */

function convertSpreadsheetToPdf(email, spreadsheetId, sheetName, pdfName) {

  var spreadsheet = spreadsheetId ? SpreadsheetApp.openById(spreadsheetId) : SpreadsheetApp.getActiveSpreadsheet();
  spreadsheetId = spreadsheetId ? spreadsheetId : spreadsheet.getId()  
  var sheetId = sheetName ? spreadsheet.getSheetByName(sheetName).getSheetId() : null;  
  var pdfName = pdfName ? pdfName : spreadsheet.getName();
  var parents = DriveApp.getFileById(spreadsheetId).getParents();
  var folder = parents.hasNext() ? parents.next() : DriveApp.getRootFolder();
  var url_base = spreadsheet.getUrl().replace(/edit$/,'');

  var url_ext = 'export?exportFormat=pdf&format=pdf'   //export as pdf

      // Print either the entire Spreadsheet or the specified sheet if optSheetId is provided
      + (sheetId ? ('&gid=' + sheetId) : ('&id=' + spreadsheetId)) 
      // following parameters are optional...
      + '&size=letter'      // paper size
      + '&portrait=true'    // orientation, false for landscape
      + '&fitw=true'        // fit to width, false for actual size
      + '&sheetnames=false&printtitle=false&pagenumbers=false'  //hide optional headers and footers
      + '&gridlines=false'  // hide gridlines
      + '&fzr=false';       // do not repeat row headers (frozen rows) on each page

  var options = {
    headers: {
      'Authorization': 'Bearer ' +  ScriptApp.getOAuthToken(),
    }
  }

  var response = UrlFetchApp.fetch(url_base + url_ext, options);
  var blob = response.getBlob().setName(pdfName + '.pdf');
  folder.createFile(blob);

  if (email) {

    var mailOptions = {
      attachments:blob
    }

    MailApp.sendEmail(
      email, 
      "Here is a file named " + pdfName, 
      "Please let me know if you have any questions or comments.", 
      mailOptions);
  }

} // convertSpreadsheetToPdf()

注意:如果表格被隐藏了,这个方法无法使用。可以使用activate()方法来取消隐藏。


哇,这是一个很棒的脚本。我想尝试在我的医院使用它,每月向经理发送摘要表格。我已经能够使用谷歌的 query= 函数将数据提取到每个月的单独选项卡中。如何使用此脚本并在特定日期向具有特定选项卡的人发送自动电子邮件?也就是说,我想在2月3日发送 1月 选项卡。或者在3月3日发送 2月 选项卡... 这可能吗? - user1837608
抱歉回复晚了,我错过了通知。你可以设置一个每月的时钟触发器 - https://developers.google.com/apps-script/reference/script/clock-trigger-builder - 来完成这个任务。 - Andrew Roberts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接