2. 文件指针与定位

文件读写这件事,很多人觉得就是打开、读取、关闭三步走。但实际项目中,你常常需要「跳着读」或者「回头改」。这时候,文件指针就是你的方向盘。

我刚开始写文件处理代码时,也踩过不少坑。有一次处理一个超大日志文件,我想跳过前1000行去读关键信息,结果傻乎乎地一行行读完再扔掉……后来同事告诉我:用 seek() 啊! 嗯,从那以后我就再也没犯过这种低级错误。

什么是文件指针?

说白了,文件指针就是一个「光标」。它标记着你当前在文件中的位置。你读一个字节,指针就往后移一个字节。你写一个字节,指针也往后移一个字节。

你可以把它想象成你在读一本纸质书时,手指头按着的那一行。手指头就是指针,书页就是文件。

核心概念:文件指针不是指针变量,而是操作系统维护的一个内部偏移量。它从0开始计数,单位是字节。

tell():看看你在哪

tell() 方法返回当前文件指针的位置。返回值是一个整数,表示从文件开头到当前位置的字节数。

with open('example.txt', 'r', encoding='utf-8') as f:
    print(f.tell())   # 输出:0,刚打开文件,指针在开头
    content = f.read(5)
    print(f.tell())   # 输出:5,读了5个字节,指针移到第5个字节后面

我个人习惯在调试文件操作时,频繁打印 tell() 的值。这就像开车时看后视镜——随时确认自己的位置。

小技巧:在文本模式下,tell() 返回的是字节偏移量,不是字符数。因为中文字符在UTF-8编码下占3个字节,所以读一个中文字符,指针会移动3个位置。

seek():想去哪就去哪

seek(offset, whence) 方法用于移动文件指针。它接受两个参数:

  • offset:偏移量,正数向后移,负数向前移
  • whence:参考位置,有三个可选值
whence 值 含义 示例
0 从文件开头算起(默认) f.seek(10, 0) → 跳到第10个字节
1 从当前位置算起 f.seek(5, 1) → 从当前位置往后移5个字节
2 从文件末尾算起 f.seek(-10, 2) → 跳到文件末尾前10个字节
with open('data.bin', 'rb') as f:
    # 跳到第100个字节
    f.seek(100, 0)
    print(f.tell())   # 100

    # 再往后跳50个字节
    f.seek(50, 1)
    print(f.tell())   # 150

    # 跳到文件末尾前20个字节
    f.seek(-20, 2)
    print(f.tell())   # 文件总字节数 - 20

注意:在文本模式下('r' 而非 'rb'),seek() 的行为有限制。你只能从文件开头(whence=0)进行定位,而且 offset 必须是 tell() 返回的值,或者为0。否则会抛出异常。我建议:处理二进制文件时用 seek(),处理文本文件时尽量用 readline() 循环。

文件指针的移动与重置

文件指针的移动,说白了就是三种操作:前进、后退、归零。

前进:读取数据

每次 read()、readline()、readlines() 都会让指针自动前进。这是最自然的移动方式。

后退:重新读取

有时候你读了一部分数据,发现需要回头再看一遍。这时候用 seek() 回到之前的位置。

with open('config.ini', 'r') as f:
    first_line = f.readline()
    print('第一行:', first_line)

    # 回到开头,重新读取
    f.seek(0, 0)
    whole_file = f.read()
    print('整个文件:', whole_file)

重置:回到起点

最常用的重置操作就是 f.seek(0, 0)。把指针拉回文件开头,相当于重新开始。

避坑指南:我曾经在循环中忘记重置文件指针,导致第二次循环直接读到空内容。代码看起来没问题,但就是不出数据。排查了半天才发现——指针已经跑到文件末尾了,再读当然啥也没有。

实战场景:日志文件尾部读取

我记得有一次需要监控一个实时写入的日志文件。文件一直在增长,我只想看最新的100行。怎么做?

def tail_log(file_path, lines=100):
    with open(file_path, 'rb') as f:
        # 跳到文件末尾
        f.seek(0, 2)
        file_size = f.tell()

        # 从末尾往前读,每次读一个块
        block_size = 1024
        data = b''
        while len(data) < lines * 200:  # 粗略估算每行200字节
            if file_size < block_size:
                f.seek(0, 0)
                data = f.read() + data
                break
            file_size -= block_size
            f.seek(file_size, 0)
            data = f.read(block_size) + data

        # 按行分割,取最后lines行
        lines_list = data.decode('utf-8', errors='ignore').splitlines()
        return lines_list[-lines:]

# 使用
last_100 = tail_log('app.log', 100)
for line in last_100:
    print(line)

这个函数的核心思路就是:先跳到文件末尾,然后一块一块往回读。你想想看,如果从头读到尾,文件有1GB,那得等到什么时候?

知识体系图

下面这张图帮你理清文件指针操作的核心逻辑:

文件指针操作核心逻辑 文件内容(字节序列) 文件指针 tell() 查看当前指针位置 返回字节偏移量 seek() 移动指针到指定位置 offset + whence 重置 seek(0, 0) 回到开头 重新开始读取 whence 参数:0=文件开头 1=当前位置 2=文件末尾 ⚠ 文本模式下 seek() 有限制,二进制模式更灵活

常见问题与避坑

  • 读完了还想读? 用 seek(0, 0) 重置指针,别重新打开文件。
  • 中文乱码? 文本模式下 seek() 到非字节边界,会导致后续读取乱码。建议用二进制模式操作指针。
  • 文件太大? 别用 read() 一次性读完。用 seek() 定位到关键区域,只读你需要的那部分。
  • 追加模式注意: 以 'a' 模式打开文件时,所有写入操作都在文件末尾,即使你用了 seek() 移动指针也没用。这是操作系统规定的。

我的习惯:处理小文件(几MB以内)时,我直接读入内存操作。处理大文件时,我一定用 seek() 和 tell() 精确定位。这两种场景的代码写法完全不同,选对方法能省一半时间。

文件指针这东西,说白了就是「你在文件里的坐标」。学会用 tell() 看坐标,用 seek() 跳坐标,你的文件操作就能指哪打哪。