首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 目錄遍歷與遞歸刪除完整實現(含中文路徑兼容方案)

     編輯:PHP 綜合知識

文件系統操作是 PHP 開發中高頻需求。本文提供三個生產級工具函數:列出目錄下文件/子目錄、遞歸遍歷所有子目錄文件、遞歸刪除目錄及內容,並重點解決 Windows 系統下中文路徑編碼問題。

1. 列出指定目錄下的文件與子目錄(非遞歸)

<?php
/**
 * 獲取目錄下的文件列表與子目錄列表
 *
 * @param string $path 目錄路徑(UTF-8 編碼)
 * @return array ['dirs' => [], 'files' => []]
 * @throws InvalidArgumentException 路徑無效
 */
function listDirectory(string $path): array {
    // 統一路徑分隔符
    $path = rtrim($path, '/\\');
    
    if (!is_dir($path)) {
        throw new InvalidArgumentException("路徑不存在或不是目錄: {$path}");
    }

    $dirs  = [];
    $files = [];
    
    // Windows 下需將 UTF-8 轉為 GBK/CP936 才能正確讀取中文路徑
    $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $handle = @opendir($isWindows ? iconv('UTF-8', 'GBK//IGNORE', $path) : $path);
    
    if ($handle === false) {
        throw new RuntimeException("無法打開目錄: {$path}");
    }

    while (($entry = readdir($handle)) !== false) {
        if ($entry === '.' || $entry === '..') continue;
        
        // Windows 下讀取到的文件名是 GBK,需轉回 UTF-8
        $entryName = $isWindows ? iconv('GBK', 'UTF-8//IGNORE', $entry) : $entry;
        $fullPath = $path . DIRECTORY_SEPARATOR . $entryName;
        
        if (is_dir($fullPath)) {
            $dirs[] = $entryName;
        } elseif (is_file($fullPath)) {
            $files[] = $entryName;
        }
    }
    closedir($handle);
    
    // 自然排序,數字順序正確(如 1.jpg, 2.jpg, 10.jpg)
    natsort($dirs);
    natsort($files);
    
    return ['dirs' => array_values($dirs), 'files' => array_values($files)];
}

// 使用示例
$result = listDirectory('/var/www/uploads');
print_r($result);
/*
Array
(
    [dirs] => Array ( [0] => images [1] => documents )
    [files] => Array ( [0] => index.php [1] => readme.md )
)
*/
?>

2. 遞歸遍歷目錄樹(獲取所有文件路徑)

<?php
/**
 * 遞歸獲取目錄下所有文件的相對路徑
 *
 * @param string $path      起始目錄
 * @param string $basePath  基準路徑(用於計算相對路徑),默認同 $path
 * @return string[] 文件相對路徑數組
 */
function scanDirectoryRecursive(string $path, string $basePath = ''): array {
    $basePath = $basePath ?: $path;
    $path = rtrim($path, '/\\');
    $basePath = rtrim($basePath, '/\\');
    
    if (!is_dir($path)) {
        throw new InvalidArgumentException("目錄不存在: {$path}");
    }

    $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $handle = @opendir($isWindows ? iconv('UTF-8', 'GBK//IGNORE', $path) : $path);
    if (!$handle) throw new RuntimeException("無法打開目錄: {$path}");

    $files = [];
    while (($entry = readdir($handle)) !== false) {
        if ($entry === '.' || $entry === '..') continue;
        
        $entryName = $isWindows ? iconv('GBK', 'UTF-8//IGNORE', $entry) : $entry;
        $fullPath = $path . DIRECTORY_SEPARATOR . $entryName;
        $relativePath = substr($fullPath, strlen($basePath) + 1); // +1 跳過分隔符
        
        if (is_dir($fullPath)) {
            $files = array_merge($files, scanDirectoryRecursive($fullPath, $basePath));
        } elseif (is_file($fullPath)) {
            $files[] = $relativePath;
        }
    }
    closedir($handle);
    
    return $files;
}

// 使用示例
$allFiles = scanDirectoryRecursive('/var/www/project/storage');
/*
Array
(
    [0] => app/config.php
    [1] => logs/2024-01-15.log
    [2] => uploads/用戶頭像/張三.png
    ...
)
*/
?>

3. 遞歸刪除目錄及其所有內容

<?php
/**
 * 遞歸刪除目錄(包含子目錄和文件)
 *
 * @param string $path 目錄路徑
 * @return bool 刪除成功返回 true
 * @throws RuntimeException 刪除失敗
 */
function deleteDirectory(string $path): bool {
    $path = rtrim($path, '/\\');
    
    if (!is_dir($path)) {
        throw new InvalidArgumentException("不是有效目錄: {$path}");
    }

    $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    $handle = @opendir($isWindows ? iconv('UTF-8', 'GBK//IGNORE', $path) : $path);
    if (!$handle) throw new RuntimeException("無法打開目錄進行刪除: {$path}");

    while (($entry = readdir($handle)) !== false) {
        if ($entry === '.' || $entry === '..') continue;
        
        $entryName = $isWindows ? iconv('GBK', 'UTF-8//IGNORE', $entry) : $entry;
        $fullPath = $path . DIRECTORY_SEPARATOR . $entryName;
        
        if (is_dir($fullPath)) {
            // 遞歸刪除子目錄
            if (!deleteDirectory($fullPath)) {
                closedir($handle);
                return false;
            }
        } elseif (is_file($fullPath) || is_link($fullPath)) {
            // 刪除文件/符號鏈接
            if (!@unlink($fullPath)) {
                closedir($handle);
                throw new RuntimeException("文件刪除失敗: {$fullPath}");
            }
        }
    }
    closedir($handle);
    
    // 最後刪除空目錄本身
    if (!@rmdir($path)) {
        throw new RuntimeException("目錄刪除失敗(可能非空或權限不足): {$path}");
    }
    
    return true;
}

// 使用示例
try {
    deleteDirectory('/tmp/cache/expired');
    echo "緩存目錄清理完成";
} catch (Exception $e) {
    error_log("清理失敗: " . $e->getMessage());
}
?>

中文路徑編碼問題深度解析

場景問題原因解決方案
Windows + PHP 讀取中文目錄 文件系統 API 期望 GBK/CP936 編碼,PHP 內部用 UTF-8 iconv('UTF-8', 'GBK//IGNORE', $path) 轉換後再操作
Windows + 讀取到的文件名顯示亂碼 readdir() 返回 GBK 編碼字符串 iconv('GBK', 'UTF-8//IGNORE', $entry) 轉回 UTF-8
Linux/Mac 文件系統原生支持 UTF-8 無需轉換,直接操作
PHP 7.2+ / 8.0+ 內部已改進 Unicode 支持 仍建議顯式處理,保證跨平台兼容

現代替代方案:SPL 與 GlobIterator

PHP 5.3+ 提供了更面向對象的 RecursiveDirectoryIterator,自動處理編碼(需配合 FilesystemIterator::CURRENT_AS_PATHNAME),代碼更簡潔:

<?php
// 遞歸獲取所有文件(PHP 5.3+)
$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator('/path/to/dir', FilesystemIterator::SKIP_DOTS)
);
$files = [];
foreach ($iterator as $fileInfo) {
    if ($fileInfo->isFile()) {
        $files[] = $fileInfo->getPathname();
    }
}

// 刪除目錄(需自寫遞歸,SPL 無直接刪除目錄方法)
?>

最佳實踐建議

  1. 統一使用 DIRECTORY_SEPARATOR,避免硬編碼 / 或 \。
  2. 異常處理:所有文件操作均應包裝 try-catch,記錄錯誤日誌。
  3. 權限檢查:操作前用 is_writable() 預檢,避免半途失敗留下髒數據。
  4. 大目錄性能:超大目錄(萬級文件)下 opendir/readdir 佔用內存大,考慮分批處理或使用 glob() 分頁。
  5. 符號鏈接保護:刪除時用 is_link() 判斷,避免誤刪鏈接目標的真實文件。

以上三個函數經過多個項目驗證,可直接納入項目公共庫。對於現代框架(Laravel、Symfony),建議優先使用框架封裝的 Storage/Filesystem 組件,底層已處理好跨平台兼容性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved