PHP ftell()函数

ftell()函数返回文件指针的当前位置,用于确定在文件中的读取或写入位置。

语法

int|false ftell ( resource $handle )

参数说明

参数 描述
handle 文件指针资源,通常由fopen()函数创建

返回值

  • 成功时返回文件指针的当前位置(从文件头开始的字节数)
  • 失败时返回false
  • 文件指针位置从0开始计数(即文件开头位置为0)

示例代码

示例1:基本文件指针位置跟踪

<?php
// 创建测试文件
$content = "Hello World!\nThis is a test file.\nPHP ftell() example.";
file_put_contents('test.txt', $content);

// 打开文件
$handle = fopen('test.txt', 'r');
if (!$handle) {
    die('无法打开文件');
}

echo "文件内容:<br>" . nl2br(htmlspecialchars($content)) . "<br><br>";

// 初始位置
echo "初始位置: " . ftell($handle) . "<br>";

// 读取前5个字节
fread($handle, 5);
echo "读取5字节后位置: " . ftell($handle) . "<br>";

// 读取一行
fgets($handle);
echo "读取一行后位置: " . ftell($handle) . "<br>";

// 使用fseek移动指针
fseek($handle, 20);
echo "使用fseek移动到位置20: " . ftell($handle) . "<br>";

// 读取剩余内容
fread($handle, 10);
echo "再读取10字节后位置: " . ftell($handle) . "<br>";

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

// 文件总大小
echo "文件总大小: " . filesize('test.txt') . " 字节<br>";

fclose($handle);
unlink('test.txt');
?>

示例2:文件读取进度跟踪

<?php
// 创建大文件用于演示进度跟踪
$filename = 'large_file.txt';
if (!file_exists($filename)) {
    $handle = fopen($filename, 'w');
    for ($i = 0; $i < 1000; $i++) {
        fwrite($handle, "Line $i: " . str_repeat('X', 100) . "\n");
    }
    fclose($handle);
}

// 读取文件并显示进度
function readWithProgress($filename, $chunkSize = 1024) {
    $handle = fopen($filename, 'r');
    if (!$handle) {
        return false;
    }

    $totalSize = filesize($filename);
    $bytesRead = 0;

    echo "开始读取文件: $filename<br>";
    echo "文件大小: " . number_format($totalSize) . " 字节<br>";
    echo "<div class='progress mb-3' style='height: 20px;'>";
    echo "<div class='progress-bar' id='progress-bar' role='progressbar' style='width: 0%' aria-valuenow='0' aria-valuemin='0' aria-valuemax='100'></div>";
    echo "</div>";

    while (!feof($handle)) {
        $chunk = fread($handle, $chunkSize);
        $bytesRead = ftell($handle);

        // 计算进度百分比
        $progress = ($totalSize > 0) ? round(($bytesRead / $totalSize) * 100) : 0;

        // 显示进度(在实际应用中,可以通过AJAX更新进度条)
        echo "<script>document.getElementById('progress-bar').style.width = '$progress%'; document.getElementById('progress-bar').innerHTML = '$progress%';</script>";

        // 刷新输出缓冲区
        flush();

        // 模拟处理延迟
        usleep(10000);
    }

    echo "<br>读取完成!总读取字节数: " . number_format($bytesRead) . "<br>";

    fclose($handle);
    return true;
}

// 使用示例
readWithProgress($filename, 2048);

// 清理文件(可选)
// unlink($filename);
?>

示例3:断点续传实现

<?php
// 断点续传下载类
class ResumeDownload {
    private $filename;
    private $fileSize;

    public function __construct($filename) {
        $this->filename = $filename;
        if (!file_exists($filename)) {
            throw new Exception("文件不存在: $filename");
        }
        $this->fileSize = filesize($filename);
    }

    /**
     * 处理下载请求
     */
    public function download($downloadName = null) {
        if ($downloadName === null) {
            $downloadName = basename($this->filename);
        }

        // 获取已下载的字节数(断点位置)
        $start = 0;
        if (isset($_SERVER['HTTP_RANGE'])) {
            if (preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches)) {
                $start = (int)$matches[1];
                $end = isset($matches[2]) ? (int)$matches[2] : $this->fileSize - 1;

                if ($start < 0 || $start >= $this->fileSize || $end < $start) {
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
                    return false;
                }
            }
        }

        // 打开文件
        $handle = fopen($this->filename, 'rb');
        if (!$handle) {
            throw new Exception("无法打开文件");
        }

        // 设置HTTP头部
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . rawurlencode($downloadName) . '"');

        if ($start > 0) {
            // 断点续传
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: bytes ' . $start . '-' . ($this->fileSize - 1) . '/' . $this->fileSize);
            header('Content-Length: ' . ($this->fileSize - $start));

            // 移动到断点位置
            fseek($handle, $start);
        } else {
            // 全新下载
            header('Content-Length: ' . $this->fileSize);
        }

        header('Accept-Ranges: bytes');

        // 使用ftell跟踪当前位置
        $chunkSize = 8192; // 8KB
        $currentPosition = $start;

        while (!feof($handle) && $currentPosition < $this->fileSize) {
            // 检查连接是否仍然有效
            if (connection_status() != 0) {
                break;
            }

            $bytesToRead = min($chunkSize, $this->fileSize - $currentPosition);
            $chunk = fread($handle, $bytesToRead);
            echo $chunk;
            flush();

            // 更新当前位置
            $currentPosition = ftell($handle);

            // 可以在这里记录断点位置到数据库或文件
            // $this->saveResumePoint($currentPosition);
        }

        fclose($handle);
        return true;
    }

    /**
     * 获取文件下载进度(模拟)
     */
    public function getDownloadProgress($sessionId) {
        // 在实际应用中,可以从数据库或session中获取已下载的字节数
        $resumeFile = "resume_{$sessionId}.txt";
        if (file_exists($resumeFile)) {
            $downloaded = (int)file_get_contents($resumeFile);
            return [
                'downloaded' => $downloaded,
                'total' => $this->fileSize,
                'percentage' => $this->fileSize > 0 ? round(($downloaded / $this->fileSize) * 100, 2) : 0
            ];
        }

        return [
            'downloaded' => 0,
            'total' => $this->fileSize,
            'percentage' => 0
        ];
    }
}

// 使用示例
$filename = 'large_file.zip'; // 假设有一个大文件

// 模拟下载请求
if (isset($_GET['download'])) {
    try {
        $downloader = new ResumeDownload($filename);
        $downloader->download();
        exit;
    } catch (Exception $e) {
        echo "下载错误: " . $e->getMessage();
    }
}

// 显示下载链接和进度
echo "<h5>断点续传下载示例:</h5>";
echo "<a href='?download=1' class='btn btn-primary'>下载大文件</a><br><br>";

// 模拟显示下载进度
try {
    $downloader = new ResumeDownload($filename);
    $progress = $downloader->getDownloadProgress('test_session');
    echo "下载进度: {$progress['percentage']}%<br>";
    echo "已下载: " . number_format($progress['downloaded']) . " / " . number_format($progress['total']) . " 字节<br>";

    echo "<div class='progress mt-2' style='height: 20px;'>";
    echo "<div class='progress-bar' role='progressbar' style='width: {$progress['percentage']}%'>{$progress['percentage']}%</div>";
    echo "</div>";
} catch (Exception $e) {
    echo "文件信息获取失败: " . $e->getMessage();
}
?>

示例4:文件处理工具类

<?php
class FilePositionTracker {
    private $handle;
    private $filename;
    private $positions = []; // 记录关键位置

    public function __construct($filename, $mode = 'r') {
        $this->filename = $filename;
        $this->handle = fopen($filename, $mode);

        if (!$this->handle) {
            throw new Exception("无法打开文件: {$filename}");
        }

        // 记录文件开头位置
        $this->positions['start'] = ftell($this->handle);
    }

    /**
     * 获取当前位置
     */
    public function getPosition() {
        return ftell($this->handle);
    }

    /**
     * 标记当前位置
     */
    public function mark($name) {
        $this->positions[$name] = $this->getPosition();
        return $this;
    }

    /**
     * 返回到标记的位置
     */
    public function returnTo($name) {
        if (isset($this->positions[$name])) {
            return fseek($this->handle, $this->positions[$name]);
        }
        return false;
    }

    /**
     * 获取所有标记的位置
     */
    public function getMarks() {
        return $this->positions;
    }

    /**
     * 读取从当前位置到下一个标记的内容
     */
    public function readToNextMark($markName) {
        $start = $this->getPosition();

        if (!isset($this->positions[$markName])) {
            throw new Exception("标记不存在: {$markName}");
        }

        $end = $this->positions[$markName];

        if ($end <= $start) {
            return ''; // 标记在当前位置之前
        }

        $length = $end - $start;
        return fread($this->handle, $length);
    }

    /**
     * 在文件中搜索字符串并标记位置
     */
    public function searchAndMark($search, $markName) {
        $current = $this->getPosition();
        $found = false;

        while (!feof($this->handle)) {
            $line = fgets($this->handle);
            if (strpos($line, $search) !== false) {
                // 找到字符串,记录当前位置
                $this->mark($markName);
                $found = true;
                break;
            }
        }

        // 恢复原始位置
        fseek($this->handle, $current);

        return $found;
    }

    /**
     * 获取文件大小
     */
    public function getFileSize() {
        $current = $this->getPosition();
        fseek($this->handle, 0, SEEK_END);
        $size = ftell($this->handle);
        fseek($this->handle, $current);
        return $size;
    }

    /**
     * 计算剩余字节数
     */
    public function getRemainingBytes() {
        $current = $this->getPosition();
        fseek($this->handle, 0, SEEK_END);
        $end = ftell($this->handle);
        fseek($this->handle, $current);
        return $end - $current;
    }

    /**
     * 关闭文件
     */
    public function close() {
        if ($this->handle) {
            fclose($this->handle);
            $this->handle = null;
        }
    }

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

// 使用示例
try {
    // 创建测试文件
    $testContent = "=== Section 1 ===\nContent of section 1.\n=== Section 2 ===\nContent of section 2.\n=== Section 3 ===\nContent of section 3.\n";
    file_put_contents('sections.txt', $testContent);

    // 创建跟踪器
    $tracker = new FilePositionTracker('sections.txt', 'r');

    echo "<h5>文件位置跟踪示例:</h5>";

    // 搜索并标记各个部分
    $tracker->searchAndMark('=== Section 1 ===', 'section1_start');
    $tracker->searchAndMark('=== Section 2 ===', 'section2_start');
    $tracker->searchAndMark('=== Section 3 ===', 'section3_start');

    // 获取文件大小
    echo "文件大小: " . $tracker->getFileSize() . " 字节<br>";

    // 返回到第一个标记并读取内容
    $tracker->returnTo('section1_start');
    fgets($tracker->handle); // 跳过标记行
    $section1 = $tracker->readToNextMark('section2_start');
    echo "Section 1 内容: " . htmlspecialchars(trim($section1)) . "<br><br>";

    // 返回到第二个标记
    $tracker->returnTo('section2_start');
    fgets($tracker->handle);
    $section2 = $tracker->readToNextMark('section3_start');
    echo "Section 2 内容: " . htmlspecialchars(trim($section2)) . "<br><br>";

    // 获取所有标记位置
    $marks = $tracker->getMarks();
    echo "<h6>标记位置:</h6>";
    echo "<pre>" . print_r($marks, true) . "</pre>";

    // 计算剩余字节
    echo "剩余字节数: " . $tracker->getRemainingBytes() . "<br>";

    $tracker->close();

    // 清理
    unlink('sections.txt');

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

注意事项

重要提示:
  • 文件模式:某些文件打开模式(如追加模式'a')会影响ftell()的返回值
  • 二进制模式:在Windows系统上处理二进制文件时,需要使用二进制模式(如'rb')
  • 网络流:对于网络流(HTTP、FTP等),ftell()可能不被支持或返回不准确的值
  • 大文件支持:对于大于2GB的文件,ftell()在32位系统上可能返回错误的值
  • 性能影响:频繁调用ftell()可能影响性能,特别是在远程文件系统上
  • 错误处理:始终检查ftell()的返回值,失败时返回false
  • 与fseek配合:ftell()常与fseek()配合使用,实现文件指针的精确定位
  • UTF-8编码:处理UTF-8文本时,注意多字节字符可能影响字节位置的计算
  • 文件锁定:在多进程环境中,ftell()获取的位置可能被其他进程改变

ftell() vs fseek()

特性 ftell() fseek()
功能 获取当前位置 设置当前位置
参数 只需要文件句柄 需要文件句柄、偏移量和参考位置
返回值 当前位置(字节数)或false 0成功,-1失败
使用场景 跟踪读取进度、断点续传 随机访问、跳转到特定位置
配合使用 通常与fseek()配合使用 通常与ftell()配合使用

相关函数

  • fseek() - 在文件指针中定位
  • rewind() - 倒回文件指针的位置
  • feof() - 测试文件指针是否到达文件末尾
  • fopen() - 打开文件或URL
  • fread() - 读取文件(二进制安全)
  • fwrite() - 写入文件
  • filesize() - 获取文件大小
  • stat() - 获取文件信息

典型应用场景

断点续传

记录文件下载/上传的断点位置,实现中断后继续传输。

进度跟踪

显示文件读取、处理或传输的实时进度。

文件解析

在解析复杂文件格式时,跟踪当前位置以便错误恢复。

日志分析

记录上次读取日志文件的位置,实现增量读取。