C#でZIPファイルを扱う方法:圧縮・解凍の基本

はい、承知いたしました。C#でZIPファイルを扱う方法、特に標準ライブラリであるSystem.IO.Compressionを使った圧縮・解凍の基本に焦点を当て、約5000語の詳細な解説記事を作成します。


C#でZIPファイルを扱う方法:圧縮・解凍の基本

はじめに:ZIPファイルとC#におけるその重要性

ファイル圧縮は、現代のデジタル世界において不可欠な技術です。ディスク容量の節約、ネットワーク転送時間の短縮、複数の関連ファイルを単一のアーカイブとしてまとめることなど、様々な利点を提供します。数ある圧縮フォーマットの中でも、ZIP形式はその高い互換性と普及率から、最も広く利用されているフォーマットの一つです。Windows、macOS、Linuxといった主要なオペレーティングシステムが標準でサポートしており、特別なソフトウェアなしに内容を確認したり解凍したりできるため、データの配布やバックアップに頻繁に使用されます。

C#は、.NETという強力なフレームワーク上で動作するプログラミング言語です。.NETは、ファイル操作、ネットワーク通信、データベースアクセスなど、多岐にわたる機能を提供する豊富なライブラリを備えています。もちろん、ZIPファイルの操作についても、標準ライブラリによって強力にサポートされています。これにより、外部のライブラリに依存することなく、C#アプリケーション内でZIPアーカイブの作成(圧縮)や展開(解凍)を簡単に行うことができます。

本記事では、C#の標準ライブラリである System.IO.Compression 名前空間を利用して、ZIPファイルを扱うための基本的な方法を詳細に解説します。具体的には、既存のZIPファイルを指定したディレクトリに解凍する方法、ディレクトリ内のファイルやフォルダをまとめてZIPファイルに圧縮する方法、そして個々のファイルやデータストリームをより細かく制御してZIPアーカイブに追加・展開する方法に焦点を当てます。

対象読者は、C#プログラミングの基本的な知識を持ち、アプリケーション内でファイル圧縮・解凍機能を実装する必要がある開発者です。記事を通じて、System.IO.Compression ライブラリの主要なクラスやメソッドの使い方、さらにはエラー処理、リソース管理、パスの扱いといった実践的な側面についても深く理解できるでしょう。約5000語を目標とし、各ステップ、各メソッド、そして関連する考慮事項について、可能な限り詳細かつ網羅的に説明を行います。

System.IO.Compression ライブラリについて

.NET Framework 4.5以降、および.NET Core/.NET 5以降では、ZIPアーカイブを簡単に操作するためのクラス群が System.IO.Compression 名前空間に含まれています。以前はサードパーティ製のライブラリ(例: DotNetZip, SharpZipLib)を利用するのが一般的でしたが、現在では標準ライブラリで多くの基本的な操作をカバーできるようになりました。

System.IO.Compression 名前空間には、ZIPファイル以外にもGZIP圧縮などを扱うクラスが含まれていますが、本記事で主に使用するのは以下のクラスです。

  • ZipFile: ZIPファイルの作成や展開といった、ディレクトリ単位での高レベルな操作を提供する静的クラスです。最も手軽に圧縮・解凍を行う際に利用します。
  • ZipArchive: ZIPアーカイブ全体を表すクラスです。ZIPファイルを開いてその内容(エントリ)を列挙したり、個々のエントリに対して操作(読み取り、書き込み、削除など)を行ったりする際に使用します。より詳細な制御が必要な場合に利用します。
  • ZipArchiveEntry: ZIPアーカイブ内の個々のエントリ(ファイルやディレクトリ)を表すクラスです。エントリ名(パス)、サイズ、最終更新日時などの情報を提供し、エントリの内容をストリームとして操作(読み書き)するためのメソッドを提供します。
  • CompressionLevel: ZIPエントリを圧縮する際の圧縮レベルを指定するための列挙型です。圧縮率と速度のバランスを調整できます。

これらのクラスを利用するためには、C#コードファイルの先頭に以下の using ディレクティブを追加する必要があります。

csharp
using System.IO;
using System.IO.Compression;

多くの場合、ファイルシステム操作も伴うため、System.IO も同時にインポートすることが一般的です。

それでは、まずZIPファイルの解凍から具体的な方法を見ていきましょう。

ZIPファイルの解凍 (Extracting ZIP Files)

ZIPファイルの解凍は、既存のZIPアーカイブからファイルやディレクトリを取り出す操作です。System.IO.Compression ライブラリでは、この操作を行うための複数の方法が提供されています。簡単な方法と、より詳細な制御が可能な方法の両方を紹介します。

1. ZipFile.ExtractToDirectory を使用した基本的な解凍

最も手軽にZIPファイルを解凍する方法は、ZipFile クラスの静的メソッド ExtractToDirectory を使用することです。このメソッドは、指定されたZIPファイルの内容を、指定されたディレクトリにすべて展開します。

メソッドシグネチャ:

csharp
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName);
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, bool overwrite);

  • sourceArchiveFileName: 解凍したいZIPファイルのパスを指定します。
  • destinationDirectoryName: 解凍したファイルを保存するディレクトリのパスを指定します。このディレクトリが存在しない場合は自動的に作成されます。
  • overwrite (オプション): .NET 6以降で利用可能な引数です。true を指定すると、展開先に同名のファイルやディレクトリが既に存在する場合に上書きします。false またはこの引数を省略(旧バージョン)した場合、既に存在すると IOException がスローされます。

使い方とサンプルコード:

以下のサンプルコードは、指定したZIPファイル (example.zip) を、指定したディレクトリ (./extracted_files) に解凍する基本的な例です。

“`csharp
using System;
using System.IO;
using System.IO.Compression;

public class ZipExtractionExample
{
public static void Main(string[] args)
{
string zipFilePath = “example.zip”; // 解凍したいZIPファイルのパス
string extractPath = “./extracted_files”; // 解凍先のディレクトリパス

    // サンプルZIPファイルを作成(テスト用)
    CreateSampleZip(zipFilePath);

    try
    {
        // 解凍先のディレクトリが存在しない場合は作成
        if (!Directory.Exists(extractPath))
        {
            Directory.CreateDirectory(extractPath);
            Console.WriteLine($"解凍先ディレクトリ '{extractPath}' を作成しました。");
        }

        Console.WriteLine($"ZIPファイル '{zipFilePath}' を '{extractPath}' に解凍します...");

        // ZIPファイルを解凍
        // .NET 6 以降で上書きを許可する場合:
        // ZipFile.ExtractToDirectory(zipFilePath, extractPath, true);
        // .NET 6 より前のバージョン、または上書きを許可しない場合:
        ZipFile.ExtractToDirectory(zipFilePath, extractPath);

        Console.WriteLine("解凍が完了しました。");
    }
    catch (FileNotFoundException)
    {
        Console.WriteLine($"エラー: ZIPファイルが見つかりません: '{zipFilePath}'");
    }
    catch (DirectoryNotFoundException)
    {
        // 展開先ディレクトリ作成前に発生する可能性は低いが、念のため
        Console.WriteLine($"エラー: 展開先ディレクトリの一部が見つかりません: '{extractPath}'");
    }
    catch (IOException ex)
    {
        // ファイルが既に存在する場合、パスが無効な場合など
        Console.WriteLine($"解凍中にI/Oエラーが発生しました: {ex.Message}");
    }
    catch (UnauthorizedAccessException)
    {
        Console.WriteLine($"エラー: 指定されたパスへのアクセス権限がありません。");
    }
    catch (InvalidDataException)
    {
         // ZIPファイルが破損している、または無効な形式の場合
         Console.WriteLine($"エラー: ZIPファイルの内容が無効です。ファイルが破損している可能性があります。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"予期しないエラーが発生しました: {ex.Message}");
    }

    // 後処理(テスト用ZIPファイルを削除)
    // if (File.Exists(zipFilePath))
    // {
    //     File.Delete(zipFilePath);
    // }
    // if (Directory.Exists(extractPath))
    // {
    //     // Directory.Delete(extractPath, true); // 内容ごと削除
    // }
}

// ZipFile.ExtractToDirectory をテストするためのサンプルZIPファイルを作成するヘルパーメソッド
public static void CreateSampleZip(string zipFileName)
{
    // テスト用のディレクトリとファイルを作成
    string sourceDir = "./sample_for_zip";
    if (Directory.Exists(sourceDir)) Directory.Delete(sourceDir, true);
    Directory.CreateDirectory(sourceDir);
    Directory.CreateDirectory(Path.Combine(sourceDir, "subdir"));
    File.WriteAllText(Path.Combine(sourceDir, "file1.txt"), "This is the first file.");
    File.WriteAllText(Path.Combine(sourceDir, "subdir", "file2.txt"), "This is the second file in a subdirectory.");
    File.WriteAllBytes(Path.Combine(sourceDir, "binaryfile.bin"), new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 });

    if (File.Exists(zipFileName)) File.Delete(zipFileName);

    try
    {
        ZipFile.CreateFromDirectory(sourceDir, zipFileName);
        Console.WriteLine($"テスト用ZIPファイル '{zipFileName}' を作成しました。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"テスト用ZIPファイルの作成に失敗しました: {ex.Message}");
    }
    finally
    {
        // 元のディレクトリは削除しないでおくか、テスト後に削除する
        // if (Directory.Exists(sourceDir)) Directory.Delete(sourceDir, true);
    }
}

}
“`

注意点:

  • ExtractToDirectory は、ZIPファイル内のディレクトリ構造を維持して展開します。例えば、ZIP内に subdir/file2.txt というエントリがあれば、展開先ディレクトリの下に subdir ディレクトリが作成され、その中に file2.txt が保存されます。
  • ZIPファイルのエントリ名に絶対パスが含まれている場合、ExtractToDirectoryIOException をスローします。セキュリティ上の理由から、絶対パスを持つエントリは展開されません。
  • ZIPファイルのエントリ名に無効なパス文字(例: *, ?, |, " など)が含まれている場合も、IOException がスローされる可能性があります。
  • デフォルトでは、展開先に同名のファイルやディレクトリが存在すると IOException が発生します。これを避けるためには、事前に展開先のファイルを削除するか、.NET 6以降であれば overwrite: true オプションを使用します。

ExtractToDirectory は非常に便利ですが、ZIPファイル内の特定のエントリだけを解凍したい場合や、解凍しながらファイルの内容をストリームで処理したい場合には向いていません。そのような場合は、次に説明する ZipArchive クラスを使用します。

2. ZipArchive を使用した詳細な解凍制御

ZipArchive クラスを使用すると、ZIPファイルの内容をより細かく制御できます。ZIPファイルを開き、その中に含まれる各エントリ(ファイルやディレクトリ)にアクセスし、個別に操作を行うことができます。

ZipArchive オブジェクトは、ZipFile.OpenRead メソッドを使用して作成するのが一般的です。このメソッドは、ZIPファイルを読み取りモードで開き、ZipArchive オブジェクトを返します。

ZIPアーカイブの読み込み:

csharp
using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
{
// ここでアーカイブ内のエントリにアクセス
} // using ブロックを抜ける際にアーカイブが閉じられ、リソースが解放される

ZipFile.OpenRead メソッドは ZipArchiveMode.Read モードでアーカイブを開きます。このモードでは、アーカイブの内容を読み取ることはできますが、変更(エントリの追加、削除、更新)はできません。読み取り専用のため、複数のリーダーが同時に同じファイルを読み取ることができます。

エントリの列挙:

ZipArchive オブジェクトは Entries プロパティを持っており、これによりアーカイブ内のすべての ZipArchiveEntry オブジェクトのコレクションを取得できます。このコレクションを foreach ループなどで反復処理することで、各エントリの情報にアクセスできます。

csharp
using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
{
Console.WriteLine($"ZIPファイル '{zipFilePath}' の内容:");
foreach (ZipArchiveEntry entry in archive.Entries)
{
Console.WriteLine($"- {entry.FullName}");
Console.WriteLine($" 圧縮サイズ: {entry.CompressedLength} バイト");
Console.WriteLine($" 非圧縮サイズ: {entry.Length} バイト");
Console.WriteLine($" 最終更新日時: {entry.LastWriteTime.ToLocalTime()}"); // UTC時刻なのでローカル時刻に変換
Console.WriteLine($" エントリ名: {entry.Name}"); // ファイル名またはディレクトリ名部分のみ
}
}

ZipArchiveEntry オブジェクトの主なプロパティ:

  • FullName: アーカイブ内のエントリの完全なパス(ディレクトリ名を含む)です。例: subdir/file2.txt
  • Name: エントリのファイル名またはディレクトリ名部分のみです。例: file2.txt
  • Length: エントリの非圧縮時のサイズ(バイト単位)です。
  • CompressedLength: エントリの圧縮時のサイズ(バイト単位)です。
  • LastWriteTime: エントリの最終更新日時(UTC時刻)です。

個々のエントリの解凍 (ExtractToFile):

ZipArchiveEntry クラスには、そのエントリをファイルとして特定の場所に解凍するための ExtractToFile メソッドがあります。

メソッドシグネチャ:

csharp
public void ExtractToFile(string destinationFileName);
public void ExtractToFile(string destinationFileName, bool overwrite);

  • destinationFileName: 解凍したファイルを保存する場所のパスを指定します。
  • overwrite (オプション): .NET 4.5 / .NET Standard 1.0 以降で利用可能です。true を指定すると、展開先に同名のファイルが既に存在する場合に上書きします。false または省略した場合、既に存在すると IOException がスローされます。

使い方とサンプルコード:

ZIPファイル内のすべてのエントリを指定されたディレクトリに解凍するサンプルコードです。ZipFile.ExtractToDirectory と似た結果になりますが、こちらの方が各エントリに対して個別に処理を行うため、より柔軟な制御が可能です。

“`csharp
using System;
using System.IO;
using System.IO.Compression;
using System.Linq; // To use LINQ methods like Where

public class DetailedZipExtraction
{
public static void Main(string[] args)
{
string zipFilePath = “example.zip”; // 解凍したいZIPファイルのパス
string extractPath = “./extracted_files_detailed”; // 解凍先のディレクトリパス

    // サンプルZIPファイルを作成(テスト用)
    // CreateSampleZip(zipFilePath); // 前述のCreateSampleZipメソッドを使用

    try
    {
        // 解凍先のディレクトリが存在しない場合は作成
        // 注: ZipArchiveEntry.ExtractToFile はディレクトリを作成しないため、手動で作成する必要がある
        if (!Directory.Exists(extractPath))
        {
            Directory.CreateDirectory(extractPath);
            Console.WriteLine($"解凍先ディレクトリ '{extractPath}' を作成しました。");
        }

        Console.WriteLine($"ZIPファイル '{zipFilePath}' を '{extractPath}' に詳細解凍します...");

        using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
        {
            // 各エントリをループ
            foreach (ZipArchiveEntry entry in archive.Entries)
            {
                // 解凍後のファイルの完全パスを生成
                // Path.Combine を使うことで、OSに応じた正しいパス区切り文字が使用される
                string destinationPath = Path.Combine(extractPath, entry.FullName);

                // ZIPエントリがディレクトリであるかどうかのチェック
                // 通常、FullNameが '/' または '\' で終わるエントリはディレクトリとして扱われる
                // ただし、空のディレクトリはEntriesコレクションに含まれない場合がある
                if (string.IsNullOrEmpty(entry.Name))
                {
                    // エントリ名が空の場合はディレクトリとみなす(例: フォルダ/)
                    Console.WriteLine($"ディレクトリを作成: {entry.FullName}");
                    Directory.CreateDirectory(destinationPath);
                }
                else
                {
                    // ファイルエントリの場合
                    Console.WriteLine($"ファイル '{entry.FullName}' を解凍中...");

                    // 展開先のディレクトリが存在しない場合は作成
                    // ファイルパスからディレクトリ部分を抽出
                    string directoryPart = Path.GetDirectoryName(destinationPath);
                    if (!string.IsNullOrEmpty(directoryPart) && !Directory.Exists(directoryPart))
                    {
                        Directory.CreateDirectory(directoryPart);
                        Console.WriteLine($"サブディレクトリ '{directoryPart}' を作成しました。");
                    }

                    try
                    {
                        // エントリをファイルに解凍
                        // .NET 4.5 / .NET Standard 1.0 以降で上書きを許可する場合:
                         entry.ExtractToFile(destinationPath, true); // 上書きを許可
                        // 上書きを許可しない場合:
                        // entry.ExtractToFile(destinationPath, false);
                        // または
                        // entry.ExtractToFile(destinationPath); // false と同じ

                        Console.WriteLine($"  -> '{destinationPath}' に解凍しました。");
                    }
                    catch (IOException ex)
                    {
                        Console.WriteLine($"  エラー: ファイル '{entry.FullName}' の解凍に失敗しました。おそらくファイルが既に存在し、上書きが許可されていません。: {ex.Message}");
                        // 必要に応じてここで他の処理を行う (例: スキップ、ログ記録など)
                    }
                     catch (UnauthorizedAccessException)
                    {
                        Console.WriteLine($"  エラー: ファイル '{entry.FullName}' の解凍先パスへのアクセス権限がありません。");
                    }
                    catch (NotSupportedException ex)
                    {
                         // 無効な文字を含むパスなど
                         Console.WriteLine($"  エラー: パス '{destinationPath}' が無効です。: {ex.Message}");
                    }
                    catch (Exception ex)
                    {
                         // その他の予期しないエラー
                         Console.WriteLine($"  エラー: ファイル '{entry.FullName}' の解凍中に予期しないエラーが発生しました: {ex.Message}");
                    }
                }
            }
        }

        Console.WriteLine("詳細解凍が完了しました。");
    }
    catch (FileNotFoundException)
    {
        Console.WriteLine($"エラー: ZIPファイルが見つかりません: '{zipFilePath}'");
    }
    catch (InvalidDataException)
    {
         Console.WriteLine($"エラー: ZIPファイルの内容が無効です。ファイルが破損している可能性があります。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"解凍中に予期しないエラーが発生しました: {ex.Message}");
    }
}

// CreateSampleZipメソッドは前述のものを利用
public static void CreateSampleZip(string zipFileName) { /* ... 同上 ... */ }

}
“`

この詳細な解凍方法の利点は以下の通りです。

  • 特定ファイルの解凍: archive.Entries をLINQなどでフィルタリングすれば、特定の名前や条件に一致するエントリだけを解凍できます。
    csharp
    // 例: 拡張子が .txt のファイルのみを解凍
    foreach (ZipArchiveEntry entry in archive.Entries.Where(e => e.Name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)))
    {
    // ... 解凍処理 ...
    }
  • 解凍時の処理のカスタマイズ: 解凍前にファイル名やパスを検証・変更したり、解凍の進捗を表示したり、特定のエントリをスキップしたりといったカスタムロジックを組み込めます。
  • ストリームアクセス: ZipArchiveEntry.Open() メソッドを使用すると、エントリの内容を Stream として取得できます。これにより、ファイルをディスクに書き出すことなく、直接メモリ内で内容を読み取ったり、別のストリームにコピーしたりすることが可能です。これは、大きなファイルを扱ったり、ファイル内容を即座に処理したい場合に非常に有用です。

エントリのストリーム読み込み:

ZipArchiveEntry.Open() メソッドは、エントリの圧縮解除された内容を読み取るための Stream オブジェクトを返します。

“`csharp
using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
{
// 特定のファイルエントリを探す
ZipArchiveEntry entryToRead = archive.GetEntry(“subdir/file2.txt”);

if (entryToRead != null)
{
    // using ステートメントを使ってストリームを開く
    using (Stream entryStream = entryToRead.Open())
    {
        // Stream を使ってエントリの内容を読み取る
        // 例: テキストファイルの場合
        using (StreamReader reader = new StreamReader(entryStream))
        {
            string content = reader.ReadToEnd();
            Console.WriteLine($"'{entryToRead.FullName}' の内容:\n{content}");
        }

        // 例: バイナリファイルの場合
        // byte[] buffer = new byte[4096];
        // int bytesRead;
        // while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0)
        // {
        //     // 読み取ったデータを処理
        //     // 例: 別のストリームに書き出すなど
        //     // someOtherStream.Write(buffer, 0, bytesRead);
        // }
    } // Stream が自動的に閉じられる
}
else
{
    Console.WriteLine("指定されたエントリが見つかりません。");
}

}
“`

重要な注意点: リソース管理 (using)

ZipArchive オブジェクトおよび ZipArchiveEntry.Open() から取得した Stream オブジェクトは、使用後に必ず適切にリソースを解放する必要があります。最も安全で推奨される方法は、using ステートメントを使用することです。using ブロックを抜ける際に、オブジェクトの Dispose() メソッドが自動的に呼び出され、ファイルハンドルなどのリソースが解放されます。これにより、ファイルのロックやメモリリークといった問題を避けることができます。

“`csharp
// 正しい例: using を使う
using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (entry.Name.EndsWith(“.txt”, StringComparison.OrdinalIgnoreCase))
{
using (Stream entryStream = entry.Open()) // Stream も using で囲む
using (StreamReader reader = new StreamReader(entryStream)) // StreamReader も using で囲む
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
}
}
} // archive が閉じられ、リソース解放

// 誤った例: using を使わない、または不適切に使う
// ZipArchive archive = ZipFile.OpenRead(zipFilePath);
// // … 処理 …
// // archive.Dispose(); // これを忘れるとリソースリーク

// Stream entryStream = entry.Open();
// // … 処理 …
// // entryStream.Dispose(); // これを忘れるとファイルロック
“`

特にループ内で Stream を開く場合は、ループ内で using を使うことが重要です。ループの外で開いてしまうと、各エントリのストリームが適切に閉じられず、問題が発生する可能性があります。

ZIPファイルの圧縮 (Creating and Compressing ZIP Files)

次に、ファイルやディレクトリをまとめてZIPファイルに圧縮する方法を見ていきましょう。解凍と同様に、手軽な方法と詳細な制御が可能な方法があります。

1. ZipFile.CreateFromDirectory を使用した基本的な圧縮

ディレクトリ内のすべてのファイルとサブディレクトリをまとめて一つのZIPファイルに圧縮する最も簡単な方法は、ZipFile クラスの静的メソッド CreateFromDirectory を使用することです。

メソッドシグネチャ:

csharp
public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName);
public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, CompressionLevel compressionLevel, bool includeBaseDirectory);
public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, CompressionLevel compressionLevel, bool includeBaseDirectory, System.Text.Encoding entryNameEncoding); // .NET Core 2.1+ / .NET Standard 2.0+

  • sourceDirectoryName: 圧縮したいファイルやディレクトリが含まれるソースディレクトリのパスを指定します。
  • destinationArchiveFileName: 作成するZIPファイルのパスを指定します。指定したファイルが既に存在する場合は上書きされます。
  • compressionLevel (オプション): 圧縮レベルを指定します。CompressionLevel 列挙型を使用します。デフォルトは Optimal です。
    • NoCompression: 圧縮を行いません。ファイルはZIPアーカイブに格納されるだけです。高速ですがファイルサイズはほとんど小さくなりません。
    • Fastest: 最も速い圧縮速度を優先します。圧縮率は低くなる可能性があります。
    • Optimal: 圧縮率を優先します。圧縮速度は遅くなる可能性があります。通常はこちらを選択しておけば良いでしょう。
  • includeBaseDirectory (オプション): true を指定すると、sourceDirectoryName で指定したディレクトリ自体がZIPファイル内のルートディレクトリとして含まれます。false または省略した場合、ソースディレクトリ内のコンテンツ(ファイルとサブディレクトリ)がZIPファイルのルートに直接配置されます。
  • entryNameEncoding (オプション): ZIPエントリ名のエンコーディングを指定します。特定の環境や古いZIPツールとの互換性が必要な場合に使用します。通常は指定する必要はありません。

使い方とサンプルコード:

以下のサンプルコードは、指定したディレクトリ (./source_files) の内容を、指定したZIPファイル (output.zip) に圧縮する基本的な例です。

“`csharp
using System;
using System.IO;
using System.IO.Compression;

public class ZipCompressionExample
{
public static void Main(string[] args)
{
string sourceDirectory = “./source_files”; // 圧縮したいディレクトリ
string zipFilePath = “output.zip”; // 作成するZIPファイルのパス

    // テスト用のソースディレクトリとファイルを作成
    CreateSampleSourceDirectory(sourceDirectory);

    try
    {
        // ZIPファイルが既に存在する場合は削除(上書きモードを使わない場合)
        // if (File.Exists(zipFilePath))
        // {
        //     File.Delete(zipFilePath);
        // }

        Console.WriteLine($"ディレクトリ '{sourceDirectory}' の内容をZIPファイル '{zipFilePath}' に圧縮します...");

        // ディレクトリからZIPを作成
        // includeBaseDirectory: true を指定すると、source_files/file1.txt のように、
        // source_files ディレクトリ自体がルートに含まれる
        // includeBaseDirectory: false を指定すると、file1.txt のように、
        // source_files の内容がZIPのルートに直接配置される
        ZipFile.CreateFromDirectory(sourceDirectory, zipFilePath, CompressionLevel.Optimal, false);

        Console.WriteLine("圧縮が完了しました。");
        Console.WriteLine($"作成されたZIPファイル: {zipFilePath}");
    }
    catch (DirectoryNotFoundException)
    {
        Console.WriteLine($"エラー: ソースディレクトリが見つかりません: '{sourceDirectory}'");
    }
    catch (IOException ex)
    {
        // ZIPファイルが既に存在し、アクセスできない場合など
        Console.WriteLine($"圧縮中にI/Oエラーが発生しました: {ex.Message}");
    }
    catch (UnauthorizedAccessException)
    {
        Console.WriteLine($"エラー: ソースディレクトリまたは出力パスへのアクセス権限がありません。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"予期しないエラーが発生しました: {ex.Message}");
    }

    // 後処理(テスト用ソースディレクトリを削除)
    // if (Directory.Exists(sourceDirectory))
    // {
    //     Directory.Delete(sourceDirectory, true);
    // }
}

// ZipFile.CreateFromDirectory をテストするためのサンプルディレクトリを作成するヘルパーメソッド
public static void CreateSampleSourceDirectory(string sourceDir)
{
     if (Directory.Exists(sourceDir)) Directory.Delete(sourceDir, true);
    Directory.CreateDirectory(sourceDir);
    Directory.CreateDirectory(Path.Combine(sourceDir, "documents"));
    Directory.CreateDirectory(Path.Combine(sourceDir, "images"));

    File.WriteAllText(Path.Combine(sourceDir, "readme.txt"), "これはテスト用ファイルです。\nここにいくつかテキストが入ります。");
    File.WriteAllBytes(Path.Combine(sourceDir, "data.bin"), new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 });
    File.WriteAllText(Path.Combine(sourceDir, "documents", "report.docx"), "ダミーのDOCXファイル内容"); // 実際にはバイナリ
    File.WriteAllText(Path.Combine(sourceDir, "images", "logo.png"), "ダミーのPNGファイル内容"); // 実際にはバイナリ

    Console.WriteLine($"テスト用ソースディレクトリ '{sourceDir}' を作成しました。");
}

}
“`

注意点:

  • CreateFromDirectory は、指定されたソースディレクトリ内のすべてのファイルとサブディレクトリを自動的に圧縮します。特定のファイルを除外したり、個別のファイルに異なる圧縮レベルを適用したりといった細かい制御はできません。
  • 出力ファイルパスに既にファイルが存在する場合、デフォルトではそのファイルが上書きされます。
  • includeBaseDirectory オプションは、ZIPアーカイブ内のルート構造に影響します。true の場合、ZIPファイルを開くとまずソースディレクトリ名と同じ名前のフォルダが見え、その中に内容が入っています。false の場合、ZIPファイルを開くとソースディレクトリの直下のファイルやフォルダが直接見えます。どちらを選択するかは用途によります。

CreateFromDirectory は非常に手軽ですが、圧縮するファイルを個別に選択したい場合や、メモリ上のデータ(ディスクに保存されていないデータ)を直接圧縮して追加したい場合など、より柔軟な処理が必要な場合は、次に説明する ZipArchive クラスを使用します。

2. ZipArchive を使用した詳細な圧縮制御

ZipArchive クラスを使用すると、ZIPファイルを作成し、そこに個々のエントリを手動で追加することができます。これにより、圧縮対象のファイルを細かく選択したり、エントリ名(パス)をカスタマイズしたり、圧縮レベルをエントリごとに変更したりといった詳細な制御が可能になります。

ZipArchive オブジェクトを作成するには、ZipFile.Open メソッド、または ZipArchive クラスのコンストラクタを使用します。ZIPファイルを新規作成したり内容を変更したりする場合は、ZipArchiveMode.Create または ZipArchiveMode.Update モードで開く必要があります。

ZIPアーカイブの作成:

csharp
// 新しいZIPファイルを作成する場合
using (ZipArchive archive = ZipFile.Open(zipFilePath, ZipArchiveMode.Create))
{
// ここでエントリを追加
} // using ブロックを抜ける際にアーカイブが閉じられ、内容が確定・保存される

ZipFile.Open メソッドの第2引数 ZipArchiveMode には以下の値を指定できます。

  • Read: 読み取り専用モードです。既存のZIPファイルを開いて内容を確認する際に使用します。ファイルの変更はできません。
  • Create: 新しいZIPファイルを作成します。指定されたパスにファイルが既に存在する場合は上書きされます。エントリを追加することができますが、既存のエントリを読み取ることはできません(読み取りたい場合は別途 OpenRead または OpenRead モードで開く必要があります)。
  • Update: 既存のZIPファイルを開いて、読み取り、追加、削除、変更を行うことができます。指定されたパスにファイルが存在しない場合は新規作成されます。

エントリの追加:

ZipArchive オブジェクトには、エントリを追加するためのメソッドがいくつかあります。

  • CreateEntry(string entryName) / CreateEntry(string entryName, CompressionLevel compressionLevel):
    アーカイブ内に新しい空のエントリを作成します。返される ZipArchiveEntry オブジェクトの Open() メソッドを呼び出して取得したストリームにデータを書き込むことで、エントリに内容を追加します。メモリ上のデータや動的に生成されるデータを圧縮して追加する場合に適しています。

  • CreateEntryFromFile(string sourceFileName, string entryName) / CreateEntryFromFile(string sourceFileName, string entryName, CompressionLevel compressionLevel):
    指定されたファイル (sourceFileName) を読み込み、その内容をアーカイブ内の指定された名前 (entryName) の新しいエントリとして追加します。既存のファイルをそのまま圧縮して追加する場合に最も便利です。

使い方とサンプルコード (既存ファイルの圧縮):

複数の既存ファイルを個別にZIPファイルに追加する例です。

“`csharp
using System;
using System.IO;
using System.IO.Compression;

public class DetailedZipCompression
{
public static void Main(string[] args)
{
string zipFilePath = “output_detailed.zip”; // 作成するZIPファイルのパス
string sourceDir = “./source_files_detailed”; // 圧縮元ファイルがあるディレクトリ

    // テスト用のソースファイルを作成
    CreateSampleSourceFiles(sourceDir);

    try
    {
        Console.WriteLine($"ZIPファイル '{zipFilePath}' を作成し、ファイルを追加します...");

        // ZipArchiveMode.Create で新しいZIPファイルを作成
        using (ZipArchive archive = ZipFile.Open(zipFilePath, ZipArchiveMode.Create))
        {
            // 圧縮対象のファイルをリストアップ
            string[] filesToCompress = new string[]
            {
                Path.Combine(sourceDir, "file_a.txt"),
                Path.Combine(sourceDir, "subdir_b", "file_b.txt"),
                Path.Combine(sourceDir, "file_c.dat")
            };

            foreach (string filePath in filesToCompress)
            {
                if (File.Exists(filePath))
                {
                    // ZIPアーカイブ内のエントリ名を決定
                    // 通常はソースディレクトリからの相対パスを使用
                    // 例: source_files_detailed/file_a.txt -> file_a.txt (includeBaseDirectory=false 相当)
                    // または subdir_b/file_b.txt (ディレクトリ構造を維持)
                    // ここでは、ソースディレクトリのパスを取り除き、アーカイブ内の相対パスを生成
                    // Path.GetRelativePath は .NET Core 2.1+ / .NET Standard 2.1+
                    // それ以前の場合は手動で部分文字列操作などを行う必要があります。
                    string entryName;
                     if (filePath.StartsWith(sourceDir + Path.DirectorySeparatorChar))
                    {
                        entryName = filePath.Substring(sourceDir.Length + 1);
                    }
                    else
                    {
                        // ソースディレクトリ外のファイルの場合、ファイル名のみを使うか、エラーとするかなど
                        entryName = Path.GetFileName(filePath);
                    }

                    // エントリ名中のパス区切り文字をZIPの標準である '/' に置換 (.NET handles this usually)
                    // entryName = entryName.Replace(Path.DirectorySeparatorChar, '/');
                    // entryName = entryName.Replace(Path.AltDirectorySeparatorChar, '/');

                    // CreateEntryFromFile を使用してファイルを追加
                    // 圧縮レベルを指定することも可能
                    archive.CreateEntryFromFile(filePath, entryName, CompressionLevel.Optimal);
                    Console.WriteLine($"ファイル '{filePath}' をZIPエントリ '{entryName}' として追加しました。");
                }
                else
                {
                    Console.WriteLine($"警告: ファイルが見つかりません、スキップします: '{filePath}'");
                }
            }
        }

        Console.WriteLine("詳細圧縮が完了しました。");
        Console.WriteLine($"作成されたZIPファイル: {zipFilePath}");
    }
    catch (DirectoryNotFoundException)
    {
        Console.WriteLine($"エラー: 圧縮元ファイルの一部が見つかりません。");
    }
    catch (IOException ex)
    {
        Console.WriteLine($"圧縮中にI/Oエラーが発生しました: {ex.Message}");
    }
     catch (UnauthorizedAccessException)
    {
        Console.WriteLine($"エラー: ファイルまたは出力パスへのアクセス権限がありません。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"予期しないエラーが発生しました: {ex.Message}");
    }

    // 後処理(テスト用ソースディレクトリを削除)
    // if (Directory.Exists(sourceDir))
    // {
    //     Directory.Delete(sourceDir, true);
    // }
}

// 詳細圧縮をテストするためのサンプルディレクトリとファイルを作成するヘルパーメソッド
public static void CreateSampleSourceFiles(string sourceDir)
{
    if (Directory.Exists(sourceDir)) Directory.Delete(sourceDir, true);
    Directory.CreateDirectory(sourceDir);
    Directory.CreateDirectory(Path.Combine(sourceDir, "subdir_b"));

    File.WriteAllText(Path.Combine(sourceDir, "file_a.txt"), "これはファイルAです。");
    File.WriteAllText(Path.Combine(sourceDir, "subdir_b", "file_b.txt"), "これはサブディレクトリ内のファイルBです。");
    File.WriteAllBytes(Path.Combine(sourceDir, "file_c.dat"), new byte[] { 11, 22, 33, 44 });

    Console.WriteLine($"テスト用ソースファイルを含むディレクトリ '{sourceDir}' を作成しました。");
}

}
“`

圧縮レベルの選択:

CompressionLevel 列挙型は、圧縮率と速度のトレードオフを決定します。

  • Optimal: デフォルト値であり、通常はこれが推奨されます。可能な限り高い圧縮率を目指しますが、処理には時間がかかります。
  • Fastest: 圧縮処理の速度を最優先します。ファイルサイズは Optimal よりも大きくなる傾向がありますが、圧縮時間は大幅に短縮されます。リアルタイム性が重要な場合や、CPUリソースを節約したい場合に適しています。
  • NoCompression: ファイルは圧縮されず、そのままアーカイブに格納されます。サイズは変わりませんが、圧縮・解凍の処理は非常に高速になります。アーカイブとしてまとめたいだけで、圧縮は不要な場合に利用します(例: 多数の小さなファイルをまとめる)。

どのレベルを選択するかは、アプリケーションの要件(ディスク容量、ネットワーク帯域、処理時間、利用可能なCPUリソースなど)に応じて決定します。

使い方とサンプルコード (ストリームからの圧縮):

既存のファイルだけでなく、メモリ上のバイト配列や動的に生成されたデータを圧縮してZIPに追加することも可能です。これは CreateEntry メソッドとストリームの書き込みを使用して行います。

“`csharp
using System;
using System.IO;
using System.IO.Compression;
using System.Text; // For Encoding

public class StreamCompressionExample
{
public static void Main(string[] args)
{
string zipFilePath = “output_from_stream.zip”; // 作成するZIPファイルのパス

    try
    {
        Console.WriteLine($"ZIPファイル '{zipFilePath}' を作成し、ストリームからエントリを追加します...");

        using (ZipArchive archive = ZipFile.Open(zipFilePath, ZipArchiveMode.Create))
        {
            // 1. 文字列データを圧縮して追加
            string textContent = "これはメモリ上の文字列データです。\nこれをZIPエントリとして圧縮します。";
            byte[] textBytes = Encoding.UTF8.GetBytes(textContent);
            string textEntryName = "in_memory_text.txt";

            // エントリを作成(ここでは Optimal 圧縮を指定)
            ZipArchiveEntry textEntry = archive.CreateEntry(textEntryName, CompressionLevel.Optimal);

            // エントリのストリームを開き、データを書き込む
            using (Stream entryStream = textEntry.Open())
            {
                entryStream.Write(textBytes, 0, textBytes.Length);
            } // entryStream が閉じられると、そのエントリの圧縮と書き込みが完了する

            Console.WriteLine($"文字列データをZIPエントリ '{textEntryName}' として追加しました。");

            // 2. ダミーのバイナリデータを圧縮して追加
            byte[] binaryData = new byte[1024]; // 1KBのダミーデータ
            new Random().NextBytes(binaryData); // ランダムなデータで埋める
            string binaryEntryName = "dummy_binary.bin";

            // エントリを作成(ここでは Fastest 圧縮を指定)
            ZipArchiveEntry binaryEntry = archive.CreateEntry(binaryEntryName, CompressionLevel.Fastest);

            using (Stream entryStream = binaryEntry.Open())
            {
                entryStream.Write(binaryData, 0, binaryData.Length);
            }
            Console.WriteLine($"バイナリデータをZIPエントリ '{binaryEntryName}' として追加しました。");

            // 3. ディレクトリ構造を持つ空のエントリを追加(オプション)
            // 通常はファイルを追加する際に自動的にパス中のディレクトリが考慮されますが、
            // 明示的に空のディレクトリエントリを作成することも可能です。
            // ZIP形式では、ディレクトリは通常末尾が '/' で終わるエントリとして表現されます。
            string emptyDirEntryName = "my_empty_folder/";
            if (archive.GetEntry(emptyDirEntryName) == null) // 既に存在しないか確認
            {
                 archive.CreateEntry(emptyDirEntryName);
                 Console.WriteLine($"空のディレクトリエントリ '{emptyDirEntryName}' を追加しました。");
            }
        }

        Console.WriteLine("ストリームからの詳細圧縮が完了しました。");
        Console.WriteLine($"作成されたZIPファイル: {zipFilePath}");
    }
    catch (IOException ex)
    {
        Console.WriteLine($"圧縮中にI/Oエラーが発生しました: {ex.Message}");
    }
     catch (UnauthorizedAccessException)
    {
        Console.WriteLine($"エラー: 出力パスへのアクセス権限がありません。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"予期しないエラーが発生しました: {ex.Message}");
    }
}

}
“`

このストリームベースの方法は、ディスクI/Oを最小限に抑えたい場合や、ファイルとして存在しないデータをアーカイブしたい場合に非常に強力です。

既存ZIPファイルへの追加/更新 (ZipArchiveMode.Update):

ZipArchiveMode.Update を使用すると、既存のZIPファイルを変更できます。新しいエントリを追加したり、既存のエントリを削除したり置き換えたりすることが可能です。ただし、このモードは内部的に一時ファイルを使用することが多く、パフォーマンスやディスク容量の面で効率が悪い場合があります。特に大きなZIPファイルを頻繁に更新するようなシナリオでは注意が必要です。

“`csharp
using System;
using System.IO;
using System.IO.Compression;

public class UpdateZipExample
{
public static void Main(string[] args)
{
string zipFilePath = “update_target.zip”; // 更新対象のZIPファイル
string fileToAdd = “new_file.txt”; // 追加するファイル
string entryToDelete = “dummy_binary.bin”; // 削除するエントリ名

    // 更新対象のZIPファイルを事前に作成(Createモードで作成し、いくつかのエントリを入れておく)
    // CreateSampleZipWithEntries(zipFilePath); // このメソッドは別途実装が必要

    // 追加するファイルを作成
    File.WriteAllText(fileToAdd, "This is a new file to be added.");

    try
    {
        Console.WriteLine($"ZIPファイル '{zipFilePath}' を更新モードで開きます...");

        // ZipArchiveMode.Update で既存のZIPファイルを開く
        using (ZipArchive archive = ZipFile.Open(zipFilePath, ZipArchiveMode.Update))
        {
            // 1. 新しいエントリを追加
            string newEntryName = Path.GetFileName(fileToAdd);
             if (archive.GetEntry(newEntryName) == null) // 既に同じ名前のエントリがないか確認
            {
                 archive.CreateEntryFromFile(fileToAdd, newEntryName, CompressionLevel.Optimal);
                 Console.WriteLine($"ファイル '{fileToAdd}' を新しいエントリ '{newEntryName}' として追加しました。");
            }
            else
            {
                 Console.WriteLine($"注意: エントリ '{newEntryName}' は既に存在するため追加をスキップしました。");
                 // 既存エントリを置き換えたい場合は、まず GetEntry でエントリを取得し、Delete() を呼び出してから CreateEntryFromFile を行う
            }


            // 2. 既存のエントリを削除
            ZipArchiveEntry entryToRemove = archive.GetEntry(entryToDelete);
            if (entryToRemove != null)
            {
                entryToRemove.Delete();
                Console.WriteLine($"エントリ '{entryToDelete}' を削除しました。");
            }
            else
            {
                Console.WriteLine($"注意: エントリ '{entryToDelete}' は見つかりませんでした。");
            }

            // 3. 既存エントリの内容を更新(例: file_a.txt の内容を変更)
            // これは少し複雑です。既存エントリを削除し、同じ名前で新しい内容を持つエントリを再作成します。
            string entryToUpdateName = "file_a.txt"; // 更新対象のエントリ名
            string updatedContent = "ファイルAの内容が更新されました!";
            byte[] updatedBytes = Encoding.UTF8.GetBytes(updatedContent);

            ZipArchiveEntry entryToUpdate = archive.GetEntry(entryToUpdateName);
            if (entryToUpdate != null)
            {
                Console.WriteLine($"エントリ '{entryToUpdateName}' を更新します...");
                // 既存エントリを削除
                entryToUpdate.Delete();
                Console.WriteLine($"  -> 既存エントリを削除しました。");

                // 同じ名前で新しいエントリを作成し、更新された内容を書き込む
                ZipArchiveEntry updatedEntry = archive.CreateEntry(entryToUpdateName, CompressionLevel.Optimal);
                using (Stream entryStream = updatedEntry.Open())
                {
                    entryStream.Write(updatedBytes, 0, updatedBytes.Length);
                }
                 Console.WriteLine($"  -> 新しい内容を持つエントリを再作成しました。");
            }
             else
            {
                 Console.WriteLine($"注意: エントリ '{entryToUpdateName}' は見つかりませんでした。");
             }
        }

        Console.WriteLine("ZIPファイルの更新が完了しました。");
    }
    catch (FileNotFoundException)
    {
        // 更新対象のZIPファイルが存在しない場合は新規作成されるため、この例外は通常発生しない
        // ただし、CreateSampleZipWithEntries が失敗した場合などは発生する可能性あり
        Console.WriteLine($"エラー: 更新対象のZIPファイルが見つかりません: '{zipFilePath}'");
    }
    catch (IOException ex)
    {
        Console.WriteLine($"更新中にI/Oエラーが発生しました: {ex.Message}");
    }
    catch (UnauthorizedAccessException)
    {
         Console.WriteLine($"エラー: ZIPファイルまたは追加/削除するファイルへのアクセス権限がありません。");
    }
    catch (InvalidDataException)
    {
         // ZIPファイルが破損している場合など
         Console.WriteLine($"エラー: 更新対象のZIPファイルの内容が無効です。ファイルが破損している可能性があります。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"予期しないエラーが発生しました: {ex.Message}");
    }

    // 後処理(追加したファイルを削除)
    // if (File.Exists(fileToAdd))
    // {
    //     File.Delete(fileToAdd);
    // }
}

// UpdateZipExample のために、更新対象となるZIPファイルを作成するヘルパーメソッド
// DetailedZipCompression Example の CreateSampleSourceFiles + 圧縮処理を組み合わせたもの
public static void CreateSampleZipWithEntries(string zipFileName)
{
    string tempSourceDir = "./temp_source_for_update_zip";
    CreateSampleSourceFiles(tempSourceDir); // ファイル作成メソッドを再利用

    if (File.Exists(zipFileName)) File.Delete(zipFileName);

    try
    {
         using (ZipArchive archive = ZipFile.Open(zipFileName, ZipArchiveMode.Create))
        {
            // CreateSampleSourceFiles で作成したファイルの一部を圧縮
             string[] filesToInclude = new string[]
            {
                Path.Combine(tempSourceDir, "file_a.txt"),
                Path.Combine(tempSourceDir, "subdir_b", "file_b.txt"), // これを後で更新対象にしても良い
                Path.Combine(tempSourceDir, "file_c.dat") // これを後で削除対象にしても良い
            };

            foreach (string filePath in filesToInclude)
            {
                 if (File.Exists(filePath))
                {
                     string entryName;
                     if (filePath.StartsWith(tempSourceDir + Path.DirectorySeparatorChar))
                    {
                        entryName = filePath.Substring(tempSourceDir.Length + 1);
                    }
                    else
                    {
                        entryName = Path.GetFileName(filePath);
                    }
                     archive.CreateEntryFromFile(filePath, entryName, CompressionLevel.Optimal);
                     Console.WriteLine($"  テストZIPに '{entryName}' を追加しました。");
                }
            }
        }
        Console.WriteLine($"更新テスト用のベースZIPファイル '{zipFileName}' を作成しました。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"更新テスト用ZIPファイルの作成に失敗しました: {ex.Message}");
    }
    finally
    {
         if (Directory.Exists(tempSourceDir)) Directory.Delete(tempSourceDir, true);
    }
}

}
“`

ZipArchiveMode.Update を使う場合も、using ステートメントを使用して ZipArchive オブジェクトが適切に閉じられるようにすることが非常に重要です。ブロックを抜ける際に変更内容がファイルに書き込まれます。

ZIPファイルの内容確認 (Inspecting ZIP File Contents)

ZIPファイルを解凍せずに、その中にどのようなファイルやフォルダが含まれているか、サイズや更新日時などの情報を確認したい場合があります。これは、ZipFile.OpenRead メソッドと ZipArchiveZipArchiveEntry クラスを組み合わせて簡単に行うことができます。

ZipFile.OpenRead は読み取り専用モードでアーカイブを開くため、ファイルのロックを気にすることなく安全に内容を調べることができます。

サンプルコード:

“`csharp
using System;
using System.IO;
using System.IO.Compression;

public class ZipInspectionExample
{
public static void Main(string[] args)
{
string zipFilePath = “example.zip”; // 確認したいZIPファイルのパス

    // 確認対象のZIPファイルが存在することを確認(またはテスト用に作成)
    // 前述の CreateSampleZip(zipFilePath); メソッドなどを実行しておく

    if (!File.Exists(zipFilePath))
    {
        Console.WriteLine($"エラー: 指定されたZIPファイルが見つかりません: '{zipFilePath}'");
        return;
    }

    try
    {
        Console.WriteLine($"ZIPファイル '{zipFilePath}' の内容を確認します:");

        // 読み取り専用モードでZIPアーカイブを開く
        using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
        {
            // Entries コレクションを反復処理して、各エントリの情報を表示
            Console.WriteLine($"  合計エントリ数: {archive.Entries.Count}");
            Console.WriteLine("--------------------------------------------------");
            foreach (ZipArchiveEntry entry in archive.Entries)
            {
                // ZipArchiveEntry プロパティの詳細
                Console.WriteLine($"エントリ名 (FullName): {entry.FullName}");
                Console.WriteLine($"  ファイル/ディレクトリ名 (Name): {entry.Name}");
                Console.WriteLine($"  非圧縮サイズ (Length): {entry.Length} バイト");
                Console.WriteLine($"  圧縮サイズ (CompressedLength): {entry.CompressedLength} バイト");
                // 圧縮率を計算(非圧縮サイズ > 0 の場合)
                if (entry.Length > 0)
                {
                    double compressionRatio = (double)(entry.Length - entry.CompressedLength) / entry.Length * 100;
                    Console.WriteLine($"  圧縮率: {compressionRatio:F2}%");
                }
                Console.WriteLine($"  最終更新日時 (UTC): {entry.LastWriteTime}");
                Console.WriteLine($"  最終更新日時 (Local): {entry.LastWriteTime.ToLocalTime()}");
                Console.WriteLine($"  推定NextEntryStreamPosition: {entry.OpenUsingLatestEncoderForExtract}"); // デバッグ情報など
                Console.WriteLine("--------------------------------------------------");
            }
        }

        Console.WriteLine("内容確認が完了しました。");
    }
    catch (FileNotFoundException)
    {
        Console.WriteLine($"エラー: ZIPファイルが見つかりません: '{zipFilePath}'");
    }
     catch (InvalidDataException)
    {
         // ZIPファイルが破損している、または無効な形式の場合
         Console.WriteLine($"エラー: ZIPファイルの内容が無効です。ファイルが破損している可能性があります。");
    }
    catch (IOException ex)
    {
         // ファイルロックなど、読み取り中に発生する可能性のあるエラー
         Console.WriteLine($"内容確認中にI/Oエラーが発生しました: {ex.Message}");
    }
    catch (UnauthorizedAccessException)
    {
         Console.WriteLine($"エラー: ZIPファイルへのアクセス権限がありません。");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"予期しないエラーが発生しました: {ex.Message}");
    }
}

// CreateSampleZipメソッドは前述のものを利用
public static void CreateSampleZip(string zipFileName) { /* ... 同上 ... */ }

}
“`

この方法を使えば、ZIPファイル全体のサイズが大きい場合でも、個々のエントリのメタデータは比較的短時間で取得できます。必要に応じて、特定の条件(ファイル名、サイズなど)を満たすエントリだけをフィルタリングして表示することも可能です。

応用トピック

System.IO.Compression は基本的なZIP操作を強力にサポートしていますが、すべての高度な機能を網羅しているわけではありません。ここでは、標準ライブラリの範囲外となる応用的な側面について触れ、関連する可能性のある情報を提供します。

パスワード付きZIPファイル

最もよく求められるZIP機能の一つに、パスワードによる暗号化(保護)があります。残念ながら、.NETの標準ライブラリSystem.IO.Compression` は、パスワード付きZIPファイルの作成や解凍を直接サポートしていません。

パスワード付きZIPファイルを扱う必要がある場合は、サードパーティ製のライブラリを利用する必要があります。人気のあるライブラリには以下のようなものがあります。

  • DotNetZip (Ionic.Zip): かつて非常に広く使われていましたが、現在はメンテナンスされていません。新しいプロジェクトでの使用は推奨されません。
  • SharpZipLib (ICSharpCode.SharpZipLib): アクティブに開発が続けられている、歴史のあるオープンソースライブラリです。ZIPだけでなく、tar、gzip、bzip2などの形式もサポートしており、パスワード付きZIPも扱えます。NuGetから入手可能です。
  • SevenZip.NET (SevenZipExtractorなど): 7z形式だけでなく、ZIP形式にも対応しているライブラリです。パスワード付きZIPも扱えます。NuGetから入手可能です。

これらのライブラリを使用する場合、それぞれ独自のAPIに従ってコードを記述する必要があります。通常は、アーカイブを開く際にパスワードを指定する、エントリを暗号化して追加するといったメソッドが提供されています。

進捗状況の表示

System.IO.Compression ライブラリ自体は、圧縮・解凍の進捗を報告するためのイベントやコールバックメカニズムを直接提供していません。ZipFile.CreateFromDirectoryZipFile.ExtractToDirectory のような高レベルなメソッドは、処理が完了するまで制御を返しません。

しかし、ZipArchive を使用して手動でエントリを一つずつ処理する場合、進捗状況を独自に実装することは可能です。例えば、処理対象のエントリ総数を把握しておき、各エントリの処理が完了するたびにカウンターをインクリメントし、進捗率を計算して表示(または報告)するといった方法が考えられます。

“`csharp
// ZipArchive を使用した進捗表示の概念コード(抜粋)
using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
{
int totalEntries = archive.Entries.Count;
int processedCount = 0;

Console.WriteLine($"合計 {totalEntries} 個のエントリを処理します。");

foreach (ZipArchiveEntry entry in archive.Entries)
{
    // ... 各エントリの解凍または圧縮処理 ...

    processedCount++;
    int percentage = (int)((double)processedCount / totalEntries * 100);
    Console.WriteLine($"  {entry.FullName} 処理完了 ({processedCount}/{totalEntries}, {percentage}%)");

    // 進捗表示をコンソールではなくGUIに表示する場合など
    // ReportProgress(percentage, entry.FullName);
}

}
“`

圧縮時 (ZipArchiveMode.Create または Update) も同様に、追加するファイルの総数を把握し、ファイルごとに CreateEntryFromFile やストリームへの書き込みが完了した時点で進捗を更新できます。

非同期処理との組み合わせ

System.IO.Compression の主要なメソッドは同期的に動作します。つまり、圧縮や解凍の処理が完了するまで呼び出し元のスレッドはブロックされます。GUIアプリケーションなどで大きなファイルを扱う場合、UIがフリーズする原因となる可能性があります。

非同期に処理を実行したい場合は、タスクベースの非同期パターン(Task.Run など)を使用して、ZIP操作をバックグラウンドスレッドで実行させることが推奨されます。

“`csharp
// 非同期実行の概念コード(抜粋)
public async Task ExtractZipAsync(string zipFilePath, string extractPath)
{
// UIスレッドをブロックしないように、ZIP操作を別のスレッドで実行
await Task.Run(() =>
{
try
{
Console.WriteLine(“非同期解凍開始…”);
ZipFile.ExtractToDirectory(zipFilePath, extractPath, true); // 必要に応じて上書き許可
Console.WriteLine(“非同期解凍完了。”);
// UIを更新する場合などはここで適切なデリゲートやイベントを使用
}
catch (Exception ex)
{
Console.WriteLine($”非同期解凍中にエラーが発生しました: {ex.Message}”);
// エラーを報告
}
});
}

// 呼び出し元
// await ExtractZipAsync(“large_file.zip”, “./destination”);
“`

System.IO.Compression 自体には、ExtractToDirectoryAsync のような非同期メソッドは用意されていません(.NET 6のストリーム操作には一部非同期メソッドがありますが、アーカイブ操作全体にはありません)。したがって、Task.Run を使用して同期メソッドをラップするのが、非同期処理を実現する一般的なアプローチとなります。

エラー処理とロギング

ファイル操作は様々な要因でエラーが発生しやすい処理です。ファイルが見つからない、ディレクトリが存在しない、アクセス権限がない、ディスク容量が不足している、ファイルが別のプロセスによってロックされている、ZIPファイル自体が破損しているなど、多くの例外が発生する可能性があります。

実用的なアプリケーションでは、これらのエラーを適切に処理することが不可欠です。具体的には、try-catch ブロックを使用して例外を捕捉し、ユーザーに分かりやすいエラーメッセージを表示したり、ログファイルに詳細なエラー情報を記録したりする必要があります。

特に、IOException, FileNotFoundException, DirectoryNotFoundException, UnauthorizedAccessException, InvalidDataException, NotSupportedException といった例外は、ZIP操作中に頻繁に発生する可能性があるため、個別に捕捉して処理内容を分けることを検討すると良いでしょう。

また、デバッグや問題診断のために、どのファイルやディレクトリを処理中にエラーが発生したのか、完全な例外情報(スタックトレースを含む)をログに記録する習慣をつけることが推奨されます。

まとめと今後の学習

本記事では、C#の標準ライブラリ System.IO.Compression を使用してZIPファイルを扱うための基本的な方法について、詳細に解説しました。

  • ZipFile.ExtractToDirectory を使った簡単な解凍
  • ZipArchiveZipArchiveEntry を使った詳細な解凍制御(エントリの列挙、個別ファイルの解凍、ストリームからの読み込み)
  • ZipFile.CreateFromDirectory を使った簡単な圧縮
  • ZipArchive を使った詳細な圧縮制御(個別のファイル追加、ストリームへの書き込み、圧縮レベルの指定、既存ZIPの更新)
  • ZipFile.OpenRead を使ったZIPファイル内容の確認

System.IO.Compression は、外部ライブラリの導入なしに、多くの一般的なZIP操作を高い互換性で実現できる強力なツールです。特に、using ステートメントを使ったリソース管理と、予期されるエラーに対する例外処理を適切に行うことが、堅牢なアプリケーションを開発する上で非常に重要であることを強調しました。

一方で、パスワード付きZIPファイルのサポートがない、進捗報告や非同期メソッドが直接提供されていないといった制限もあります。より高度な機能や特定の要件が必要な場合は、SharpZipLibのようなサードパーティ製ライブラリの利用を検討する必要があります。

ZIP操作は様々なファイルシステム操作と密接に関連しています。System.IO 名前空間の他のクラス(File, Directory, Path, StreamReader, StreamWriter, FileStream など)の知識も、ZIPファイルを効果的に扱う上で不可欠です。これらのクラスについても学習を深めることで、より複雑なシナリオにも対応できるようになるでしょう。

本記事が、C#でZIPファイルを扱うための基礎を固め、さらに応用的な課題に取り組むための出発点となることを願っています。

関連ドキュメント:


注: 本記事は、約5000語という文字数要件を満たすため、各セクションの説明やコード例の詳細度を高めて記述されています。実際のコーディングにおいては、エラー処理やパスの扱いに十分注意し、必要に応じて本記事で紹介した応用トピックや関連ドキュメントを参照してください。コード例は基本的な動作を示すものであり、本番環境で使用する際は、より堅牢なエラー処理、入力値の検証、リソース管理などを追加する必要があります。


これで、C#でZIPファイルを扱う方法(圧縮・解凍の基本)に関する約5000語の詳細な説明記事が完成しました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール