LoginSignup
3
1

More than 3 years have passed since last update.

[GAS]Utilities.unzip()で発生するエラーの原因と解決策

Posted at

Google Apps Script(以下GAS)で実装されているUtilities.unzip()についてのTipsです。

事象

Exception: Utilities オブジェクトでの unzip メソッドまたはプロパティの取得中に予期しないエラーが発生しました。

後述しますが、このエラーの原因を調査したところ、対象の添付ファイルはGASのAPIでは解凍できないことがわかりました。
従って、このエラーに対する代替策を考えました。

エラーの原因

さて、このエラーですが、圧縮ファイル内の文字コードが原因でした。

通常は以下のようなコードになるかと思います。

var targetId = "Drive上のファイルID"
var currentDirId = "Drive上の親フォルダID(解凍したファイル保管用)"

// zipファイルのBlobを取得
var zipblob = DriveApp.getFileById(targetId).getBlob()

// 解凍して中身を抽出
var files = Utilities.unzip(zipblob)

var folder = DriveApp.getFolderById(currentDirId)

// 中身のファイルをフォルダに作成
for(i=0; i<files.length; i++){
  folder.createFile(files[i])
}

以下の手順で問題を切り分けて行きました。

ローカル上(Mac)で解凍したらどうなるか

こちらは問題なくダウンロード、解凍が行われました。
ファイル名も内容も特に問題はありません。

解凍したものを再度zip圧縮してunzip()にかけたらどうなるか

こちらはエラーが発生せず、正常に解凍されました。
zipファイルが壊れているわけでも、unzip()そもそもが動かない、というわけではないようです。

Pythonで解凍してみたらどうなるか

試しに普通に解凍してみました。

import zipfile

with zipfile.ZipFile('drive/My Drive/test.zip') as existing_zip:
    existing_zip.extractall('drive/My Drive/test')

ここでようやく気づいたのですが、ファイル名が文字化けしているではありませんか!

文字コードを変換して解凍してみたらどうなるか

ということで、ファイル名の文字コードを変換して解凍してみました。

import zipfile

SRC = 'drive/My Drive/test.zip'
DST = 'drive/My Drive/test'

with zipfile.ZipFile(SRC) as z:
    for info in z.infolist():
        info.filename = info.filename.encode('cp437').decode('cp932')
        z.extract(info, path=DST)

これで正しく解凍できました。

問題の解決策

原因の切り分けはできました。
しかし、GASはJavaScriptベース。どうやって解凍したものか。

その解決策についてご紹介します。

世の中のライブラリに頼る

自前でzip解凍の処理を作るのはかなりハードル高いので、先人達のお知恵を拝借することとしました。
JSのライブラリに頼ることにします。

下準備

まずは既存のライブラリをGASライブラリ化して参照できるようにします。

imaya/zlib.js

imaya/zlib.js より引用

zlib.js は ZLIB(RFC1950), DEFLATE(RFC1951), GZIP(RFC1952), PKZIP の JavaScript 実装です。

このライブラリにはzip解凍の機能も付随されていましたので、こちらを試してみました。

  1. 新規にGASのプロジェクトを作成する(プロジェクト名は「unzipjs」としました)
  2. binの中にあるunzip.min.jsの中身をスクリプトエディタに貼り付ける
  3. [ファイル]→[版を管理]で1版として保存する

polygonplanet/encoding.js

polygonplanet/encoding.js より引用

Converts character encoding in JavaScript.
JavaScript で文字コード変換をします

そのままですね。文字コード変換用のライブラリです。
こちらもGASライブラリ化します。

  1. 新規にGASのプロジェクトを作成する(プロジェクト名は「encodingjs」としました)
  2. binの中にあるencoding.min.jsの中身をスクリプトエディタに貼り付ける
  3. [ファイル]→[版を管理]で1版として保存する

実装

準備が整ったところで、zip解凍処理を実装します。(V8エンジン対応、適宜置き換えてください)

対象のzipファイルの中身がPDF(BINARY、UTF32)、XMLファイル(UTF8)でしたので、今回はUTF8、BINARY、UTF32のみを対象として処理を記載します。

  // zipファイルからバイトから配列を取得
  let byteary = DriveApp.getFileById('zipファイルのID').getBlob().getBytes()

  // Uint8Arrayに変換
  let a = new Uint8Array(byteary)

  // unzip.min.jsを実行
  let unzip = new unzipjs.Zlib.Unzip(a)

  // 圧縮ファイル内のファイル名を取得
  let filenames = unzip.getFilenames()

  // 解凍先のフォルダ
  let folder = DriveApp.getFolderById('解凍先のフォルダID')

  // ファイル変換処理
  for (const i in filenames) {
    const filename = filenames[i]

    // 解凍してUint8Arrayを抽出
    let u8 = unzip.decompress(filename)
    // 文字列に変換
    let s = Array.from(u8, e => String.fromCharCode(e)).join("")

    // ファイル名の文字化けを解消
    let convertedFilename = encodingjs.Encoding.convert(filename)

    // 文字コードを判定
    let enc = encodingjs.Encoding.detect(u8)
    // UTF8ファイルの場合
    if (enc == 'UTF8') {
      // 文字列をエンコードしてBlobを作成
      s = decodeURIComponent(escape(encodingjs.Encoding.convert(s, 'UTF8')))
      let blob = Utilities.newBlob('', 'application/octet-binary', convertedFilename).setDataFromString(s).setContentTypeFromExtension()
      // Drive上にファイルを作成
      folder.createFile(blob)
    // バイナリ、UTF32
    } else if (enc == 'BINARY' || enc == 'UTF32') {
      // Uint8Arrayからバイト配列を抽出
      let ba = []
      for (const value of u8.values()) {
        ba.push(value)
      }
      // Blobを作成
      let blob = Utilities.newBlob(ba, null, convertedFilename).setContentTypeFromExtension()
      // Drive上にファイルを作成
      folder.createFile(blob)
    } else {
      // それ以外は個別に実装
    }
  }

おわりに

GAS単体で解決できない問題は他にも存在すると考えています。
せっかく便利なサービスですので、こうした問題への解決策を今後も取り上げて行きたいと思います。
ありがとうございました。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1