PHP ftruncate()函数

ftruncate()函数用于将文件截断到指定的大小。如果文件原来的大小大于指定大小,则多余的部分会被丢弃;如果小于指定大小,则文件会被扩展,扩展的部分用空字节(\0)填充。

语法

bool ftruncate ( resource $handle , int $size )

参数说明

参数 描述
handle 文件指针资源,通常由fopen()函数创建,且必须以可写模式打开(如"r+"、"w"、"a+"等)
size 截断后文件的大小(字节数)。如果大于原文件大小,则用空字节填充到指定大小;如果小于原文件大小,则截断多余部分。

返回值

  • 成功时返回true
  • 失败时返回false

示例代码

示例1:基本文件截断操作

<?php
// 创建测试文件
$filename = 'test.txt';
$content = "This is a test file with some content that we will truncate.";
file_put_contents($filename, $content);

echo "原始文件大小: " . filesize($filename) . " 字节<br>";
echo "原始内容: " . htmlspecialchars(file_get_contents($filename)) . "<br><br>";

// 以读写模式打开文件
$handle = fopen($filename, 'r+');
if ($handle) {
    // 截断到20字节
    if (ftruncate($handle, 20)) {
        echo "截断到20字节成功<br>";
        // 注意:ftruncate后文件指针位置不变,需要重新定位到文件开头读取
        fseek($handle, 0);
        echo "截断后内容: " . htmlspecialchars(fread($handle, filesize($filename))) . "<br>";
        echo "截断后大小: " . filesize($filename) . " 字节<br><br>";
    } else {
        echo "截断失败<br>";
    }

    // 再截断到50字节(扩展文件)
    if (ftruncate($handle, 50)) {
        echo "扩展到50字节成功<br>";
        fseek($handle, 0);
        echo "扩展后内容: " . htmlspecialchars(fread($handle, filesize($filename))) . "<br>";
        echo "扩展后大小: " . filesize($filename) . " 字节<br>";
        // 注意:扩展的部分是空字节,显示为空白
    }

    fclose($handle);
}

// 清理
unlink($filename);
?>

示例2:日志文件轮转(log rotation)

<?php
// 模拟日志文件轮转
class LogRotator {
    private $filename;
    private $maxSize; // 最大文件大小(字节)
    private $backupCount; // 保留的备份文件数量

    public function __construct($filename, $maxSize = 1024 * 1024, $backupCount = 5) {
        $this->filename = $filename;
        $this->maxSize = $maxSize;
        $this->backupCount = $backupCount;
    }

    /**
     * 检查并轮转日志文件
     */
    public function rotateIfNeeded() {
        if (!file_exists($this->filename)) {
            return false;
        }

        $currentSize = filesize($this->filename);
        if ($currentSize < $this->maxSize) {
            return false; // 文件未达到最大大小,无需轮转
        }

        echo "日志文件达到 {$this->maxSize} 字节,开始轮转...<br>";

        // 1. 重命名现有的备份文件
        for ($i = $this->backupCount - 1; $i >= 0; $i--) {
            $oldFile = $this->getBackupFilename($i);
            $newFile = $this->getBackupFilename($i + 1);

            if (file_exists($oldFile)) {
                rename($oldFile, $newFile);
            }
        }

        // 2. 将当前日志文件重命名为第一个备份
        rename($this->filename, $this->getBackupFilename(0));

        // 3. 创建新的日志文件
        touch($this->filename);

        echo "日志轮转完成<br>";
        return true;
    }

    /**
     * 截断日志文件(保留最后N字节)
     */
    public function truncateLog($keepBytes = 1024) {
        if (!file_exists($this->filename)) {
            return false;
        }

        $fileSize = filesize($this->filename);
        if ($fileSize <= $keepBytes) {
            return false; // 文件小于等于要保留的大小,无需截断
        }

        // 打开文件
        $handle = fopen($this->filename, 'r+');
        if (!$handle) {
            return false;
        }

        // 移动到要保留的内容的开始位置
        $offset = $fileSize - $keepBytes;
        fseek($handle, $offset);

        // 读取要保留的内容
        $keepContent = fread($handle, $keepBytes);

        // 截断文件并写入保留的内容
        rewind($handle);
        ftruncate($handle, $keepBytes);
        fwrite($handle, $keepContent);

        fclose($handle);

        echo "日志文件已截断,保留了最后 {$keepBytes} 字节<br>";
        return true;
    }

    /**
     * 获取备份文件名
     */
    private function getBackupFilename($index) {
        if ($index === 0) {
            return $this->filename . '.0';
        }
        return $this->filename . '.' . $index;
    }
}

// 使用示例
// 创建一个大日志文件
$logFile = 'app.log';
$logContent = str_repeat("Log entry: " . date('Y-m-d H:i:s') . " - Some log message\n", 1000);
file_put_contents($logFile, $logContent);

echo "原始日志文件大小: " . filesize($logFile) . " 字节<br>";

$rotator = new LogRotator($logFile, 10240, 3); // 最大10KB,保留3个备份

// 尝试轮转
$rotator->rotateIfNeeded();

// 如果文件仍然很大,截断保留最后5KB
$rotator->truncateLog(5120);

echo "处理后文件大小: " . filesize($logFile) . " 字节<br>";

// 清理
unlink($logFile);
for ($i = 0; $i <= 3; $i++) {
    $backupFile = $logFile . '.' . $i;
    if (file_exists($backupFile)) {
        unlink($backupFile);
    }
}
?>

示例3:数据库文件清理(模拟)

<?php
// 模拟数据库文件清理
class DatabaseFileManager {
    private $filename;

    public function __construct($filename) {
        $this->filename = $filename;
    }

    /**
     * 清理数据库文件中的过期数据(模拟)
     */
    public function cleanupOldData($maxRecords = 100) {
        if (!file_exists($this->filename)) {
            return false;
        }

        // 打开文件
        $handle = fopen($this->filename, 'r+');
        if (!$handle) {
            return false;
        }

        // 读取所有记录
        $records = [];
        while (!feof($handle)) {
            $line = fgets($handle);
            if (trim($line) !== '') {
                $records[] = $line;
            }
        }

        // 如果记录数超过最大限制,删除最旧的记录
        if (count($records) > $maxRecords) {
            $recordsToKeep = array_slice($records, -$maxRecords);

            // 截断文件并写入保留的记录
            rewind($handle);
            ftruncate($handle, 0);

            foreach ($recordsToKeep as $record) {
                fwrite($handle, $record);
            }

            $removedCount = count($records) - $maxRecords;
            echo "已删除 {$removedCount} 条旧记录,保留 {$maxRecords} 条最新记录<br>";
        } else {
            echo "记录数未超过限制,无需清理<br>";
        }

        fclose($handle);
        return true;
    }

    /**
     * 压缩数据库文件(删除空行和空白)
     */
    public function compressFile() {
        if (!file_exists($this->filename)) {
            return false;
        }

        // 读取文件内容
        $content = file_get_contents($this->filename);
        $originalSize = strlen($content);

        // 移除空行和多余空白
        $lines = explode("\n", $content);
        $compressedLines = [];

        foreach ($lines as $line) {
            $trimmedLine = trim($line);
            if ($trimmedLine !== '') {
                $compressedLines[] = $trimmedLine;
            }
        }

        $compressedContent = implode("\n", $compressedLines);
        $compressedSize = strlen($compressedContent);

        // 如果压缩后有变化,则写入文件
        if ($compressedSize < $originalSize) {
            $handle = fopen($this->filename, 'r+');
            if ($handle) {
                ftruncate($handle, 0);
                fwrite($handle, $compressedContent);
                fclose($handle);

                $saved = $originalSize - $compressedSize;
                echo "文件压缩完成,节省 {$saved} 字节<br>";
                return true;
            }
        } else {
            echo "文件无需压缩<br>";
        }

        return false;
    }
}

// 使用示例
// 创建模拟数据库文件
$dbFile = 'data.db';
$data = "";
for ($i = 1; $i <= 150; $i++) {
    $data .= "Record $i: " . str_repeat('x', 50) . "\n";
}
file_put_contents($dbFile, $data);

echo "原始数据库文件大小: " . filesize($dbFile) . " 字节<br>";

$manager = new DatabaseFileManager($dbFile);

// 清理旧数据
$manager->cleanupOldData(100);

// 压缩文件
$manager->compressFile();

echo "处理后文件大小: " . filesize($dbFile) . " 字节<br>";

// 清理
unlink($dbFile);
?>

注意事项

重要提示:
  • 文件模式:文件必须以可写模式打开(如"r+"、"w"、"a+"等),否则ftruncate()会失败
  • 文件指针位置:ftruncate()不会改变文件指针的位置,截断后可能需要使用fseek()重新定位
  • 扩展内容:如果指定的大小大于原文件大小,扩展的部分会用空字节(\0)填充
  • 二进制安全:ftruncate()是二进制安全的,可以处理任何类型的文件
  • 权限问题:确保PHP进程有足够的权限写入文件
  • 网络文件系统:在NFS等网络文件系统上,ftruncate()的行为可能有所不同
  • 并发访问:在多进程环境中,使用flock()进行文件锁定,避免数据损坏
  • 数据丢失:截断操作会永久删除数据,操作前应确保有备份或确认数据不再需要
  • 大文件处理:对于大文件,ftruncate()可能需要一些时间,且会占用相应磁盘空间

ftruncate() vs file_put_contents()

特性 ftruncate() file_put_contents()
主要用途 调整文件大小 写入文件内容
文件处理 需要已打开的文件句柄 直接操作文件路径
性能 对于大文件调整大小更高效 适合写入内容或创建新文件
灵活性 可以精确控制文件大小 更关注文件内容而非大小
使用场景 日志轮转、文件清理、空间预分配 配置文件写入、数据导出、内容更新

相关函数

  • fopen() - 打开文件或URL
  • fclose() - 关闭一个已打开的文件指针
  • fseek() - 在文件指针中定位
  • ftell() - 返回文件指针读/写的位置
  • filesize() - 获取文件大小
  • file_put_contents() - 将字符串写入文件
  • unlink() - 删除文件
  • flock() - 便携式文件锁定

典型应用场景

日志文件管理

轮转日志文件,限制日志文件大小,清理旧日志数据。

数据库文件维护

清理数据库文件中的过期记录,压缩数据库文件。

临时文件清理

定期清理临时文件,释放磁盘空间。

文件空间预分配

预先分配文件空间,提高后续写入性能。