Google Apps Script超最大执行时间 分批续跑逻辑失效求助
现有代码超时、逻辑不生效的核心原因
- 分片续跑逻辑完全没跑通:函数启动第一行就把存储进度的D1单元格重置为0,紧接着直接调用全量导出函数,这个全量导出会遍历所有日历、逐行写表,本身就会直接触发执行时长限制,后面的循环、触发器逻辑根本没有执行机会。
- 存在未定义变量:循环中使用的
xngData从未被赋值,就算全量导出没超时,执行到这一步也会直接抛出引用错误。 - 写入性能极差:导出逻辑每处理1条事件就单独调用1次值写入、7次公式设置,Apps Script的表格读写接口存在固定网络开销,逐行操作的耗时是批量写入的几十上百倍,是超时的核心诱因。
- 分片逻辑冲突:导出函数每次启动第一行就清空整张导出表,就算实现了分片续跑,下一次执行会直接清空上一次分片写入的内容,永远无法得到完整结果。
- 时间阈值设置不合理:注释标注的时间阈值和实际代码设置值不符,且没有给状态写入、触发器创建预留冗余时间,很容易卡着时间点触发超时。
优化实现方案
不用拆分8份独立脚本,直接用文档属性存储执行进度,把逐行读写替换为批量操作,按日历粒度分片,每次执行尽可能多处理任务,临近执行时限自动存储进度、创建续跑触发器,全程无需人工干预。
优化后批量写入的性能比原实现高10~100倍,大部分数据量场景下甚至不需要分片就能一次执行完成。
// 统一配置常量,无需在代码中零散查找修改 const CONFIG = { CAL_SHEET_NAME: "Id Calendriers - Dates Debut et Fin", EXPORT_SHEET_NAME: "Extraction 1 - Calendrier", TIME_LIMIT_MS: 1000 * 60 * 4.5, // 预留30秒执行状态存储、触发器创建,避免卡线超时 TRIGGER_DELAY_MS: 1000 * 30, // 下次执行延迟30秒,避免触发接口频控 HEADER: [["Titre", "Description", "Location", "Début", "Fin", "Heures effectives","Extraction 2","Extraction 3","Heures Planifiées", "Vacances", "Maladie","Congé légal", "Absence"]] } function update_main_master() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const calSheet = ss.getSheetByName(CONFIG.CAL_SHEET_NAME); const exportSheet = ss.getSheetByName(CONFIG.EXPORT_SHEET_NAME); const docProps = PropertiesService.getDocumentProperties(); // 读取上次执行中断的位置,首次执行默认从0开始 let currentCalendarIndex = Number(docProps.getProperty('last_processed_cal_index')) || 0; const startTime = new Date(); // 首次执行时清空导出表、写入表头 if (currentCalendarIndex === 0) { exportSheet.clear(); exportSheet.getRange(7,1,1,13).setValues(CONFIG.HEADER); } // 一次性读取所有配置、日历ID,避免循环内重复读表 const startDate = calSheet.getRange('k1').getValue(); const endDate = calSheet.getRange('k2').getValue(); const allCalendars = calSheet.getRange('b3:B').getValues().flat().filter(id => id !== ""); const allRowData = []; const allFormulas = []; // 从上次中断的日历位置开始处理 while (currentCalendarIndex < allCalendars.length) { // 处理每个日历前先判断剩余执行时间,不足则提前中断 if (new Date() - startTime > CONFIG.TIME_LIMIT_MS) break; const calId = allCalendars[currentCalendarIndex]; const cal = CalendarApp.getCalendarById(calId); if (!cal) { currentCalendarIndex++; continue; } const events = cal.getEvents(startDate, endDate); // 把当前日历所有事件的静态值、公式全部攒到内存,不逐行写表 events.forEach(event => { const rowIndex = exportSheet.getLastRow() + allRowData.length + 1; // 静态字段值 allRowData.push([ event.getTitle(), event.getDescription(), event.getLocation(), event.getStartTime(), event.getEndTime(), "", "", "", "", "", "", "", "" ]); // 对应行的公式,按列顺序拼接 allFormulas.push([ `=(HOUR(RIGHT(B${rowIndex};5))+(MINUTE(RIGHT(B${rowIndex};5))/60))-(HOUR(LEFT(B${rowIndex};5))+(MINUTE(LEFT(B${rowIndex};5))/60))`, `=IFERROR(TEXT(INDEX(SPLIT(A${rowIndex};" ");2);"hh:mm");"")`, `=IFERROR(TEXT(INDEX(SPLIT(A${rowIndex};" ");3);"hh:mm");"")`, `=IF(OR(G${rowIndex}="Maladie";G${rowIndex}="Congé";G${rowIndex}="Absence";G${rowIndex}="00:00";G${rowIndex}="Vacances");0;(HOUR(H${rowIndex})+(MINUTE(H${rowIndex})/60))-(HOUR(G${rowIndex})+(MINUTE(G${rowIndex})/60)))`, `=IF(IFNA(VLOOKUP(D${rowIndex}; feries;1;FALSE);1)<>1;0;IF(AND(G${rowIndex}="00:00";H${rowIndex}="Vacances");0,5;IF(G${rowIndex}="Vacances";1;0)))`, `=IF(G${rowIndex}="Maladie";1;0)`, `=IF(G${rowIndex}="Congé";1;0)`, `=IF(G${rowIndex}="Absence";1;0)` ]); }); currentCalendarIndex++; } // 攒完一批数据一次性写入表格,性能比逐行写高几十倍 if (allRowData.length > 0) { const startWriteRow = exportSheet.getLastRow() + 1; // 批量写入静态值 exportSheet.getRange(startWriteRow, 1, allRowData.length, 13).setValues(allRowData); // 批量写入公式 exportSheet.getRange(startWriteRow, 6, allFormulas.length, 8).setFormulas(allFormulas); // 统一设置工时列数字格式 exportSheet.getRange(startWriteRow, 6, allFormulas.length, 1).setNumberFormat('.00'); } // 判断是否处理完全部日历 if (currentCalendarIndex < allCalendars.length) { // 存储当前进度 docProps.setProperty('last_processed_cal_index', currentCalendarIndex.toString()); // 清理旧的续跑触发器,避免重复触发 ScriptApp.getProjectTriggers().forEach(t => { if (t.getHandlerFunction() === 'update_main_master') ScriptApp.deleteTrigger(t); }); // 创建新触发器下次续跑 ScriptApp.newTrigger("update_main_master") .timeBased() .after(CONFIG.TRIGGER_DELAY_MS) .create(); } else { // 全部处理完成,清理进度标记,写入更新时间 docProps.deleteProperty('last_processed_cal_index'); calSheet.getRange('e1').setValue('Updated'); calSheet.getRange('f1').setValue(new Date()); // 清理所有续跑触发器 ScriptApp.getProjectTriggers().forEach(t => { if (t.getHandlerFunction() === 'update_main_master') ScriptApp.deleteTrigger(t); }); } }
使用注意事项
- 首次运行前,删除项目中所有之前手动创建的定时触发器,避免重复执行导出。
- 如果存在单个日历事件量过万的极端场景,可以把分片粒度从「单个日历」进一步拆分为「单日历的事件分页」,只需要把存储的进度值从「日历索引」改为「日历索引+事件偏移量」即可,绝大多数业务场景不需要做这层调整。
- 所有和表格、日历的交互尽量批量操作,不要在循环里重复发起读写请求,这是规避Apps Script执行超时最有效的手段。
内容的提问来源于stack exchange,提问作者Julien
相关产品推荐
相关产品推荐

