4. 文件指针与定位:tell()、seek() 方法,随机访问文件内容

文件读写,很多人以为只能从头读到尾。其实不然。

我刚开始学Python时,也以为文件就是个「水管」,水只能从一头流向另一头。后来遇到一个需求——要读取一个日志文件的最后100行,如果从头读到尾,几GB的文件得读到什么时候?

嗯,这时候就该文件指针出场了。

4.1 什么是文件指针?

文件指针,说白了就是一个「光标」。它标记着当前读写操作的位置。

你打开文件时,指针默认在开头(第0个字节)。每读一个字节,指针就往后挪一个字节。写操作也一样。

核心概念:文件指针 = 当前读写位置(字节偏移量)

我举个例子你就明白了。假设文件内容是 Hello, World!,共13个字节。打开文件后,指针在位置0。读5个字节,指针就跑到位置5了。

4.2 tell():查看当前指针位置

tell() 方法返回当前指针的字节偏移量。很简单,但很实用。

with open('demo.txt', 'w') as f:
    f.write('Hello, World!')

with open('demo.txt', 'r') as f:
    print(f'tell(): {f.tell()}')  # 0,刚打开
    data = f.read(5)
    print(f'读取了: {data}')
    print(f'tell(): {f.tell()}')  # 5,指针移动了

输出结果:

tell(): 0
读取了: Hello
tell(): 5

我在项目中常用 tell() 来调试。比如怀疑文件读取位置不对,打印一下指针位置,问题一目了然。

4.3 seek():移动指针到指定位置

seek() 才是重头戏。它让你可以「跳」到文件的任意位置。

基本语法:

f.seek(offset, whence)
参数 说明
offset 偏移量(字节数),正数向后,负数向前
whence 参考位置:0=文件开头,1=当前位置,2=文件末尾

小技巧:whence 参数默认是0,也就是从文件开头算起。这是最常用的方式。

来看几个典型用法:

with open('demo.txt', 'r') as f:
    # 跳到第6个字节(从0开始数)
    f.seek(6)
    print(f.read(5))  # 输出: World

    # 回到开头
    f.seek(0)
    print(f.read(5))  # 输出: Hello

    # 跳到末尾前5个字节
    f.seek(-5, 2)
    print(f.read())   # 输出: orld!

4.4 三种定位模式详解

我建议你记住这三种模式,工作中经常用到。

whence值 模式 说明 示例
0 SEEK_SET 从文件开头算起 f.seek(10, 0) → 跳到第10字节
1 SEEK_CUR 从当前位置算起 f.seek(5, 1) → 向后跳5字节
2 SEEK_END 从文件末尾算起 f.seek(-10, 2) → 跳到末尾前10字节

注意:文本模式下('r'),seek() 只能从文件开头(whence=0)定位。如果你需要从当前位置或末尾跳转,请使用二进制模式('rb')。

我曾经在这个坑里摔过一跤——用文本模式打开一个UTF-8文件,想从末尾往前跳,结果直接报错。后来改成二进制模式才搞定。

4.5 实战:读取大文件的最后N行

这个需求太常见了。日志文件、监控数据,经常要看最后几行。

def tail(filepath, n=10):
    """读取文件最后n行"""
    with open(filepath, 'rb') as f:
        # 跳到文件末尾
        f.seek(0, 2)
        file_size = f.tell()
        
        # 从末尾往前读
        buffer = bytearray()
        pos = file_size
        lines = []
        
        while pos > 0 and len(lines) < n:
            # 一次读一块(比如1024字节)
            chunk_size = min(1024, pos)
            pos -= chunk_size
            f.seek(pos)
            chunk = f.read(chunk_size)
            
            # 把新读的内容加到buffer前面
            buffer = bytearray(chunk) + buffer
            
            # 按换行符分割
            lines = buffer.decode('utf-8').splitlines()
        
        # 返回最后n行
        return lines[-n:]

这个函数我写过不下十次。每次处理日志文件,直接调它就行。

4.6 随机访问:跳着读文件

有时候你不需要顺序读取。比如一个结构化的二进制文件,每条记录固定长度100字节,你想读第5条记录。

RECORD_SIZE = 100

with open('records.dat', 'rb') as f:
    # 读第5条记录(索引从0开始)
    record_index = 5
    f.seek(record_index * RECORD_SIZE)
    record = f.read(RECORD_SIZE)
    print(f'第{record_index}条记录: {record}')

你想想看,如果文件有100万条记录,顺序读到第5条得花多少时间?用 seek() 直接跳过去,几乎不花时间。

4.7 知识体系图

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

文件指针与定位核心逻辑 文件内容(字节序列) | H | e | l | l | o | , | | W | o | r | l | d | ! | 指针 SEEK_SET (0) 从文件开头算起 f.seek(6, 0) SEEK_CUR (1) 从当前位置算起 f.seek(5, 1) SEEK_END (2) 从文件末尾算起 f.seek(-5, 2) tell() 查看当前指针位置 seek() 移动指针到指定位置 核心:文件指针 = 当前读写位置的字节偏移量

4.8 避坑指南

我踩过的坑,你最好别踩。

  • 文本模式下的seek限制:文本模式只支持 seek(0, 0) 和 seek(0, 2)。想自由跳转?用二进制模式。
  • 编码问题:UTF-8 中一个中文字符占3个字节。如果你用 seek(1) 跳到某个中文字符中间,读出来的就是乱码。
  • 文件末尾的seek:seek(0, 2) 跳到末尾后,再 read() 返回空。想读最后的内容?先 seek(-N, 2) 往回跳。

我的习惯:处理文本文件时,尽量用 seek(0) 回到开头。处理二进制文件时,随便跳。遇到大文件,优先用 seek() 而不是一行行读到内存里。

文件指针这个东西,说白了就是「指哪打哪」。用好了,读写效率能翻好几倍。下次遇到大文件处理,别傻乎乎从头读到尾了。

可可精品资料库