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 知识体系图
下面这张图帮你理清文件指针的核心逻辑:
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() 而不是一行行读到内存里。
文件指针这个东西,说白了就是「指哪打哪」。用好了,读写效率能翻好几倍。下次遇到大文件处理,别傻乎乎从头读到尾了。