C#のZipFileでZIP圧縮・解凍する方法|System.IO.Compressionの使い方とエラー対策をサンプルコードで解説

はじめに

C#でZIPファイルを扱うなら、標準ライブラリのSystem.IO.Compression名前空間に含まれるZipFileクラスを使うのが基本です。外部ライブラリを追加しなくても、フォルダをZIP圧縮したり、ZIPファイルを指定フォルダへ解凍したりできます。

この記事では、C#のZipFileを使ったZIP圧縮・解凍の基本から、ZipArchiveを使ったZIP内ファイルの操作、よくあるエラー、安全な解凍処理までをサンプルコード付きで解説します。

1. C#のZipFileとは?System.IO.Compressionでできること

1-1. ZipFileクラスの概要

ZipFileは、C#でZIPファイルを簡単に作成・展開するための静的クラスです。主に次のような処理に使います。

  • フォルダ全体をZIPファイルに圧縮する

  • ZIPファイルをフォルダへ解凍する

  • ZIPファイルを開いてZipArchiveとして操作する

Microsoft公式ドキュメントでも、System.IO.Compression名前空間にはZipFileZipArchiveZipArchiveEntryなど、圧縮ファイルの読み取り・変更・展開に使うクラスが含まれていると説明されています。Microsoft Learn

たとえば、フォルダをZIP化してすぐに解凍するだけなら、次のように非常に短いコードで実装できます。

C#
using System.IO.Compression;

string sourceDirectory = @"C:\work\sample";
string zipPath = @"C:\work\sample.zip";
string extractDirectory = @"C:\work\extract";

ZipFile.CreateFromDirectory(sourceDirectory, zipPath);
ZipFile.ExtractToDirectory(zipPath, extractDirectory);

1-2. ZipArchive・ZipArchiveEntryとの違い

ZipFileZipArchiveZipArchiveEntryは役割が異なります。

ZipFileは、ZIPファイルを「まとめて作る」「まとめて解凍する」ための便利クラスです。フォルダ全体をZIP化したい場合や、ZIPを丸ごと解凍したい場合に向いています。

ZipArchiveは、ZIPファイルそのものを表すクラスです。ZIP内のファイル一覧を取得したり、ファイルを追加・削除したりする場合に使います。

ZipArchiveEntryは、ZIP内に含まれる1つ1つのファイルを表すクラスです。エントリ名、サイズ、更新日時などを確認したり、対象ファイルのストリームを開いて読み書きしたりできます。

使い分けるなら、次のように考えると分かりやすいです。

C#
// フォルダ全体をZIP化・解凍したい
ZipFile.CreateFromDirectory(...);
ZipFile.ExtractToDirectory(...);

// ZIPの中身を細かく操作したい
using ZipArchive archive = ZipFile.Open(...);
foreach (ZipArchiveEntry entry in archive.Entries)
{
Console.WriteLine(entry.FullName);
}

1-3. ZIP圧縮・解凍で使う主なメソッド一覧

C#のZipFileでよく使うメソッドは次のとおりです。

メソッド用途
CreateFromDirectory指定フォルダをZIPファイルに圧縮する
ExtractToDirectoryZIPファイルを指定フォルダへ解凍する
OpenZIPファイルをZipArchiveとして開く
OpenReadZIPファイルを読み取り専用で開く

CreateFromDirectoryは、指定したディレクトリ内のファイルとディレクトリを含むZIPアーカイブを作成するメソッドです。Microsoft Learn

ExtractToDirectoryは、ZIPアーカイブ内のファイルをファイルシステム上の指定ディレクトリへ抽出するメソッドです。上書き可否を指定できるオーバーロードも用意されています。Microsoft Learn

1-4. ZipFileが向いているケース・向いていないケース

ZipFileが向いているのは、次のようなケースです。

  • フォルダを丸ごとZIP化したい

  • ZIPファイルを丸ごと解凍したい

  • シンプルなバックアップ機能を作りたい

  • ログファイルや出力ファイルをまとめて圧縮したい

  • 標準機能だけでZIP圧縮・解凍を実装したい

一方で、次のようなケースではZipArchiveや外部ライブラリの検討が必要です。

  • ZIP内の一部ファイルだけを追加・削除したい

  • ZIP内のファイルをストリームで読み書きしたい

  • パスワード付きZIPを作成・解凍したい

  • 暗号化ZIPを扱いたい

  • 文字コードや互換性を細かく制御したい

ZipFileは便利ですが、パスワード付きZIPや暗号化ZIPには対応していません。パスワード付きZIPを扱う場合は、SharpZipLib、DotNetZip、7-Zip連携など、別の方法を検討する必要があります。

2. ZipFileを使うための準備

2-1. 必要な名前空間:System.IO.Compression

ZipFileを使うには、C#ファイルの先頭に次のusingを追加します。

C#
using System.IO.Compression;

ファイルパスやディレクトリ操作も行う場合は、次の名前空間もよく使います。

C#
using System;
using System.IO;
using System.IO.Compression;
using System.Text;

基本的なZIP圧縮・解凍だけなら、中心になるのはSystem.IO.Compressionです。

2-2. .NET Framework・.NET Core・.NET 5以降での違い

ZipFileは多くの.NET環境で利用できますが、プロジェクトの種類によって参照設定が異なります。

.NET FrameworkアプリでZipFileクラスを使う場合は、System.IO.Compression.FileSystemアセンブリへの参照が必要です。Microsoft公式ドキュメントにも、.NET FrameworkアプリでZipFileを使用するにはSystem.IO.Compression.FileSystemへの参照を追加する必要があると記載されています。Microsoft Learn

.NET Core、.NET 5以降のプロジェクトでは、多くの場合、using System.IO.Compression;を追加するだけで利用できます。ただし、プロジェクト構成やターゲットフレームワークによってはNuGetパッケージの追加が必要になる場合があります。

2-3. NuGetパッケージが必要になるケース

次のような場合は、NuGetでパッケージを追加する必要があります。

  • 古い.NET Frameworkプロジェクトを使っている

  • クラスライブラリで参照が不足している

  • ターゲットフレームワークの都合でZipFileが見つからない

  • ZipArchiveや拡張メソッドが認識されない

NuGetで追加する主なパッケージは次のとおりです。

PowerShell
Install-Package System.IO.Compression
Install-Package System.IO.Compression.ZipFile

Microsoftの圧縮・展開サンプルでも、プロジェクトにSystem.IO.CompressionSystem.IO.Compression.ZipFileを追加する例が案内されています。Microsoft Learn

2-4. サンプルで使うフォルダ構成

この記事のサンプルでは、次のようなフォルダ構成を想定します。

C:\zip-sample
├─ input
│ ├─ sample.txt
│ ├─ image.png
│ └─ docs
│ └─ readme.txt
├─ output
└─ archive.zip

inputフォルダをZIP化し、archive.zipを作成します。解凍先にはoutputフォルダを使います。

3. C#のZipFileでフォルダをZIP圧縮する方法

3-1. CreateFromDirectoryでフォルダをZIP化する基本コード

フォルダをZIPファイルに圧縮するには、ZipFile.CreateFromDirectoryを使います。

C#
using System.IO.Compression;

string sourceDirectory = @"C:\zip-sample\input";
string zipPath = @"C:\zip-sample\archive.zip";

ZipFile.CreateFromDirectory(sourceDirectory, zipPath);

Console.WriteLine("ZIPファイルを作成しました。");

このコードを実行すると、inputフォルダ内のファイルとサブフォルダがarchive.zipに圧縮されます。

注意点として、zipPathに指定したZIPファイルがすでに存在する場合、基本的には例外が発生します。上書き作成したい場合は、事前に既存ファイルを削除してから作成します。

3-2. 既存のZIPファイルがある場合の注意点

CreateFromDirectoryは、既存のZIPファイルを自動で上書きしません。そのため、同じパスにZIPファイルがある状態で実行すると、IOExceptionが発生することがあります。

上書きしたい場合は、次のように事前に削除します。

C#
using System.IO;
using System.IO.Compression;

string sourceDirectory = @"C:\zip-sample\input";
string zipPath = @"C:\zip-sample\archive.zip";

if (File.Exists(zipPath))
{
File.Delete(zipPath);
}

ZipFile.CreateFromDirectory(sourceDirectory, zipPath);

ただし、既存ZIPを削除してから作成する処理では、作成途中にエラーが発生すると元のZIPも失われます。重要なファイルを扱う場合は、一時ファイルにZIPを作成してから置き換える方法が安全です。

C#
string tempZipPath = zipPath + ".tmp";

if (File.Exists(tempZipPath))
{
File.Delete(tempZipPath);
}

ZipFile.CreateFromDirectory(sourceDirectory, tempZipPath);

if (File.Exists(zipPath))
{
File.Delete(zipPath);
}

File.Move(tempZipPath, zipPath);

3-3. CompressionLevelで圧縮率を指定する方法

CreateFromDirectoryでは、CompressionLevelを指定できます。

C#
using System.IO.Compression;

string sourceDirectory = @"C:\zip-sample\input";
string zipPath = @"C:\zip-sample\archive.zip";

ZipFile.CreateFromDirectory(
sourceDirectory,
zipPath,
CompressionLevel.Optimal,
includeBaseDirectory: false
);

CompressionLevelには主に次の値があります。

説明
CompressionLevel.Optimal圧縮率と処理速度のバランスを取る
CompressionLevel.Fastest圧縮率より速度を優先する
CompressionLevel.NoCompression圧縮せずZIP形式にまとめる
CompressionLevel.SmallestSizeより小さいサイズを優先する環境で使用可能

通常はCompressionLevel.Optimalで問題ありません。大量のログファイルを高速にまとめたい場合はFastest、すでに圧縮済みの画像や動画をまとめるだけならNoCompressionも選択肢になります。

3-4. ZIP内のルートフォルダ構成を調整する方法

CreateFromDirectoryの第4引数includeBaseDirectoryを使うと、ZIP内に元フォルダ自体を含めるかどうかを指定できます。

C#
ZipFile.CreateFromDirectory(
sourceDirectoryName: @"C:\zip-sample\input",
destinationArchiveFileName: @"C:\zip-sample\archive.zip",
compressionLevel: CompressionLevel.Optimal,
includeBaseDirectory: true
);

includeBaseDirectory: trueの場合、ZIP内にinputフォルダが含まれます。

archive.zip
└─ input
├─ sample.txt
└─ docs
└─ readme.txt

includeBaseDirectory: falseの場合、inputフォルダの中身だけがZIPのルートに配置されます。

archive.zip
├─ sample.txt
└─ docs
└─ readme.txt

配布用ZIPならfalse、バックアップ用途で元フォルダ名も残したいならtrueが分かりやすいです。

3-5. 日本語ファイル名を含むZIP圧縮の注意点

日本語ファイル名を含むZIPを作成する場合、基本的にはUTF-8で扱える環境同士なら問題ありません。ただし、古い解凍ソフトや別OSとのやり取りでは文字化けすることがあります。

CreateFromDirectoryには、エントリ名の文字エンコーディングを指定できるオーバーロードがあります。

C#
using System.IO.Compression;
using System.Text;

string sourceDirectory = @"C:\zip-sample\input";
string zipPath = @"C:\zip-sample\日本語ファイル.zip";

ZipFile.CreateFromDirectory(
sourceDirectoryName: sourceDirectory,
destinationArchiveFileName: zipPath,
compressionLevel: CompressionLevel.Optimal,
includeBaseDirectory: false,
entryNameEncoding: Encoding.UTF8
);

ただし、相手側の解凍ソフトがUTF-8のZIPファイル名に対応していない場合は、アプリ側だけでは完全に解決できません。文字化けが問題になる運用では、ファイル名を英数字にする、利用する解凍ソフトを統一する、文字コードを検証するなどの対策が必要です。

4. C#のZipFileでZIPファイルを解凍する方法

4-1. ExtractToDirectoryでZIPを解凍する基本コード

ZIPファイルを解凍するには、ZipFile.ExtractToDirectoryを使います。

C#
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";
string extractDirectory = @"C:\zip-sample\output";

ZipFile.ExtractToDirectory(zipPath, extractDirectory);

Console.WriteLine("ZIPファイルを解凍しました。");

このコードを実行すると、archive.zip内のファイルがoutputフォルダへ展開されます。

4-2. 解凍先フォルダが存在しない場合の処理

ExtractToDirectoryは、解凍先ディレクトリが存在しない場合に作成してくれることがありますが、親ディレクトリが存在しない場合やパスが不正な場合は例外になる可能性があります。

安全に処理したい場合は、事前にディレクトリを作成しておくと分かりやすいです。

C#
using System.IO;
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";
string extractDirectory = @"C:\zip-sample\output";

Directory.CreateDirectory(extractDirectory);

ZipFile.ExtractToDirectory(zipPath, extractDirectory);

Directory.CreateDirectoryは、すでにフォルダが存在していても例外になりにくいため、解凍前の準備処理としてよく使われます。

4-3. 同名ファイルがある場合に上書きする方法

.NET Core 2.0以降や.NET 5以降では、ExtractToDirectoryのオーバーロードで上書き可否を指定できます。Microsoft公式ドキュメントにも、必要に応じて転送先ディレクトリ内のファイルを上書きするかどうかを選択できるオーバーロードが記載されています。Microsoft Learn

C#
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";
string extractDirectory = @"C:\zip-sample\output";

ZipFile.ExtractToDirectory(
sourceArchiveFileName: zipPath,
destinationDirectoryName: extractDirectory,
overwriteFiles: true
);

overwriteFiles: trueを指定すると、解凍先に同名ファイルがある場合でも上書きされます。

古い.NET Frameworkなどでこのオーバーロードが使えない場合は、ZipArchiveEntry.ExtractToFileを使って個別に上書き指定します。

C#
using System.IO;
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";
string extractDirectory = @"C:\zip-sample\output";

Directory.CreateDirectory(extractDirectory);

using ZipArchive archive = ZipFile.OpenRead(zipPath);

foreach (ZipArchiveEntry entry in archive.Entries)
{
string destinationPath = Path.Combine(extractDirectory, entry.FullName);

string? directory = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}

if (!string.IsNullOrEmpty(entry.Name))
{
entry.ExtractToFile(destinationPath, overwrite: true);
}
}

4-4. 一部のファイルだけを取り出す方法

ZIP内の一部ファイルだけを解凍したい場合は、ZipFile.ExtractToDirectoryではなくZipArchiveを使います。

C#
using System.IO;
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";
string extractDirectory = @"C:\zip-sample\output";
string targetEntryName = "docs/readme.txt";

Directory.CreateDirectory(extractDirectory);

using ZipArchive archive = ZipFile.OpenRead(zipPath);

ZipArchiveEntry? entry = archive.GetEntry(targetEntryName);

if (entry != null)
{
string destinationPath = Path.Combine(extractDirectory, entry.Name);
entry.ExtractToFile(destinationPath, overwrite: true);

Console.WriteLine("指定ファイルを解凍しました。");
}
else
{
Console.WriteLine("指定ファイルがZIP内に見つかりません。");
}

GetEntryに指定するパスは、ZIP内のエントリ名です。Windowsの\ではなく、ZIP内では/区切りになることが多い点に注意してください。

4-5. パスワード付きZIPはZipFileで扱えるのか

C#標準のZipFileでは、パスワード付きZIPの作成・解凍はできません。ZipFileは通常のZIP圧縮・解凍には便利ですが、暗号化やパスワード保護には対応していません。

パスワード付きZIPが必要な場合は、次のような方法を検討します。

  • SharpZipLibなどの外部ライブラリを使う

  • 7-Zipをコマンドライン実行する

  • Windows標準機能や別ツールと連携する

  • そもそもZIPではなく別の暗号化方式を使う

業務システムで機密ファイルを扱う場合は、パスワード付きZIPよりも、アクセス制御、暗号化ストレージ、セキュアなファイル転送の利用を検討したほうが安全なケースもあります。

5. ZipArchiveを使ってZIPファイル内のファイルを操作する方法

5-1. ZIP内のファイル一覧を取得する

ZIPファイル内の一覧を取得するには、ZipFile.OpenReadでZIPを開き、Entriesをループします。

C#
using System;
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";

using ZipArchive archive = ZipFile.OpenRead(zipPath);

foreach (ZipArchiveEntry entry in archive.Entries)
{
Console.WriteLine($"ファイル名: {entry.FullName}");
Console.WriteLine($"圧縮前サイズ: {entry.Length} bytes");
Console.WriteLine($"圧縮後サイズ: {entry.CompressedLength} bytes");
Console.WriteLine($"更新日時: {entry.LastWriteTime}");
Console.WriteLine();
}

entry.FullNameにはZIP内の相対パスが入ります。フォルダ内のファイルであれば、docs/readme.txtのような形式になります。

5-2. ZIPにファイルを追加する

既存のZIPファイルにファイルを追加するには、ZipFile.OpenZipArchiveMode.Updateを指定します。

C#
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";
string fileToAdd = @"C:\zip-sample\add.txt";
string entryName = "add.txt";

using ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Update);

archive.CreateEntryFromFile(
sourceFileName: fileToAdd,
entryName: entryName,
compressionLevel: CompressionLevel.Optimal
);

サブフォルダ内に追加したい場合は、entryNameにパスを含めます。

C#
archive.CreateEntryFromFile(fileToAdd, "docs/add.txt");

5-3. ZIP内のファイルを削除する

ZIP内のファイルを削除するには、対象のZipArchiveEntryを取得してDeleteを呼び出します。

C#
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";
string entryName = "docs/readme.txt";

using ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Update);

ZipArchiveEntry? entry = archive.GetEntry(entryName);

if (entry != null)
{
entry.Delete();
Console.WriteLine("ZIP内のファイルを削除しました。");
}
else
{
Console.WriteLine("指定したファイルはZIP内に存在しません。");
}

ZIP内のファイルを変更するには、ZipArchiveMode.Updateで開く必要があります。読み取り専用のOpenReadでは削除や追加はできません。

5-4. ZIP内のファイルを読み込む

ZIP内のテキストファイルを直接読み込むには、ZipArchiveEntry.Openでストリームを開きます。

C#
using System;
using System.IO;
using System.IO.Compression;

string zipPath = @"C:\zip-sample\archive.zip";
string entryName = "docs/readme.txt";

using ZipArchive archive = ZipFile.OpenRead(zipPath);

ZipArchiveEntry? entry = archive.GetEntry(entryName);

if (entry != null)
{
using Stream stream = entry.Open();
using StreamReader reader = new StreamReader(stream);

string content = reader.ReadToEnd();

Console.WriteLine(content);
}

この方法なら、ZIP全体を解凍せずに特定ファイルだけを読み込めます。設定ファイルや小さなテキストファイルをZIP内から確認したい場合に便利です。

5-5. メモリ上でZIPを作成・操作する方法

ファイルとして保存せず、メモリ上でZIPを作成したい場合は、MemoryStreamZipArchiveを組み合わせます。

C#
using System.IO;
using System.IO.Compression;
using System.Text;

byte[] zipBytes;

using (MemoryStream memoryStream = new MemoryStream())
{
using (ZipArchive archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
{
ZipArchiveEntry entry = archive.CreateEntry("message.txt");

using Stream entryStream = entry.Open();
using StreamWriter writer = new StreamWriter(entryStream, Encoding.UTF8);

writer.Write("メモリ上で作成したZIPファイルです。");
}

zipBytes = memoryStream.ToArray();
}

File.WriteAllBytes(@"C:\zip-sample\memory.zip", zipBytes);

Web APIでZIPをレスポンスとして返したい場合や、一時ファイルを作らずにZIPを生成したい場合に使えます。

6. ZipFileでよくあるエラーと原因・対策

6-1. IOException:ファイルが既に存在する

CreateFromDirectoryで出力先ZIPがすでに存在する場合、IOExceptionが発生することがあります。

System.IO.IOException: The file already exists.

対策は、事前に削除することです。

C#
if (File.Exists(zipPath))
{
File.Delete(zipPath);
}

ZipFile.CreateFromDirectory(sourceDirectory, zipPath);

ただし、重要なZIPファイルを扱う場合は、一時ファイルに作成してから置き換える方法が安全です。

6-2. DirectoryNotFoundException:パスが見つからない

存在しないフォルダを圧縮しようとした場合や、出力先の親フォルダが存在しない場合にDirectoryNotFoundExceptionが発生します。

C#
if (!Directory.Exists(sourceDirectory))
{
throw new DirectoryNotFoundException($"圧縮元フォルダが見つかりません: {sourceDirectory}");
}

string? zipDirectory = Path.GetDirectoryName(zipPath);
if (!string.IsNullOrEmpty(zipDirectory))
{
Directory.CreateDirectory(zipDirectory);
}

圧縮元は存在チェック、出力先は親フォルダを作成する、という流れにすると安定します。

6-3. UnauthorizedAccessException:アクセス権限がない

アクセス権限がないフォルダやファイルを読み書きしようとすると、UnauthorizedAccessExceptionが発生します。

主な原因は次のとおりです。

  • 管理者権限が必要なフォルダに出力している

  • 読み取り権限のないファイルを圧縮しようとしている

  • 書き込み権限のないフォルダに解凍しようとしている

  • 読み取り専用ファイルを上書きしようとしている

対策として、アプリケーションが書き込み可能なフォルダを使いましょう。たとえば、ユーザーの作業フォルダやアプリ専用のデータフォルダを使うと安全です。

C#
string outputDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp",
"ZipOutput"
);

Directory.CreateDirectory(outputDirectory);

6-4. InvalidDataException:ZIPファイルが壊れている

壊れたZIPファイルやZIP形式ではないファイルを解凍しようとすると、InvalidDataExceptionが発生することがあります。

C#
try
{
ZipFile.ExtractToDirectory(zipPath, extractDirectory);
}
catch (InvalidDataException)
{
Console.WriteLine("ZIPファイルが壊れているか、ZIP形式ではありません。");
}

ユーザーがアップロードしたZIPを扱う場合は、必ず例外処理を入れてください。拡張子が.zipでも、中身が正しいZIP形式とは限りません。

6-5. 日本語ファイル名が文字化けする場合の対策

日本語ファイル名が文字化けする場合は、ZIPの作成側と解凍側でファイル名エンコーディングの扱いが異なっている可能性があります。

ExtractToDirectoryには、エントリ名の文字エンコーディングを指定できるオーバーロードがあります。公式ドキュメントでは、エントリ名やコメントのデコードに関するentryNameEncodingの挙動が説明されています。Microsoft Learn

C#
using System.IO.Compression;
using System.Text;

ZipFile.ExtractToDirectory(
sourceArchiveFileName: zipPath,
destinationDirectoryName: extractDirectory,
entryNameEncoding: Encoding.UTF8
);

古いZIP作成ツールで作られたZIPの場合、Shift_JIS系のエンコーディングが必要になるケースもあります。

C#
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

ZipFile.ExtractToDirectory(
sourceArchiveFileName: zipPath,
destinationDirectoryName: extractDirectory,
entryNameEncoding: Encoding.GetEncoding("shift_jis")
);

.NET Coreや.NET 5以降でEncoding.GetEncoding("shift_jis")を使う場合は、System.Text.Encoding.CodePagesパッケージが必要になることがあります。

6-6. ファイル使用中で圧縮・解凍できない場合の対策

対象ファイルが別プロセスで使用中の場合、圧縮や解凍に失敗することがあります。

よくある原因は次のとおりです。

  • テキストエディタでファイルを開いたままにしている

  • アプリがFileStreamを閉じていない

  • ウイルス対策ソフトがスキャン中

  • 同時に別スレッドが同じファイルへアクセスしている

対策として、usingを使ってストリームを確実に閉じます。

C#
using (FileStream stream = File.OpenRead(@"C:\zip-sample\input\sample.txt"))
{
// 読み取り処理
}

// usingを抜けると自動的にCloseされる

ZIP操作の前後で同じファイルを開く場合は、必ずDisposeされているか確認しましょう。

7. ZipFileを安全に使うための注意点

7-1. Zip Slip脆弱性とは

ZIP解凍処理では、Zip Slipと呼ばれる脆弱性に注意が必要です。

Zip Slipとは、ZIP内のエントリ名に../のような相対パスを含めることで、解凍先フォルダの外にファイルを書き込ませる攻撃です。

たとえば、ZIP内に次のようなエントリがあると危険です。

../../../../Windows/System32/example.txt

解凍処理でパス検証をしていないと、本来の解凍先フォルダ外にファイルを作成・上書きされる可能性があります。

7-2. 解凍時にパスを検証する方法

安全に解凍するには、ZIP内エントリの展開先パスが、必ず解凍先フォルダの配下に収まることを確認します。

C#
using System.IO;
using System.IO.Compression;

static void SafeExtractToDirectory(string zipPath, string extractDirectory, bool overwrite)
{
Directory.CreateDirectory(extractDirectory);

string destinationRoot = Path.GetFullPath(extractDirectory);
if (!destinationRoot.EndsWith(Path.DirectorySeparatorChar))
{
destinationRoot += Path.DirectorySeparatorChar;
}

using ZipArchive archive = ZipFile.OpenRead(zipPath);

foreach (ZipArchiveEntry entry in archive.Entries)
{
string destinationPath = Path.GetFullPath(
Path.Combine(destinationRoot, entry.FullName)
);

if (!destinationPath.StartsWith(destinationRoot, StringComparison.OrdinalIgnoreCase))
{
throw new IOException($"不正なパスが含まれています: {entry.FullName}");
}

if (string.IsNullOrEmpty(entry.Name))
{
Directory.CreateDirectory(destinationPath);
continue;
}

string? directory = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}

entry.ExtractToFile(destinationPath, overwrite);
}
}

ユーザーがアップロードしたZIPや、外部から受け取ったZIPを解凍する場合は、このようなパス検証を入れることをおすすめします。

7-3. 大容量ZIPでメモリ不足を防ぐ方法

大容量ZIPを扱う場合は、メモリにすべて読み込まないことが重要です。

避けたい例は次のようなコードです。

C#
byte[] zipBytes = File.ReadAllBytes(zipPath);

小さなZIPなら問題ありませんが、数GBのZIPではメモリ不足の原因になります。

大容量ファイルでは、ファイルパスやストリームを使って処理しましょう。

C#
using FileStream zipStream = File.OpenRead(zipPath);
using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Read);

foreach (ZipArchiveEntry entry in archive.Entries)
{
Console.WriteLine(entry.FullName);
}

また、解凍前にentry.Lengthを確認して、想定以上に大きいファイルを拒否することも有効です。

C#
long maxFileSize = 100 * 1024 * 1024; // 100MB

foreach (ZipArchiveEntry entry in archive.Entries)
{
if (entry.Length > maxFileSize)
{
throw new IOException($"ファイルサイズが大きすぎます: {entry.FullName}");
}
}

7-4. 例外処理を入れた安全なサンプルコード

実際のアプリでは、ZIP圧縮・解凍に例外処理を入れておきましょう。

C#
using System;
using System.IO;
using System.IO.Compression;

static bool TryCreateZip(string sourceDirectory, string zipPath)
{
try
{
if (!Directory.Exists(sourceDirectory))
{
Console.WriteLine("圧縮元フォルダが存在しません。");
return false;
}

string? zipDirectory = Path.GetDirectoryName(zipPath);
if (!string.IsNullOrEmpty(zipDirectory))
{
Directory.CreateDirectory(zipDirectory);
}

if (File.Exists(zipPath))
{
File.Delete(zipPath);
}

ZipFile.CreateFromDirectory(
sourceDirectory,
zipPath,
CompressionLevel.Optimal,
includeBaseDirectory: false
);

return true;
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("アクセス権限がありません。");
return false;
}
catch (IOException ex)
{
Console.WriteLine($"ファイル入出力エラー: {ex.Message}");
return false;
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラー: {ex.Message}");
return false;
}
}

解凍側も同様に、壊れたZIP、不正パス、権限不足を考慮します。

C#
static bool TryExtractZip(string zipPath, string extractDirectory)
{
try
{
if (!File.Exists(zipPath))
{
Console.WriteLine("ZIPファイルが存在しません。");
return false;
}

SafeExtractToDirectory(zipPath, extractDirectory, overwrite: true);

return true;
}
catch (InvalidDataException)
{
Console.WriteLine("ZIPファイルが壊れているか、形式が正しくありません。");
return false;
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("アクセス権限がありません。");
return false;
}
catch (IOException ex)
{
Console.WriteLine($"ファイル入出力エラー: {ex.Message}");
return false;
}
}

7-5. 一時ファイル・一時フォルダの扱い方

ZIP作成や解凍では、一時ファイル・一時フォルダを使うと安全に処理できます。

C#
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

Directory.CreateDirectory(tempDirectory);

try
{
ZipFile.ExtractToDirectory(zipPath, tempDirectory);

// 検証後、正式な場所へ移動する
}
finally
{
if (Directory.Exists(tempDirectory))
{
Directory.Delete(tempDirectory, recursive: true);
}
}

一時フォルダを使うメリットは、解凍途中で失敗しても本来の出力先を汚しにくいことです。

特に、アップロードされたZIPを検証してから保存する処理では、一時フォルダへの解凍、ファイルチェック、正式フォルダへの移動という流れにすると安全です。

8. 実用的なサンプルコード集

8-1. 指定フォルダをZIP化するサンプル

C#
using System;
using System.IO;
using System.IO.Compression;

class Program
{
static void Main()
{
string sourceDirectory = @"C:\zip-sample\input";
string zipPath = @"C:\zip-sample\archive.zip";

if (!Directory.Exists(sourceDirectory))
{
Console.WriteLine("圧縮元フォルダが存在しません。");
return;
}

if (File.Exists(zipPath))
{
File.Delete(zipPath);
}

ZipFile.CreateFromDirectory(
sourceDirectory,
zipPath,
CompressionLevel.Optimal,
includeBaseDirectory: false
);

Console.WriteLine("ZIPファイルを作成しました。");
}
}

8-2. ZIPファイルを指定フォルダへ解凍するサンプル

C#
using System;
using System.IO;
using System.IO.Compression;

class Program
{
static void Main()
{
string zipPath = @"C:\zip-sample\archive.zip";
string extractDirectory = @"C:\zip-sample\output";

if (!File.Exists(zipPath))
{
Console.WriteLine("ZIPファイルが存在しません。");
return;
}

Directory.CreateDirectory(extractDirectory);

ZipFile.ExtractToDirectory(
zipPath,
extractDirectory,
overwriteFiles: true
);

Console.WriteLine("ZIPファイルを解凍しました。");
}
}

8-3. ZIP内のファイル一覧を表示するサンプル

C#
using System;
using System.IO.Compression;

class Program
{
static void Main()
{
string zipPath = @"C:\zip-sample\archive.zip";

using ZipArchive archive = ZipFile.OpenRead(zipPath);

foreach (ZipArchiveEntry entry in archive.Entries)
{
Console.WriteLine(entry.FullName);
}
}
}

ファイルサイズも表示したい場合は、次のようにします。

C#
foreach (ZipArchiveEntry entry in archive.Entries)
{
Console.WriteLine($"{entry.FullName} / {entry.Length} bytes");
}

8-4. ZIPにファイルを追加するサンプル

C#
using System;
using System.IO.Compression;

class Program
{
static void Main()
{
string zipPath = @"C:\zip-sample\archive.zip";
string fileToAdd = @"C:\zip-sample\add.txt";

using ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Update);

archive.CreateEntryFromFile(
fileToAdd,
"add.txt",
CompressionLevel.Optimal
);

Console.WriteLine("ZIPにファイルを追加しました。");
}
}

同じエントリ名がすでに存在する可能性がある場合は、事前に削除してから追加します。

C#
string entryName = "add.txt";

ZipArchiveEntry? existingEntry = archive.GetEntry(entryName);
existingEntry?.Delete();

archive.CreateEntryFromFile(fileToAdd, entryName);

8-5. ZIPから特定ファイルだけを解凍するサンプル

C#
using System;
using System.IO;
using System.IO.Compression;

class Program
{
static void Main()
{
string zipPath = @"C:\zip-sample\archive.zip";
string extractDirectory = @"C:\zip-sample\output";
string targetEntryName = "docs/readme.txt";

Directory.CreateDirectory(extractDirectory);

using ZipArchive archive = ZipFile.OpenRead(zipPath);

ZipArchiveEntry? entry = archive.GetEntry(targetEntryName);

if (entry == null)
{
Console.WriteLine("指定したファイルはZIP内にありません。");
return;
}

string destinationPath = Path.Combine(extractDirectory, entry.Name);

entry.ExtractToFile(destinationPath, overwrite: true);

Console.WriteLine("指定ファイルを解凍しました。");
}
}

このサンプルでは、docs/readme.txtを解凍先フォルダ直下のreadme.txtとして保存しています。ZIP内のフォルダ構成を維持したい場合は、entry.FullNameを使います。ただし、その場合はZip Slip対策としてパス検証を必ず行ってください。

8-6. エラー処理付きの圧縮・解凍サンプル

C#
using System;
using System.IO;
using System.IO.Compression;

class Program
{
static void Main()
{
string sourceDirectory = @"C:\zip-sample\input";
string zipPath = @"C:\zip-sample\archive.zip";
string extractDirectory = @"C:\zip-sample\output";

if (CreateZip(sourceDirectory, zipPath))
{
Console.WriteLine("圧縮に成功しました。");
}

if (ExtractZip(zipPath, extractDirectory))
{
Console.WriteLine("解凍に成功しました。");
}
}

static bool CreateZip(string sourceDirectory, string zipPath)
{
try
{
if (!Directory.Exists(sourceDirectory))
{
Console.WriteLine("圧縮元フォルダが存在しません。");
return false;
}

if (File.Exists(zipPath))
{
File.Delete(zipPath);
}

ZipFile.CreateFromDirectory(
sourceDirectory,
zipPath,
CompressionLevel.Optimal,
includeBaseDirectory: false
);

return true;
}
catch (Exception ex) when (
ex is IOException ||
ex is UnauthorizedAccessException ||
ex is DirectoryNotFoundException)
{
Console.WriteLine($"圧縮エラー: {ex.Message}");
return false;
}
}

static bool ExtractZip(string zipPath, string extractDirectory)
{
try
{
if (!File.Exists(zipPath))
{
Console.WriteLine("ZIPファイルが存在しません。");
return false;
}

Directory.CreateDirectory(extractDirectory);

ZipFile.ExtractToDirectory(
zipPath,
extractDirectory,
overwriteFiles: true
);

return true;
}
catch (Exception ex) when (
ex is IOException ||
ex is UnauthorizedAccessException ||
ex is InvalidDataException)
{
Console.WriteLine($"解凍エラー: {ex.Message}");
return false;
}
}
}

このように例外処理を入れておくと、ファイル存在エラー、権限エラー、ZIP破損などに対応しやすくなります。

9. ZipFileの使い方に関するよくある質問

9-1. ZipFileでパスワード付きZIPは作成できる?

C#標準のZipFileでは、パスワード付きZIPは作成できません。解凍も標準機能では対応していません。

パスワード付きZIPを作りたい場合は、外部ライブラリや7-Zipなどのツール連携を検討してください。ただし、セキュリティ目的でファイルを保護したい場合は、ZIPパスワードだけに頼らず、暗号化ストレージやアクセス制御も含めて設計することが重要です。

9-2. ZIPファイルを上書き作成できる?

CreateFromDirectoryは既存ZIPを自動上書きしません。上書き作成したい場合は、事前に既存ファイルを削除します。

C#
if (File.Exists(zipPath))
{
File.Delete(zipPath);
}

ZipFile.CreateFromDirectory(sourceDirectory, zipPath);

より安全にしたい場合は、一時ZIPを作成してから置き換える方法を使います。

9-3. ファイル単位でZIP圧縮するには?

1つのファイルだけをZIP化したい場合は、ZipArchiveを使ってZIPを作成し、CreateEntryFromFileでファイルを追加します。

C#
using System.IO.Compression;

string zipPath = @"C:\zip-sample\single.zip";
string filePath = @"C:\zip-sample\sample.txt";

using ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Create);

archive.CreateEntryFromFile(
filePath,
"sample.txt",
CompressionLevel.Optimal
);

ZipFile.CreateFromDirectoryはフォルダ単位の圧縮に向いているため、ファイル単位ならZipArchiveを使うのが自然です。

9-4. 空のフォルダはZIPに含められる?

CreateFromDirectoryでは、空のフォルダもZIP内に含まれる場合があります。ただし、解凍ソフトや操作方法によって見え方が異なることがあります。

空フォルダを確実に扱いたい場合は、ZipArchiveでディレクトリエントリを作成する方法があります。

C#
using ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Update);

archive.CreateEntry("empty-folder/");

ZIP内では、末尾に/を付けたエントリ名を使うことでフォルダとして扱われます。

9-5. 文字化けを防ぐにはどうすればいい?

日本語ファイル名の文字化けを防ぐには、作成時と解凍時のエンコーディングを意識します。

作成時にUTF-8を指定する例です。

C#
ZipFile.CreateFromDirectory(
sourceDirectory,
zipPath,
CompressionLevel.Optimal,
includeBaseDirectory: false,
entryNameEncoding: Encoding.UTF8
);

解凍時にUTF-8を指定する例です。

C#
ZipFile.ExtractToDirectory(
zipPath,
extractDirectory,
entryNameEncoding: Encoding.UTF8
);

ただし、ZIPファイルを作成したツールや解凍するソフトによっては、UTF-8以外の文字コードが使われていることもあります。文字化けが頻繁に起きる場合は、ファイル名を英数字に統一する、使用するZIPソフトを統一する、entryNameEncodingを明示する、といった対策を検討してください。

まとめ

C#でZIP圧縮・解凍を行う場合、標準ライブラリのSystem.IO.Compression.ZipFileを使うと、シンプルなコードで実装できます。

フォルダをZIP化する場合はZipFile.CreateFromDirectory、ZIPファイルを解凍する場合はZipFile.ExtractToDirectoryを使います。ZIP内のファイル一覧取得、追加、削除、読み込みなど、より細かい操作が必要な場合はZipArchiveZipArchiveEntryを使います。

一方で、ZipFileにはパスワード付きZIPを扱えない、既存ZIPを自動上書きしない、文字コードの違いで日本語ファイル名が文字化けすることがある、といった注意点もあります。

実用的なアプリケーションで使う場合は、次のポイントを押さえておきましょう。

  • 既存ZIPがある場合は削除または一時ファイル経由で置き換える

  • 解凍先フォルダは事前に作成する

  • 同名ファイルの上書き可否を明示する

  • 日本語ファイル名を扱う場合はentryNameEncodingを検討する

  • 外部ZIPを解凍する場合はZip Slip対策としてパス検証を行う

  • 大容量ZIPではメモリに全読み込みしない

  • 例外処理で権限エラー、破損ZIP、ファイル使用中に対応する

基本的なZIP圧縮・解凍であればZipFileだけで十分対応できます。ZIP内のファイルを個別に操作したい場合は、ZipArchiveを組み合わせることで、より柔軟なZIP処理をC#で実装できます。