PHP ftp_nlist() 函数

PHP ftp_nlist() 函数用于获取FTP服务器上指定目录中的文件和目录列表。

特点:此函数返回一个包含文件和目录名称的数组,仅包含名称而不包含其他信息(如文件大小、修改时间等)。

语法

ftp_nlist(resource $ftp, string $directory): array|false

参数说明

参数 描述
ftp 必需。FTP连接的标识符,由ftp_connect()ftp_ssl_connect()返回
directory 必需。要列出内容的目录路径。如果未指定或为空字符串,则列出当前工作目录

返回值

  • 成功时返回包含文件和目录名称的数组
  • 失败时返回 false
  • 返回的文件名可能是相对路径或绝对路径,具体取决于服务器配置

与ftp_rawlist()的区别

特性 ftp_nlist() ftp_rawlist()
返回内容 仅文件名/目录名数组 原始服务器响应行数组,包含详细信息
信息量 简单,仅名称 详细,包括权限、所有者、大小、时间等
处理难度 简单,直接使用 复杂,需要解析
适用场景 只需文件名列表时 需要文件详细信息时
服务器兼容性 较好 可能因服务器而异

示例

示例1:基本用法 - 列出目录内容

<?php
// 连接FTP服务器
$ftp_server = "ftp.example.com";
$ftp_user = "username";
$ftp_pass = "password";

$ftp_conn = ftp_connect($ftp_server);
if (!$ftp_conn) {
    die("无法连接到 $ftp_server");
}

// 登录
if (!ftp_login($ftp_conn, $ftp_user, $ftp_pass)) {
    die("登录失败");
}

// 启用被动模式
ftp_pasv($ftp_conn, true);

// 列出根目录内容
echo "列出根目录内容:\n";
$file_list = ftp_nlist($ftp_conn, "/");

if ($file_list !== false) {
    echo "找到 " . count($file_list) . " 个条目:\n";
    foreach ($file_list as $file) {
        echo "  - $file\n";
    }
} else {
    echo "获取目录列表失败\n";
}

// 列出指定目录内容
$directory = "/public_html";
echo "\n列出目录 '$directory' 内容:\n";
$dir_list = ftp_nlist($ftp_conn, $directory);

if ($dir_list !== false) {
    echo "找到 " . count($dir_list) . " 个条目:\n";
    foreach ($dir_list as $item) {
        echo "  - $item\n";
    }
} else {
    echo "目录 '$directory' 不存在或无权限访问\n";
}

// 关闭FTP连接
ftp_close($ftp_conn);
?>

示例2:递归列出目录树

<?php
function listFTPDirectoryRecursive($ftp_conn, $directory, $depth = 0) {
    // 获取目录列表
    $items = ftp_nlist($ftp_conn, $directory);

    if ($items === false) {
        return [];
    }

    $result = [];

    foreach ($items as $item) {
        // 计算缩进
        $indent = str_repeat("  ", $depth);

        // 检查是否是目录
        $is_dir = false;

        // 方法1:尝试切换到该目录
        $current_dir = ftp_pwd($ftp_conn);
        if (@ftp_chdir($ftp_conn, $item)) {
            $is_dir = true;
            // 切换回原目录
            ftp_chdir($ftp_conn, $current_dir);
        }

        // 方法2:检查是否以斜杠结尾(某些服务器可能这样表示目录)
        if (!$is_dir && (substr($item, -1) == '/' || substr($item, -1) == '\\')) {
            $is_dir = true;
        }

        $result[] = [
            'name' => $item,
            'is_dir' => $is_dir,
            'path' => $directory . '/' . basename($item),
            'depth' => $depth
        ];

        // 如果是目录且不是当前目录或上级目录,递归处理
        if ($is_dir) {
            $dir_name = basename($item);
            if ($dir_name != '.' && $dir_name != '..') {
                $subdir_path = ($directory == '/' ? '' : $directory) . '/' . $dir_name;
                $sub_items = listFTPDirectoryRecursive($ftp_conn, $subdir_path, $depth + 1);
                $result = array_merge($result, $sub_items);
            }
        }
    }

    return $result;
}

// 使用示例
$ftp_conn = ftp_connect('localhost');
if ($ftp_conn && ftp_login($ftp_conn, 'user', 'pass')) {
    ftp_pasv($ftp_conn, true);

    echo "递归列出FTP目录结构:\n";
    $tree = listFTPDirectoryRecursive($ftp_conn, '/');

    foreach ($tree as $item) {
        $prefix = str_repeat("│   ", $item['depth']) . ($item['is_dir'] ? "├── 📁 " : "├── 📄 ");
        echo $prefix . basename($item['name']) . "\n";
    }

    ftp_close($ftp_conn);
} else {
    echo "无法连接FTP服务器\n";
}
?>

示例3:带过滤功能的文件列表

<?php
class FTPFileLister {
    private $ftp_conn;

    public function __construct($server, $username, $password) {
        $this->ftp_conn = ftp_connect($server);
        if (!$this->ftp_conn) {
            throw new Exception("无法连接到FTP服务器: $server");
        }

        if (!ftp_login($this->ftp_conn, $username, $password)) {
            throw new Exception("FTP登录失败");
        }

        ftp_pasv($this->ftp_conn, true);
    }

    /**
     * 列出目录中的文件和目录
     * @param string $directory 目录路径
     * @param array $filters 过滤器配置
     * @return array 过滤后的列表
     */
    public function listFiles($directory = '', $filters = []) {
        // 默认过滤器
        $default_filters = [
            'extensions' => [],      // 文件扩展名过滤,如 ['jpg', 'png', 'pdf']
            'exclude_dirs' => [],    // 排除的目录名
            'exclude_files' => [],   // 排除的文件名
            'search_pattern' => '',  // 搜索模式,支持通配符
            'max_depth' => 1,        // 最大深度,1表示不递归
            'show_hidden' => false,  // 是否显示隐藏文件(以.开头的文件)
        ];

        $filters = array_merge($default_filters, $filters);

        // 获取目录列表
        $items = ftp_nlist($this->ftp_conn, $directory);

        if ($items === false) {
            return [];
        }

        $result = [
            'directories' => [],
            'files' => []
        ];

        foreach ($items as $item) {
            $basename = basename($item);

            // 跳过隐藏文件(如果不显示)
            if (!$filters['show_hidden'] && substr($basename, 0, 1) == '.') {
                continue;
            }

            // 检查是否是目录
            $is_dir = $this->isDirectory($item);

            // 排除目录过滤
            if ($is_dir && in_array($basename, $filters['exclude_dirs'])) {
                continue;
            }

            // 排除文件过滤
            if (!$is_dir && in_array($basename, $filters['exclude_files'])) {
                continue;
            }

            // 搜索模式过滤
            if ($filters['search_pattern'] && !fnmatch($filters['search_pattern'], $basename)) {
                continue;
            }

            if ($is_dir) {
                // 目录处理
                $result['directories'][] = [
                    'name' => $basename,
                    'path' => $item,
                    'full_path' => $directory . '/' . $basename
                ];
            } else {
                // 文件处理
                $extension = pathinfo($basename, PATHINFO_EXTENSION);

                // 扩展名过滤
                if (!empty($filters['extensions']) && !in_array(strtolower($extension), $filters['extensions'])) {
                    continue;
                }

                $result['files'][] = [
                    'name' => $basename,
                    'path' => $item,
                    'full_path' => $directory . '/' . $basename,
                    'extension' => $extension,
                    'size' => ftp_size($this->ftp_conn, $item),
                    'modified' => ftp_mdtm($this->ftp_conn, $item)
                ];
            }
        }

        // 排序
        usort($result['directories'], function($a, $b) {
            return strcasecmp($a['name'], $b['name']);
        });

        usort($result['files'], function($a, $b) {
            return strcasecmp($a['name'], $b['name']);
        });

        return $result;
    }

    /**
     * 检查是否是目录
     */
    private function isDirectory($path) {
        $current_dir = ftp_pwd($this->ftp_conn);

        if (@ftp_chdir($this->ftp_conn, $path)) {
            ftp_chdir($this->ftp_conn, $current_dir);
            return true;
        }

        return false;
    }

    /**
     * 获取文件统计信息
     */
    public function getStats($directory = '') {
        $items = $this->listFiles($directory);

        $stats = [
            'total_dirs' => count($items['directories']),
            'total_files' => count($items['files']),
            'total_size' => 0,
            'by_extension' => [],
            'largest_file' => null,
            'oldest_file' => null,
            'newest_file' => null
        ];

        foreach ($items['files'] as $file) {
            // 统计总大小
            if ($file['size'] > 0) {
                $stats['total_size'] += $file['size'];
            }

            // 按扩展名统计
            $ext = $file['extension'] ?: '无扩展名';
            if (!isset($stats['by_extension'][$ext])) {
                $stats['by_extension'][$ext] = 0;
            }
            $stats['by_extension'][$ext]++;

            // 最大文件
            if (!$stats['largest_file'] || $file['size'] > $stats['largest_file']['size']) {
                $stats['largest_file'] = $file;
            }

            // 最旧文件
            if ($file['modified'] > 0) {
                if (!$stats['oldest_file'] || $file['modified'] < $stats['oldest_file']['modified']) {
                    $stats['oldest_file'] = $file;
                }

                // 最新文件
                if (!$stats['newest_file'] || $file['modified'] > $stats['newest_file']['modified']) {
                    $stats['newest_file'] = $file;
                }
            }
        }

        // 按文件数排序扩展名
        arsort($stats['by_extension']);

        return $stats;
    }

    public function __destruct() {
        if ($this->ftp_conn) {
            ftp_close($this->ftp_conn);
        }
    }
}

// 使用示例
try {
    $lister = new FTPFileLister('localhost', 'user', 'pass');

    // 列出当前目录
    echo "当前目录内容:\n";
    $current_dir = $lister->listFiles();

    echo "目录 (" . count($current_dir['directories']) . "):\n";
    foreach ($current_dir['directories'] as $dir) {
        echo "  📁 {$dir['name']}\n";
    }

    echo "\n文件 (" . count($current_dir['files']) . "):\n";
    foreach ($current_dir['files'] as $file) {
        $size = $file['size'] > 0 ? formatBytes($file['size']) : '未知';
        echo "  📄 {$file['name']} ($size)\n";
    }

    // 使用过滤器列出图片文件
    echo "\n图片文件:\n";
    $images = $lister->listFiles('/public_html/images', [
        'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'bmp'],
        'exclude_dirs' => ['thumbnails', 'backup']
    ]);

    foreach ($images['files'] as $image) {
        echo "  🖼️ {$image['name']}\n";
    }

    // 获取统计信息
    echo "\n目录统计信息:\n";
    $stats = $lister->getStats('/public_html');

    echo "总目录数: {$stats['total_dirs']}\n";
    echo "总文件数: {$stats['total_files']}\n";
    echo "总大小: " . formatBytes($stats['total_size']) . "\n";

    if ($stats['largest_file']) {
        echo "最大文件: {$stats['largest_file']['name']} (" .
             formatBytes($stats['largest_file']['size']) . ")\n";
    }

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

// 辅助函数:格式化字节大小
function formatBytes($bytes, $precision = 2) {
    if ($bytes <= 0) return '0 Bytes';

    $units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
    $base = log($bytes, 1024);
    $pow = floor($base);
    $pow = min($pow, count($units) - 1);

    return round(pow(1024, $base - $pow), $precision) . ' ' . $units[$pow];
}
?>

注意事项

  • 返回的文件名可能是相对路径或绝对路径,具体取决于FTP服务器
  • 某些FTP服务器可能不返回 ... 目录项
  • 如果目录不存在或无权限访问,函数将返回 false
  • 对于空目录,函数可能返回空数组或包含 ... 的数组
  • 返回的列表不区分文件和目录,需要额外判断
  • 某些FTP服务器可能对目录路径大小写敏感
  • 使用被动模式(ftp_pasv())可以提高兼容性

最佳实践建议

推荐做法
  • 使用ftp_pasv($ftp_conn, true)启用被动模式
  • 总是检查返回值是否为false,以处理错误
  • 使用ftp_chdir()辅助判断条目是否为目录
  • 对返回的列表进行排序,提供更好的用户体验
  • 实现递归函数以遍历整个目录树
  • 添加适当的错误处理和日志记录
避免做法
  • 不要假设返回的列表包含...目录项
  • 避免在不检查返回值的情况下直接使用结果数组
  • 不要假设返回的文件名是相对路径还是绝对路径
  • 避免在循环中多次调用ftp_nlist(),可能造成性能问题
  • 不要忘记处理特殊字符和编码问题
  • 避免在没有错误处理的情况下使用

常见问题解答

ftp_nlist() 返回的列表不直接区分文件和目录。可以通过以下方法判断:
  1. 使用 ftp_chdir() 尝试切换到该条目,成功则为目录
  2. 检查条目是否以斜杠结尾(某些FTP服务器会这样表示目录)
  3. 使用 ftp_rawlist() 获取详细信息并解析文件类型
  4. 结合 ftp_size(),目录通常返回 -1

这取决于FTP服务器的配置:
  • 有些服务器返回相对于当前目录的相对路径
  • 有些服务器返回绝对路径
  • 可以通过 ftp_pwd() 获取当前目录,然后拼接相对路径
  • 建议使用 basename() 获取文件名,dirname() 获取目录名
  • 为了兼容性,最好编写能处理两种情况的代码

当目录包含大量文件时:
  1. 使用分页或懒加载技术,避免一次性获取所有文件
  2. 考虑使用 ftp_rawlist() 并解析,可能更高效
  3. 实现缓存机制,避免重复查询
  4. 设置超时时间,避免长时间等待
  5. 对结果进行过滤,只获取需要的文件
  6. 考虑使用异步处理,避免阻塞主线程

相关函数

  • ftp_rawlist() - 返回指定目录的详细列表
  • ftp_chdir() - 改变FTP服务器上的当前目录
  • ftp_pwd() - 返回当前目录名称
  • ftp_size() - 返回指定文件的大小
  • ftp_mdtm() - 返回指定文件的最后修改时间
  • ftp_cdup() - 切换到上级目录
  • ftp_mkdir() - 在FTP服务器上创建目录
  • ftp_rmdir() - 删除FTP服务器上的目录