網站搜尋

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 請求配額等都無法被此方法繞過。