文件读写

文件读写是PHP中最基本也是最重要的操作之一。通过文件读写,我们可以存储数据、读取配置、处理日志等。本节将详细介绍PHP中的各种文件读写方法。

学习目标

完成本节学习后,你将能够:

  • 掌握不同的文件读取方法
  • 学会安全地写入文件内容
  • 理解文件指针和定位操作
  • 处理大文件的读写技巧
  • 了解文件锁定的使用方法

文件读取的基本方法

1. file_get_contents() - 最简单的读取方法

file_get_contents() 是最简单易用的文件读取函数,适合读取小文件。

<?php
// 基本用法
$content = file_get_contents('example.txt');

if ($content === false) {
    echo "读取文件失败";
} else {
    echo "文件内容:" . htmlspecialchars($content);
}

// 读取远程文件(如果PHP配置允许)
$remoteContent = file_get_contents('https://www.example.com');
if ($remoteContent !== false) {
    echo "远程文件长度:" . strlen($remoteContent) . " 字节";
}
?>

2. file() - 按行读取文件

file() 函数将文件的每一行作为数组元素返回。

<?php
// 读取文件到数组
$lines = file('example.txt');

if ($lines === false) {
    echo "读取失败";
} else {
    echo "文件共有 " . count($lines) . " 行<br>";

    // 显示前5行
    for ($i = 0; $i < min(5, count($lines)); $i++) {
        $lineNumber = $i + 1;
        echo "第{$lineNumber}行:" . htmlspecialchars($lines[$i]) . "<br>";
    }
}

// 使用参数控制读取行为
$lines = file('example.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// FILE_IGNORE_NEW_LINES:忽略每行的换行符
// FILE_SKIP_EMPTY_LINES:跳过空行
// FILE_TEXT:以文本模式读取(Windows下转换换行符)
// FILE_BINARY:以二进制模式读取

echo "处理后的行数:" . count($lines) . "<br>";
?>

3. fopen/fread/fclose - 流式读取

对于大文件,使用流式读取更节省内存。

<?php
// 基本的流式读取
$handle = fopen('large_file.txt', 'r');

if ($handle === false) {
    echo "无法打开文件";
} else {
    // 读取整个文件(适合大文件)
    $content = '';
    while (!feof($handle)) {
        $chunk = fread($handle, 8192); // 每次读取8KB
        if ($chunk !== false) {
            $content .= $chunk;
        }
    }

    fclose($handle);
    echo "读取完成,总长度:" . strlen($content) . " 字节";
}

// 按行读取
$handle = fopen('data.csv', 'r');
if ($handle !== false) {
    $lineCount = 0;
    while (($line = fgets($handle)) !== false) {
        $lineCount++;
        echo "第{$lineCount}行:" . htmlspecialchars(trim($line)) . "<br>";

        // 只显示前10行
        if ($lineCount >= 10) {
            break;
        }
    }
    fclose($handle);
}
?>

文件写入的基本方法

1. file_put_contents() - 最简单的写入方法

<?php
// 基本写入
$data = "Hello, PHP文件写入!\n这是第二行。";
$result = file_put_contents('output.txt', $data);

if ($result === false) {
    echo "写入失败";
} else {
    echo "成功写入 {$result} 字节";
}

// 追加写入
$additionalData = "\n这是追加的内容。";
file_put_contents('output.txt', $additionalData, FILE_APPEND);

// 使用锁定防止并发写入
$lockedData = "需要安全写入的内容";
$result = file_put_contents('important.txt', $lockedData, LOCK_EX);

// FILE_APPEND: 追加模式
// LOCK_EX: 独占锁定
// FILE_TEXT: 文本模式
// FILE_BINARY: 二进制模式
?>

2. fwrite/fopen - 流式写入

<?php
// 写入文件
$handle = fopen('output.txt', 'w');
if ($handle !== false) {
    fwrite($handle, "第一行内容\n");
    fwrite($handle, "第二行内容\n");
    fwrite($handle, "第三行内容\n");
    fclose($handle);
    echo "文件写入完成";
}

// 追加写入
$handle = fopen('output.txt', 'a');
if ($handle !== false) {
    fwrite($handle, "这是追加的内容\n");
    fclose($handle);
}

// 文件模式说明:
// 'r'  - 只读,从文件开头开始
// 'r+' - 读写,从文件开头开始
// 'w'  - 只写,清空文件内容(如果不存在则创建)
// 'w+' - 读写,清空文件内容
// 'a'  - 只写,从文件末尾开始追加(如果不存在则创建)
// 'a+' - 读写,从文件末尾开始追加
// 'x'  - 只写,创建新文件(如果文件已存在则失败)
// 'x+' - 读写,创建新文件
?>

高级文件操作

1. 文件指针定位

<?php
// 文件指针操作示例
$handle = fopen('data.txt', 'r+');
if ($handle !== false) {
    // 写入初始内容
    fwrite($handle, "这是第一行\n这是第二行\n这是第三行\n");

    // 回到文件开头
    rewind($handle);
    echo "当前位置:" . ftell($handle) . "<br>"; // 输出: 0

    // 读取第一行
    $firstLine = fgets($handle);
    echo "第一行:" . htmlspecialchars($firstLine) . "<br>";
    echo "当前位置:" . ftell($handle) . "<br>";

    // 移动到指定位置
    fseek($handle, 10); // 移动到第10个字节
    echo "移动后位置:" . ftell($handle) . "<br>";

    // 从当前位置读取
    $remaining = fread($handle, 100);
    echo "剩余内容:" . htmlspecialchars($remaining) . "<br>";

    // 移动到文件末尾
    fseek($handle, 0, SEEK_END);
    echo "文件末尾位置:" . ftell($handle) . "<br>";

    // 在文件末尾添加内容
    fwrite($handle, "追加到末尾的内容\n");

    fclose($handle);
}
?>

2. CSV文件处理

<?php
// 写入CSV文件
$data = [
    ['姓名', '年龄', '邮箱'],
    ['张三', 25, 'zhangsan@example.com'],
    ['李四', 30, 'lisi@example.com'],
    ['王五', 28, 'wangwu@example.com']
];

$handle = fopen('users.csv', 'w');
if ($handle !== false) {
    foreach ($data as $row) {
        fputcsv($handle, $row);
    }
    fclose($handle);
    echo "CSV文件写入完成";
}

// 读取CSV文件
$handle = fopen('users.csv', 'r');
if ($handle !== false) {
    echo "<h3>用户列表:</h3>";
    echo "<table border='1'>";
    echo "<tr><th>姓名</th><th>年龄</th><th>邮箱</th></tr>";

    $isFirstRow = true;
    while (($row = fgetcsv($handle)) !== false) {
        if ($isFirstRow) {
            $isFirstRow = false;
            continue; // 跳过标题行
        }

        echo "<tr>";
        echo "<td>" . htmlspecialchars($row[0]) . "</td>";
        echo "<td>" . htmlspecialchars($row[1]) . "</td>";
        echo "<td>" . htmlspecialchars($row[2]) . "</td>";
        echo "</tr>";
    }

    echo "</table>";
    fclose($handle);
}
?>

3. 配置文件读写

<?php
// 写入INI配置文件
$config = [
    'database' => [
        'host' => 'localhost',
        'username' => 'root',
        'password' => 'password',
        'database' => 'myapp'
    ],
    'app' => [
        'name' => 'My Application',
        'version' => '1.0.0',
        'debug' => true
    ]
];

// 写入PHP数组格式
$content = "<?php\nreturn " . var_export($config, true) . ";\n";
file_put_contents('config.php', $content);

// 写入JSON格式
$jsonContent = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
file_put_contents('config.json', $jsonContent);

// 读取配置文件
$phpConfig = include 'config.php';
$jsonConfig = json_decode(file_get_contents('config.json'), true);

echo "<h3>PHP配置:</h3>";
echo "数据库主机:" . $phpConfig['database']['host'] . "<br>";

echo "<h3>JSON配置:</h3>";
echo "应用名称:" . $jsonConfig['app']['name'] . "<br>";
?>

实用示例

1. 简单的计数器

<?php
// 网站访问计数器
class Counter {
    private $file;

    public function __construct($file = 'counter.txt') {
        $this->file = $file;

        // 初始化计数器文件
        if (!file_exists($this->file)) {
            file_put_contents($this->file, '0');
        }
    }

    public function increment() {
        $current = (int)file_get_contents($this->file);
        $current++;

        // 使用文件锁定
        file_put_contents($this->file, $current, LOCK_EX);
        return $current;
    }

    public function getCount() {
        return (int)file_get_contents($this->file);
    }

    public function reset() {
        file_put_contents($this->file, '0', LOCK_EX);
    }
}

// 使用计数器
$counter = new Counter();
$count = $counter->increment();

echo "您是第 {$count} 位访问者!<br>";
echo "总访问次数:" . $counter->getCount();
?>

2. 简单的日志记录器

<?php
// 日志记录器类
class Logger {
    private $logFile;

    public function __construct($file = 'app.log') {
        $this->logFile = $file;
    }

    public function log($message, $level = 'INFO') {
        $timestamp = date('Y-m-d H:i:s');
        $logEntry = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;

        // 追加到日志文件
        file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);
    }

    public function error($message) {
        $this->log($message, 'ERROR');
    }

    public function warning($message) {
        $this->log($message, 'WARNING');
    }

    public function info($message) {
        $this->log($message, 'INFO');
    }

    public function getRecentLogs($lines = 10) {
        if (!file_exists($this->logFile)) {
            return [];
        }

        $content = file_get_contents($this->logFile);
        $allLines = explode(PHP_EOL, trim($content));

        // 返回最后几行
        return array_slice($allLines, -$lines);
    }
}

// 使用日志记录器
$logger = new Logger();

$logger->info('应用程序启动');
$logger->warning('内存使用率较高');
$logger->error('数据库连接失败');

echo "日志记录完成<br>";

// 显示最近5条日志
$recentLogs = $logger->getRecentLogs(5);
echo "<h3>最近日志:</h3>";
foreach ($recentLogs as $log) {
    echo htmlspecialchars($log) . "<br>";
}
?>

3. 文件备份工具

<?php
// 简单的文件备份类
class FileBackup {
    private $backupDir;

    public function __construct($backupDir = 'backup') {
        $this->backupDir = $backupDir;

        if (!is_dir($this->backupDir)) {
            mkdir($this->backupDir, 0755, true);
        }
    }

    public function backupFile($sourceFile) {
        if (!file_exists($sourceFile)) {
            throw new Exception("源文件不存在:{$sourceFile}");
        }

        $basename = basename($sourceFile);
        $timestamp = date('YmdHis');
        $backupFile = $this->backupDir . "/{$basename}.{$timestamp}.backup";

        if (copy($sourceFile, $backupFile)) {
            return $backupFile;
        } else {
            throw new Exception("备份失败:{$sourceFile}");
        }
    }

    public function restoreBackup($backupFile, $targetFile) {
        if (!file_exists($backupFile)) {
            throw new Exception("备份文件不存在:{$backupFile}");
        }

        // 备份当前文件(如果存在)
        if (file_exists($targetFile)) {
            $this->backupFile($targetFile);
        }

        if (copy($backupFile, $targetFile)) {
            return true;
        } else {
            throw new Exception("恢复失败:{$backupFile}");
        }
    }

    public function listBackups($pattern = null) {
        $backups = [];
        $files = scandir($this->backupDir);

        foreach ($files as $file) {
            if ($file === '.' || $file === '..') {
                continue;
            }

            if ($pattern !== null && strpos($file, $pattern) === false) {
                continue;
            }

            if (preg_match('/^(.+)\.(\d+)\.backup$/', $file, $matches)) {
                $originalFile = $matches[1];
                $timestamp = $matches[2];
                $backupPath = $this->backupDir . '/' . $file;

                $backups[] = [
                    'original' => $originalFile,
                    'timestamp' => $timestamp,
                    'date' => date('Y-m-d H:i:s', strtotime($timestamp)),
                    'path' => $backupPath,
                    'size' => filesize($backupPath)
                ];
            }
        }

        return $backups;
    }
}

// 使用备份工具
try {
    $backup = new FileBackup();

    // 创建测试文件
    file_put_contents('test.txt', '这是测试文件内容');

    // 备份文件
    $backupFile = $backup->backupFile('test.txt');
    echo "文件已备份到:" . htmlspecialchars($backupFile) . "<br>";

    // 修改原文件
    file_put_contents('test.txt', '修改后的内容');

    // 再次备份
    $backup->backupFile('test.txt');

    // 列出所有备份
    $backups = $backup->listBackups('test.txt');
    echo "<h3>备份列表:</h3>";
    foreach ($backups as $b) {
        echo "文件:{$b['original']},日期:{$b['date']},大小:{$b['size']} 字节<br>";
    }

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

最佳实践

1. 错误处理

<?php
// 安全的文件读取函数
function safeFileRead($filename) {
    if (!file_exists($filename)) {
        throw new Exception("文件不存在:{$filename}");
    }

    if (!is_readable($filename)) {
        throw new Exception("文件不可读:{$filename}");
    }

    $content = file_get_contents($filename);
    if ($content === false) {
        throw new Exception("读取文件失败:{$filename}");
    }

    return $content;
}

// 安全的文件写入函数
function safeFileWrite($filename, $content) {
    // 确保目录存在
    $directory = dirname($filename);
    if (!is_dir($directory)) {
        if (!mkdir($directory, 0755, true)) {
            throw new Exception("无法创建目录:{$directory}");
        }
    }

    // 使用锁定写入
    $result = file_put_contents($filename, $content, LOCK_EX);
    if ($result === false) {
        throw new Exception("写入文件失败:{$filename}");
    }

    return $result;
}

// 使用示例
try {
    $content = safeFileRead('input.txt');
    $modifiedContent = strtoupper($content);
    safeFileWrite('output.txt', $modifiedContent);
    echo "文件处理完成";
} catch (Exception $e) {
    echo "错误:" . $e->getMessage();
}
?>

2. 内存管理

<?php
// 处理大文件的逐行处理函数
function processLargeFile($filename, $callback) {
    $handle = fopen($filename, 'r');
    if ($handle === false) {
        throw new Exception("无法打开文件:{$filename}");
    }

    try {
        $lineNumber = 0;
        while (($line = fgets($handle)) !== false) {
            $lineNumber++;
            $callback($line, $lineNumber);
        }
    } finally {
        fclose($handle);
    }
}

// 使用示例:统计文件行数和字符数
$lineCount = 0;
$totalChars = 0;

processLargeFile('large_file.txt', function($line, $lineNum) use (&$lineCount, &$totalChars) {
    $lineCount++;
    $totalChars += strlen($line);

    // 每处理1000行输出一次进度
    if ($lineNum % 1000 === 0) {
        echo "已处理 {$lineNum} 行<br>";
    }
});

echo "处理完成:<br>";
echo "总行数:{$lineCount}<br>";
echo "总字符数:{$totalChars}<br>";
?>

总结

文件读写是PHP开发中的基础技能,掌握好这些技巧对实际开发非常重要。

关键要点:

  1. 选择合适的方法

    • file_get_contents/file_put_contents:适合小文件和简单操作
    • fopen/fread/fwrite:适合大文件和精细控制
    • file/fgetcsv/fputcsv:适合按行处理和CSV文件
  2. 安全考虑

    • 始终检查文件是否存在和可读写
    • 使用文件锁定防止并发问题
    • 处理错误和异常情况
  3. 性能优化

    • 对大文件使用流式处理
    • 及时关闭文件句柄
    • 避免一次性读取过大的文件
  4. 实用技巧

    • 使用文件锁定确保数据一致性
    • 创建备份文件保护重要数据
    • 使用适当的文件模式

通过本节的学习,你应该能够熟练处理PHP中的各种文件读写操作。