C#でZIPファイルを圧縮・解凍する方法|ZipFile/ZipArchiveの使い分けとサンプルコード

はじめに

C#でZIPファイルを圧縮・解凍したい場合、まず候補になるのが標準ライブラリのSystem.IO.Compressionです。外部ライブラリを追加しなくても、フォルダをZIP化したり、ZIPファイルを展開したり、ZIP内のファイル一覧を取得したりできます。

「csharp zip」「C# ZIP 圧縮」「C# ZIP 解凍」といったキーワードで調べると、ZipFileZipArchiveという2つのクラスがよく出てきます。どちらもZIPを扱うためのクラスですが、用途が少し異なります。

ざっくり言うと、フォルダ全体を簡単に圧縮・解凍したい場合はZipFile、ZIP内の一部ファイルを読み書き・追加・削除したい場合はZipArchiveが向いています。Microsoft公式ドキュメントでも、ZIPアーカイブを扱う主なクラスとしてZipFileZipArchiveZipArchiveEntryが説明されています。Microsoft Learn+1

この記事では、C#でZIPファイルを圧縮・解凍する基本から、ZipFileZipArchiveの使い分け、実用的なサンプルコード、エラー対策、安全な解凍方法までまとめて解説します。

1. C#でZIPファイルを扱う基本

1-1. C#でZIP圧縮・解凍する主な方法

C#でZIPファイルを扱う方法は、大きく分けると次の3つです。

1つ目は、ZipFileを使う方法です。ZipFile.CreateFromDirectoryを使えばフォルダをZIPファイルに圧縮でき、ZipFile.ExtractToDirectoryを使えばZIPファイルをフォルダに解凍できます。フォルダ単位の処理であれば、最もシンプルに書ける方法です。

2つ目は、ZipArchiveを使う方法です。ZIPファイルをアーカイブとして開き、Entriesから中身を確認したり、CreateEntryでファイルを追加したり、ZipArchiveEntry.Deleteで削除したりできます。ZIP内の一部ファイルだけを操作したい場合に向いています。

3つ目は、Streamと組み合わせる方法です。ファイルとして保存せず、メモリ上でZIPファイルを作成したり、Web APIのレスポンスとしてZIPを返したりする場合に使います。

1-2. System.IO.Compressionとは

System.IO.Compressionは、.NETで圧縮・解凍処理を行うための名前空間です。ZIPファイルを扱う場合は、主に次のクラスを使用します。

C#
using System.IO.Compression;

代表的なクラスは次のとおりです。

クラス主な用途
ZipFileフォルダ単位でZIPを作成・解凍する
ZipArchiveZIPファイル全体を開いて読み書きする
ZipArchiveEntryZIP内の1ファイルを表す
CompressionLevel圧縮レベルを指定する
ZipArchiveMode読み取り、作成、更新モードを指定する

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

1-3. ZipFileとZipArchiveの違い

ZipFileZipArchiveの違いは、操作の粒度です。

ZipFileは、フォルダ全体をまとめて圧縮したり、ZIPファイル全体をまとめて解凍したりするための便利クラスです。コード量が少なく、単純なZIP処理に向いています。

一方、ZipArchiveはZIPファイルを開いて、内部のエントリを1つずつ操作するためのクラスです。ZIP内のファイル一覧を取得したい、特定のファイルだけ解凍したい、ZIPにファイルを追加したい、ZIP内のファイルを削除したい、といった場合に使います。

たとえば、フォルダをそのままZIP化するだけなら次のように書けます。

C#
ZipFile.CreateFromDirectory(@"C:\work\input", @"C:\work\output.zip");

一方、ZIP内のファイルを1つずつ処理する場合は、次のようにZipArchiveを使います。

C#
using ZipArchive archive = ZipFile.OpenRead(@"C:\work\output.zip");

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

1-4. この記事でできるようになること

この記事を読むと、次のようなC#のZIP処理を実装できるようになります。

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

  • 圧縮レベルを指定してZIPを作成する

  • ZIPファイルを指定フォルダに解凍する

  • 既存ファイルを上書きしながら解凍する

  • ZIP内のファイル一覧を取得する

  • ZIP内のテキストファイルを読み込む

  • ZIPにファイルを追加する

  • ZIP内のファイルを削除する

  • 特定ファイルだけを安全に解凍する

  • メモリ上でZIPファイルを作成する

  • 文字化けやZIP Slipなどの注意点に対応する

2. ZipFileとZipArchiveの使い分け

2-1. ZipFileが向いているケース

ZipFileは、処理内容がシンプルな場合に向いています。

たとえば、次のようなケースです。

  • フォルダを丸ごとZIPファイルにしたい

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

  • ZIP内の個別ファイルを細かく操作しない

  • 短いコードで実装したい

  • バックアップファイルを作成したい

  • ログフォルダをまとめて圧縮したい

ZipFileを使うと、フォルダの圧縮はほぼ1行で実装できます。

C#
ZipFile.CreateFromDirectory(sourceDirectory, zipPath);

解凍も同じようにシンプルです。

C#
ZipFile.ExtractToDirectory(zipPath, extractDirectory);

Microsoft公式ドキュメントでも、ZipFile.CreateFromDirectoryはディレクトリからZIPアーカイブを作成し、ZipFile.ExtractToDirectoryはZIPアーカイブの内容をディレクトリに展開するメソッドとして説明されています。Microsoft Learn+1

2-2. ZipArchiveが向いているケース

ZipArchiveは、ZIPファイルの中身を細かく扱いたい場合に向いています。

たとえば、次のようなケースです。

  • ZIP内のファイル一覧を取得したい

  • ZIP内の特定ファイルだけ解凍したい

  • ZIP内のテキストファイルを直接読みたい

  • ZIPにファイルを追加したい

  • ZIP内の不要なファイルを削除したい

  • ZIPをメモリ上で作成したい

  • Web APIでZIPファイルを動的に返したい

ZipArchiveでは、ZipArchiveMode.ReadZipArchiveMode.CreateZipArchiveMode.Updateを使い分けます。公式ドキュメントでは、Readは読み取り、Createは新規作成、Updateは読み書きの両方に使うモードとして説明されています。Updateモードでは、基になるファイルやストリームが読み取り・書き込み・シークをサポートしている必要があります。Microsoft Learn

2-3. フォルダ単位で簡単に圧縮・解凍したい場合

フォルダ単位で圧縮・解凍するだけなら、基本的にはZipFileを選びます。

C#
using System.IO.Compression;

string sourceDir = @"C:\work\input";
string zipPath = @"C:\work\input.zip";
string extractDir = @"C:\work\extract";

ZipFile.CreateFromDirectory(sourceDir, zipPath);
ZipFile.ExtractToDirectory(zipPath, extractDir);

このように、フォルダ全体を扱う処理ではZipFileが非常に簡潔です。業務アプリケーションで、帳票フォルダ、ログフォルダ、アップロードファイル一式などをまとめてZIP化する場合によく使います。

2-4. ZIP内の一部ファイルを操作したい場合

ZIP内の一部ファイルを操作したい場合は、ZipArchiveを使います。

たとえば、ZIP内にある.txtファイルだけを一覧表示する場合は次のように書けます。

C#
using System.IO.Compression;

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

using ZipArchive archive = ZipFile.OpenRead(zipPath);

foreach (ZipArchiveEntry entry in archive.Entries)
{
if (Path.GetExtension(entry.FullName).Equals(".txt", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(entry.FullName);
}
}

ZipArchiveEntryは、ZIP内の1つのファイルを表します。公式ドキュメントでも、ZipArchiveEntryはZIPアーカイブ内の圧縮されたファイルを表し、プロパティ確認、オープン、削除などに使えるクラスとして説明されています。Microsoft Learn+1

2-5. 使い分け早見表

やりたいことおすすめ
フォルダを丸ごとZIP化したいZipFile.CreateFromDirectory
ZIPを丸ごと解凍したいZipFile.ExtractToDirectory
ZIP内の一覧を取得したいZipArchive
特定ファイルだけ解凍したいZipArchive
ZIP内のテキストを読みたいZipArchiveEntry.Open
ZIPにファイルを追加したいZipArchive
ZIP内のファイルを削除したいZipArchiveEntry.Delete
メモリ上でZIPを作りたいZipArchive + MemoryStream
とにかく短く書きたいZipFile
細かく制御したいZipArchive

迷った場合は、「フォルダ単位ならZipFile」「ファイル単位ならZipArchive」と覚えておくと判断しやすくなります。

3. C#でZIPファイルを圧縮する方法

3-1. ZipFile.CreateFromDirectoryでフォルダをZIP化する

C#でフォルダをZIP化する最も簡単な方法は、ZipFile.CreateFromDirectoryを使うことです。

C#
using System.IO.Compression;

string sourceDirectory = @"C:\work\files";
string zipPath = @"C:\work\files.zip";

ZipFile.CreateFromDirectory(sourceDirectory, zipPath);

このコードを実行すると、C:\work\filesフォルダ内のファイルとサブフォルダがfiles.zipとして圧縮されます。

注意点として、出力先のZIPファイルがすでに存在している場合は例外が発生します。そのため、既存のZIPファイルを上書きしたい場合は、事前に削除しておく必要があります。

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

ZipFile.CreateFromDirectory(sourceDirectory, zipPath);

3-2. 圧縮レベルを指定してZIPファイルを作成する

ZipFile.CreateFromDirectoryでは、圧縮レベルを指定できます。

C#
using System.IO.Compression;

string sourceDirectory = @"C:\work\files";
string zipPath = @"C:\work\files.zip";

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

CompressionLevelには、主に次のような値があります。

説明
Optimal圧縮率を重視する
Fastest処理速度を重視する
NoCompression圧縮せず格納する
SmallestSizeサイズをより小さくすることを重視する環境向け

通常はCompressionLevel.Optimalを使えば問題ありません。大量のファイルを高速に処理したい場合はFastestを検討します。画像、動画、PDFなど、すでに圧縮済みのファイルはZIP化してもサイズがあまり小さくならないことがあります。

includeBaseDirectorytrueにすると、元フォルダ自体をZIP内に含めます。falseにすると、元フォルダの中身だけがZIPに入ります。

C#
ZipFile.CreateFromDirectory(
@"C:\work\files",
@"C:\work\files.zip",
CompressionLevel.Optimal,
includeBaseDirectory: true
);

3-3. ZipArchiveで複数ファイルをZIPに追加する

任意のファイルだけを選んでZIPに追加したい場合は、ZipArchiveを使います。

C#
using System.IO.Compression;

string zipPath = @"C:\work\selected.zip";

string[] files =
{
@"C:\work\docs\readme.txt",
@"C:\work\docs\manual.pdf",
@"C:\work\images\logo.png"
};

using FileStream zipStream = new FileStream(zipPath, FileMode.Create);
using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create);

foreach (string file in files)
{
string entryName = Path.GetFileName(file);
archive.CreateEntryFromFile(file, entryName, CompressionLevel.Optimal);
}

CreateEntryFromFileを使うと、指定したファイルをZIP内のエントリとして追加できます。公式ドキュメントでも、CreateEntryFromFileはファイルを圧縮してZIPアーカイブへ追加する拡張メソッドとして説明されています。Microsoft Learn

3-4. ZIP内のファイル名や階層を指定する

ZIP内のファイル名や階層は、エントリ名で指定します。

C#
archive.CreateEntryFromFile(
@"C:\work\docs\readme.txt",
"documents/readme.txt",
CompressionLevel.Optimal
);

この場合、ZIP内には次のようなパスで格納されます。

documents/readme.txt

複数フォルダに分けてZIP化したい場合は、エントリ名に相対パスを指定します。

C#
archive.CreateEntryFromFile(@"C:\work\a.txt", "text/a.txt");
archive.CreateEntryFromFile(@"C:\work\b.csv", "data/b.csv");
archive.CreateEntryFromFile(@"C:\work\c.png", "images/c.png");

ZIP内のパス区切りには、基本的にスラッシュ/を使うと扱いやすくなります。Windowsのバックスラッシュ\をそのまま使うと、環境や展開ツールによって見え方が変わることがあります。

3-5. 圧縮時のサンプルコード

次のサンプルは、指定フォルダをZIP化し、既存ZIPがある場合は削除してから作成する実用的なコードです。

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

class Program
{
static void Main()
{
string sourceDirectory = @"C:\work\input";
string zipPath = @"C:\work\output.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ファイルを作成しました。");
}
}

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

4-1. ZipFile.ExtractToDirectoryでZIPを解凍する

ZIPファイルを解凍する最も簡単な方法は、ZipFile.ExtractToDirectoryを使うことです。

C#
using System.IO.Compression;

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

ZipFile.ExtractToDirectory(zipPath, extractDirectory);

このコードを実行すると、sample.zipの中身がC:\work\extractに展開されます。

解凍先フォルダが存在しない場合、通常は必要に応じて作成されます。ただし、親フォルダが存在しない、アクセス権限がない、同名ファイルが存在するなどの場合は例外が発生することがあります。

4-2. 上書きありで解凍する方法

既存ファイルを上書きしながら解凍したい場合は、overwriteFilesを指定できるオーバーロードを使います。

C#
using System.IO.Compression;

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

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

ZipFile.ExtractToDirectoryには、文字エンコーディングや上書き可否を指定できるオーバーロードがあります。公式ドキュメントでも、ExtractToDirectory(String, String, Encoding, Boolean)などのオーバーロードが説明されています。Microsoft Learn

上書きしたくない場合は、overwriteFiles: falseにするか、引数を省略します。

C#
ZipFile.ExtractToDirectory(zipPath, extractDirectory, overwriteFiles: false);

4-3. ZipArchiveで特定のファイルだけ解凍する

ZIP内の特定ファイルだけを解凍したい場合は、ZipArchiveを使います。

C#
using System.IO.Compression;

string zipPath = @"C:\work\sample.zip";
string entryName = "documents/readme.txt";
string outputPath = @"C:\work\extract\readme.txt";

using ZipArchive archive = ZipFile.OpenRead(zipPath);

ZipArchiveEntry? entry = archive.GetEntry(entryName);

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

Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
entry.ExtractToFile(outputPath, overwrite: true);

ExtractToFileを使うと、ZIP内の1エントリを指定したファイルパスへ抽出できます。overwrite: trueを指定すると、既存ファイルを上書きできます。

4-4. 解凍先フォルダが存在しない場合の対応

解凍先フォルダが存在しない場合は、事前にDirectory.CreateDirectoryで作成しておくと安全です。

C#
string extractDirectory = @"C:\work\extract";

Directory.CreateDirectory(extractDirectory);

ZipFile.ExtractToDirectory(
@"C:\work\sample.zip",
extractDirectory,
overwriteFiles: true
);

Directory.CreateDirectoryは、すでにフォルダが存在していても例外になりにくいため、解凍処理の前に呼んでおくと実装が安定します。

4-5. 解凍時のサンプルコード

次のサンプルは、ZIPファイルの存在確認、解凍先フォルダの作成、上書き解凍をまとめたコードです。

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

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

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

Directory.CreateDirectory(extractDirectory);

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

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

5. ZipArchiveでZIPファイルを編集・読み取りする方法

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

ZIP内のファイル一覧を取得するには、ZipArchive.Entriesを使います。

C#
using System.IO.Compression;

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

using ZipArchive archive = ZipFile.OpenRead(zipPath);

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

FullNameには、ZIP内での相対パスが入ります。たとえば、ZIP内にdocuments/readme.txtがある場合、FullNamedocuments/readme.txtになります。

ファイルサイズや更新日時も取得できます。

C#
foreach (ZipArchiveEntry entry in archive.Entries)
{
Console.WriteLine($"Name: {entry.FullName}");
Console.WriteLine($"Size: {entry.Length}");
Console.WriteLine($"Compressed: {entry.CompressedLength}");
Console.WriteLine($"LastWriteTime: {entry.LastWriteTime}");
}

ZipArchive.Entriesは、ZIPアーカイブ内のエントリコレクションを返すプロパティです。Microsoft Learn

5-2. ZIP内のテキストファイルを読み込む

ZIP内のテキストファイルを解凍せずに読み込むには、ZipArchiveEntry.Openを使います。

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

string zipPath = @"C:\work\sample.zip";
string entryName = "documents/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, Encoding.UTF8);

string text = reader.ReadToEnd();
Console.WriteLine(text);
}

ZipArchiveEntry.Openは、エントリの内容を表すStreamを返します。公式ドキュメントでも、戻り値はエントリの内容を表すStreamと説明されています。Microsoft Learn

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

既存のZIPファイルにファイルを追加するには、ZipArchiveMode.Updateで開きます。

C#
using System.IO.Compression;

string zipPath = @"C:\work\sample.zip";
string fileToAdd = @"C:\work\add.txt";

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

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

同じエントリ名のファイルがすでに存在している場合、同名エントリが重複して作成される可能性があります。上書きのように扱いたい場合は、既存エントリを削除してから追加します。

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

string entryName = "add.txt";

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

archive.CreateEntryFromFile(fileToAdd, entryName, CompressionLevel.Optimal);

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

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

C#
using System.IO.Compression;

string zipPath = @"C:\work\sample.zip";
string entryName = "old.txt";

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

ZipArchiveEntry? entry = archive.GetEntry(entryName);

if (entry != null)
{
entry.Delete();
Console.WriteLine("ZIP内のファイルを削除しました。");
}

ZipArchiveEntry.Deleteは、ZIPアーカイブからエントリを削除するメソッドです。ただし、アーカイブがUpdate以外のモードで開かれている場合などは例外になるため注意が必要です。Microsoft Learn

5-5. Streamを使ってZIPをメモリ上で扱う

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

C#
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("hello.txt", CompressionLevel.Optimal);

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

writer.Write("Hello, ZIP!");
}

zipBytes = memoryStream.ToArray();
}

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

leaveOpen: trueを指定しているのは、ZipArchiveを破棄したあともMemoryStreamからデータを取り出すためです。Web APIでZIPを返す場合も、このようにメモリ上でZIPを作成する実装がよく使われます。

6. C#でZIP処理を実装するときの注意点

6-1. 既存ファイルの上書きに注意する

ZIP作成時、出力先のZIPファイルがすでに存在すると例外になることがあります。上書きしたい場合は、事前に削除してから作成します。

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

ZipFile.CreateFromDirectory(sourceDirectory, zipPath);

解凍時も同様に、同名ファイルが解凍先に存在すると例外になる場合があります。上書きを許可するなら、overwriteFiles: trueを指定します。

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

ただし、誤って重要なファイルを上書きしないように、解凍先フォルダは専用の作業ディレクトリにするのがおすすめです。

6-2. 日本語ファイル名の文字化け対策

ZIP内に日本語ファイル名が含まれる場合、文字化けが起きることがあります。特に、古いツールで作成されたZIPや、Shift_JIS系のエンコーディングで作成されたZIPを扱う場合は注意が必要です。

UTF-8で扱う場合は、エンコーディングを指定できるオーバーロードを使います。

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

ZipFile.ExtractToDirectory(
@"C:\work\japanese.zip",
@"C:\work\extract",
Encoding.UTF8,
overwriteFiles: true
);

CP932、つまりWindowsの日本語環境で使われるShift_JIS系の文字コードを扱いたい場合は、環境によってはSystem.Text.Encoding.CodePagesパッケージを追加し、コードページプロバイダーを登録します。

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

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding sjis = Encoding.GetEncoding(932);

ZipFile.ExtractToDirectory(
@"C:\work\legacy.zip",
@"C:\work\extract",
sjis,
overwriteFiles: true
);

新しく作成するZIPではUTF-8に統一し、古いZIPや外部システムから受け取るZIPではエンコーディングを確認する、という運用にするとトラブルを減らせます。

6-3. パス指定ミスによるエラーを防ぐ

ZIP処理では、パス指定ミスによるエラーがよく起きます。

よくあるミスは次のとおりです。

  • 圧縮元フォルダが存在しない

  • ZIPファイルの保存先フォルダが存在しない

  • 解凍先フォルダにアクセス権限がない

  • ファイルパスとフォルダパスを取り違えている

  • 相対パスの基準ディレクトリを誤解している

  • ZIP内エントリ名に絶対パスを使ってしまう

対策として、処理前に存在確認を行います。

C#
if (!Directory.Exists(sourceDirectory))
{
throw new DirectoryNotFoundException(sourceDirectory);
}

string? outputDir = Path.GetDirectoryName(zipPath);

if (!string.IsNullOrEmpty(outputDir))
{
Directory.CreateDirectory(outputDir);
}

ZIP内のエントリ名には、基本的に相対パスを使います。

C#
archive.CreateEntryFromFile(filePath, "documents/report.txt");

6-4. ZIP Slip対策と安全な解凍処理

外部から受け取ったZIPファイルを解凍する場合は、ZIP Slipに注意が必要です。

ZIP Slipとは、ZIP内のエントリ名に../などを含めることで、解凍先フォルダの外にファイルを書き出させる攻撃手法です。たとえば、悪意のあるZIPに次のようなエントリが含まれている可能性があります。

../../malicious.txt

安全に解凍するには、展開先のフルパスを確認し、解凍先フォルダの外に出ないことをチェックします。

C#
using System.IO.Compression;

static void SafeExtractToDirectory(string zipPath, string destinationDirectory)
{
Directory.CreateDirectory(destinationDirectory);

string destinationFullPath = Path.GetFullPath(destinationDirectory);

if (!destinationFullPath.EndsWith(Path.DirectorySeparatorChar))
{
destinationFullPath += Path.DirectorySeparatorChar;
}

using ZipArchive archive = ZipFile.OpenRead(zipPath);

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

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

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

Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
entry.ExtractToFile(destinationPath, overwrite: true);
}
}

ユーザーがアップロードしたZIP、メール添付のZIP、外部サービスから取得したZIPなど、信頼できないZIPファイルを扱う場合は、単純なExtractToDirectoryだけでなく、安全なパス検証を入れることをおすすめします。

6-5. パスワード付きZIPへの対応可否

System.IO.Compressionの標準機能では、パスワード付きZIPの作成・解凍を直接扱うためのAPIは用意されていません。

パスワード付きZIPを扱いたい場合は、次のような選択肢があります。

  • 7-Zipなどの外部コマンドを呼び出す

  • パスワード付きZIPに対応した外部ライブラリを使う

  • ZIP以外の暗号化方式を検討する

  • ファイル自体を暗号化してからZIP化する

業務システムでは、単にパスワード付きZIPを作るよりも、アクセス制御されたストレージや暗号化済みファイル転送を使う方が安全な場合もあります。要件に応じて検討しましょう。

7. よくあるエラーと対処法

7-1. System.IO.Compressionが見つからない場合

ZipFileZipArchiveが見つからない場合は、まずusingが不足していないか確認します。

C#
using System.IO.Compression;

.NET Frameworkの場合は、参照設定にSystem.IO.CompressionSystem.IO.Compression.FileSystemが必要になることがあります。

プロジェクトファイルにパッケージ参照を追加する場合は、環境に応じて次のような対応を行います。

XML
<ItemGroup>
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
</ItemGroup>

.NET 6以降のプロジェクトでは、通常はusing System.IO.Compression;を追加すれば使えるケースが多いです。

7-2. ファイルが使用中で解凍・圧縮できない場合

次のような例外が出る場合、ファイルが別のプロセスで使用中の可能性があります。

The process cannot access the file because it is being used by another process.

対処法は次のとおりです。

  • 対象ファイルを開いているアプリケーションを閉じる

  • FileStreamZipArchiveusingで確実に破棄する

  • 同じZIPファイルを同時に読み書きしない

  • 出力先ZIPを別名にする

  • 一時ファイルに作成してから移動する

ZipArchiveを使う場合は、必ずusingを使ってリソースを解放します。

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

7-3. 同名ファイルが存在してエラーになる場合

ZIP作成時に同名のZIPファイルが存在するとエラーになる場合があります。上書きしたい場合は、事前に削除します。

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

解凍時に同名ファイルが存在する場合は、上書きオプションを使います。

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

ZIP内に同名エントリを追加したくない場合は、既存エントリを削除してから追加します。

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

ZipArchiveEntry? oldEntry = archive.GetEntry("data/report.csv");
oldEntry?.Delete();

archive.CreateEntryFromFile(
@"C:\work\report.csv",
"data/report.csv",
CompressionLevel.Optimal
);

7-4. 空のZIPファイルになってしまう場合

ZIPファイルが空になってしまう場合は、次の点を確認します。

  • 圧縮元フォルダにファイルが存在するか

  • 検索条件でファイルを除外していないか

  • ZipArchiveを正しく破棄しているか

  • entry.Open()で書き込んだあとにストリームを閉じているか

  • MemoryStreamからデータを取り出すタイミングが早すぎないか

特にZipArchiveは、破棄時にZIPの情報を書き込むことがあります。そのため、メモリ上でZIPを作成する場合は、ZipArchiveを閉じたあとにToArray()を呼び出します。

C#
byte[] bytes;

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

using StreamWriter writer = new StreamWriter(entry.Open());
writer.Write("sample");
}

bytes = ms.ToArray();
}

7-5. 文字化け・パス区切りの問題が起きる場合

文字化けが起きる場合は、ZIPのファイル名エンコーディングを確認します。UTF-8で作成されたZIPなのか、Shift_JIS系で作成されたZIPなのかによって対処が変わります。

UTF-8で解凍する例です。

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

Shift_JIS系で解凍する例です。

C#
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

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

また、ZIP内のパス区切りはスラッシュ/に統一すると、Windows以外の環境でも扱いやすくなります。

C#
string entryName = relativePath.Replace('\\', '/');

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

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

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

public static class ZipSample
{
public static void CompressFolder(string sourceDirectory, string zipPath)
{
if (!Directory.Exists(sourceDirectory))
{
throw new DirectoryNotFoundException($"圧縮元フォルダが存在しません: {sourceDirectory}");
}

string? outputDirectory = Path.GetDirectoryName(zipPath);

if (!string.IsNullOrEmpty(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}

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

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

呼び出し例です。

C#
ZipSample.CompressFolder(
@"C:\work\input",
@"C:\work\output.zip"
);

8-2. ZIPファイルを解凍するサンプル

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

public static class UnzipSample
{
public static void ExtractZip(string zipPath, string extractDirectory)
{
if (!File.Exists(zipPath))
{
throw new FileNotFoundException("ZIPファイルが存在しません。", zipPath);
}

Directory.CreateDirectory(extractDirectory);

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

呼び出し例です。

C#
UnzipSample.ExtractZip(
@"C:\work\sample.zip",
@"C:\work\extract"
);

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

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

public static class ZipListSample
{
public static void PrintEntries(string zipPath)
{
using ZipArchive archive = ZipFile.OpenRead(zipPath);

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

呼び出し例です。

C#
ZipListSample.PrintEntries(@"C:\work\sample.zip");

拡張子で絞り込む場合は、次のように書けます。

C#
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (Path.GetExtension(entry.FullName).Equals(".csv", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(entry.FullName);
}
}

8-4. 特定ファイルだけを解凍するサンプル

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

public static class ExtractEntrySample
{
public static bool ExtractSingleFile(
string zipPath,
string entryName,
string outputPath,
bool overwrite = true)
{
using ZipArchive archive = ZipFile.OpenRead(zipPath);

ZipArchiveEntry? entry = archive.GetEntry(entryName);

if (entry == null)
{
return false;
}

string? outputDirectory = Path.GetDirectoryName(outputPath);

if (!string.IsNullOrEmpty(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}

entry.ExtractToFile(outputPath, overwrite);

return true;
}
}

呼び出し例です。

C#
bool result = ExtractEntrySample.ExtractSingleFile(
@"C:\work\sample.zip",
"documents/readme.txt",
@"C:\work\extract\readme.txt"
);

Console.WriteLine(result ? "解凍しました。" : "ファイルが見つかりませんでした。");

8-5. メモリ上でZIPファイルを作成するサンプル

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

public static class MemoryZipSample
{
public static byte[] CreateZipInMemory()
{
using MemoryStream memoryStream = new MemoryStream();

using (ZipArchive archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
{
AddTextFile(archive, "readme.txt", "これはREADMEです。");
AddTextFile(archive, "data/sample.csv", "id,name\n1,山田\n2,佐藤");
}

return memoryStream.ToArray();
}

private static void AddTextFile(ZipArchive archive, string entryName, string content)
{
ZipArchiveEntry entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);

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

writer.Write(content);
}
}

呼び出し例です。

C#
byte[] zipBytes = MemoryZipSample.CreateZipInMemory();

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

ASP.NET CoreのAPIでZIPを返す場合は、作成したbyte[]Fileレスポンスとして返す実装にも応用できます。

C#
return File(zipBytes, "application/zip", "download.zip");

9. C#のZIP処理に関するよくある質問

9-1. ZipFileとZipArchiveはどちらを使うべき?

フォルダを丸ごと圧縮・解凍するだけならZipFileを使うのがおすすめです。コードが短く、実装もわかりやすいためです。

一方、ZIP内のファイル一覧を取得したい、特定ファイルだけ解凍したい、ZIPにファイルを追加したい、ZIP内のファイルを削除したい、といった場合はZipArchiveを使います。

判断基準は次のとおりです。

条件使うクラス
フォルダ単位で処理するZipFile
ZIP全体をまとめて解凍するZipFile
ZIP内のファイルを個別に扱うZipArchive
メモリ上でZIPを作るZipArchive
Web APIでZIPを動的生成するZipArchive

9-2. パスワード付きZIPは標準ライブラリで作れる?

C#の標準ライブラリであるSystem.IO.Compressionだけでは、パスワード付きZIPの作成や解凍を直接行うためのAPIは用意されていません。

パスワード付きZIPが必要な場合は、外部ライブラリや7-Zipなどの外部ツールを検討します。ただし、セキュリティ要件によっては、パスワード付きZIPではなく、暗号化ストレージ、アクセス制御、HTTPS経由の安全なダウンロードなどを使う方が適している場合もあります。

9-3. .NET Frameworkでも使える?

.NET FrameworkでもSystem.IO.Compressionを使ってZIP処理を実装できます。ただし、ZipFileを使う場合は、プロジェクトにSystem.IO.Compression.FileSystemへの参照を追加する必要がある場合があります。Microsoft Learn

Visual Studioで参照を追加する場合は、プロジェクトの参照設定からSystem.IO.CompressionおよびSystem.IO.Compression.FileSystemを確認してください。

.NET 6、.NET 8、.NET 10などのモダンな.NETでは、基本的にusing System.IO.Compression;を追加して使うケースが多くなっています。

9-4. ZIPファイルを非同期で処理できる?

ZIP処理の一部は、ストリーム操作と組み合わせることで非同期処理にできます。

たとえば、ZIP内にテキストを書き込む場合は、StreamWriter.WriteAsyncを使えます。

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

using MemoryStream memoryStream = new MemoryStream();

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

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

await writer.WriteAsync("非同期で書き込みます。");
}

また、公式ドキュメントには、指定ディレクトリを含むZIPアーカイブを指定ストリームに非同期で作成するZipFile.CreateFromDirectoryAsyncも掲載されています。Microsoft Learn

ただし、すべてのZIP操作が完全に非同期化できるわけではありません。大容量ファイルを扱う場合は、メモリ使用量、ディスクI/O、同時実行数に注意しましょう。

9-5. 大容量ファイルをZIP化するときの注意点

大容量ファイルをZIP化する場合は、次の点に注意します。

  • メモリ上に全データを読み込まない

  • FileStreamを使ってストリーム処理する

  • 一時フォルダの空き容量を確認する

  • 圧縮レベルを必要以上に高くしない

  • すでに圧縮済みのファイルはサイズ削減効果が小さい

  • Web APIで返す場合はタイムアウトに注意する

  • 複数リクエストで同時にZIP化しすぎない

  • ZipArchiveMode.Updateは大きなZIPでメモリ使用量が増えやすい

特にZipArchiveMode.Updateでは、公式ドキュメントにあるように、更新モードでアーカイブ全体の内容がメモリに保持される点に注意が必要です。大容量ZIPを編集する場合は、既存ZIPを直接更新するより、新しいZIPを作り直す設計の方が安定することがあります。Microsoft Learn

まとめ

C#でZIPファイルを圧縮・解凍するには、標準ライブラリのSystem.IO.Compressionを使うのが基本です。

フォルダを丸ごとZIP化する、ZIPファイルを丸ごと解凍する、といったシンプルな処理にはZipFileが向いています。

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

一方、ZIP内のファイル一覧を取得する、特定ファイルだけ解凍する、ZIPにファイルを追加する、ZIP内のファイルを削除する、といった細かい操作にはZipArchiveが向いています。

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

使い分けの基本は、次のとおりです。

用途使うクラス
フォルダ単位の圧縮・解凍ZipFile
ZIP内の個別ファイル操作ZipArchive
ZIP内の1ファイルを扱うZipArchiveEntry
メモリ上でZIPを作るZipArchive + MemoryStream

実装時には、既存ファイルの上書き、日本語ファイル名の文字化け、パス指定ミス、ZIP Slip、パスワード付きZIPの非対応、大容量ファイルのメモリ使用量に注意しましょう。

「csharp zip」や「C# ZIP 圧縮 解凍」の実装では、まずZipFileで簡単に実装できるかを考え、細かい制御が必要になったらZipArchiveを使う、という流れで設計すると、シンプルで保守しやすいコードになります。