javascript - 使用 Drive API/DriveApp 將 PDF 轉換為 Google 文件
這個問題已經成功解決。我正在編輯我的文章,以記錄我的經驗,供後世和未來參考。
任務
我有117個PDF檔案(平均大小約238 KB)上傳到Google雲端硬碟。我想要把它們全部轉換成Google文件,並保存在不同的硬碟資料夾中。
問題
我嘗試使用 Drive.Files.insert 來轉換這些文件。然而,在大多數情況下,這種方法最多只能轉換 5 個文件,然後該函數會在出現此錯誤前過早終止
超出限制:DriveApp。(第 # 行,檔案「Code」)
上面提到的那一行是在呼叫 insert 函數時。第一次呼叫此函數後,後續的呼叫通常會立即失敗,且不會創建額外的 Google 文件。
方法
我使用了三種主要方法來達成我的目標。其中一種如上所述,是使用 Drive.Files.insert。其他兩種則涉及使用 Drive.Files.copy 以及發送一批 HTTP 請求。這最後兩種方法是由 Tanaike 提出的,我建議閱讀他下面的答案以獲取更多資訊。insert 和 copy 函數來自 Google Drive REST v2 API,而批量多個 HTTP 請求則來自 Drive REST v3。
使用 Drive.Files.insert 時,我遇到了處理執行限制的問題(在上方的問題部分已解釋)。一個解決方案是多次運行這些函數。為此,我需要一種方法來追踪哪些檔案已經被轉換。我有兩個選擇:使用試算表和使用續傳令牌。因此,我有四種不同的方法來測試:本段中提到的兩種、批次處理 HTTP 請求,以及調用 Drive.Files.copy。
因為團隊雲端硬碟的行為與一般雲端硬碟不同,我覺得有必要對每一種方法進行兩次嘗試,一次是將包含 PDF 的資料夾設為一般非團隊雲端硬碟資料夾,另一次則是將該資料夾放在團隊雲端硬碟下。總體而言,這意味著我有 8 種不同的方法需要測試。
這些正是我使用的函數。每個函數都使用了兩次,唯一的差異是源文件夾和目標文件夾的 ID(如上所述的原因):
方法 A:使用 Drive.Files.insert 和試算表
function toDocs() {
var sheet = SpreadsheetApp.openById(/* spreadsheet id*/).getSheets()[0];
var range = sheet.getRange("A2:E118");
var table = range.getValues();
var len = table.length;
var resources = {
title: null,
mimeType: MimeType.GOOGLE_DOCS,
parents: [{id: /* destination folder id */}]
};
var count = 0;
var files = DriveApp.getFolderById(/* source folder id */).getFiles();
while (files.hasNext()) {
var blob = files.next().getBlob();
var blobName = blob.getName();
for (var i=0; i<len; i++) {
if (table[i][0] === blobName.slice(5, 18)) {
if (table[i][4])
break;
resources.title = blobName;
Drive.Files.insert(resources, blob); // Limit Exceeded: DriveApp. (line 51, file "Code")
table[i][4] = "yes";
}
}
if (++count === 10) {
range.setValues(table);
Logger.log("time's up");
}
}
}
方法 B:使用 Drive.Files.insert 和續傳標記
function toDocs() {
var folder = DriveApp.getFolderById(/* source folder id */);
var sprop = PropertiesService.getScriptProperties();
var contToken = sprop.getProperty("contToken");
var files = contToken ? DriveApp.continueFileIterator(contToken) : folder.getFiles();
var options = {
ocr: true
};
var resource = {
title: null,
mimeType: null,
parents: [{id: /* destination folder id */}]
};
while (files.hasNext()) {
var blob = files.next().getBlob();
resource.title = blob.getName();
resource.mimeType = blob.getContentType();
Drive.Files.insert(resource, blob, options); // Limit Exceeded: DriveApp. (line 113, file "Code")
sprop.setProperty("contToken", files.getContinuationToken());
}
}
方法 C:使用 Drive.Files.copy
這個功能的功勞歸於 Tanaike —— 有關更多細節請參見他下面的回答。
function toDocs() {
var sourceFolderId = /* source folder id */;
var destinationFolderId = /* destination folder id */;
var files = DriveApp.getFolderById(sourceFolderId).getFiles();
while (files.hasNext()) {
var res = Drive.Files.copy({parents: [{id: destinationFolderId}]}, files.next().getId(), {convert: true, ocr: true});
Logger.log(res)
}
}
方法 D:發送批次 HTTP 請求
這個功能的功勞歸於 Tanaike —— 有關更多細節請參見他下面的回答。
function toDocs() {
var sourceFolderId = /* source folder id */;
var destinationFolderId = /* destination folder id */;
var files = DriveApp.getFolderById(sourceFolderId).getFiles();
var rBody = [];
while (files.hasNext()) {
rBody.push({
method: "POST",
endpoint: "https://www.googleapis.com/drive/v3/files/" + files.next().getId() + "/copy",
requestBody: {
mimeType: "application/vnd.google-apps.document",
parents: [destinationFolderId]
}
});
}
var cycle = 20; // Number of API calls at 1 batch request.
for (var i = 0; i < Math.ceil(rBody.length / cycle); i++) {
var offset = i * cycle;
var body = rBody.slice(offset, offset + cycle);
var boundary = "xxxxxxxxxx";
var contentId = 0;
var data = "--" + boundary + "\r\n";
body.forEach(function(e){
data += "Content-Type: application/http\r\n";
data += "Content-ID: " + ++contentId + "\r\n\r\n";
data += e.method + " " + e.endpoint + "\r\n";
data += e.requestBody ? "Content-Type: application/json; charset=utf-8\r\n\r\n" : "\r\n";
data += e.requestBody ? JSON.stringify(e.requestBody) + "\r\n" : "";
data += "--" + boundary + "\r\n";
});
var options = {
method: "post",
contentType: "multipart/mixed; boundary=" + boundary,
payload: Utilities.newBlob(data).getBytes(),
headers: {'Authorization': 'Bearer ' + ScriptApp.getOAuthToken()},
muteHttpExceptions: true,
};
var res = UrlFetchApp.fetch("https://www.googleapis.com/batch", options).getContentText();
// Logger.log(res); // If you use this, please remove the comment.
}
}
有效的和無效的
使用 Drive.Files.insert 的任何函數都無法運作。每個使用
insert進行轉換的函數都出現此錯誤超出限制:DriveApp。(第 # 行,檔案「Code」)
(行號已替換為通用符號)。找不到任何其他關於此錯誤的細節或描述。一個值得注意的變化是,我使用了試算表,PDF 則位於團隊雲端硬碟資料夾中;當其他所有方法都無法立即轉換任何檔案時,這個方法卻成功轉換了 5 個檔案後才失敗。然而,當考慮到為什麼這個變化比其他方法表現得更好時,我認為這更像是一個偶然事件,而不是與使用特定資源(試算表、團隊雲端硬碟等)有關的原因。
使用 Drive.Files.copy 和批次 HTTP 請求只有在來源資料夾是個人(非團隊雲端硬碟)資料夾時才有效。
在從團隊磁碟資料夾讀取時嘗試使用
copy函數會失敗,並出現此錯誤:找不到檔案:1RAGxe9a_-euRpWm3ePrbaGaX5brpmGXu(第 # 行,檔案「Code」)
(行號已替換為通用符號)。被引用的行是
var res = Drive.Files.copy({parents: [{id: destinationFolderId}]}, files.next().getId(), {convert: true, ocr: true});在從團隊磁碟機資料夾讀取時使用批次 HTTP 請求沒有任何作用——既不會創建文件,也不會拋出錯誤。函數在未完成任何操作的情況下悄無聲息地終止。
結論
如果你希望將大量的 PDF 轉換成 Google 文件或文字檔,請使用 Drive.Files.copy 或發送批次的 HTTP 請求,並確保 PDF 是存放在個人雲端硬碟而不是團隊硬碟中。
特別感謝 @tehhowch 對我的問題表現出如此濃厚的興趣,並且多次回來提供反饋,也感謝 @Tanaike 提供了代碼及說明,成功解決了我的問題(有一個注意事項,詳情請見上文)。
答案#1
你想將資料夾中的 PDF 檔案轉換為 Google 文件。PDF 檔案位於團隊硬碟的一個資料夾中。你想將轉換後的檔案匯入到你 Google 雲端硬碟的一個資料夾。如果我的理解正確,這個方法怎麼樣?
對於從 PDF 轉換為 Google 文件,不僅可以使用 Drive.Files.insert() 進行轉換,也可以使用 Drive.Files.copy()。使用 Drive.Files.copy() 的優點是
- 儘管
Drive.Files.insert()有 5 MB 的大小限制,Drive.Files.copy()可以使用超過 5 MB 的大小。 - 在我的環境中,處理速度比
Drive.Files.insert()更快。
對於這種方法,我想提出以下兩種模式。
範例 1:使用 Drive API v2
在這種情況下,使用進階 Google 服務的 Drive API v2 來轉換檔案。
function myFunction() {
var sourceFolderId = "/* source folder id */";
var destinationFolderId = "/* dest folder id */";
var files = DriveApp.getFolderById(sourceFolderId).getFiles();
while (files.hasNext()) {
var res = Drive.Files.copy({parents: [{id: destinationFolderId}]}, files.next().getId(), {convert: true, ocr: true});
// Logger.log(res) // If you use this, please remove the comment.
}
}
範例 2:使用 Drive API v3
在這種情況下,使用 Drive API v3 來轉換檔案。在這裡,我使用了批次請求來應對這種情況。因為批次請求可以一次 API 呼叫使用 100 個 API 呼叫。透過這個方式,可以解決 API 配額的問題。
function myFunction() {
var sourceFolderId = "/* source folder id */";
var destinationFolderId = "/* dest folder id */";
var files = DriveApp.getFolderById(sourceFolderId).getFiles();
var rBody = [];
while (files.hasNext()) {
rBody.push({
method: "POST",
endpoint: "https://www.googleapis.com/drive/v3/files/" + files.next().getId() + "/copy",
requestBody: {
mimeType: "application/vnd.google-apps.document",
parents: [destinationFolderId]
}
});
}
var cycle = 100; // Number of API calls at 1 batch request.
for (var i = 0; i < Math.ceil(rBody.length / cycle); i++) {
var offset = i * cycle;
var body = rBody.slice(offset, offset + cycle);
var boundary = "xxxxxxxxxx";
var contentId = 0;
var data = "--" + boundary + "\r\n";
body.forEach(function(e){
data += "Content-Type: application/http\r\n";
data += "Content-ID: " + ++contentId + "\r\n\r\n";
data += e.method + " " + e.endpoint + "\r\n";
data += e.requestBody ? "Content-Type: application/json; charset=utf-8\r\n\r\n" : "\r\n";
data += e.requestBody ? JSON.stringify(e.requestBody) + "\r\n" : "";
data += "--" + boundary + "\r\n";
});
var options = {
method: "post",
contentType: "multipart/mixed; boundary=" + boundary,
payload: Utilities.newBlob(data).getBytes(),
headers: {'Authorization': 'Bearer ' + ScriptApp.getOAuthToken()},
muteHttpExceptions: true,
};
var res = UrlFetchApp.fetch("https://www.googleapis.com/batch", options).getContentText();
// Logger.log(res); // If you use this, please remove the comment.
}
}
注意:
- 如果一次批次請求的 API 調用數量很大(目前的值是 100),請修改
var cycle=100。 - 如果無法將 Drive API v3 用於團隊硬碟,請告訴我。我可以將它轉換為 Drive API v2。
- 如果團隊雲端硬碟是你情況問題的原因,你能在它將 PDF 檔案複製到你的 Google 雲端硬碟後嘗試這個方法嗎?
參考:
- 批量請求
如果這些對你沒有用,我很抱歉。
答案#2
你可以首先在 Google 試算表中抓取並存儲所有文件的 ID。然後,你可以使用其 ID 正常處理每個文件。處理完之後,將該文件標記為已處理。在處理文件之前,檢查該文件是否已經被處理過。
如果有多個檔案,你也可以儲存已處理到的行號,下次從那裡繼續。
然後最後建立一個觸發器,大約每10分鐘執行一次你的函數。
透過這個方法,你可以克服單次執行的時間限制。但 API 請求配額等都無法被此方法繞過。
