PHP 內建壓縮擴展概覽
| 擴展/類別 | 支援格式 | 特點 | 適用場景 |
| ZipArchive | ZIP | 原生、效能好、功能完整 | 最常用、跨平台相容 |
| PharData | TAR、ZIP、PHAR | 統一介面、支援串流、可建立 PHAR | 需要多格式支援、打包部署 |
| Zlib (gz*) | GZIP | 單檔壓縮、串流處理 | 日誌壓縮、HTTP 壓縮傳輸 |
| Bz2 | BZIP2 | 壓縮率高、速度較慢 | 大檔案長期存儲 |
| Rar (PECL) | RAR | 需額外安裝、授權限制 | 解壓 RAR 封存 |
ZipArchive 完整實戰 (推薦首選)
建立壓縮檔:目錄遞歸壓縮
<?php
function zipDirectory(string $sourceDir, string $outputZip, array $options = []): bool {
$defaults = [
'includeHidden' => false,
'followSymlinks' => false,
'compression' => ZipArchive::CM_DEFAULT,
'password' => null,
'encryption' => ZipArchive::EM_AES_256,
];
$opts = array_merge($defaults, $options);
if (!is_dir($sourceDir)) {
throw new InvalidArgumentException("來源目錄不存在: $sourceDir");
}
$zip = new ZipArchive();
$flags = $opts['password'] ? ZipArchive::CREATE | ZipArchive::OVERWRITE : ZipArchive::CREATE;
if ($zip->open($outputZip, $flags) !== true) {
throw new RuntimeException("無法建立 ZIP: $outputZip");
}
if ($opts['password']) {
$zip->setPassword($opts['password']);
$zip->setEncryptionName('', $opts['encryption']);
}
$sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR);
$baseLen = strlen(dirname($sourceDir)) + 1;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceDir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
$path = $file->getPathname();
$relativePath = substr($path, $baseLen);
if (!$opts['includeHidden'] && preg_match('/(^|\/)\./', $relativePath)) {
continue;
}
if (!$opts['followSymlinks'] && $file->isLink()) {
continue;
}
if ($file->isDir()) {
$zip->addEmptyDir($relativePath . '/');
} else {
$zip->addFile($path, $relativePath);
$zip->setCompressionName($relativePath, $opts['compression']);
}
}
return $zip->close();
}
// 使用範例
try {
zipDirectory('/var/www/html/app', '/backups/app-' . date('Ymd-His') . '.zip', [
'password' => 'strong_password',
'compression' => ZipArchive::CM_BZIP2
]);
echo "壓縮完成";
} catch (Exception $e) {
echo "失敗: " . $e->getMessage();
}
?>
解壓縮與檔案列表
<?php
function extractZip(string $zipPath, string $destDir, ?string $password = null): bool {
$zip = new ZipArchive();
$flags = $password ? ZipArchive::RDONLY : 0;
if ($zip->open($zipPath, $flags) !== true) {
throw new RuntimeException("無法開啟 ZIP: $zipPath");
}
if ($password) {
$zip->setPassword($password);
}
// 列出內容
echo "壓縮檔包含 {$zip->count()} 個項目:\n";
for ($i = 0; $i < $zip->count(); $i++) {
$stat = $zip->statIndex($i);
printf(" %s %10d %s\n",
$stat['name'],
$stat['size'],
date('Y-m-d H:i:s', $stat['mtime'])
);
}
// 解壓 (可指定特定檔案)
$result = $zip->extractTo($destDir);
$zip->close();
return $result;
}
// 串流讀取單一檔案 (不解壓整包)
function readZipEntry(string $zipPath, string $entryName, ?string $password = null): ?string {
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) return null;
if ($password) $zip->setPassword($password);
$content = $zip->getFromName($entryName);
$zip->close();
return $content;
}
?>
PharData:統一處理 TAR/ZIP/PHAR
<?php
// 建立 TAR.GZ
$phar = new PharData('project.tar.gz');
$phar->buildFromDirectory('/path/to/source');
$phar->compress(Phar::GZ); // 或 Phar::BZ2
$phar->delete(); // 刪除中間檔
// 解壓 TAR.GZ
$phar = new PharData('project.tar.gz');
$phar->decompress(); // 產生 project.tar
$phar = new PharData('project.tar');
$phar->extractTo('/destination');
// 建立 PHAR 部署包 (需 php.ini phar.readonly=0)
$phar = new Phar('myapp.phar');
$phar->buildFromDirectory('/path/to/src');
$phar->setStub($phar->createDefaultStub('index.php'));
$phar->compressFiles(Phar::GZ);
?>
串流壓縮:大檔案/記憶體受限環境
<?php
// GZIP 串流壓縮 (不載入整個檔案到記憶體)
function gzipStream(string $sourcePath, string $destPath): void {
$in = fopen($sourcePath, 'rb');
$out = gzopen($destPath, 'wb9'); // 9 = 最高壓縮
while (!feof($in)) {
gzwrite($out, fread($in, 8192));
}
fclose($in);
gzclose($out);
}
// HTTP 回應直接輸出 GZIP
header('Content-Encoding: gzip');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="export.csv.gz"');
$out = gzopen('php://output', 'wb');
foreach ($largeDataset as $row) {
gzwrite($out, implode(',', $row) . "\n");
}
gzclose($out);
?>
第三方庫推薦:更豐富功能
| 庫 | 特點 | Composer |
| maennchen/ZipStream-PHP | 串流式 ZIP 下載、不佔記憶體、支援大檔案 | composer require maennchen/zipstream-php |
| chumper/zipper | Laravel 風格鏈式 API、簡潔易用 | composer require chumper/zipper |
| stechstudio/laravel-zipstream | Laravel 整合、回應宏 | composer require stechstudio/laravel-zipstream |
ZipStream-PHP 範例:大量檔案即時打包下載
<?php
require 'vendor/autoload.php';
use ZipStream\ZipStream;
use ZipStream\Option\Archive as ArchiveOptions;
$options = new ArchiveOptions();
$options->setSendHttpHeaders(true)
->setContentType('application/zip')
->setFilename('export-' . date('Ymd-His') . '.zip');
$zip = new ZipStream($options);
// 從資料庫/雲端逐筆新增,不佔用記憶體
foreach ($hugeFileList as $file) {
$zip->addFileFromPath($file['local_path'], $file['archive_name']);
}
$zip->finish();
?>
效能與安全建議
- 大檔案用串流:避免
memory_limit 耗盡
- 密碼保護用 AES-256:
ZipArchive::EM_AES_256,舊版 ZipCrypto 弱
- 驗證解壓路徑:防止 Zip Slip 攻擊 (路徑遍歷
../../etc/passwd)
- 清理暫存檔:使用
sys_get_temp_dir() + register_shutdown_function
- 備份驗證:壓縮後抽樣解壓驗證完整性
Zip Slip 防禦範例
<?php
function safeExtract(ZipArchive $zip, string $destDir): void {
$destDir = realpath($destDir);
for ($i = 0; $i < $zip->count(); $i++) {
$entry = $zip->getNameIndex($i);
$targetPath = $destDir . DIRECTORY_SEPARATOR . $entry;
$targetPath = realpath($targetPath) ?: $targetPath; // 解析 symlink
// 關鍵:確保目標路徑在目的目錄內
if (strpos($targetPath, $destDir) !== 0) {
throw new RuntimeException("Zip Slip 攻擊偵測: $entry");
}
$zip->extractTo($destDir, [$entry]);
}
}
?>
PHP 內建擴展已足應付多數壓縮需求,配合串流處理與第三方庫,能優雅解決從小檔案打包到 TB 級資料匯出的各種場景。
- 上一篇文章: phpMyAdmin 安裝、配置與安全防護完整指南
- 下一篇文章: PHP 浮點數取整函數完整指南:ceil、floor、round、intval 與精度陷阱
Copyright ©
程式碼之家 All Rights Reserved