Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
1 | 作者:李晓辉 |
- Python 系列(一) 从零开始认识 Python,入门其实没那么难
- Python 系列(二) 学 Python 的第一步,把开发环境搭建好
- Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
- Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
- Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
- Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
- Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
- Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
- Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
- Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
- Python 系列(十一) list、tuple、set、dict,到底该怎么选?
- Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
- Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
- Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
- Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
- Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
- Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
- Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
- Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
- Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
- Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
- Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
- Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
- Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
- Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
- Python 系列(二十六) Python 模块入门,从 import 到模块复用
- Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
- Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
- Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
- Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
- Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
- Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
- Python 系列(三十三) 什么是类和对象?零基础理解 Python class
- Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
- Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
- Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
- Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
- Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
- Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
- Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
- Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十三) 别只会处理异常!学会自定义异常,让程序更容易维护
- Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
- Python 系列(四十五) 使用 sys 模块处理标准输入与输出
- Python 系列(四十六) 使用 open() 读取和写入文件
- Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
- Python 系列(四十八) 搞懂 bytes、bytearray 和二进制读写
- Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
- Python 系列(五十) 掌握 os 和 os.path,实现文件与目录自动化管理
- Python 系列(五十一) 掌握列表推导式,轻松实现列表生成与数据筛选
- Python 系列(五十二) 掌握字典推导式,让字典创建更简单
- Python 系列(五十三) 从零理解生成器,轻松处理大量数据
- Python 系列(五十五) 使用 zip() 同时遍历多个集合
前面我们学习了如何使用 Python 打开文件、读取文本、写入内容,以及处理二进制文件。
在这些操作中,我们经常会使用 read() 方法读取文件内容。例如,读取一个日志文件,查看里面记录的服务器运行情况。
但是,如果文件内容比较长,我们可能会遇到这样的需求:
- 文件已经读取了一部分,想回到开头重新读取,怎么办?
- 只想从文件的某个位置开始读取,而不是每次都从头开始,怎么办?
- 读取文件的过程中,想知道当前已经读到了什么位置,怎么办?
如果每次都重新打开文件,再从头读取,不仅操作繁琐,也可能做很多不必要的工作。
Python 提供了两个非常实用的方法:seek() 和 tell()。
seek():移动文件的读取或写入位置。tell():查看当前文件位置。
学会这两个方法,我们就可以更灵活地控制文件读写过程。
理解文件指针:Python 是从哪里开始读取的?
在学习 seek() 和 tell() 之前,我们需要先理解一个概念:文件指针。
这里的文件指针,并不是我们用鼠标操作的光标,而是 Python 在处理文件时,用来记录当前读写位置的一个概念。
可以把它想象成一本书里的书签。
假设一本书有 100 页,我们先读完第 10 页。如果想继续往下读,就从第 11 页开始;如果想重新阅读第 1 页,就需要把书签移回开头。
Python 读取文件时,也有类似的机制。
flowchart TD
A["打开文件"] --> B["文件位置:开头"]
B --> C["读取一部分内容"]
C --> D["文件位置向后移动"]
D --> E{"接下来想做什么?"}
E -->|继续读取| F["从当前位置继续"]
E -->|重新读取| G["使用 seek() 返回指定位置"]
E -->|查看位置| H["使用 tell() 获取当前位置"]例如,我们创建一个名为 demo.txt 的文件,内容如下:
1 | Hello |
注意,文件中的每一行末尾通常还包含一个换行符。换行符也是文件内容的一部分,会占用相应的位置。
接下来,我们编写一个简单的程序,看看 Python 是如何记录文件读取位置的。
1 | with open("demo.txt", "r", encoding="utf-8") as f: |
运行结果:
1 | 0 |
为什么会得到这些结果?
- 第一次调用
tell(),返回0,表示当前位于文件开头。 f.read(5)读取 5 个字符,也就是Hello。- 读取完成后,文件位置向后移动,因此再次调用
tell(),返回5。
这里的 read(5) 表示读取最多 5 个字符,而不是读取 5 行。
在这个示例中,文件内容是纯英文字符,所以读取 5 个字符后,位置也是 5。
对于文本文件,尤其是包含中文等多字节字符的文件,tell() 返回的位置值不能简单地理解为已经读取了多少个字符。它返回的是一个可用于恢复文本流位置的定位值。
理解这一点之后,我们就可以开始学习两个方法了。
使用 tell() 查看当前文件位置
tell() 的作用很简单:告诉我们当前文件的读写位置在哪里。
基本语法如下:
1 | position = f.tell() |
其中:
f是通过open()打开的文件对象。tell()用于获取当前位置。position是我们自己定义的变量,用于保存返回的位置值。
我们继续使用前面的 demo.txt 文件。
1 | with open("demo.txt", "r", encoding="utf-8") as f: |
运行结果:
1 | 初始位置:0 |
为什么第二次读取后的位置是 12?
第一次读取了 Hello,共 5 个字符。
接着,read(6) 从当前位置继续读取 6 个字符。文件中 Hello 后面有一个换行符,再接着是 Python。
因此,这 6 个字符是:
1 |
|
其中包含 1 个换行符和 5 个英文字母?这里需要特别注意:Python 本身有 6 个英文字母,所以 read(6) 实际读取的是换行符以及 Pytho,而不是完整的 Python。
为了避免把换行符的影响和位置计算混在一起,我们将示例调整为更直观的内容。
创建 simple.txt,写入以下内容,注意不要在内容末尾额外添加换行符:
1 | ABCDEFGHIJ |
运行下面的程序:
1 | with open("simple.txt", "r", encoding="utf-8") as f: |
运行结果:
1 | 初始位置:0 |
这个例子就非常直观了。
- 开始时,位置是
0。 - 读取
ABC后,位置移动到3。 - 再读取
DE后,位置移动到5。
需要注意,tell() 只负责查看位置,不会主动读取文件内容,也不会改变当前的位置。
使用 seek() 移动文件位置
了解如何查看文件位置之后,我们再来看如何主动改变它。
seek() 可以让我们把文件位置移动到指定位置,而不必一直从当前位置向后读取。
基本语法如下:
1 | f.seek(offset) |
其中,offset 表示定位时使用的位置偏移量。它的具体含义取决于定位方式以及文件打开模式。
对于普通文本文件,最常见的用法是:
1 | f.seek(0) |
它会把文件位置恢复到开头。
我们还是使用 simple.txt 文件,内容为:
1 | ABCDEFGHIJ |
编写以下程序:
1 | with open("simple.txt", "r", encoding="utf-8") as f: |
运行结果:
1 | ABC |
第一次调用 read(3),读取了 ABC。
接着,f.seek(0) 将文件位置重新移动到开头。
所以第二次调用 read(3) 时,又读取到了 ABC。
整个过程可以这样理解:
flowchart TD
A["文件开头:位置 0"] --> B["读取 ABC"]
B --> C["当前位置:3"]
C --> D["seek(0)"]
D --> E["返回文件开头"]
E --> F["再次读取 ABC"]这就是 seek() 最常见的用途:让文件从指定位置重新开始读取。
从文件的指定位置开始读取
除了返回文件开头,我们还可以把文件位置移动到其他位置。
继续使用内容为 ABCDEFGHIJ 的 simple.txt。
1 | with open("simple.txt", "r", encoding="utf-8") as f: |
运行结果:
1 | FGHIJ |
为什么从 F 开始?
因为在这个只包含英文字符的示例中,每个字符都占据一个简单的位置:
1 | 字符: A B C D E F G H I J |
调用 f.seek(5) 后,文件位置移动到 F 所在的位置。
之后调用 read(),Python 就会从 F 开始,一直读取到文件末尾。
需要注意:这里的位置从 0 开始计算,而不是从 1 开始计算。
对于这个示例,seek(0) 对应 A,seek(5) 对应 F。
不过,这种按数字直接定位的方式主要适用于简单的文本示例。真实文本文件可能包含中文、换行符或其他编码内容,因此不能总是把偏移量当作字符数量使用。
使用 seek() 和 tell() 记录并恢复位置
有时候,我们并不知道接下来需要返回文件的哪个位置。
这时可以先用 tell() 记录当前位置,之后再用 seek() 返回这个位置。
例如,我们读取一段内容后,想暂时去其他位置查看数据,随后再回到之前的位置继续处理。
1 | with open("simple.txt", "r", encoding="utf-8") as f: |
运行结果:
1 | ABC |
我们按照执行顺序来理解。
首先,f.read(3) 读取 ABC,当前位置变为 3。
接着,position = f.tell() 将当前位置保存到变量 position 中。
然后,f.seek(0) 返回文件开头,读取 AB。
此时文件位置已经发生变化,但变量 position 中保存的仍然是之前记录的值 3。
最后,执行 f.seek(position),Python 就会恢复到之前记录的位置,从 D 开始继续读取。
这里需要记住一个关键点:变量 position 保存的是之前获取的位置值,不会随着文件位置变化而自动改变。
这种先记录位置、再恢复位置的方式,在需要反复检查文件内容时非常有用。
在 Linux 运维中,什么时候会用到它们?
学习 Python 不只是为了记住几个方法,更重要的是知道它们能解决什么问题。
假设我们需要编写一个程序,读取服务器生成的日志文件。
日志文件可能包含大量记录,例如:
1 | 2026-10-21 10:00:01 INFO nginx started |
如果只是读取文件开头的部分内容,可以使用:
1 | with open("app.log", "r", encoding="utf-8") as f: |
如果希望再次检查文件开头的内容,就可以使用:
1 | with open("app.log", "r", encoding="utf-8") as f: |
这段程序演示了如何重新读取文件开头。
不过,在真正的日志分析工作中,如果需要查找 ERROR、统计异常数量或逐行处理日志,通常会使用 for 循环、字符串方法或其他更适合的工具,而不是频繁移动文件位置。
seek() 和 tell() 更适合需要明确控制文件位置的场景,例如:
- 读取文件的一部分后重新读取。
- 记录当前位置,稍后恢复。
- 在支持定位的文件中读取指定位置附近的数据。
- 在二进制文件处理中,根据文件结构定位特定数据。
需要注意,seek() 并不意味着所有文件都能像数据库一样高效地随机访问。对于普通磁盘文件,定位通常比较直接;对于管道、终端等不可随意定位的流,定位操作可能不受支持。
文本文件和二进制文件的定位有什么区别?
前面我们分别学习了文本文件和二进制文件。两种文件都可以使用 seek() 和 tell(),但它们的定位规则有所不同。
文本文件
通常通过下面的方式打开:
1 | with open("simple.txt", "r", encoding="utf-8") as f: |
文本模式下,tell() 返回的位置值可能是一个用于恢复文件位置的特殊定位值。不要总是把它当作字符数量,也不要随意对它进行加减运算。
最稳妥的常见用法是保存 tell() 的返回值,再将这个值传给 seek(),恢复之前的位置。
二进制文件
使用 rb 模式打开:
1 | with open("data.bin", "rb") as f: |
二进制模式下,读取操作直接处理字节。tell() 返回的是从文件开头计算的字节位置,seek() 也可以按照字节偏移量定位。
例如,文件内容为:
1 | ABCDEFGHIJ |
使用二进制模式读取:
1 | with open("data.bin", "rb") as f: |
运行结果:
1 | b'ABC' |
这里的 b'ABC' 和 b'FG' 是 bytes 类型的数据显示形式。
因为每个英文字母在这个示例中占一个字节,所以从位置 5 开始读取两个字节,得到 FG。
对于包含中文的 UTF-8 文件,一个汉字通常占用多个字节。因此,二进制模式下不能把字节位置直接当作字符位置。
使用 seek() 时需要注意什么?
学习这两个方法时,有几个容易出错的地方值得特别注意。
不要把位置编号和字符编号混为一谈。
在纯英文示例中,字符位置与字节位置看起来一致。但对于中文文本,二者可能不同。
文本模式下,不要随意计算定位值。
如果需要返回之前的位置,推荐先保存 tell() 的返回值,再传给 seek():
1 | position = f.tell() |
不要默认所有文本定位值都可以直接进行加减运算。
不是所有文件流都支持任意定位。
普通磁盘文件通常支持定位,但管道、终端或其他非定位流可能不支持。遇到不支持定位的情况时,调用 seek() 可能会引发 OSError。
使用 with open() 管理文件。
推荐始终使用 with 语句打开文件。代码块执行结束后,Python 会自动关闭文件,减少资源管理方面的问题。
总结
今天我们学习了两个控制文件位置的方法:
| 方法 | 作用 | 示例 |
|---|---|---|
tell() | 查看当前文件位置 | f.tell() |
seek() | 移动到指定文件位置 | f.seek(0) |
记住最重要的区别:
tell()负责查看位置,不会主动移动位置。seek()负责移动位置,让后续读取或写入从指定位置开始。tell()获取的位置值可以保存下来,在适当的条件下通过seek()恢复。- 文本模式和二进制模式的定位规则不同,需要根据文件类型正确使用。
对于初学者来说,可以先熟练掌握 f.seek(0),理解它如何让文件重新从开头读取,再逐步练习记录和恢复文件位置。
写在最后
到这里,我们已经学习了 Python 文件操作中的打开文件、文本读写、二进制读写,以及使用 seek() 和 tell() 控制文件位置。
下一篇,我们将学习 Python 的 os 和 os.path 模块,了解如何使用 Python 查看目录、判断文件是否存在、获取文件信息,以及创建和管理目录。
这些操作会让我们的 Python 程序逐渐具备自动处理文件和目录的能力,为后续编写 Linux 运维自动化脚本打下基础。
