C#でZIPファイルを圧縮・解凍する方法|ZipFile/ZipArchiveの使い分けとサンプルコード
はじめに
C#でZIPファイルを圧縮・解凍したい場合、まず候補になるのが標準ライブラリのSystem.IO.Compressionです。外部ライブラリを追加しなくても、フォルダをZIP化したり、ZIPファイルを展開したり、ZIP内のファイル一覧を取得したりできます。
「csharp zip」「C# ZIP 圧縮」「C# ZIP 解凍」といったキーワードで調べると、ZipFileとZipArchiveという2つのクラスがよく出てきます。どちらもZIPを扱うためのクラスですが、用途が少し異なります。
ざっくり言うと、フォルダ全体を簡単に圧縮・解凍したい場合はZipFile、ZIP内の一部ファイルを読み書き・追加・削除したい場合はZipArchiveが向いています。Microsoft公式ドキュメントでも、ZIPアーカイブを扱う主なクラスとしてZipFile、ZipArchive、ZipArchiveEntryが説明されています。Microsoft Learn+1
この記事では、C#でZIPファイルを圧縮・解凍する基本から、ZipFileとZipArchiveの使い分け、実用的なサンプルコード、エラー対策、安全な解凍方法までまとめて解説します。
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を作成・解凍する |
ZipArchive | ZIPファイル全体を開いて読み書きする |
ZipArchiveEntry | ZIP内の1ファイルを表す |
CompressionLevel | 圧縮レベルを指定する |
ZipArchiveMode | 読み取り、作成、更新モードを指定する |
.NET FrameworkでZipFileを使う場合は、System.IO.Compression.FileSystemへの参照が必要になることがあります。Microsoft公式ドキュメントにも、.NET FrameworkアプリでZipFileを使用する場合はこのアセンブリ参照を追加する必要があると記載されています。Microsoft Learn
1-3. ZipFileとZipArchiveの違い
ZipFileとZipArchiveの違いは、操作の粒度です。
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.Read、ZipArchiveMode.Create、ZipArchiveMode.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化してもサイズがあまり小さくならないことがあります。
includeBaseDirectoryをtrueにすると、元フォルダ自体を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がある場合、FullNameはdocuments/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を作成したい場合は、MemoryStreamとZipArchiveを組み合わせます。
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が見つからない場合
ZipFileやZipArchiveが見つからない場合は、まずusingが不足していないか確認します。
C#using System.IO.Compression;
.NET Frameworkの場合は、参照設定にSystem.IO.CompressionやSystem.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.
対処法は次のとおりです。
対象ファイルを開いているアプリケーションを閉じる
FileStreamやZipArchiveをusingで確実に破棄する同じ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を使う、という流れで設計すると、シンプルで保守しやすいコードになります。

