如何规避Google Apps脚本Maximum Execution Time Limit并实现断点续爬?
Google Drive文件列表脚本超时断点续跑方案
问题背景
现有脚本用于遍历Google Drive指定文件夹下的所有文件和文件夹,但因目录体量过大,频繁触发Maximum Execution Time Limit错误,且每次报错后需从头开始遍历,效率极低。需要修改脚本实现断点续跑,从最后一条已记录的位置继续执行。
核心思路
要实现断点续跑,关键是记录并恢复遍历进度:
- 仅首次运行初始化表头,后续运行保留已有记录,避免重复清空表格
- 用脚本属性保存当前遍历到的文件夹ID、路径,以及文件分页标记(pageToken)
- 改用迭代+分页API替代递归,既避免调用栈溢出,又方便保存进度
- 提前检测超时风险,保存当前进度后退出,下次运行自动读取进度继续
修改后的完整代码
function onOpen() { var SS = SpreadsheetApp.getActiveSpreadsheet(); var ui = SpreadsheetApp.getUi(); ui.createMenu('List Files/Folders') .addItem('开始/继续遍历', 'startOrResumeTraversal') .addItem('重置遍历进度', 'resetTraversal') .addToUi(); }; // 开始或继续遍历流程 function startOrResumeTraversal(){ var folderId = Browser.inputBox('输入目标文件夹ID', Browser.Buttons.OK_CANCEL); if (folderId === "") { Browser.msgBox('文件夹ID无效'); return; } var sheet = SpreadsheetApp.getActiveSheet(); var props = PropertiesService.getScriptProperties(); // 首次运行初始化表头和进度 if (sheet.getLastRow() === 0) { sheet.appendRow(["Full Path", "Name","Type" ,"Date", "URL", "Last Updated", "Description", "Size","Owner Email"]); var rootFolder = DriveApp.getFolderById(folderId); props.setProperties({ rootFolderId: folderId, currentFolderPath: rootFolder.getName(), currentFolderId: folderId, filePageToken: "", folderQueue: JSON.stringify([folderId]) // 待遍历文件夹队列 }); } // 读取已保存的进度 var progress = props.getProperties(); var folderQueue = JSON.parse(progress.folderQueue); var currentFolderId = progress.currentFolderId; var filePageToken = progress.filePageToken; var currentFolderPath = progress.currentFolderPath; try { // 先处理当前文件夹未完成的文件遍历 if (filePageToken !== "") { filePageToken = processFilesInFolder(currentFolderId, currentFolderPath, sheet, filePageToken); // 如果文件还没处理完且时间快到了,保存进度退出 if (filePageToken !== "" && isTimeAlmostUp()) { saveProgress(props, progress.rootFolderId, currentFolderPath, currentFolderId, filePageToken, folderQueue); Browser.msgBox("时间即将耗尽,已保存进度,下次点击继续即可"); return; } } // 遍历待处理文件夹队列 while (folderQueue.length > 0 && !isTimeAlmostUp()) { var targetFolderId = folderQueue.shift(); var targetFolder = DriveApp.getFolderById(targetFolderId); var targetFolderPath = progress.rootFolderId === targetFolderId ? targetFolder.getName() : currentFolderPath + "/" + targetFolder.getName(); // 记录当前文件夹信息 var folderData = [ targetFolderPath, targetFolder.getName(), "Folder", targetFolder.getDateCreated(), targetFolder.getUrl(), targetFolder.getLastUpdated(), targetFolder.getDescription(), targetFolder.getSize()/1024, targetFolder.getOwner().getEmail() ]; sheet.appendRow(folderData); // 处理当前文件夹下的文件 filePageToken = processFilesInFolder(targetFolderId, targetFolderPath, sheet, ""); // 如果文件未处理完,保存进度退出 if (filePageToken !== "") { saveProgress(props, progress.rootFolderId, targetFolderPath, targetFolderId, filePageToken, folderQueue); Browser.msgBox("时间即将耗尽,已保存进度,下次点击继续即可"); return; } // 将子文件夹加入待处理队列 var childFolders = targetFolder.getFolders(); while (childFolders.hasNext()) { folderQueue.push(childFolders.next().getId()); } // 更新队列进度 props.setProperty("folderQueue", JSON.stringify(folderQueue)); currentFolderPath = targetFolderPath; } // 遍历完成,清空进度 if (folderQueue.length === 0) { props.deleteAllProperties(); Browser.msgBox("遍历完成!"); } } catch (e) { if (e.toString().includes("Maximum execution time exceeded")) { // 超时后保存进度 saveProgress(props, progress.rootFolderId, currentFolderPath, currentFolderId, filePageToken, folderQueue); Browser.msgBox("超时!已保存进度,下次点击继续遍历"); } else { Logger.log(e.toString()); Browser.msgBox("发生错误:" + e.toString()); } } }; // 处理单个文件夹内的文件,返回下一页token(若有剩余) function processFilesInFolder(folderId, folderPath, sheet, pageToken) { var folder = DriveApp.getFolderById(folderId); var files; do { files = pageToken === "" ? folder.getFiles() : folder.getFilesContinuationToken(pageToken); while (files.hasNext() && !isTimeAlmostUp()) { var file = files.next(); var fileData = [ folderPath + "/" + file.getName(), file.getName(), "File", file.getDateCreated(), file.getUrl(), file.getLastUpdated(), file.getDescription(), file.getSize()/1024, file.getOwner().getEmail() ]; sheet.appendRow(fileData); } pageToken = files.getContinuationToken(); if (pageToken !== "" && isTimeAlmostUp()) { return pageToken; } } while (pageToken !== "" && !isTimeAlmostUp()); return ""; }; // 检查是否即将超时(预留10秒保存进度) function isTimeAlmostUp() { var executionTime = new Date().getTime() - ScriptApp.getScriptTime(); return executionTime > (ScriptApp.getMaxExecutionTime() - 10) * 1000; }; // 保存当前遍历进度 function saveProgress(props, rootFolderId, currentFolderPath, currentFolderId, filePageToken, folderQueue) { props.setProperties({ rootFolderId: rootFolderId, currentFolderPath: currentFolderPath, currentFolderId: currentFolderId, filePageToken: filePageToken, folderQueue: JSON.stringify(folderQueue) }); }; // 重置遍历进度和表格 function resetTraversal() { var sheet = SpreadsheetApp.getActiveSheet(); sheet.clear(); PropertiesService.getScriptProperties().deleteAllProperties(); Browser.msgBox("已重置进度和表格"); };
关键改进说明
- 进度持久化:使用
PropertiesService保存遍历状态,包括待处理文件夹队列、当前文件夹信息、文件分页token,确保超时后能精准恢复 - 超时预判:
isTimeAlmostUp函数提前10秒检测超时风险,预留足够时间保存进度 - 分页遍历:利用Drive API的续传token实现文件分页加载,避免一次性读取大量文件导致超时
- 队列替代递归:改用文件夹队列管理遍历顺序,避免递归调用栈溢出,同时更易控制进度
- 操作人性化:新增「重置遍历进度」菜单,方便用户重新开始任务
内容的提问来源于stack exchange,提问作者Hatem Husam
相关产品推荐
相关产品推荐

