文件

处理文件是任何编程语言中重要的一部分,PHP 也不例外。无论你处理文件的原因是什么,PHP 都可以通过一系列函数来满足这些需求。在开始之前,确保你已经阅读并理解了本书前五个章节中介绍的概念。

文件夹操作

  • 显示当前目录:dirname()
  • 改变目录:chdir()
  • 创建新目录:mkdir()

fopen() 和 fclose()

fopen() 是文件操作的基础。它以你指定的模式打开文件并返回一个句柄。使用这个句柄,你可以读取或写入文件,然后用 fclose() 函数关闭它。

示例用法:

<?php
$handle = fopen('data.txt', 'r'); // 打开文件以供读取
fclose($handle); // 关闭文件
?>

在上面的例子中,文件是通过指定模式 'r' 打开以供读取的。关于 fopen() 可用的所有模式的完整列表,可以参考 PHP 手册。

打开和关闭文件本身没有问题,但为了执行有用的操作,你需要了解 fread()fwrite()

当 PHP 脚本执行完毕时,所有打开的文件会自动关闭。因此,尽管在打开文件后不严格要求必须关闭文件,但作为良好的编程习惯,应该显式关闭文件。

读取文件

文件的读取可以通过多种方式进行。如果你只想获取文件的所有内容,可以使用 file_get_contents() 函数。如果你希望将文件的每一行作为数组元素,你可以使用 file() 函数。要对文件的读取进行完全控制,可以使用 fread()

这些函数通常可以互换使用,每个函数都可以完成另一个函数的功能。前两个函数不需要你先用 fopen() 打开文件,也不需要用 fclose() 关闭文件。它们适用于快速、一时性的文件操作。如果你计划对文件进行多次操作,最好将 fopen()fread()fwrite()fclose() 结合使用,因为它更高效。

使用 file_get_contents() 的示例:

<?php
$contents = file_get_contents('data.txt');
echo $contents;
?>

输出:

I am the contents of data.txt

这个函数将整个文件读入一个字符串,从此你可以像处理字符串一样处理它。

使用 file() 的示例:

<?php
$lines = file('data.txt');
foreach($lines as $Key => $line) {
    $lineNum = $Key + 1;
    echo "Line $lineNum: $line";
}
?>

输出:

Line 1: I am the first line of file
Line 2: I am the second line the of the file
Line 3: If I said I was the fourth line of the file, I'd be lying

这个函数将整个文件读入一个数组。数组中的每一项对应文件中的一行。

使用 fread() 的示例:

<?php
$handle = fopen('data.txt', 'r');
$string = fread($handle, 64);
fclose($handle);

echo $string;
?>

输出:

I am the first 64 bytes of data.txt (if it was ASCII encoded). I

这个函数可以从文件中读取指定数量的字节,并将其作为字符串返回。通常情况下,前两个函数更为常用,但在某些情况下,fread() 是必需的。

通过这三个函数,你可以轻松地将文件中的数据读取到适合操作的格式中。接下来的部分将展示如何使用这些函数来实现彼此的功能,但这部分是可选的。如果你不感兴趣,可以跳过并直接进入写入部分。

以下是更复杂的示例:

<?php
$file = 'data.txt';

function detectLineEndings($contents) {
    if(false !== strpos($contents, "\r\n")) return "\r\n";
    else if(false !== strpos($contents, "\r")) return "\r";
    else return "\n";
}

/* 这等同于 file_get_contents($file),但效率较低 */
$handle = fopen($file, 'r');
$contents = fread($handle, filesize($file));
fclose($handle);

/* 这等同于 file($file),但你需要检查行结束符类型。
   Windows 系统使用 \r\n,Mac 系统使用 \r,Unix 系统使用 \n。
   file($file) 会自动检测行结束符,而 fread/file_get_contents 不会 */
$lineEnding = detectLineEndings($contents);
$contents = file_get_contents($file);
$lines = explode($lineEnding, $contents);

/* 这也等同于 file_get_contents($file) */
$lines = file($file);
$contents = implode("\n", $lines);

/* 这等同于 fread($file, 64),如果文件是 ASCII 编码 */
$contents = file_get_contents($file);
$string = substr($contents, 0, 64);
?>

通过这些函数和方法,你可以更灵活地处理文件内容,无论是简单的读取操作还是更复杂的文件解析需求。

写入文件

使用 fwrite() 函数结合 fopen()fclose() 来向文件写入内容。正如你所看到的,写入文件的选项没有读取文件时那么多。然而,PHP 5 引入了一个简化写入过程的函数 file_put_contents()。该函数将在 PHP 5 部分讨论,因为它比较直观,不需要在这里深入讨论。

写入的额外选项并不是来自于函数的数量,而是来自于打开文件时可用的模式。如果你希望向文件写入内容,可以传递三种不同的模式给 fopen() 函数。第一种模式 'w' 会清空文件的所有内容,之后写入的内容将完全替换原有的内容。第二种模式 'a' 会将内容追加到文件的末尾。第三种模式 'x' 只会在文件不存在时使用。所有三种写入模式都会尝试创建文件,如果文件不存在,而 'r' 模式则不会。

使用 'w' 模式的示例:

<?php
$handle = fopen('data.txt', 'w'); // 打开文件并清空内容
$data = "I am new content\nspread across\nseveral lines.";
fwrite($handle, $data);
fclose($handle);

echo file_get_contents('data.txt');
?>

输出:

I am new content
spread across
several lines.

使用 'a' 模式的示例:

<?php
$handle = fopen('data.txt', 'a'); // 打开文件以追加内容
$data = "\n\nI am new content.";
fwrite($handle, $data);
fclose($handle);

echo file_get_contents('data.txt');
?>

输出:

I am the original content.

I am new content.

使用 'x' 模式的示例:

<?php
$handle = fopen('newfile.txt', 'x'); // 只在文件不存在时打开文件
$data = "I am this file's first ever content!";
fwrite($handle, $data);
fclose($handle);

echo file_get_contents('newfile.txt');
?>

输出:

I am this file's first ever content!

在上面展示的三种模式中,'w''a' 是最常用的,但所有模式的写入过程本质上是相同的。

读取与写入

如果你想同时读取和写入一个文件,只需在模式的末尾加上一个 +。例如,读取文件需要使用 'r' 模式。如果你想同时读取和写入该文件,则需要使用 'r+' 模式。类似地,你也可以使用 'w+' 模式来读取和写入文件,但这种模式会将文件截断为零长度。有关更多详细信息,参考 fopen() 页面,其中有一个很有用的表格列出了所有可用的模式。

错误检查

错误检查对于任何编程任务都非常重要,尤其是在 PHP 中处理文件时。进行错误检查的主要原因来自于文件所在的文件系统。如今,大多数 Web 服务器都是基于 Unix 的,因此,如果你正在使用 PHP 开发 Web 应用程序,你必须考虑文件权限。在某些情况下,PHP 可能没有权限读取文件,这将导致你编写的读取代码出现错误。更常见的情况是,PHP 没有权限写入文件,这同样会导致错误。而且,文件的存在(显然)是很重要的。在尝试读取文件时,必须确保文件存在。另一方面,如果你尝试使用 'x' 模式创建并写入文件,那么必须确保文件不存在。

简而言之,在编写与文件交互的代码时,始终假设最糟的情况。假设文件不存在,并且你没有权限读取或写入它。在大多数情况下,这意味着你需要告诉用户,为了让脚本正常工作,他们需要调整文件权限,以便 PHP 能够创建文件并读取/写入它们。但这也意味着你的脚本可以进行调整并执行替代操作。

错误检查的两种主要方法

第一种方法是使用 @ 操作符来抑制在操作文件时发生的任何错误,然后检查结果是否为 false。第二种方法是使用 file_exists()is_readable()is_writeable() 等函数。

使用 @ 操作符的示例:

<?php
$handle = @fopen('data.txt', 'r');
if (!$handle) {
    echo 'PHP does not have permission to read this file or the file in question doesn\'t exist.';
} else {
    $string = fread($handle, 64);
    fclose($handle);
}

$handle = @fopen('data.txt', 'w'); // 'a' 同样适用
if (!$handle) {
    echo 'PHP either does not have permission to write to this file or it does not have permission to create this file in the current directory.';
} else {
    fwrite($handle, 'I can has content?');
    fclose($handle);
}

$handle = @fopen('data.txt', 'x');
if (!$handle) {
    echo 'Either this file exists or PHP does not have permission to create this file in the current directory.';
} else {
    fwrite($handle, 'I can has content?');
    fclose($handle);
}
?>

如上所示,@ 操作符主要用于与 fopen() 函数一起使用。它也可以用于其他情况,但通常效率较低。

使用特定检查函数的示例:

<?php
$file = 'data.txt';

if (!file_exists($file)) {
    // 文件不存在,不需要读取内容
    $contents = '';
    
    // 但可能需要创建文件
    $handle = @fopen($file, 'x'); // 仍然需要进行错误检查
    if (!$handle) {
        echo 'PHP does not have permission to create a file in the current directory.';
    } else {
        fwrite($handle, 'Default data');
        fclose($handle);
    }
} else {
    // 文件存在,可以尝试读取其内容
    if (is_readable($file)) {
        $contents = file_get_contents($file);
    } else {
        echo 'PHP does not have permission to read that file.';
    }
}

if (file_exists($file) && is_writeable($file)) {
    $handle = fopen($file, 'w');
    fwrite($handle, 'I can has content?');
    fclose($handle);
}
?>

从最后的例子中可以看出,错误检查使得你的代码非常健壮。它可以准备应对大多数情况并作出相应的处理,这是任何程序或脚本的关键特性。

行结束符

在本章“读取”部分的最后一个示例中,简要提到过行结束符,在处理文件时,了解它们非常重要。当从文本文件中读取数据时,了解该文件使用了什么类型的行结束符非常关键。行结束符是特殊字符,用来告诉程序显示一个新行。例如,Notepad 只有在找到 \r\n(回车换行符)时,才会将文本移到新的一行(如果启用自动换行,也会显示新的行)。

如果某人在 Windows 系统上写入文本文件,那么每一行很可能会以 \r\n 结束。类似地,在经典的 Macintosh(Mac OS 9 及以下)系统上写入的文件,每一行很可能以 \r 结束。最后,在基于 Unix 的系统(如 Mac OS X 和 GNU/Linux)上写入的文件,每一行通常以 \n 结束。

这为什么重要呢?因为当你使用 file_get_contents() 读取文件时,读取到的字符串会包含这些行结束符,并且这些字符会分散在整个字符串中。有时这些行结束符会影响你想要对字符串做的操作,因此可以使用以下代码移除它们:

<?php
$string = str_replace(array("\n", "\r"), '', $string);
?>

有时,你可能需要知道文本中使用了什么类型的行结束符,以便在添加新文本时保持一致性。幸运的是,在 99% 的情况下,行结束符类型不会在文本中变化,因此可以使用自定义函数 detectLineEndings 来快速检测:

<?php
function detectLineEndings($string) {
    if(false !== strpos($string, "\r\n")) return "\r\n";
    else if(false !== strpos($string, "\r")) return "\r";
    else return "\n";
}
?>

不过大多数情况下,只要意识到文本中存在行结束符,就足够了,你可以根据需要调整脚本来妥善处理。

二进制安全

到目前为止,本章中提到的所有文本都假定采用某种形式的纯文本编码(如 UTF-8 或 ASCII)。然而,文件不一定要采用这种格式,事实上,存在大量不同的文件格式(如图片或可执行文件)。如果你想处理这些文件,必须确保你使用的函数是“二进制安全”的。过去,你需要在模式后添加 b 来告诉 PHP 将文件当作二进制文件处理。如果没有这样做,可能会得到意外的结果,通常是“看起来很奇怪”的数据。

自 PHP 4.3 版本以来,这种做法已不再必要,因为 PHP 会自动检测文件是否需要以文本文件或二进制文件的方式打开,因此你仍然可以遵循这里展示的大部分示例。

与纯文本字符串和字符操作不同,处理二进制数据有很多不同的地方,并且涉及的函数更多,超出了本章的讨论范围。然而,了解这些差异仍然很重要。

序列化

序列化是一种用于程序员的技术,它可以将工作数据保存为一种格式,稍后可以恢复到原来的形式。在简单的情况下,这意味着将一个普通变量(如数组)转换为字符串并将其存储在某个地方。然后,这些数据可以被反序列化,程序员就可以再次使用这个数组。

本书有一个完整的章节介绍序列化,因为它是一个非常有用的技术,掌握它能够有效地使用。在这里提到它,是因为序列化的一个主要用途是在没有数据库的情况下,将数据存储到普通文件中。它还可以用来存储脚本的状态或缓存数据,以便稍后更快速地访问,而文件是其中最常用的存储介质。

在 PHP 中,序列化非常简单,只需要使用 serialize()unserialize() 函数。以下是使用序列化与文件操作结合的示例:

将用户详细信息存储到文件中,以便以后可以轻松检索。

<?php
/* 该部分脚本将数据保存到文件中 */
$data = array(
    'id' => 114,
    'first name' => 'Foo',
    'last name' => 'Bartholomew',
    'age' => 21,
    'country' => 'England'
);
$string = serialize($data);

$handle = fopen('data.dat', 'w');
fwrite($handle, $string);
fclose($handle);

/* 然后,稍后从文件中检索数据并输出 */
$string = file_get_contents('data.dat');
$data = unserialize($string);

$output = '';
foreach ($data as $key => $datum) {
    $field = ucwords($key);
    $output .= "$field: $datum\n";
}

echo $output;
?>

输出:

Id: 114
First Name: Foo
Last Name: Bartholomew
Age: 21
Country: England

PHP 5

PHP 5 引入了一个特定的文件操作函数,即 file_put_contents()。它提供了一种替代写入文件的方法,这在 PHP 4 中是没有的。为了理解它的区别,最简单的方式是通过一个例子来看。

PHP 4 与 PHP 5 使用 file_put_contents() 函数写入文件的示例:

<?php
$file = 'data.txt';
$content = 'New content.';

// PHP 4,覆盖整个文件的数据
$handle = fopen($file, 'w');
fwrite($handle, $content);
fclose($handle);

// PHP 5
file_put_contents($file, $content);

// PHP 4,向文件追加内容
$handle = fopen($file, 'a');
fwrite($handle, $content);
fclose($handle);

// PHP 5
file_put_contents($file, $content, FILE_APPEND);
?>

file_put_contents() 还会尝试创建文件,如果文件不存在,它是二进制安全的。file_get_contents() 并没有 x 模式的等价函数。

file_put_contents() 通常比 fopen() 方法更可取,尤其是在对同一文件执行多次操作时。它在写入时比 file_get_contents() 读文件时更具优势,因此,下面提供了一个函数来模拟 PHP 4 中 file_put_contents() 的行为:

<?php
if (!function_exists('file_put_contents')) {
    function file_put_contents($file, $data, $append = false) {
        if (!$append) $mode = 'w';
        else $mode = 'a';

        $handle = @fopen($file, $mode);
        if (!$handle) return false;

        $bytes = fwrite($handle, $data);
        fclose($handle);

        return $bytes;
    }
}
?>
Last modified: Thursday, 9 January 2025, 11:24 PM