文件指针与定位:tell()、seek()、文件指针的移动与重置
文件读写这件事,很多人觉得就是打开、读取、关闭三步走。但实际项目中,你常常需要「跳着读」或者「回头改」。这时候,文件指针就是你的方向盘。
我刚开始写文件处理代码时,也踩过不少坑。有一次处理一个超大日志文件,我想跳过前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,那得等到什么时候?
知识体系图
下面这张图帮你理清文件指针操作的核心逻辑:
常见问题与避坑
- 读完了还想读? 用
seek(0, 0)重置指针,别重新打开文件。 - 中文乱码? 文本模式下 seek() 到非字节边界,会导致后续读取乱码。建议用二进制模式操作指针。
- 文件太大? 别用
read()一次性读完。用seek()定位到关键区域,只读你需要的那部分。 - 追加模式注意: 以 'a' 模式打开文件时,所有写入操作都在文件末尾,即使你用了
seek()移动指针也没用。这是操作系统规定的。
我的习惯:处理小文件(几MB以内)时,我直接读入内存操作。处理大文件时,我一定用 seek() 和 tell() 精确定位。这两种场景的代码写法完全不同,选对方法能省一半时间。
文件指针这东西,说白了就是「你在文件里的坐标」。学会用 tell() 看坐标,用 seek() 跳坐标,你的文件操作就能指哪打哪。