4. 文件指针与定位:tell()、seek()方法,文件指针的移动与重置

好,咱们继续聊文件操作。前面我们讲了怎么打开文件、怎么读写,但有个细节你可能没注意到——文件指针。

说白了,文件指针就像你在书里夹的书签。你读到哪一页,书签就停在哪儿。下次再打开,接着往下读。Python 里的文件对象,内部就维护着这样一个「书签」。每次读写,书签都会自动往后移动。

我刚开始学的时候,总觉得这东西很抽象。直到有一次写日志分析脚本,发现读出来的数据总是少了一半……嗯,后来才明白是文件指针跑偏了。

4.1 文件指针是什么?

当你用 open() 打开一个文件时,Python 会在内存里创建一个文件对象。这个对象里有个隐藏的计数器,记录着当前读写位置距离文件开头的字节数。

  • 刚打开文件时,指针在位置 0(文件开头)
  • 每读一个字节,指针就往后挪一个字节
  • 每写一个字节,指针也往后挪一个字节

你想想看,如果不知道指针在哪儿,你怎么知道读到哪了?

核心概念:文件指针 = 当前读写位置。它决定了下一次 read() 或 write() 从哪儿开始。

4.2 tell() —— 问问指针在哪儿

tell() 方法返回当前指针的位置(从文件开头算起的字节数)。

我个人习惯在调试文件操作时,到处打印一下指针位置。这样能清楚地看到读写过程发生了什么。

# 打开一个文件
with open('demo.txt', 'w', encoding='utf-8') as f:
    f.write('Hello, 世界!')

with open('demo.txt', 'r', encoding='utf-8') as f:
    print(f'tell() 初始位置:{f.tell()}')   # 0
    
    content = f.read(5)
    print(f'读取了:{content}')
    print(f'tell() 读取后位置:{f.tell()}')  # 5(因为 'Hello' 是5个字节)
    
    # 继续读
    rest = f.read()
    print(f'剩余内容:{rest}')
    print(f'tell() 读完位置:{f.tell()}')   # 文件末尾

注意:中文字符在 UTF-8 编码下占 3 个字节。所以「世界」两个字占了 6 个字节。如果你用 tell() 看到的位置和预期不一样,先检查编码。

小技巧:在循环读取大文件时,可以用 tell() 来监控读取进度。我曾经用它做过一个文件上传的进度条,效果还不错。

4.3 seek() —— 让指针跳来跳去

seek() 方法可以移动文件指针。它有两个参数:

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

说白了,seek(offset, whence) 就是告诉 Python:「从 whence 位置开始,移动 offset 个字节」。

with open('demo.txt', 'r', encoding='utf-8') as f:
    # 跳到第 7 个字节(从文件开头算起)
    f.seek(7, 0)
    print(f'当前位置:{f.tell()}')  # 7
    print(f'读取:{f.read()}')      # 世界!
    
    # 回到开头
    f.seek(0, 0)
    print(f'回到开头后读取:{f.read()}')  # Hello, 世界!

我在项目中遇到过一个问题:用 seek() 在文本文件中跳来跳去,结果读出来的中文全是乱码。为什么?因为 seek() 是按字节跳的,而中文字符是多字节的。你跳到某个字节的中间位置,自然就乱码了。

避坑指南:在文本模式下('r' 或 'w'),seek() 只能从文件开头(whence=0)移动。从当前位置或文件末尾移动,可能会报错。如果你需要自由跳转,请使用二进制模式('rb' 或 'wb')。

4.4 常用场景:重新读取文件

有时候你读了一遍文件,想再读一遍。最直接的办法是重新 open(),但用 seek(0) 更高效。

with open('config.txt', 'r', encoding='utf-8') as f:
    # 第一遍读
    first_pass = f.read()
    print(f'第一遍:{first_pass}')
    
    # 指针回到开头
    f.seek(0)
    
    # 第二遍读
    second_pass = f.read()
    print(f'第二遍:{second_pass}')

嗯,这里要注意:seek(0) 只是把指针移回开头,文件本身没有重新打开。所以效率比重新 open() 高得多。

4.5 知识体系图

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

文件指针操作核心流程 文件对象 内部维护指针位置 tell() 返回当前位置 seek() 移动指针位置 whence=0 从文件开头 whence=1 从当前位置 whence=2 从文件末尾 💡 记住:读写操作都会自动移动指针,seek() 让你手动控制

4.6 实战:文件指针的完整示例

最后,我给你一个完整的例子。把前面讲的东西串起来:

# 创建一个测试文件
with open('test.txt', 'w', encoding='utf-8') as f:
    f.write('0123456789\n')
    f.write('ABCDEFGHIJ\n')
    f.write('abcdefghij\n')

# 演示指针操作
with open('test.txt', 'r', encoding='utf-8') as f:
    # 读第一行
    line1 = f.readline()
    print(f'第一行:{line1.strip()}')  # 0123456789
    print(f'当前指针:{f.tell()}')     # 11(10个数字 + 1个换行符)
    
    # 跳到第 15 个字节
    f.seek(15)
    print(f'跳转后指针:{f.tell()}')   # 15
    print(f'读取:{f.read(5)}')        # CDEFG
    
    # 回到开头
    f.seek(0)
    print(f'回到开头后读取全部:{f.read()}')

输出结果:

第一行:0123456789
当前指针:11
跳转后指针:15
读取:CDEFG
回到开头后读取全部:0123456789
ABCDEFGHIJ
abcdefghij

看到了吧?指针就像个灵活的游标。你可以用它来精确定位,也可以用它来重新读取。掌握好 tell() 和 seek(),文件操作就灵活多了。

我的建议:刚开始用的时候,多打印几次 tell() 的值。亲眼看到指针在移动,比死记硬背效果好得多。我曾经在调试一个 CSV 解析器时,就是靠 tell() 定位到了数据错位的问题。

好了,文件指针这块就聊到这儿。记住三个关键点:tell() 问位置,seek() 跳位置,读写操作自动移位置。下次遇到文件读不全的问题,先检查一下指针在哪儿。