sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# ZIP ファイルの作成・操作方法(テキストデータ)

最初に

利用している主なクラスはこちら。

  • ZipFile クラス
  • ZipArchive クラス

これらのクラスは .NET Framework 4.5 から追加されています。

プログラムで zip ファイルを作成する例として、JAVA の jar ファイルや Android の apk ファイルのようなものがわかりやすく、関連するファイルをひとまとめにして保存することがあります。

Windows の古いノベルゲームなんかは、音声ファイルや画像ファイルがひとつひとつファイルとして用意されていたものがあって、データの展開や削除にとても時間がかかったりしました。その後は、ファイルをまとめてひとつに圧縮・暗号化される流れとなり、諸々効率的になりました。

そんなわけで、データファイルを作成するとき、1つのテキストデータだけだと表現しづらい複雑なデータの保存は、zip ファイルに圧縮してしまおうというサンプルの記事です。よくあるケースだと、「すでにあるファイルをまとめて zip に圧縮する」だと思いますが、ここでのサンプルは「メモリー上のテキストデータを zip にいくつかのファイルにして保存できる」です。

ZIP ファイルを扱うための基本的なコード

以下のコードを実行するときは、次のアセンブリを追加する必要があります。

  • System.IO.Compression
  • System.IO.Compression.FileSystem

f:id:shikaku_sh:20200527132945p:plain:w500
参照の追加


ZIP ファイルの作成

まず、実際のファイルのひな形を用意するソースコードです。

Create("abc.zip");

private void Create(string zipPath)
{
    if (File.Exists(zipPath))
    {
        throw new System.IO.IOException("すでに同じ名前のファイル、または、フォルダーが存在しています。");
    }

    var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create);
    zip.Dispose();
}

zip ファイル(0 KB)を作成できますが、中身がないと壊れたファイルとして認識されるかもしれません。通常は、Write メソッドを使って zip ファイルを生成しつつ、中身のデータを追加すると思います。


ZIP ファイルのエントリー(ファイル、ディレクトリー)を調べる

abc.zip ファイルの中に sample.txt というテキストファイルが存在しているかどうかを調べます。圧縮ファイルの中にディレクトリー構造があって、たとえば data ディレクトリーの中に同じ名前のテキストファイルがあったとすると、data/sample.txt がエントリー名になります。

Exists("abc.zip", "sample.txt");

private bool Exists(string zipPath, string name)
{
    using (var zip = ZipFile.OpenRead(zipPath))
    {
        var selectedFile = zip.Entries.FirstOrDefault(p => p.FullName == name);

        return selectedFile != null;
    }
}

private IEnumerable<string> EnumerateFiles(string zipPath)
{
    var files = new List<string>();

    using (var zip = ZipFile.OpenRead(zipPath))
    {
        foreach (var entry in zip.Entries)
        {
            files.Add(entry.FullName);
            Console.WriteLine(entry.FullName);
        }
    }

    return files;
}


ZIP ファイルにあるテキストデータを読み込む

最初から末尾まで一度にテキストデータとして読み込むパターンと、一行ずつテキストデータをコレクションにして読み込むパターンです。

ReadToEnd("abc.zip", "sample.txt");

private string ReadToEnd(string zipPath, string name)
{
    using (var zip = ZipFile.OpenRead(zipPath))
    {
        var selectedFile = zip.Entries.FirstOrDefault(p => p.FullName == name);

        if (selectedFile == null) throw new System.IO.FileNotFoundException();

        using (var reader = new StreamReader(selectedFile.Open()))
        {
            return reader.ReadToEnd();
        }
    }
}

private IEnumerable<string> ReadLine(string zipPath, string name)
{
    var lines = new List<string>();

    using (var zip = ZipFile.OpenRead(zipPath))
    {
        var selectedFile = zip.Entries.FirstOrDefault(p => p.FullName == name);

        if (selectedFile == null) throw new System.IO.FileNotFoundException();

        using (var reader = new StreamReader(selectedFile.Open()))
        {
            string line;
            while((line = reader.ReadLine()) != null)
            {
                lines.Add(line);
            }
        }
    }

    return lines;
}


ZIP ファイルにテキストデータのファイルを追加する

テキストデータのファイルを追加するときは、zip ファイル内にすでに同名のファイル(エントリー)があるかどうかを確認する必要があります。

すでに、同じ名前のファイルが存在しているときは、いわゆる、追記や上書きに相当する方法が無いため、ファイルを消して新しく追加するか、上書きされるファイルのデータを読み込んで、読み込んだデータ+新しいデータとして再保存することになります。

private void Write(string zipPath, string name, string text, bool overwrite = false)
{
    var beforeText = "";

    if (Exists(zipPath, name))
    {
        if (overwrite == false) throw new System.IO.IOException("すでに同じ名前のファイル、または、フォルダーが存在しています。");

        beforeText = ReadToEnd(zipPath, name);

        Delete(zipPath, name);
    }

    using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Update))
    {
        var newFile = zip.CreateEntry(name);

        using (var writer = new StreamWriter(newFile.Open(), System.Text.Encoding.UTF8))
        {
            if (!string.IsNullOrEmpty(beforeText))
            {
                writer.Write(beforeText);
            }

            writer.Write(text);
        }
    }
}

private void WriteLine(string zipPath, string name, string text, bool overwrite = false)
{
    var beforeText = "";

    if (Exists(zipPath, name))
    {
        if (overwrite == false) throw new System.IO.IOException();

        beforeText = ReadToEnd(zipPath, name);

        Delete(zipPath, name);
    }

    using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Update))
    {
        var newFile = zip.CreateEntry(name);

        using (var writer = new StreamWriter(newFile.Open(), System.Text.Encoding.UTF8))
        {
            if (!string.IsNullOrEmpty(beforeText)) 
            {
                writer.Write(beforeText); 
            }

            writer.WriteLine(text);
        }
    }
}

追加するファイルの圧縮率は以下のようにエントリーを作成するときに設定することができます。

var file = zip.CreateEntry(entryName, CompressionLevel.Fastest);
識別子 圧縮方法 アクセス速度
Optimal 高圧縮 低速
Fastest 低圧縮率 中速
NoCompression 無圧縮 高速

Delete メソッドはすぐ「↓」のとおりです。


ZIP ファイル内のファイル(エントリー)を削除する

削除は、シンプルなんで特にコメントなし。

Delete("abc.zip", "sample.txt");

public void Delete(string zipPath, string name)
{
    using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Update))
    {
        var selectedFile = zip.Entries.FirstOrDefault(p => p.FullName == name);

        if (selectedFile == null) throw new System.IO.FileNotFoundException();

        selectedFile.Delete();
    }
}

ZIP ファイル内にディレクトリーを作成する

基本的には Write メソッドと同じだけど、エントリーの末尾に /\ をつけておくと、ディレクトリーとして追加されます。Write メソッドで dir/sample.txt でエントリーを追加すると、ディレクトリーを作成しつつファイルを追加することになるので、通常はあまり利用しません。

public void MakeDirectory(string zipPath, string directoryName)
{
    if (Exists(directoryName, true))
    {
        throw new System.IO.IOException("すでに同じ名前のファイル、または、フォルダーが存在しています。");
    }

    using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Update))
    {
        if (!directoryName.EndsWith(@"/") && !directoryName.EndsWith(@"\"))
        {
            directoryName += "/";
        }

        zip.CreateEntry(directoryName);
    }

}

サンプル

テストプログラムは GitHub の「Samples」に公開しています。今回のプログラムは「ZipFileTest」です。

ZipFile クラスを操作するためのサンプルのアダプタークラス SimpleZip を作成してみました。

参考

詳解 圧縮処理プログラミング

詳解 圧縮処理プログラミング

暗号解読(上)(新潮文庫)

暗号解読(上)(新潮文庫)