«Хороший сайт делается для людей, а поисковики приходят следом»

— из принципов редакции SeoLiga.ru
Главная » Статьи » .NET и C# » Сжатие файлов и архивы средствами .NET

Сжатие файлов и архивы средствами .NET

Сжатие данных нужно в самых разных задачах: отправить пользователю отчёт одним файлом, сохранить журналы с экономией места, передать большой объём по сети или упаковать резервную копию. Встроенных возможностей платформы .NET для этого хватает в большинстве случаев, и сторонние библиотеки не требуются. В этой статье рассмотрим два основных подхода: потоковое сжатие через GZip и Deflate и работу с ZIP-архивами как с набором файлов.

Какие средства есть в платформе

Все нужные классы находятся в пространстве имён System.IO.Compression. Основные из них:

  • GZipStream — сжатие и распаковка в формате GZip, совместимом со стандартными утилитами;
  • DeflateStream — тот же алгоритм без заголовков и контрольной суммы, чуть компактнее;
  • ZipFile — статические методы для создания и распаковки архивов целиком;
  • ZipArchive и ZipArchiveEntry — работа с отдельными записями внутри ZIP.

Важно понимать разницу между GZip и ZIP. GZip сжимает один поток данных и не хранит структуру папок, поэтому для набора файлов его обычно сочетают с упаковщиком. ZIP же сам по себе является контейнером с несколькими файлами и каталогами.

Сжатие одного файла через GZipStream

Принцип работы потоковых классов прост: вы оборачиваете выходной поток в сжимающий, а затем копируете в него данные. Всё остальное делает платформа.

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

public static void CompressFile(string source, string target)
{
    using (FileStream input = File.OpenRead(source))
    using (FileStream output = File.Create(target))
    using (GZipStream gzip = new GZipStream(output, CompressionMode.Compress))
    {
        input.CopyTo(gzip);
    }
}

Обратите внимание на порядок: сжимающий поток закрывается первым, и именно при закрытии он дописывает хвост данных. Если не освободить его (или освободить после закрытия выходного файла), архив получится повреждённым. Конструкция using гарантирует правильную очерёдность.

Распаковка

Обратная операция устроена зеркально: вы оборачиваете входной поток и читаете из него уже разжатые данные.

public static void DecompressFile(string source, string target)
{
    using (FileStream input = File.OpenRead(source))
    using (GZipStream gzip = new GZipStream(input, CompressionMode.Decompress))
    using (FileStream output = File.Create(target))
    {
        gzip.CopyTo(output);
    }
}

Такой код читает файл порциями, а не загружает целиком в память, поэтому подходит и для очень больших файлов.

Сжатие в памяти

Иногда нужно сжать строку или массив байтов, не создавая файл: например, перед отправкой по сети или сохранением в базу. Для этого используют MemoryStream:

public static byte[] Compress(byte[] data)
{
    using (MemoryStream ms = new MemoryStream())
    {
        using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true))
        {
            gzip.Write(data, 0, data.Length);
        }
        return ms.ToArray();
    }
}

public static byte[] Decompress(byte[] data)
{
    using (MemoryStream input = new MemoryStream(data))
    using (GZipStream gzip = new GZipStream(input, CompressionMode.Decompress))
    using (MemoryStream output = new MemoryStream())
    {
        gzip.CopyTo(output);
        return output.ToArray();
    }
}

Третий параметр конструктора, true, оставляет базовый поток открытым после закрытия сжимающего. Без него вызов ToArray всё равно сработает, но поведение лучше задавать явно. Главное — закрыть GZipStream до чтения результата, иначе в массиве будут не все данные.

Уровень сжатия

В конструкторе можно указать значение перечисления CompressionLevel. Основные варианты:

  1. Optimal — баланс между скоростью и степенью сжатия, значение по умолчанию;
  2. Fastest — быстрее, но файл получится больше;
  3. NoCompression — только упаковка без сжатия;
  4. SmallestSize — максимальное сжатие ценой времени, доступно в новых версиях платформы.

Для логов и текстов выигрыш от сильного сжатия обычно заметен. Для уже сжатых форматов, таких как фотографии JPEG, видео, музыка и сами архивы, он стремится к нулю, а время тратится впустую.

Работа с ZIP: быстрый путь через ZipFile

Чтобы упаковать целую папку или распаковать готовый архив, есть самые короткие методы. Не забудьте подключить сборку System.IO.Compression.FileSystem, если проект этого требует.

using System.IO.Compression;

ZipFile.CreateFromDirectory(@"C:\Data\Reports", @"C:\Backup\reports.zip");
ZipFile.ExtractToDirectory(@"C:\Backup\reports.zip", @"C:\Restore");

Два вызова решают большинство бытовых задач. Но если нужно добавить несколько файлов выборочно или прочитать содержимое, не распаковывая всё на диск, пригодится более гибкий класс.

Тонкая работа через ZipArchive

Класс ZipArchive открывает архив и даёт доступ к записям. Добавим файл и перечислим содержимое:

using (FileStream zipToOpen = new FileStream(@"C:\Backup\data.zip", FileMode.OpenOrCreate))
using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Update))
{
    archive.CreateEntryFromFile(@"C:\Data\notes.txt", "docs/notes.txt");

    foreach (ZipArchiveEntry entry in archive.Entries)
    {
        Console.WriteLine(entry.FullName + " - " + entry.Length + " байт");
    }
}

Режимы открытия:

  • Read — только чтение содержимого;
  • Create — создание нового архива, запись только вперёд;
  • Update — чтение и изменение, но архив при этом держится в памяти, что неудобно для огромных файлов.

Чтобы прочитать одну запись, вызовите entry.Open() — вы получите обычный поток и сможете читать его как любой другой.

Нюансы и частые ошибки

  • Уязвимость путей при распаковке. Записи в чужом архиве могут содержать путь с переходом в родительский каталог и записать файл за пределы целевой папки. Всегда проверяйте итоговый путь перед сохранением, если архив получен от пользователя.
  • Кодировка имён. Старые архивы нередко хранят русские имена в нестандартной кодировке, и на выходе вместо файлов появляются кракозябры. Используйте перегрузки с указанием кодировки.
  • Неосвобождённые потоки. Без закрытия архив будет неполным. Всегда заворачивайте работу в using.
  • Бомбы распаковки. Маленький архив может раскрыться в гигантский объём. Ограничивайте суммарный размер распаковываемых данных.
  • Занятые файлы. Попытка упаковать файл, открытый другим процессом, завершится исключением. Открывайте с разрешением совместного чтения.

Когда встроенных средств мало

Платформенные классы не умеют работать с форматами RAR и 7z, не поддерживают шифрование паролем в ZIP и не позволяют создавать многотомные архивы. В таких случаях придётся подключать сторонние решения или вызывать внешний архиватор. Для повседневных задач — логи, выгрузки, резервные копии — стандартного набора с запасом достаточно.

Практический пример: архивация журналов

Представьте задачу: раз в сутки собирать файлы журналов старше недели, упаковывать их в один архив и удалять оригиналы. Алгоритм простой: перебрать файлы в папке, выбрать подходящие по дате изменения, добавить каждый в архив через CreateEntryFromFile и только после успешного закрытия архива удалить исходные файлы. Удаление до закрытия опасно: при сбое вы потеряете и архив, и данные.

Добавьте проверки: достаточно ли места на диске, не заблокирован ли файл, не существует ли уже архив с таким именем. Записывайте в отдельный журнал, что именно было упаковано. Такая небольшая автоматизация экономит много места и избавляет от ручной уборки.

Производительность и большие объёмы

При работе с крупными файлами следите за размером буфера и избегайте загрузки всего содержимого в память. Потоковые методы копируют данные порциями, поэтому расход памяти остаётся небольшим. Если нужно упаковать много независимых файлов, их сжатие можно распараллелить, но запись в один и тот же архив должна оставаться последовательной.

Заключение

Сжатие в .NET делается просто: для потоков берите GZipStream, для набора файлов — ZipFile или ZipArchive. Помните о правильном закрытии потоков, выбирайте уровень сжатия по задаче и проверяйте пути при распаковке чужих архивов. С этими правилами вы сэкономите место, трафик и время, не добавив в проект ни одной внешней зависимости.

ИБ
Ирина Белова
Автор SeoLiga.ru. Пишет о практике: что работает на реальных проектах, а что лучше обойти стороной. Хотите предложить свою статью? Загляните в раздел «Авторам».