トップAndroid Java > ZIPファイルから個々のエントリを読み込む

ZIPファイルから個々のエントリを読み込む

はじめに

自作OSM(OpenStreetMap)地図では、1万を超えるような多数のOSMバイナリレコードファイルから タイル地図画像データを作成(レンダリング)する。

現在はこれらのファイルを階層ディレクトリで管理しているが、 全体(370フォルダ、12,646ファイル、6.33GB[6,799,916,209B])を一つのZIPファイル(3.52GB)として 個々のファイル(エントリ)をZIPファイルから読みだす場合のパフォーマンスを調べる。

ZIPファイルから個々のエントリを読み込む

ここでは、全体としては1万を超えるような多数のファイルを一つのひとまとめにした ZIPファイルから 個々のファイルを読みだす。

メモリ使用量および読み込み時間が問題なければ、次のようにするのが一番簡単であろう。

File zipFile = new File("path/to/archive.zip");
try (ZipFile zip = new ZipFile(zipFile)) {
    // 階層パスを指定する (スラッシュ '/' を使用)
    ZipEntry entry = zip.getEntry("folder1/subfolder/target.txt");
    if (entry != null) {
        // 入力ストリームを取得して読み込む
        // InputStream is = zip.getInputStream(entry);
        // ...
    }
} catch (Exception e) {
    e.printStackTrace();
}

地図データでは階層パスは japan-high12/3458/1762.dat のようになる。

個別ファイルからの読み込みとZIPファイルからの読み込みの比較

大きなファイル(22MB)と平均的なファイル(1.1MB)について、読み込み時間の比較を行った。

初回はディスクキャッシュ効果はないため、両者の差は小さい。 二回目以降は平均的にはZIPファイルの方が3倍程度時間がかかっている。

地図アプリでは読み込み時間よりもその後の処理に数倍の時間がかかる。 また、読み込んだデータはメモリに保存され、何度も使用されるため、 読み込み時間が全体の処理時間に占める比重はごく小さくなる。

また、地図アプリでは同じ datファイルを繰り返し読み込むことは稀であり、 大抵が一度読み込まれるだけである。したがって、キャッシュディスク効果は殆ど期待できない。 このため、ZIPファイルと個別ファイルの読み込み時間の差はさほど大きくないであろう。

ZIP  22.35MB 348ミリ秒 japan-high12/3637/1614.dat
個別 22.35MB 347ミリ秒 /storage/9C33-6BBD/Map/dat/japan-high12/3637/1614.dat
ZIP  1.12MB   29ミリ秒 japan-high12/3600/1626.dat
個別 1.12MB   20ミリ秒 /storage/9C33-6BBD/Map/dat/japan-high12/3600/1626.dat
ZIP  22.35MB 192ミリ秒 japan-high12/3637/1614.dat
個別 22.35MB  47ミリ秒 /storage/9C33-6BBD/Map/dat/japan-high12/3637/1614.dat
ZIP  1.12MB   28ミリ秒 japan-high12/3600/1626.dat
個別 1.12MB    3ミリ秒 /storage/9C33-6BBD/Map/dat/japan-high12/3600/1626.dat
ZIP  22.35MB 187ミリ秒 japan-high12/3637/1614.dat
個別 22.35MB  49ミリ秒 /storage/9C33-6BBD/Map/dat/japan-high12/3637/1614.dat
ZIP  1.12MB   31ミリ秒 japan-high12/3600/1626.dat
個別 1.12MB   11ミリ秒 /storage/9C33-6BBD/Map/dat/japan-high12/3600/1626.dat

同様のテストを関東地方のOSMデータについて行った。 japan.zip は 3.52GB、kanto.zip は 710MB である。 /kanto-high12/3600/1626.dat は存在しないので代わりに /kanto-high12/3630/1618.dat を選んだ。

1回目の個別ファイルの読み込み時間が短いのは、サイズの小さいファイルを選びなおしたことから、 直近で大きいファイルを読み込んだことが影響していると思われる。 後日、テストをやり直してみよう。

ZIPファイルの大きさは個々のエントリの読み込み時間に殆ど影響がないようである。

ZIP  22.35MB 339ミリ秒 kanto-high12/3637/1614.dat
個別 22.35MB 155ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3637/1614.dat
ZIP  1.21MB   35ミリ秒 kanto-high12/3630/1618.dat
個別 1.21MB   37ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3630/1618.dat
ZIP  22.35MB 180ミリ秒 kanto-high12/3637/1614.dat
個別 22.35MB  47ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3637/1614.dat
ZIP  1.21MB   18ミリ秒 kanto-high12/3630/1618.dat
個別 1.21MB    5ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3630/1618.dat
ZIP  22.35MB 184ミリ秒 kanto-high12/3637/1614.dat
個別 22.35MB  46ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3637/1614.dat
ZIP  1.21MB   17ミリ秒 kanto-high12/3630/1618.dat
個別 1.21MB    4ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3630/1618.dat

1日後にテストをやり直した。昨日の初回、個別ファイルの読み込み時間が短かったのはやはり ディスクキャッシュ効果であったと言えよう。

ZIPファイルから初回読み込みがこれまでより2割ほど速かった。 22MBの個別ファイルにディスクキャッシュ効果は見られないので、 710MB の kanto.zip にディスクキャッシュ効果が残っているとは考えにくい。 2回目、3回目の結果は昨日と殆ど同じである。

ZIP  22.35MB 266ミリ秒 kanto-high12/3637/1614.dat
個別 22.35MB 340ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3637/1614.dat
ZIP   1.21MB  36ミリ秒 kanto-high12/3630/1618.dat
個別  1.21MB  35ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3630/1618.dat
ZIP  22.35MB 185ミリ秒 kanto-high12/3637/1614.dat
個別 22.35MB  46ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3637/1614.dat
ZIP   1.21MB  17ミリ秒 kanto-high12/3630/1618.dat
個別  1.21MB   4ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3630/1618.dat
ZIP  22.35MB 191ミリ秒 kanto-high12/3637/1614.dat
個別 22.35MB  46ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3637/1614.dat
ZIP   1.21MB  18ミリ秒 kanto-high12/3630/1618.dat
個別  1.21MB   4ミリ秒 /storage/9C33-6BBD/Map/dat/kanto-high12/3630/1618.dat
    static void test() {
        readFromZipFile(Tile.Source.japan, Tile.Range.high, 12, 3637, 1614);
        readFromIndividualFile(Tile.Source.japan, Tile.Range.high, 12, 3637, 1614);
        readFromZipFile(Tile.Source.japan, Tile.Range.high, 12, 3600, 1626);
        readFromIndividualFile(Tile.Source.japan, Tile.Range.high, 12, 3600, 1626);
    }

    static short[] readFromZipFile(Tile.Source src, Tile.Range range, int zoom, int x, int y) {
        long start = System.currentTimeMillis();
        byte[] bytes = new byte[1024*1024*4];
        String zipPath = Map.DIR + src.name() + ".zip";
        File zipFile = new File(zipPath);
        try (ZipFile zip = new ZipFile(zipFile)) {
            String entryname = src.name() + "-" + range.name() + zoom  + "/" + x + "/" + y + ".dat";
            ZipEntry entry = zip.getEntry(entryname);
            if (entry != null) {
                int size = (int)entry.getSize();	// 単位:バイト
                BufferedInputStream bis = new BufferedInputStream(zip.getInputStream(entry));
                short[] vals = new short[size/2];
                int read, ix = 0;
                while ((read = bis.read(bytes, 0, bytes.length)) != -1) {
                    for (int n = 0; n < read; n += 2) {
                        vals[ix++] = (short) ((bytes[n] << 8) | bytes[n + 1] & 0xff);
                    }
                }
                System.out.printf("ZIP %.2fMB %dミリ秒 %s\n", size/1024.0/1024,
                        System.currentTimeMillis()-start, entryname);
                return vals;
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    static short[] readFromIndividualFile(Tile.Source src, Tile.Range range, int zoom, int x, int y) {
        long start = System.currentTimeMillis();
        String path = Map.DIR + "dat/" + src.name() + "-" + range.name() + zoom + "/" + x + "/" + y + ".dat";
        byte[] bytes = new byte[1024*1024*4];
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path))) {
            int fileSize = (int)Files.size(Paths.get(path));
            short[] vals = new short[fileSize/2];
            int read, ix = 0;
            while ((read = bis.read(bytes, 0, bytes.length)) != -1) {
                for (int n = 0; n < read; n += 2) {
                    vals[ix++] = (short) ((bytes[n] << 8) | bytes[n + 1] & 0xff);
                }   // 割り当てられたメモリに short配列としてコピーする
            }       // 配列bytesには、インデックスreadまでデータが読みこまれている
            System.out.printf("個別 %.2fMB %dミリ秒 %s\n", fileSize/1024.0/1024,
                    System.currentTimeMillis()-start, path);
            return vals;
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

public String readTextFromZipFile(File zipFile, String entryName) {
    StringBuilder sb = new StringBuilder();
    try (ZipFile zip = new ZipFile(zipFile)) {
        ZipEntry entry = zip.getEntry(entryName);
        if (entry != null) {
            try (InputStream is = zip.getInputStream(entry);
                 BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
                String line;
                while ((line = br.readLine()) != null) {
                    sb.append(line).append("\n");
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return sb.toString();
}

GoogleのAI検索などで得られたプログラム

File zipFile = new File("path/to/archive.zip");
try (ZipFile zip = new ZipFile(zipFile)) {
    // 階層パスを指定する (スラッシュ '/' を使用)
    ZipEntry entry = zip.getEntry("folder1/subfolder/target.txt");
    if (entry != null) {
        // 入力ストリームを取得して読み込む
        // InputStream is = zip.getInputStream(entry);
        // ...
    }
} catch (Exception e) {
    e.printStackTrace();
}



// ZIPエントリのイテレーション例
try (ZipFile zipFile = new ZipFile(new File(zipPath))) {
    Enumeration entries = zipFile.entries();
    while (entries.hasMoreElements()) {
        ZipEntry entry = entries.nextElement();
        // entry.getName()などで処理
    }
} catch (IOException e) { e.printStackTrace(); }



try (ZipFile zipFile = new ZipFile(zipFilePath)) {
    Enumeration entries = zipFile.entries();
    while (entries.hasMoreElements()) {
        ZipEntry entry = entries.nextElement();
        // Check if entry is a directory
        if (!entry.isDirectory()) {
            try (InputStream inputStream = zipFile.getInputStream(entry)) {
                // Read and process the entry contents using the inputStream
            }
        }
    }
}

try (ZipFile zipFile = new ZipFile(filePath)) {
    ZipEntry entry = zipFile.getEntry("target_file.txt"); // 目次から直接検索
    if (entry != null) {
        InputStream is = zipFile.getInputStream(entry);
        // 読み取り処理...
    }
}

Enumaration の代わりに Iterator を使うべきか?

      // Iteratorを取得
        Iterator iterator = vector.iterator();

        // 要素を列挙
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }

A.リファレンス

[1] ANDROIDでZIPファイルの圧縮・展開をする
[2] How to Read Zip Files Entries With Java