PHP ftp_rawlist() 函数

PHP ftp_rawlist() 函数用于获取FTP服务器上指定目录的详细列表,返回包含文件和目录完整信息的原始行数组。

特点:此函数返回FTP服务器对LIST命令的原始响应,包含文件权限、所有者、组、大小、修改时间等详细信息,但不同服务器的格式可能不同。

语法

ftp_rawlist(resource $ftp, string $directory, bool $recursive = false): array|false

参数说明

参数 描述
ftp 必需。FTP连接的标识符,由ftp_connect()ftp_ssl_connect()返回
directory 必需。要列出详细信息的目录路径。如果为空字符串,则列出当前目录
recursive 可选。如果为true,则递归列出目录内容。注意:并非所有服务器都支持递归列表

返回值

  • 成功时返回包含原始服务器响应行的数组
  • 失败时返回 false
  • 数组的每个元素是一个字符串,包含一行目录列表信息
  • 返回的信息格式取决于FTP服务器,通常是Unix风格的ls -l格式

与ftp_nlist()的区别

特性 ftp_nlist() ftp_rawlist()
返回内容 仅文件名/目录名数组 原始服务器响应行数组,包含完整信息
信息量 简单,仅名称 详细,包括权限、所有者、大小、时间等
处理难度 简单,直接使用 复杂,需要解析服务器特定的格式
适用场景 只需文件名列表时 需要文件详细信息时
服务器兼容性 较好,标准化 较差,不同服务器格式不同
性能 较快 较慢,传输更多数据

常见的FTP LIST格式

服务器类型 典型格式 示例
Unix/Linux -rwxr-xr-x 1 user group 1234 Jan 1 12:00 filename -rw-r--r-- 1 ftp ftp 123456 Jan 1 12:00 index.html
drwxr-xr-x 2 ftp ftp 4096 Jan 1 12:00 images
Windows (IIS) 01-01-24 12:00PM 123456 filename 01-01-2024 12:00PM 123456 index.html
01-01-2024 12:00PM <DIR> images
Windows (FileZilla) drwxr-xr-x 1 ftp ftp 0 Jan 1 12:00 directory -rwxr-xr-x 1 user group 4096 Jan 1 12:00 file.txt
MS-DOS 01-01-24 12:00 123456 FILENAME.EXT 01-01-24 12:00 123456 INDEX.HTM

Unix格式解析

drwxr-xr-x 2 ftp ftp 4096 Jan 1 12:00 images
├──┬── ┬── ┬── ┬── ┬── ┬── ┬── ┬── ┬──
  │   │   │   │   │   │   │   │   └── 文件名
  │   │   │   │   │   │   │   └── 修改时间
  │   │   │   │   │   │   └── 修改月份
  │   │   │   │   │   └── 修改日期
  │   │   │   │   └── 文件大小(字节)
  │   │   │   └── 组名
  │   │   └── 所有者
  │   └── 链接数
  └── 文件类型和权限
权限字符串解析

第一位:文件类型

  • -:普通文件
  • d:目录
  • l:符号链接
  • c:字符设备
  • b:块设备
  • p:命名管道
  • s:套接字

后续9位:权限(3组)

每组:r(读) w(写) x(执行)

所有者 | 组 | 其他用户

示例

示例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);

// 获取当前目录的详细列表
$current_dir = ".";
echo "获取目录 '$current_dir' 的详细列表:\n";
$raw_list = ftp_rawlist($ftp_conn, $current_dir);

if ($raw_list !== false) {
    echo "找到 " . count($raw_list) . " 个条目:\n";
    echo "========================================\n";

    foreach ($raw_list as $index => $line) {
        echo "条目 " . ($index + 1) . ":\n";
        echo "  原始数据: $line\n";

        // 尝试解析Unix格式
        if (preg_match('/^([d\-l])([rwx\-]{9})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\w{3}\s+\d+\s+[\d:]+)\s+(.+)$/', $line, $matches)) {
            echo "  解析结果:\n";
            echo "    类型: " . ($matches[1] == 'd' ? '目录' : ($matches[1] == 'l' ? '链接' : '文件')) . "\n";
            echo "    权限: {$matches[2]}\n";
            echo "    链接数: {$matches[3]}\n";
            echo "    所有者: {$matches[4]}\n";
            echo "    组: {$matches[5]}\n";
            echo "    大小: {$matches[6]} 字节\n";
            echo "    修改时间: {$matches[7]}\n";
            echo "    名称: {$matches[8]}\n";
        } elseif (preg_match('/^(\d{2}-\d{2}-\d{2,4}\s+\d{2}:\d{2}[AP]?M?)\s+(<DIR>|\d+)\s+(.+)$/i', $line, $matches)) {
            echo "  解析结果 (Windows格式):\n";
            echo "    修改时间: {$matches[1]}\n";
            echo "    类型: " . (strtoupper($matches[2]) == '<DIR>' ? '目录' : '文件') . "\n";
            echo "    大小: " . (is_numeric($matches[2]) ? "{$matches[2]} 字节" : "目录") . "\n";
            echo "    名称: {$matches[3]}\n";
        } else {
            echo "  无法解析的行格式\n";
        }

        echo "----------------------------------------\n";
    }
} else {
    echo "获取目录列表失败\n";
}

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

示例2:通用FTP列表解析器

<?php
class FTPListParser {
    /**
     * 解析FTP原始列表行
     * @param string $line 原始行
     * @return array 解析后的信息
     */
    public static function parseLine($line) {
        $result = [
            'raw' => $line,
            'type' => 'unknown',
            'name' => '',
            'size' => 0,
            'permissions' => '',
            'owner' => '',
            'group' => '',
            'modified' => 0,
            'is_dir' => false,
            'is_link' => false,
            'link_target' => ''
        ];

        // 移除行首尾空格
        $line = trim($line);

        // 尝试Unix格式解析
        if (self::parseUnixFormat($line, $result)) {
            return $result;
        }

        // 尝试Windows/IIS格式解析
        if (self::parseWindowsFormat($line, $result)) {
            return $result;
        }

        // 尝试MS-DOS格式解析
        if (self::parseMSDOSFormat($line, $result)) {
            return $result;
        }

        // 如果都无法解析,尝试提取文件名(假设是简单列表)
        $result['name'] = basename($line);

        return $result;
    }

    /**
     * 解析Unix格式: drwxr-xr-x 1 user group 1234 Jan 1 12:00 filename
     */
    private static function parseUnixFormat($line, &$result) {
        // 正则匹配Unix格式
        $pattern = '/^([d\-lcbps])([rwx\-]{9})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+[\d:]+|\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s+(.+)$/';

        if (preg_match($pattern, $line, $matches)) {
            $result['type'] = self::getFileTypeFromChar($matches[1]);
            $result['permissions'] = $matches[2];
            $result['owner'] = $matches[4];
            $result['group'] = $matches[5];
            $result['size'] = intval($matches[6]);
            $result['modified'] = self::parseUnixDate($matches[7]);

            // 处理文件名(可能包含空格)
            $name = $matches[8];

            // 检查是否是符号链接
            if ($matches[1] == 'l' && strpos($name, ' -> ') !== false) {
                list($result['name'], $result['link_target']) = explode(' -> ', $name, 2);
                $result['is_link'] = true;
            } else {
                $result['name'] = $name;
                $result['is_link'] = false;
            }

            $result['is_dir'] = ($matches[1] == 'd');

            return true;
        }

        return false;
    }

    /**
     * 解析Windows/IIS格式: 01-01-24 12:00PM <DIR> filename
     */
    private static function parseWindowsFormat($line, &$result) {
        // 正则匹配Windows格式
        $pattern = '/^(\d{1,2}-\d{1,2}-\d{2,4}\s+\d{1,2}:\d{2}(?:[AP]M)?)\s+(<DIR>|\d+)\s+(.+)$/i';

        if (preg_match($pattern, $line, $matches)) {
            $result['modified'] = self::parseWindowsDate($matches[1]);

            if (strtoupper($matches[2]) == '<DIR>') {
                $result['type'] = 'dir';
                $result['is_dir'] = true;
                $result['size'] = 0;
            } else {
                $result['type'] = 'file';
                $result['is_dir'] = false;
                $result['size'] = intval($matches[2]);
            }

            $result['name'] = trim($matches[3]);

            return true;
        }

        return false;
    }

    /**
     * 解析MS-DOS格式: 01-01-24 12:00 123456 FILENAME.EXT
     */
    private static function parseMSDOSFormat($line, &$result) {
        // 正则匹配MS-DOS格式
        $pattern = '/^(\d{1,2}-\d{1,2}-\d{2,4}\s+\d{1,2}:\d{2}(?:[AP]M)?)\s+(\d+)\s+(\S.+)$/i';

        if (preg_match($pattern, $line, $matches)) {
            $result['modified'] = self::parseWindowsDate($matches[1]);
            $result['type'] = 'file';
            $result['is_dir'] = false;
            $result['size'] = intval($matches[2]);
            $result['name'] = trim($matches[3]);

            return true;
        }

        return false;
    }

    /**
     * 根据字符获取文件类型
     */
    private static function getFileTypeFromChar($char) {
        $types = [
            '-' => 'file',
            'd' => 'dir',
            'l' => 'link',
            'c' => 'char',
            'b' => 'block',
            'p' => 'pipe',
            's' => 'socket'
        ];

        return isset($types[$char]) ? $types[$char] : 'unknown';
    }

    /**
     * 解析Unix日期格式
     */
    private static function parseUnixDate($dateStr) {
        $dateStr = trim($dateStr);

        // 尝试解析 "Jan 1 12:00" 格式(当年)
        if (preg_match('/^(\w{3})\s+(\d{1,2})\s+(\d{1,2}):(\d{2})$/', $dateStr, $matches)) {
            $year = date('Y');
            return strtotime("{$matches[1]} {$matches[2]} {$year} {$matches[3]}:{$matches[4]}");
        }

        // 尝试解析 "Jan 1 2024" 格式
        if (preg_match('/^(\w{3})\s+(\d{1,2})\s+(\d{4})$/', $dateStr, $matches)) {
            return strtotime("{$matches[1]} {$matches[2]} {$matches[3]}");
        }

        // 尝试解析 "2024-01-01 12:00" 格式
        if (preg_match('/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})$/', $dateStr, $matches)) {
            return mktime($matches[4], $matches[5], 0, $matches[2], $matches[3], $matches[1]);
        }

        return strtotime($dateStr) ?: 0;
    }

    /**
     * 解析Windows日期格式
     */
    private static function parseWindowsDate($dateStr) {
        $dateStr = trim($dateStr);

        // 尝试常见格式
        $formats = [
            'm-d-y H:i',    // 01-01-24 12:00
            'm-d-y H:iA',   // 01-01-24 12:00PM
            'm-d-Y H:i',    // 01-01-2024 12:00
            'm-d-Y H:iA',   // 01-01-2024 12:00PM
            'd-m-y H:i',    // 01-01-24 12:00 (欧洲格式)
            'd-m-Y H:i'     // 01-01-2024 12:00 (欧洲格式)
        ];

        foreach ($formats as $format) {
            $date = DateTime::createFromFormat($format, $dateStr);
            if ($date !== false) {
                return $date->getTimestamp();
            }
        }

        return strtotime($dateStr) ?: 0;
    }

    /**
     * 解析FTP原始列表
     * @param array $rawList 原始列表数组
     * @return array 解析后的文件信息数组
     */
    public static function parseList($rawList) {
        $parsed = [];

        foreach ($rawList as $line) {
            // 跳过空行和特殊行(如总计行)
            if (empty(trim($line)) || strpos($line, 'total ') === 0) {
                continue;
            }

            $parsed[] = self::parseLine($line);
        }

        return $parsed;
    }

    /**
     * 格式化权限字符串为可读格式
     */
    public static function formatPermissions($permString) {
        if (empty($permString) || strlen($permString) != 9) {
            return $permString;
        }

        $readable = '';
        for ($i = 0; $i < 9; $i += 3) {
            $group = substr($permString, $i, 3);
            $readable .= ($group[0] == 'r' ? '读' : '-');
            $readable .= ($group[1] == 'w' ? '写' : '-');
            $readable .= ($group[2] == 'x' ? '执行' : '-');
            if ($i < 6) $readable .= ' ';
        }

        return $readable;
    }

    /**
     * 格式化文件大小为人类可读
     */
    public static function formatSize($bytes) {
        if ($bytes <= 0) return '0 B';

        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = floor(log($bytes, 1024));

        return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
    }
}

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

    echo "=== FTP目录详细列表解析 ===\n\n";

    // 获取原始列表
    $raw_list = ftp_rawlist($ftp_conn, '.');

    if ($raw_list !== false) {
        // 解析列表
        $parsed_list = FTPListParser::parseList($raw_list);

        echo "找到 " . count($parsed_list) . " 个项目:\n";
        echo "========================================\n";

        foreach ($parsed_list as $item) {
            $icon = $item['is_dir'] ? '📁' : ($item['is_link'] ? '🔗' : '📄');
            $type = $item['is_dir'] ? '目录' : ($item['is_link'] ? '链接' : '文件');
            $size = $item['is_dir'] ? '-' : FTPListParser::formatSize($item['size']);
            $date = $item['modified'] > 0 ? date('Y-m-d H:i', $item['modified']) : '未知';

            echo "$icon {$item['name']}\n";
            echo "  类型: $type\n";
            echo "  大小: $size\n";
            echo "  修改时间: $date\n";

            if (!empty($item['permissions'])) {
                $perm_readable = FTPListParser::formatPermissions($item['permissions']);
                echo "  权限: {$item['permissions']} ($perm_readable)\n";
            }

            if (!empty($item['owner'])) {
                echo "  所有者: {$item['owner']}\n";
            }

            if ($item['is_link'] && !empty($item['link_target'])) {
                echo "  链接目标: {$item['link_target']}\n";
            }

            echo "----------------------------------------\n";
        }

        // 统计信息
        $dir_count = array_sum(array_map(function($item) {
            return $item['is_dir'] ? 1 : 0;
        }, $parsed_list));

        $file_count = count($parsed_list) - $dir_count;
        $total_size = array_sum(array_map(function($item) {
            return $item['size'];
        }, $parsed_list));

        echo "\n统计信息:\n";
        echo "  目录数: $dir_count\n";
        echo "  文件数: $file_count\n";
        echo "  总大小: " . FTPListParser::formatSize($total_size) . "\n";

    } else {
        echo "获取目录列表失败\n";
    }

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

示例3:高级文件浏览器实现

<?php
class FTPFileBrowser {
    private $conn;
    private $parser;

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

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

        ftp_pasv($this->conn, true);
        $this->parser = new FTPListParser();
    }

    public function browseDirectory($path = '', $recursive = false) {
        $raw_list = ftp_rawlist($this->conn, $path, $recursive);

        if ($raw_list === false) {
            return [
                'success' => false,
                'error' => '无法获取目录列表',
                'path' => $path
            ];
        }

        $parsed_items = $this->parser::parseList($raw_list);

        // 按类型和名称排序
        usort($parsed_items, function($a, $b) {
            // 目录在前
            if ($a['is_dir'] && !$b['is_dir']) return -1;
            if (!$a['is_dir'] && $b['is_dir']) return 1;

            // 按名称排序
            return strcasecmp($a['name'], $b['name']);
        });

        // 提取目录信息
        $dir_info = $this->getDirectoryInfo($path);

        return [
            'success' => true,
            'path' => $path ?: '/',
            'items' => $parsed_items,
            'count' => count($parsed_items),
            'dir_info' => $dir_info,
            'recursive' => $recursive
        ];
    }

    public function getDirectoryInfo($path) {
        $info = [
            'path' => $path ?: '/',
            'exists' => false,
            'readable' => false,
            'writable' => false,
            'item_count' => 0,
            'dir_count' => 0,
            'file_count' => 0,
            'total_size' => 0,
            'largest_file' => null,
            'newest_file' => null
        ];

        $raw_list = ftp_rawlist($this->conn, $path);

        if ($raw_list === false) {
            return $info;
        }

        $info['exists'] = true;
        $info['readable'] = true;

        $parsed_items = $this->parser::parseList($raw_list);
        $info['item_count'] = count($parsed_items);

        foreach ($parsed_items as $item) {
            if ($item['is_dir']) {
                $info['dir_count']++;
            } else {
                $info['file_count']++;
                $info['total_size'] += $item['size'];

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

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

        // 测试写入权限(尝试创建测试文件)
        $info['writable'] = $this->testWritePermission($path);

        return $info;
    }

    public function searchFiles($pattern, $search_path = '', $search_type = 'both') {
        $results = [];

        // 获取目录列表
        $browse_result = $this->browseDirectory($search_path);

        if (!$browse_result['success']) {
            return $results;
        }

        foreach ($browse_result['items'] as $item) {
            $matches = false;

            // 检查类型
            if ($search_type == 'both' ||
                ($search_type == 'dir' && $item['is_dir']) ||
                ($search_type == 'file' && !$item['is_dir'])) {

                // 使用通配符匹配
                if (fnmatch($pattern, $item['name'])) {
                    $matches = true;
                }
            }

            if ($matches) {
                $results[] = $item;
            }

            // 如果是目录,递归搜索(避免无限递归)
            if ($item['is_dir'] && $item['name'] != '.' && $item['name'] != '..') {
                $sub_path = rtrim($search_path, '/') . '/' . $item['name'];
                $sub_results = $this->searchFiles($pattern, $sub_path, $search_type);
                $results = array_merge($results, $sub_results);
            }
        }

        return $results;
    }

    public function getFileTree($path = '', $max_depth = 3, $current_depth = 0) {
        if ($current_depth >= $max_depth) {
            return [
                'path' => $path,
                'name' => basename($path) ?: '/',
                'type' => 'dir',
                'truncated' => true
            ];
        }

        $tree = [
            'path' => $path,
            'name' => basename($path) ?: '/',
            'type' => 'dir',
            'contents' => []
        ];

        $browse_result = $this->browseDirectory($path);

        if (!$browse_result['success']) {
            $tree['error'] = $browse_result['error'];
            return $tree;
        }

        foreach ($browse_result['items'] as $item) {
            // 跳过当前目录和上级目录
            if ($item['name'] == '.' || $item['name'] == '..') {
                continue;
            }

            if ($item['is_dir']) {
                // 目录,递归获取子树
                $sub_path = rtrim($path, '/') . '/' . $item['name'];
                $subtree = $this->getFileTree($sub_path, $max_depth, $current_depth + 1);
                $tree['contents'][] = $subtree;
            } else {
                // 文件
                $tree['contents'][] = [
                    'path' => rtrim($path, '/') . '/' . $item['name'],
                    'name' => $item['name'],
                    'type' => 'file',
                    'size' => $item['size'],
                    'modified' => $item['modified'],
                    'permissions' => $item['permissions']
                ];
            }
        }

        return $tree;
    }

    private function testWritePermission($path) {
        $test_filename = '.write_test_' . time();
        $test_path = rtrim($path, '/') . '/' . $test_filename;

        // 创建临时文件内容
        $temp_file = tempnam(sys_get_temp_dir(), 'ftp_test');
        file_put_contents($temp_file, 'test');

        // 尝试上传测试文件
        $result = @ftp_put($this->conn, $test_path, $temp_file, FTP_ASCII);

        // 清理
        unlink($temp_file);

        if ($result) {
            // 删除测试文件
            @ftp_delete($this->conn, $test_path);
            return true;
        }

        return false;
    }

    public function close() {
        if ($this->conn) {
            ftp_close($this->conn);
        }
    }

    public function __destruct() {
        $this->close();
    }
}

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

    echo "=== FTP文件浏览器 ===\n\n";

    // 浏览当前目录
    echo "1. 浏览当前目录:\n";
    $result = $browser->browseDirectory('.');

    if ($result['success']) {
        echo "路径: {$result['path']}\n";
        echo "项目数: {$result['count']}\n\n";

        foreach ($result['items'] as $item) {
            $icon = $item['is_dir'] ? '📁' : '📄';
            $size = $item['is_dir'] ? '-' : FTPListParser::formatSize($item['size']);
            echo "  $icon {$item['name']} ($size)\n";
        }

        // 显示目录信息
        $info = $result['dir_info'];
        echo "\n目录信息:\n";
        echo "  可读: " . ($info['readable'] ? '是' : '否') . "\n";
        echo "  可写: " . ($info['writable'] ? '是' : '否') . "\n";
        echo "  目录数: {$info['dir_count']}\n";
        echo "  文件数: {$info['file_count']}\n";
        echo "  总大小: " . FTPListParser::formatSize($info['total_size']) . "\n";
    }

    // 搜索文件
    echo "\n2. 搜索文件:\n";
    $search_results = $browser->searchFiles('*.txt', '.', 'file');
    echo "找到 " . count($search_results) . " 个文本文件:\n";
    foreach ($search_results as $file) {
        echo "  📄 {$file['name']} (" . FTPListParser::formatSize($file['size']) . ")\n";
    }

    // 获取目录树
    echo "\n3. 目录树结构:\n";
    $tree = $browser->getFileTree('.', 2);
    $this->printTree($tree);

    $browser->close();

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

// 辅助函数:打印目录树
function printTree($tree, $indent = '', $last = true) {
    $prefix = $indent . ($last ? '└── ' : '├── ');
    $icon = $tree['type'] == 'dir' ? '📁 ' : '📄 ';

    echo $prefix . $icon . $tree['name'];

    if ($tree['type'] == 'file' && isset($tree['size'])) {
        echo ' (' . FTPListParser::formatSize($tree['size']) . ')';
    }

    if (isset($tree['truncated'])) {
        echo ' [...]';
    }

    echo "\n";

    if (isset($tree['contents']) && is_array($tree['contents'])) {
        $child_indent = $indent . ($last ? '    ' : '│   ');
        $count = count($tree['contents']);

        foreach ($tree['contents'] as $i => $child) {
            $is_last = ($i == $count - 1);
            printTree($child, $child_indent, $is_last);
        }
    }
}
?>

注意事项

  • ftp_rawlist() 返回的格式取决于FTP服务器,不同服务器可能有不同格式
  • Windows和Unix服务器的列表格式完全不同,需要不同的解析逻辑
  • 递归列表(recursive = true)并非所有服务器都支持
  • 返回的列表可能包含总计行(如total 1234)或其他元数据行
  • 文件名可能包含空格或其他特殊字符,解析时需要注意
  • 符号链接信息通常包含在文件名部分(如linkname -> target
  • 时间格式可能因服务器区域设置而异

最佳实践建议

推荐做法
  • 实现通用的解析器,支持多种服务器格式
  • 先使用ftp_systype()确定服务器类型,选择合适的解析策略
  • 对解析失败的行提供降级处理(如至少提取文件名)
  • 缓存解析结果以提高性能,避免重复调用
  • 对大型目录使用分页或懒加载
  • 记录无法解析的格式以便改进解析器
  • 提供用户可配置的解析选项
避免做法
  • 不要假设所有服务器都使用相同的列表格式
  • 避免硬编码特定的格式解析逻辑
  • 不要忽略可能包含空格或特殊字符的文件名
  • 避免在不必要时使用递归列表,可能性能低下
  • 不要忘记处理目录中的...条目
  • 避免在循环中频繁调用ftp_rawlist()
  • 不要假设所有服务器都支持相同的权限模型

常见问题解答

处理不同服务器格式的策略:

  1. 服务器类型检测:使用ftp_systype()获取服务器类型
  2. 格式嗅探:分析返回的第一行数据判断格式
  3. 多重解析器:实现多种解析器,按顺序尝试
  4. 降级处理:如果无法解析详细格式,至少提取文件名
  5. 配置驱动:允许用户配置或选择解析器
  6. 学习模式:记录无法解析的格式,后续改进解析器
// 示例:自动选择解析器
function autoParseFTPList($rawList) {
    // 尝试检测服务器类型
    $firstLine = isset($rawList[0]) ? $rawList[0] : '';

    if (preg_match('/^[d\-l][rwx\-]{9}/', $firstLine)) {
        return parseUnixFormat($rawList);
    } elseif (preg_match('/<DIR>/i', $firstLine)) {
        return parseWindowsFormat($rawList);
    } elseif (preg_match('/^\d{1,2}-\d{1,2}-\d{2,4}/', $firstLine)) {
        return parseDOSFormat($rawList);
    } else {
        // 降级到简单文件名提取
        return extractFilenames($rawList);
    }
}

ftp_rawlist() 的性能考虑:

性能因素 影响 优化策略
目录大小 文件越多,传输和解析时间越长 分页显示,懒加载,虚拟滚动
网络延迟 高延迟显著增加获取时间 缓存结果,减少请求次数
递归列表 可能返回大量数据,消耗大量内存 避免深度递归,限制返回数量
解析复杂度 复杂解析消耗CPU时间 优化解析算法,异步解析
服务器负载 繁忙服务器响应慢 添加超时和重试机制
// 示例:分页获取目录列表
function getDirectoryPage($ftp, $path, $page = 1, $pageSize = 50) {
    // 获取完整列表
    $fullList = ftp_rawlist($ftp, $path);
    if ($fullList === false) return false;

    // 解析列表
    $parsedList = parseFTPList($fullList);

    // 分页
    $total = count($parsedList);
    $offset = ($page - 1) * $pageSize;
    $pageItems = array_slice($parsedList, $offset, $pageSize);

    return [
        'items' => $pageItems,
        'page' => $page,
        'page_size' => $pageSize,
        'total' => $total,
        'total_pages' => ceil($total / $pageSize)
    ];
}

处理文件权限和所有者信息的注意事项:

  1. 权限表示:Unix使用9位rwx格式,Windows可能没有权限信息
  2. 数字权限:可以将rwx格式转换为八进制数字(如755)
  3. 所有者映射:所有者ID可能需要映射到用户名
  4. 组信息:组ID可能需要映射到组名
  5. 跨平台兼容:不同系统权限模型不同
  6. 特殊权限:处理setuid、setgid、sticky位
// 示例:解析Unix权限字符串
function parseUnixPermissions($permString) {
    if (strlen($permString) != 10) return null;

    $type = $permString[0];
    $perms = substr($permString, 1);

    $octal = 0;
    if ($perms[0] == 'r') $octal += 0400; // 所有者读
    if ($perms[1] == 'w') $octal += 0200; // 所有者写
    if ($perms[2] == 'x') $octal += 0100; // 所有者执行
    if ($perms[3] == 'r') $octal += 0040; // 组读
    if ($perms[4] == 'w') $octal += 0020; // 组写
    if ($perms[5] == 'x') $octal += 0010; // 组执行
    if ($perms[6] == 'r') $octal += 0004; // 其他读
    if ($perms[7] == 'w') $octal += 0002; // 其他写
    if ($perms[8] == 'x') $octal += 0001; // 其他执行

    // 检查特殊权限位
    if ($perms[2] == 's') $octal += 04000; // setuid
    if ($perms[5] == 's') $octal += 02000; // setgid
    if ($perms[8] == 't') $octal += 01000; // sticky bit

    return [
        'type' => $type,
        'string' => $permString,
        'octal' => sprintf('%04o', $octal),
        'human' => self::formatPermissions($perms)
    ];
}

// 使用示例
$perms = parseUnixPermissions('-rwxr-xr-x');
// 返回:
// [
//     'type' => '-',
//     'string' => '-rwxr-xr-x',
//     'octal' => '0755',
//     'human' => '所有者: 读写执行, 组: 读执行, 其他: 读执行'
// ]

相关函数

  • ftp_nlist() - 获取简单的文件名列表
  • ftp_raw() - 发送原始FTP命令
  • ftp_systype() - 获取FTP服务器系统类型
  • ftp_size() - 获取文件大小
  • ftp_mdtm() - 获取文件修改时间
  • ftp_chdir() - 改变当前目录
  • ftp_pwd() - 获取当前目录
  • ftp_exec() - 在FTP服务器上执行命令