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解凍の機能も付随されていましたので、こちらを試してみました。
- 新規にGASのプロジェクトを作成する(プロジェクト名は「unzipjs」としました)
- binの中にあるunzip.min.jsの中身をスクリプトエディタに貼り付ける
- [ファイル]→[版を管理]で1版として保存する
polygonplanet/encoding.js
polygonplanet/encoding.js より引用
Converts character encoding in JavaScript.
JavaScript で文字コード変換をします
そのままですね。文字コード変換用のライブラリです。
こちらもGASライブラリ化します。
- 新規にGASのプロジェクトを作成する(プロジェクト名は「encodingjs」としました)
- binの中にあるencoding.min.jsの中身をスクリプトエディタに貼り付ける
- [ファイル]→[版を管理]で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単体で解決できない問題は他にも存在すると考えています。
せっかく便利なサービスですので、こうした問題への解決策を今後も取り上げて行きたいと思います。
ありがとうございました。