【问题标题】:Problem with Google Apps Script maximum execution timeGoogle Apps 脚本最长执行时间问题
【发布时间】:2020-04-27 19:07:22
【问题描述】:

我是编码新手,最近我创建了一个 Google 脚本(基于其他两个脚本),它执行以下操作:

  1. 按主题行搜索 Gmail 草稿
  2. 获取 Gmail 草稿并将其用作模板来创建带有唯一附件的多个草稿
  3. 在草稿创建后添加确认短语。

代码如下:

//Change these to match the column names you are using for email recepient addresses and merge status column//
var RECIPIENT_COL  = "Email";
var MERGE_STATUS_COL = "M";

//Creates the menu item "Mail Merge" for user to run scripts on drop-down//
function onOpen(e) {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('Mail Merge')
  .addItem('???? Create Drafts', 'createDrafts').addToUi(); 
}

function createDrafts() {
  // search for the draft Gmail message to merge with by its subject line
  var subjectLine = Browser.inputBox("Select draft " + "to merge with:", "Paste the subject line:", Browser.Buttons.OK_CANCEL);

  if (subjectLine === "cancel" || subjectLine == ""){ 
   // if no subject line finish up
   return;
  }

  // get the draft Gmail message to use as a template
  var emailTemplate = getGmailTemplateFromDrafts_(subjectLine);
  emailTemplate.subject = subjectLine;

  // get the data from the active sheet
  var sheet = SpreadsheetApp.getActiveSheet();
  var dataRange = sheet.getDataRange();
  // fetch values for each row in the Range.
  var data = dataRange.getValues();
  // assuming row 1 contains our column headings
  var header = data.shift(); 

  // get the index of column named 'M' (Assume header names are unique)
  var draftCreatedColIdx = header.indexOf(MERGE_STATUS_COL);

  var object = data.map(function(row) {  
    // create a new object for next row using the header as a key
    var nextRowObject = header.reduce(function(accumulator, currentValue, currentIndex) {
      accumulator[currentValue] = row[currentIndex];      
      return accumulator;
    }, {}) // Use {} here rather than initialAccumulatorValue
    return nextRowObject;
  });

  // loop through all the rows of data
  object.forEach(function(row, rowIdx){

    // only create drafts if mail merge status cell is blank
    if (row[MERGE_STATUS_COL] === ''){

        var msgObj = fillInTemplateFromObject_(emailTemplate, row);
        var attachment_id = "File Name";

        // split the values taken from cell into array
        var pdfName = row[attachment_id].split(', ');
          // initialize files as empty array
          var files = []; 

          // run through cell values and perform search
          for(var j in pdfName){ 
            // perform the search,results is a FileIterator
            var results = DriveApp.getFilesByName(pdfName[j]); 
            // interate through files found and add to attachment results
            while(results.hasNext()) {
        // add files to array
        files.push(results.next());
            }
          }     

        // @see https://developers.google.com/apps-script/reference/gmail/gmail-app#sendemailrecipient-subject-body-options
        GmailApp.createDraft(row[RECIPIENT_COL], msgObj.subject, msgObj.text, {htmlBody: msgObj.html, attachments: files});
        // create a confirmation phrase in the first column
        sheet.getRange("A" + (rowIdx + 2)).setValue("DRAFT");    
     }
  }); 
}

/**
 * Get a Gmail draft message by matching the subject line.
 * @param {string} subject_line to search for draft message
 * @return {object} containing the plain and html message body
*/
function getGmailTemplateFromDrafts_(subject_line) {
  try {
    // get drafts
    var drafts = GmailApp.getDrafts();
    // filter the drafts that match subject line
    var draft = drafts.filter(subjectFilter_(subject_line))[0];
    // get the message object
    var msg = draft.getMessage();
    return {text: msg.getPlainBody(), html:msg.getBody()};
  } catch(e) {
    throw new Error("Oops - can't find Gmail draft");
  }
}

/**
 * Filter draft objects with the matching subject linemessage by matching the subject line.
 * @param {string} subject_line to search for draft message
 * @return {object} GmailDraft object
*/
function subjectFilter_(subject_line){
  return function(element) {
    if (element.getMessage().getSubject() === subject_line) {
      return element;
    }
  }
}

/**
 * Fill HTML string with data object.
 * @param {string} template string containing {{}} markers which are replaced with data
 * @param {object} data object used to replace {{}} markers
 * @return {object} message replaced with data
 * H/T https://developers.google.com/apps-script/articles/mail_merge
*/
function fillInTemplateFromObject_(template, data) {
  // convert object to string for simple find and replace
  template = JSON.stringify(template);
  // Search for all the variables to be replaced, for instance {{Column name}}
  var templateVars = template.match(/{{([^}]+)}}/g);

  // Replace variables from the template with the actual values from the data object.
  // If no value is available, replace with the empty string.
  for (var i = 0; i < templateVars.length; ++i) {
    // strip out {{ }} 
    var variableData = data[templateVars[i].substring(2, templateVars[i].length - 2)];
    template = template.replace(templateVars[i], variableData || "");
  }
  // convert back to object
  return JSON.parse(template);
}

脚本按预期工作,但是当我尝试处理带有太多附件的太多行时它超过了 6 分钟的 Google 脚本最长执行时间。

在尝试解决这个问题时,我发现了一个简单的script,它使用了continuationToken,这样做永远不会超过限制。 我的目标是尝试在我自己的脚本中使用相同的原理并处理数十行。不幸的是,到目前为止我还没有运气,需要一些帮助。这是我找到的脚本的代码:

代码.gs

function onOpen() {
  SpreadsheetApp.getUi().createMenu("List Drive files").addItem('Start', 'start').addToUi();
}

function start() {
  var ui = HtmlService.createHtmlOutputFromFile('ui');
  return SpreadsheetApp.getUi().showSidebar(ui);
}

function getDriveFiles(continuationToken) {
  if(continuationToken) {
    var files = DriveApp.continueFileIterator(continuationToken);
  }
  else {
    var files = DriveApp.getFiles();
  }
  var i = 0;
  while (files.hasNext() && i < 10) {
    var file = files.next();
    SpreadsheetApp.getActiveSheet().appendRow([file.getName(), file.getUrl()]);
    i++;
    if(i == 10) {
      return files.getContinuationToken();
    }
  }
} 

ui.html

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<div style="text-align:center; margin-top:10px">
<div>Files processed:</div>
<div id="nbOfFilesProcessed">0</div>
<br>
<button id="startButton" class="blue" onclick="start()">Start</button>
<div class="secondary">Close the sidebar to stop the script.</div>
</div>

<script>
function start() {
  document.getElementById("startButton").disabled = true;
  google.script.run.withSuccessHandler(onSuccess).getDriveFiles();
}
function onSuccess(continuationToken){
  // If server function returned a continuationToken it means the task is not complete
  // so ask the server to process a new batch.
  if(continuationToken) {
    var nbOfFilesProcessedEl = document.getElementById("nbOfFilesProcessed");
    nbOfFilesProcessedEl.innerHTML = parseInt(nbOfFilesProcessedEl.innerHTML) + 10;
    google.script.run.withSuccessHandler(onSuccess).getDriveFiles(continuationToken);
  }
}
</script> 

【问题讨论】:

  • 你必须比need some help更具体
  • 我希望能够以小批量(10 个文件)处理我的脚本中的行,然后返回客户端再次调用服务器并处理新的批处理(如我发布的第二个脚本)。这样做,据我所知,计时器将被重置,我永远不会接近 6 分钟的限制。不过,我不确定该怎么做。这就是我需要帮助的原因
  • 那么你只需要使用 google.script.run.withSuccessHandler().runBatch(obj);对象包含运行另一批次所需的所有信息。在每个批次结束时,您将信息存储在属性服务或缓存服务中,以便在您开始另一个批次时它具有下一个批次的当前信息。并且您在每批之后返回到 .withSuccessHandler() 。然后您可以使用计时器开始另一批。
  • 您仍然必须保持在每日配额内。

标签: javascript html google-apps-script google-sheets google-apps-script-editor


【解决方案1】:

根据我在您发布的代码中看到的内容,您必须以这种方式编辑您的 createDrafts 函数:

  • 编辑函数的触发方式:您必须使用 HTML ui 元素在其中运行 javascript。
  • 编辑 while 循环,使其在达到批处理限制时具有 return 语句。
  • 在 HTML ui 元素中创建一个 Javascript 函数,用于处理 createDrafts 函数的成功,并在返回 continuationToken 时递归调用它。

片段

界面组件

您可以保留您的自定义菜单,并在单击时将此 HTML 添加到 UI 对话框。

- code.gs -
//Creates the menu item "Mail Merge" for user to run scripts on drop-down//
function onOpen(e) {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('Mail Merge')
  .addItem('? Create Drafts', 'openDialog').addToUi(); 
}
function openDialog() {
  // Display a modal dialog box with custom HtmlService content.
  var htmlOutput = HtmlService
      .createHtmlOutputFromFile('Dialog')
      .setWidth(250)
      .setHeight(300);
  SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Create Drafts');
}
- Dialog.html -
<!-- The UI will be very similar to the one you found, I will keep only the strictly necessary statements for this example -->
<div>
<button id="startButton" onclick="startBatch()">Start</button>
</div>

<script>
function startBatch() {
  google.script.run.withSuccessHandler(onSuccess).createDrafts();
}
function onSuccess(continuationToken){
  // If server function returned a continuationToken it means the task is not complete
  // so ask the server to process a new batch.
  if(continuationToken) {
  google.script.run.withSuccessHandler(onSuccess).createDrafts(continuationToken);
  }
}
</script> 

应用脚本组件

function createDrafts(continuationToken) {
 var batchLimit = 10;
 // ...
 // run through cell values and perform search
 for(var j in pdfName){ 
     // perform the search,results is a FileIterator
     if (continuationToken) {
         var results = DriveApp.continueFileIterator(continuationToken);
     } else {
         var results = DriveApp.getFilesByName(pdfName[j]);
     } 
     // interate through files found and add to attachment results
     let i = 0;
     while(results.hasNext() && i<batchLimit) {
         // add files to array
         files.push(results.next());
         i++;
         if (i === batchLimit) {
             return results.getContinuationToken();
         }
     }
 }     

最后的考虑

作为对您的批处理操作的改进,我将保存所有用户输入,以便您无需再次提示即可继续执行脚本。您可以将这些值传递给 javascript 对象的返回函数,也可以使用 CacheService 实用程序将它们保存在缓存中。

此外,请尝试在执行时间和批处理限制之间找到正确的权衡:小批量限制永远不会达到时间限制,但会很快消耗您的配额。

参考资料:

Client Side API

Cache Service

Apps Script UI

【讨论】:

  • 哇!非常感谢您的回复。你说得非常清楚,现在它似乎正在工作。我现在唯一遇到的两个小问题是 html 文件中的这些代码行不会更新创建的草稿数量:var nbOfFilesProcessedEl = document.getElementById("nbOfFilesProcessed"); nbOfFilesProcessedEl.innerHTML = parseInt(nbOfFilesProcessedEl.innerHTML) + 10; 另外,我正在努力寻找插入确认短语的方法(草稿) 处理完每批 10 个之后。这些更改将帮助我查看脚本是否成功运行。
  • 您是否将&lt;div id="nbOfFilesProcessed"&gt;0&lt;/div&gt; 放入您的html 对话框中? “此外,我正在努力寻找一种方法来在每批 10 个处理完后插入我的确认短语 (DRAFT)”你是什么意思?哪个代码指令插入确认短语?如果此解决方案解决了您的问题,您应该接受答案并针对您的其他问题提出另一个问题。这将使您的帖子对 Stack Overflow 社区中的其他人有用,并使事情更有条理。谢谢!
  • 我确实放了你提到的那行代码,但它没有更新 html 文件。这是我的原始脚本中用于放置确认短语的代码:sheet.getRange("A" + (rowIdx + 2)).setValue("DRAFT"); 在处理完所有行后,它在第一列中放置了一个单词“DRAFT”。当有大约 10-20 行要处理但有大约 100 行时它是无用的,因为没有视觉指示器表明脚本实际上正常工作,除非您转到草稿文件夹并重新加载它或等到脚本停止。是的,我会接受你的回答。
  • 嘿,迈克尔,你刚刚投了赞成票。 Accepting the answer.
  • 亲爱的 Alessandro,我刚刚运行了大约 70 行的最终测试,每行有两个附件,并且您修改的脚本在运行大约 6 分钟后仍然停止。也许我做错了什么,但由于某种原因它没有任何效果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-15
  • 2021-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多