先日、Zip圧縮された大量のファイルから、特定のフォルダに格納されているファイルのみ解凍する必要に迫られました。
そういえば、今までも大量のZipファイルを解凍したことが何度かあったような・・・
そこで、これを機に .NET 標準機能だけで Zipファイルの解凍、圧縮を行うクラスを作ったので、紹介したいと思います。
もちろん、指定した条件に合致するファイルのみ解凍することも可能です。
興味のある方は、是非ご一読ください。
ZipFile、ZipArchiveクラスについて
.NET Framework 4.5 以上と、.NetCore から、標準で Zipファイルを扱うためのクラスが3つ追加されました。
- ZipFileクラス:書庫の作成/解凍
- ZipArchiveクラス:書庫の操作
- ZipArchiveEntryクラス:書庫のエントリ情報を管理
下図は、この2つのクラスの関係を表した図です。
ZipFile クラスは、ZipFileをオープンするためのクラスです。単にフォルダを丸ごと圧縮するとか、Zipファイルを単純に解凍するだけなら、このクラスだけで事が足ります。
ZipArchiveクラスは、指定したファイルを書庫に追加する時に使います。
ZipArchiveEntryクラスは、書庫内のエントリ情報(ファイル名、書庫内のフォルダ階層、圧縮サイズ、最終更新日時など)を保持していますが、この情報を使って任意のファイルの解凍や、書庫内のエントリからの削除が行えます。
エントリについて
エントリとは、書庫内に格納されたファイルパスのことです。
ルートからファイルに到達するまでのフォルダ名、ファイル名は、スラッシュ( '/' )で区切るところが、通常のファイルパスとの違いです。
例えば下図を例にすると、Window1.xaml.cs を指定したい場合、ルートが Deliverables となり、Source の下にWindow1.xaml.cs があるので、次のように記述します。
Deliverables/Source/Window1.xaml.cs
同様に、Suites.Utils.dll を指定する場合は、次のようになります。
Deliverables/Source/bin/Suites.Utils.dll
ZipFile、ZipArchiveを使うための準備
ZipFile、ZipArchive クラスを使うには、あらかじめ プログラム先頭に下記の1行を追加しておいてください。
using System.IO.Compression;
次に、.Net Core では必要ありませんが、.Net Framework で使う場合には、次のアセンブリを参照設定しておく必要があります。
- System.IO.Compression
- System.IO.Compression.FileSystem
以上で準備は完了です。
ZipFile、ZipArchiveクラスの使い方
それでは、一通りに使い方について簡単なソースコードを紹介していきます。
フォルダを丸ごと圧縮する
ZipFile.CreateFromDirectory(ソースディレクトリ,書庫のパス ,[圧縮レベル],[ベースディレクトリの追加]);
ZipFile.CreateFromDirectory(@"d:\mylib",@"d:\mylib.zip");
圧縮レベルは Fastest、Optimal、SmallestSize、NoCompression の4種類が選べます。
省略した場合は、Optimal が選択されます。
速度優先で圧縮 | CompressionLevel.Fastest |
---|---|
速度とサイズのバランスを取って圧縮 | CompressionLevel.Optimal |
サイズ優先で圧縮 | CompressionLevel.SmallestSize |
無圧縮 | CompressionLevel.NoCompression |
ベースディレクトリの追加をtrue にした場合と false にした場合の違いは以下の通りです。
フォルダ丸ごと解凍
ZipFile.ExtractToDirectory(書庫のパス, 解凍先フォルダ)
ZipFile.ExtractToDirectory(@"d:\mylib.zip", @"d:\temp");
尚、展開先のフォルダ内にファイルが存在する場合、エラーとなります。
.NET Core では、3番目の引数に overwriteFiles というオプションが追加されており、これを true にすることで、既に展開済みフォルダがあっても上書きすることが可能です。
#.NET Core の場合、3番目の引数に上書きするか否かを指定できる
ZipFile.ExtractToDirectory(@"d:\mylib.zip", @"d:\temp",true);
書庫内のエントリ(フォルダ、フォルダ名)を取り出す
ZipFile.OpenRead(書庫のパス) で書庫をオープン後、 Entries でエントリを取得
ZipArchive zip = ZipFile.OpenRead(@"d:\mylib.zip");
foreach (ZipArchiveEntry entry in zip.Entries)
{
Console.Write(entry.FullName);
}
ZipArchiveEntry には、以下のプロパティが指定できます。
Name | ファイル名 |
---|---|
FullName | エントリ |
Length | 元のサイズ |
CompressedLength | 圧縮サイズ |
LastWriteTime | 最終更新日時 |
ExternalAttributes | OSおよびアプリケーション固有のファイル属性 |
Crc32 | 32ビット巡回冗長検査(.NET Core利用時のみ参照可能) |
例えば、tempフォルダの中に存在するフォルダのみ解凍したい場合を考えてみます。
通常は、ループの中でFullName を参照し、temp が含まれていれば、そのエントリを解凍するような処理を考えますが、temp の中に 空のフォルダ(今回の例では dummy)のエントリがあると、それも取得されてしまいます。
後述する ExtractToFile に空フォルダのエントリ渡すとエラーになってしまうので、空フォルダのエントリか否かを識別したくなります。
幸いなことに、空フォルダのエントリだと Name プロパティ は空文字になるため、Name != "" の条件を付け加えることで識別可能です。
#tempフォルダ配下のファイルを解凍するサンプル
ZipArchive zip = ZipFile.OpenRead(@"d:\mylib.zip");
foreach (ZipArchiveEntry entry in zip.Entries)
{
if(entry.Name != "" && entry.FullName.Contains("/temp/")
{
Console.Write(entry.FullName);
}
}
指定したファイルのみ解凍
ZipFile.OpenRead(書庫のパス) で書庫をオープン後、GetEntry(エントリ) で対象ファイルを特定、ZipArchiveEntryのExtractToFile(出力ファイル名,[上書きモード]) で取り出します。
ZipArchive zip = ZipFile.OpenRead(@"d:\mylib.zip");
ZipArchiveEntry entry = zip.GetEntry("Deliverables/Source/Window1.xaml.cs");
entry.ExtractToFile(@"d:\Window1.xaml.cs",true);
書庫から指定したファイルを削除
ZipFile.Open(書庫のパス, ZipArchiveMode.Update) で書庫をオープン後、GetEntry(エントリ) で対象ファイルを特定、ZipArchiveEntry の Delete() で削除します。
ZipArchive zip = ZipFile.Open(@"d:\mylib.zip", ZipArchiveMode.Update);
ZipArchiveEntry entry = zip.GetEntry("Deliverables/Source/Window1.xaml.cs");
entry.Delete();
書庫にファイルを追加
ZipFile.Open(書庫のパス, ZipArchiveMode.Update) で書庫をオープン後、CreateEntryFromFile(追加したいファイル名,追加先のエントリ,[圧縮レベル]) でファイルを追加します。
ZipArchive zip = ZipFile.Open(@"d:\mylib.zip", ZipArchiveMode.Update);
zip.CreateEntryFromFile(@"d:\Window1.xaml.cs", "Deliverables/Source");
解凍せず指定したファイルの中身を取得
ZipFile.OpenRead(書庫のパス) で書庫をオープン後、GetEntry(エントリ) で対象ファイルを特定、ZipArchiveEntryのOpen()で生成した Stream を使って中身を取得します。
ZipArchive zip = ZipFile.OpenRead(@"d:\mylib.zip");
ZipArchiveEntry entry = zip.GetEntry("Deliverables/Source/Window1.xaml.cs");
StreamReader sr = new StreamReader(entry.Open(), Encoding.UTF8);
var str = sr.ReadToEnd();
ConsoleWrite(str);
自作クラスのソースコード
いままでの内容を元に、少しだけ使いやすくしたクラスを作ってみました。
namaspace は CommonClass 、クラス名は Zip としています。
リファレンス
本クラスが実装しているメソッドは次の通りです。
指定フォルダの圧縮 | void Compress( string folder, string zipFile, bool isOverwrite = false, CompressionLevel compressionLevel = CompressionLevel.Optimal ) |
---|---|
書庫の解凍 | void Extract( string zipFile, string outputFolder = "" ) |
書庫の解凍(条件指定) | int Extract( string zipFile, string outputFolder, Func func, bool isOverwrite = false ) |
書庫内のファイル名取得 | string[] Files( string zipFile, string format = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}" ) |
エントリ一覧取得 | IEnumerable Entries(string zipFile) |
指定ファイルの解凍 | bool TakeOut( string zipFile, string entryName, string outuptName = "", bool isOverwrite = false ) |
指定エントリの削除 | bool Delete( string zipFile, string entryName ) |
書庫へのファイル追加 | void Append( string zipFile, string fileName, string entryPath = "", CompressionLevel compressionLevel = CompressionLevel.Optimal ) |
解凍せず中身を参照 | string Read( string zipFile, string entryPath, string encoderName = "UTF-8" ) |
使い方
Zipのインスタンスを生成し、必要なメソッドを読んでください。
具体的なサンプルは次の通りです。
//インスタンス生成
Zip zip = new Zip();
//書庫の作成
zip.Compress(@"p:\Deliverables",@"p:\delivrables.zip",true);
//書庫の解凍
//出力先フォルダ(第2引数)に指定したフォルダが無い場合、自動作成される
//出力先フォルダを省略すると、書庫と同じ場所に展開される
//出力先フォルダにファイルが存在する場合、エラーとなる
zip.Extract(@"p:\delivrables.zip",@"p:\temp");
//書庫の解凍
//第3引数に指定した関数が true の場合、そのエントリ(ファイル)が解凍される
zip.Extract(@"p:\delivrables.zip", @"p:\res", (s) => s.FullName.Contains("/temp"));
//書庫のエントリを取得する
var lst = zip.Files(@"p:\delivrables.zip");
//指定したファイルを解凍する
zip.TakeOut(@"p:\delivrables.zip", @"Deliverables\Webサイトコンセプト.pptx", @"p:\work", true);
//書庫にファイルを追加する
zip.Append(@"p:\delivrables.zip", @"p:\work\Webサイトコンセプト.pptx", @"Deliverables\Source\bin");
//書庫からエントリを削除する
zip.Delete(@"p:\delivrables.zip", @"Flower2\rakuten口コミ.csv");
//解凍せずに書庫内のファイルを取得する
var buff = zip.Read(@"p:\delivrables.zip", @"Deliverables\Source\MainWindow.xaml.cs", "shift-jis");
ソースコード
今回のソースコード一式です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;
namespace CommonClass
{
public class Zip
{
/// <summary>
/// フォルダの書庫を作成する。
/// </summary>
/// <param name="folder"></param>
/// <param name="zipFile"></param>
/// <param name="compressionLevel"></param>
public void Compress(string folder, string zipFile, bool isOverwrite = false, CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
if (isOverwrite && File.Exists(zipFile))
{
File.Delete(zipFile);
}
ZipFile.CreateFromDirectory(folder, zipFile, compressionLevel, true);
}
/// <summary>
/// 書庫を解凍する
/// </summary>
/// <param name="zipFile"></param>
/// <param name="outputFolder"></param>
public void Extract(string zipFile, string outputFolder = "")
{
//outputFolder が指定されていない場合、書庫が存在する場所を出力フォルダに指定する
outputFolder = (outputFolder == "") ? zipFile.Substring(0,zipFile.Length - System.IO.Path.GetExtension(zipFile).Length) : outputFolder;
ZipFile.ExtractToDirectory(zipFile, outputFolder);
}
/// <summary>
/// 指定した条件に一致した場合のみ解凍し、戻り値として解凍したファイル数を返す
/// </summary>
/// <param name="zipFile"></param>
/// <param name="outputFolder"></param>
/// <param name="func"></param>
/// <param name="isOverwrite"></param>
public int Extract(string zipFile, string outputFolder, Func<ZipArchiveEntry, bool> func, bool isOverwrite = false)
{
var cnt = 0;
foreach (var entry in Entries(zipFile))
{
if (entry.Name != "" && func(entry))
{
var dir = System.IO.Path.Combine(outputFolder, System.IO.Path.GetDirectoryName(entry.FullName)?.Replace("/", "\\"));
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
//1ファイルのみ解凍
TakeOut(zipFile, entry.FullName, dir, isOverwrite: isOverwrite);
cnt++;
}
}
return cnt;
}
/// <summary>
/// 書庫内のファイル一覧を取得する
/// </summary>
/// <param name="zipFile"></param>
/// <param name="format"></param>
/// <returns></returns>
public string[] Files(string zipFile, string format = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}")
{
List<string> list = new List<string>();
using (ZipArchive zip = ZipFile.OpenRead(zipFile))
{
//書庫内のファイルとディレクトリを列挙
foreach (ZipArchiveEntry entry in zip.Entries)
{
//entry が ファイルでなければ読み飛ばす
if (entry.Name == "") continue;
//entry が ファイルならリストに追加
list.Add(string.Format(
format,
entry.Name, //ファイル名
entry.FullName, //フルパス
entry.Length, //サイズ
entry.CompressedLength, //圧縮サイズ
entry.LastWriteTime, //最終更新日時
entry.ExternalAttributes //OSおよびアプリケーション固有のファイル属性
));
}
return list.ToArray();
}
}
/// <summary>
/// 書庫内のエントリ(ファイルとディレクトリ)の取得
/// </summary>
/// <param name="zipFile"></param>
/// <returns></returns>
public IEnumerable<ZipArchiveEntry> Entries(string zipFile)
{
using (ZipArchive zip = ZipFile.OpenRead(zipFile))
{
return zip.Entries;
}
}
/// <summary>
/// 書庫から指定したファイルのみ解凍する。
/// entryName には書庫内のフォルダ階層上のフルパス(\文字で区切られたフォルダ+ファイル名)を指定する。
/// outputFileを省略すると、書庫と同じ場所に、 entryName で指定したファイル名を使って出力する。
/// </summary>
/// <param name="zipFile"></param>
/// <param name="entryName"></param>
/// <param name="outuptName"></param>
/// <param name="isOverwrite"></param>
/// <returns></returns>
public bool TakeOut(string zipFile, string entryName, string outuptName = "", bool isOverwrite = false)
{
using (ZipArchive zip = ZipFile.OpenRead(zipFile))
{
//書庫内のエントリを取得
ZipArchiveEntry entry = zip.GetEntry(entryName.Replace("\\", "/"));
//エントリが見つかれば解凍
if (entry != null)
{
//出力ファイル名が空白なら、書庫と同じフォルダにエントリ名で出力するよう outuptName を設定
if (outuptName == "")
{
outuptName = System.IO.Path.Combine(
System.IO.Path.GetDirectoryName(zipFile),
System.IO.Path.GetFileName(entryName)
);
}
//出力先にフォルダ名が指定されたら、そのフォルダにエントリ名で出力するよう outuptName を設定
if (System.IO.Directory.Exists(outuptName))
{
outuptName = System.IO.Path.Combine(outuptName, System.IO.Path.GetFileName(entryName));
}
//書庫から指定されたエントリ名のファイルを解凍し、outputName に出力
entry.ExtractToFile(outuptName, isOverwrite);
return true;
}
return false;
}
}
/// <summary>
/// 書庫から指定したファイルを削除する。
/// entryName には書庫内のフォルダ階層上のフルパス(\文字で区切られたフォルダ+ファイル名)を指定する。
/// </summary>
/// <param name="zipFile"></param>
/// <param name="entryName"></param>
/// <returns></returns>
public bool Delete(string zipFile, string entryName)
{
using (ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Update))
{
//書庫内のエントリを取得
ZipArchiveEntry entry = zip.GetEntry(entryName);
//エントリが見つかれば解凍
if (entry != null)
{
entry.Delete();
return true;
}
return false;
}
}
/// <summary>
/// 書庫にファイルを追加する
/// entryPath には書庫内のフォルダ階層上のフォルダ(\文字で区切られたフォルダ)を指定する。
/// entryPath を省略すると、書庫内のルート(階層の一番上)に、fileName で指定されたファイル名で追加される。
/// </summary>
/// <param name="zipFile"></param>
/// <param name="fileName"></param>
/// <param name="entryPath"></param>
/// <param name="compressionLevel"></param>
public void Append(string zipFile, string fileName, string entryPath = "", CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
using (ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Update))
{
//書庫内のバスが指定されていなければファイル名からエントリを作成
if (entryPath == "")
{
var drive = System.IO.Path.GetPathRoot(fileName) ?? "";
entryPath = (drive == "") ? fileName.Replace("\\", "/") : fileName.Substring(drive.Length);
}
//書庫内のパスが指定されていれば、それにファイル名を付けてエントリを作成
else
{
entryPath = System.IO.Path.Combine(entryPath, System.IO.Path.GetFileName(fileName));
}
//書庫にファイルを追加(書庫が無ければ作成)
zip.CreateEntryFromFile(fileName, entryPath, compressionLevel);
}
}
/// <summary>
/// 解凍せず指定したファイルの中身を取得する
/// </summary>
/// <param name="zipFile"></param>
/// <param name="entryPath"></param>
/// <param name="encoderName"></param>
/// <returns></returns>
public string Read(string zipFile, string entryPath, string encoderName = "UTF-8")
{
using (ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Update))
{
// エントリを取得する
ZipArchiveEntry entry = zip.GetEntry(entryPath.Replace("\\", "/"));
if (entry != null)
{
//エントリから内容を取得する
using (StreamReader sr = new StreamReader(entry.Open(), Encoding.GetEncoding(encoderName)))
{
return sr.ReadToEnd();
}
}
}
return "";
}
}
}
.NET Core への対応
.NET Core で利用する場合は、必要に応じてExtract(第3引数に Func<ZipArchiveEntry, bool> func引数を指定しない方)と Files メソッドを下記に置き換えてください。
Extractを下記の内容に置き換えると、解凍時にフォルダがある場合、上書きできるようになります。
/// <summary>
/// 書庫を解凍する(.NET CORE用)
/// </summary>
/// <param name="zipFile"></param>
/// <param name="outputFolder"></param>
/// <param name="isOverwrite"></param>
public void Extract(string zipFile, string outputFolder = "", bool isOverwrite = false)
{
//outputFolder が指定されていない場合、書庫が存在する場所を出力フォルダに指定する
outputFolder = (outputFolder == "") ? System.IO.Path.GetDirectoryName(zipFile) : outputFolder;
ZipFile.ExtractToDirectory(zipFile, outputFolder, isOverwrite);
}
Filesを下記のものに置き換えると、Crc32 (32ビット巡回冗長検査)が参照できるようになります。
/// <summary>
//// 書庫内のファイル一覧を取得する(.NET CORE用)
/// </summary>
/// <param name = "zipFile" ></ param >
/// < param name= "format" ></ param >
/// < returns ></ returns
public string[] Files(string zipFile, string format = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}")
{
List<string> list = new List<string>();
using (ZipArchive zip = ZipFile.OpenRead(zipFile))
{
//書庫内のファイルとディレクトリを列挙
foreach (ZipArchiveEntry entry in zip.Entries)
{
//entry が ファイルでなければ読み飛ばす
if (entry.Name == "") continue;
//entry が ファイルならリストに追加
list.Add(string.Format(
format,
entry.Name, //ファイル名
entry.FullName, //フルパス
entry.Length, //サイズ
entry.CompressedLength, //圧縮サイズ
entry.LastWriteTime, //最終更新日時
entry.ExternalAttributes, //OSおよびアプリケーション固有のファイル属性
entry.Crc32 //32ビット巡回冗長検査
));
}
return list.ToArray();
}
}
まとめ
今回は、C#でZipファイルを扱う上で必要となる ZipFile、ZipArchive の全体像、基本的な使い方、そしてそれらを使った自作の便利クラスについて紹介しました。
既にネットではフリーの圧縮解凍ツールが出回っており、Windowsの標準機能としても搭載されているため、プログラムで圧縮解凍を行うケースは少ないかもしれません。
しかし、圧縮するファイルがあちこにのフォルダにまたがっていたり、大量のZIPファイルから特定の条件を満たすファイルのみ解凍したいような場合は、プログラムを書いて自動で行う方が圧倒的に楽です。
そんな時、この記事を参考にして頂ければ幸いです。
コメント