1. 项目概述:为什么我们需要关心文件大小?
在Python里处理文件,获取文件大小这个操作,听起来简单得不能再简单了,对吧?但就是这么个基础操作,在实际项目中却无处不在,而且不同的场景下,选择哪种方法还真有点讲究。我遇到过不少新手,甚至一些有经验的开发者,在处理文件时,要么是性能上吃了亏,要么是代码写得不够优雅,要么是在跨平台兼容性上栽了跟头。
想想这些场景:你写一个文件上传接口,需要在前端就限制用户上传文件的大小,或者在服务器端进行二次校验;你开发一个日志分析工具,需要监控日志文件的增长,在超过某个阈值时进行切割或告警;你写一个备份脚本,需要统计备份目录的总大小,以决定是否要清理旧数据;甚至,你在做自动化测试时,需要验证生成的文件是否符合预期的尺寸。所有这些,第一步都是:获取文件的大小。
Python提供了不止一种方法来完成这个任务,从经典的os.path.getsize,到更底层的os.stat,再到面向对象的pathlib,甚至还有直接读取文件对象的方式。每种方法背后都有其设计哲学和适用场景。这篇文章,我就结合自己十多年的踩坑经验,把这四种主流方法掰开揉碎了讲清楚,告诉你它们各自的原理、性能差异、使用时的“坑”,以及在不同情况下,我个人会怎么选。目标很简单:让你看完之后,不仅能写出能跑的代码,更能写出高效、健壮、优雅的代码。
2. 四种核心方法深度解析与横向对比
获取文件大小,本质上就是向操作系统询问一个文件的元数据(metadata)。在Unix/Linux和Windows系统中,文件大小是文件元数据(如inode或文件记录)中的一个标准字段。Python的几种方法,都是对这个系统调用的不同层次的封装。
为了有一个直观的认识,我们先创建一个测试文件,并快速浏览一下这四种方法的基本用法。假设我们有一个名为test_file.txt的文件。
# 快速示例:四种方法的基本形态 import os from pathlib import Path file_path = ‘test_file.txt’ # 方法1: os.path.getsize size1 = os.path.getsize(file_path) print(f“os.path.getsize: {size1} bytes”) # 方法2: os.stat stat_info = os.stat(file_path) size2 = stat_info.st_size print(f“os.stat: {size2} bytes”) # 方法3: pathlib.Path path_obj = Path(file_path) size3 = path_obj.stat().st_size # 方式3.1, 类似 os.stat size4 = path_obj.stat().st_size # 注意:pathlib.Path 没有直接的 getsize, 但 .stat() 是标准方式 # 更地道的 pathlib 写法是: size3_proper = path_obj.stat().st_size print(f“pathlib.Path.stat(): {size3_proper} bytes”) # 方法4: 通过文件对象 with open(file_path, ‘rb’) as f: f.seek(0, 2) # 将指针移动到文件末尾 size4 = f.tell() # 获取当前指针位置,即文件大小 print(f“File object seek/tell: {size4} bytes”)看起来都能得到结果,但内在的区别大了去了。下面我们逐一深入。
2.1 方法一:os.path.getsize – 简洁的“快捷方式”
os.path.getsize(path)可能是最广为人知的方法。它属于os.path模块,这个模块包含了很多用于处理路径名的函数。
原理与底层实现:在大多数Python实现(如CPython)中,os.path.getsize本质上是对os.stat的一个简单封装。你可以近似地把它理解为:
def getsize(path): return os.stat(path).st_size它内部调用了os.stat()函数,获取文件的完整状态信息,然后只返回其中的st_size字段。这是一个同步阻塞调用,会直接向操作系统发起请求。
使用示例与特点:
import os file_path = ‘/home/user/data/report.pdf’ try: file_size = os.path.getsize(file_path) print(f“文件大小为: {file_size} 字节”) print(f“约为: {file_size / 1024 / 1024:.2f} MB”) # 转换为MB except FileNotFoundError: print(“文件不存在!”) except OSError as e: print(f“访问文件时出错: {e}”)优点:
- 接口极其简洁:函数名即功能,一目了然,学习成本几乎为零。
- 直接返回目标值:你不需要从一堆元数据中手动提取
st_size。
缺点与注意事项:
- 功能单一:它只返回大小。如果你还需要文件的修改时间、访问权限等其他信息,你就得再调用一次
os.stat,造成重复的系统调用,影响性能。 - 错误处理:它可能抛出多种异常,如
FileNotFoundError(文件不存在)、PermissionError(权限不足)等,必须用try…except块妥善处理。 - 符号链接(Symbolic Link):这是一个关键点!
os.path.getsize跟随符号链接。也就是说,如果path是一个指向另一个文件的软链接,该方法返回的是目标文件的大小,而不是链接文件本身的大小(链接文件本身很小,只存储一个路径)。在某些需要处理链接本身的场景下,这可能不是你想要的行为。
实操心得:
os.path.getsize就像一把瑞士军刀里的开瓶器,干一件特定的事非常顺手。在快速脚本、一次性任务,或者你百分之百确定只需要文件大小且不关心符号链接的场景下,它是首选。但在严肃的、可能涉及性能或需要更多元数据的生产代码中,我通常会考虑其他方法。
2.2 方法二:os.stat – 强大而底层的“信息库”
os.stat(path)是更底层、更强大的工具。它返回一个os.stat_result对象,这个对象包含了文件或文件描述符的所有状态信息。
原理与信息深度:os.stat直接对应着操作系统底层的stat()系统调用(在Windows上是类似的机制)。这个调用会获取文件inode(或类似结构)中的所有信息。返回的os.stat_result对象是一个命名元组,包含多个属性:
| 属性 | 描述 | 常见用途 |
|---|---|---|
st_mode | 文件类型和权限位(mode bits) | 判断是否为文件、目录,检查读写权限 |
st_ino | inode 编号 | 文件系统内部标识,可用于硬链接判断 |
st_dev | 设备ID | 标识文件所在的设备 |
st_nlink | 硬链接数量 | |
st_uid | 所有者的用户ID | 权限管理 |
st_gid | 所有者的组ID | 权限管理 |
st_size | 文件大小,以字节为单位 | 我们关注的核心 |
st_atime | 最近访问时间(秒) | 缓存策略,日志分析 |
st_mtime | 最近修改时间(秒) | 判断文件是否更新,触发构建 |
st_ctime | 元数据修改时间(秒) | 在Unix上通常是inode变更时间 |
使用示例与进阶技巧:
import os import time file_path = ‘/var/log/syslog’ try: stat_info = os.stat(file_path) # 获取大小 size = stat_info.st_size print(f“大小: {size} bytes”) # 同时获取其他有用信息 print(f“最后修改时间: {time.ctime(stat_info.st_mtime)}”) print(f“文件模式: {oct(stat_info.st_mode)}”) # 以八进制显示权限 print(f“是否为文件: {os.path.isfile(file_path)}”) # 结合其他判断 # 判断文件是否过大(例如超过100MB) if size > 100 * 1024 * 1024: print(“警告:文件超过100MB,建议进行切割或清理。”) except FileNotFoundError: print(“日志文件不存在,可能尚未创建或已被轮转。”) except PermissionError: print(“权限不足,无法读取系统日志。请尝试使用sudo或检查用户组。”)处理符号链接:os.stat()vsos.lstat()这是os.stat相关的一个重要知识点。默认的os.stat()会跟随符号链接,就像os.path.getsize一样。如果你需要获取符号链接本身的信息(其大小通常只是存储的路径字符串的长度),你需要使用os.lstat()。
import os # 创建一个指向文件的符号链接 os.symlink(‘/path/to/target_file’, ‘my_link’) info_stat = os.stat(‘my_link’) # 返回的是 target_file 的信息 info_lstat = os.lstat(‘my_link’) # 返回的是 my_link 本身的信息 print(f“os.stat size (target): {info_stat.st_size}“) print(f“os.lstat size (link itself): {info_lstat.st_size}“) # 这个值很小优点:
- 信息全面:一次调用,获取所有元数据,避免为获取不同信息而多次调用系统函数,效率更高。
- 功能强大:通过
st_mode可以判断文件类型,通过时间戳可以实现复杂的文件监控逻辑。 - 灵活性高:可以选择使用
stat()或lstat()来处理符号链接。
缺点与注意事项:
- 接口稍显复杂:需要从结果对象中提取
st_size,多了一步操作。 - 需要记忆属性名:虽然属性名是标准的,但新手可能需要查阅文档。
- 时间戳精度:注意
st_atime,st_mtime,st_ctime在不同操作系统上的精度和更新时机可能不同(例如,某些系统为了性能可能不会实时更新st_atime)。
实操心得:
os.stat是我在生产环境代码中最常用的方法。尤其是在需要同时获取文件大小和修改时间来判断文件是否变化的场景(比如实现一个简单的文件监视器),或者需要检查文件权限时。一次系统调用拿到全部信息,从性能和维护性上看,通常都优于多次调用单一功能的函数。记住这个原则:如果除了大小,你很可能还需要其他信息,那就直接用os.stat。
2.3 方法三:pathlib.Path – 面向对象的“现代派”
pathlib模块从Python 3.4开始成为标准库的一部分,它提供了一种面向对象的、更符合直觉的文件系统路径操作方法。对于从其他现代语言(如C#、Java)转过来的开发者来说,pathlib的API设计会感觉非常亲切。
面向对象的设计哲学:pathlib的核心是Path类。你创建一个Path对象,这个对象就代表了一条路径。然后,你可以调用这个对象的各种方法来操作路径和文件。这种风格将路径从普通的字符串提升为“一等公民”,可以链式调用方法,代码更清晰。
获取文件大小的方式:Path对象本身没有getsize方法。要获取大小,你需要先获取文件的stat信息,然后再取大小。这其实和os.stat的流程一致,但写法更面向对象。
使用示例与链式调用:
from pathlib import Path # 创建Path对象 log_file = Path(‘/var/log/app.log’) backup_dir = Path(‘/backups’) # 检查是否存在并获取大小 if log_file.is_file(): # 使用对象方法判断是否为文件 stat_result = log_file.stat() # 获取stat对象 size = stat_result.st_size print(f“{log_file} 的大小是 {size} 字节”) # 更简洁的链式写法(在一行内完成) size_chain = log_file.stat().st_size print(f“(链式调用)大小: {size_chain} bytes”) # 同时利用其他信息 if stat_result.st_mtime > (time.time() - 3600): # 一小时内有修改 print(“日志文件最近非常活跃!”) # 结合glob模式,计算目录下所有.txt文件的总大小 total_size = 0 for txt_file in backup_dir.glob(‘*.txt’): if txt_file.is_file(): total_size += txt_file.stat().st_size print(f“备份目录下所有txt文件总大小: {total_size} bytes”)处理符号链接:Path对象同样区分stat()和lstat()。
Path(‘symlink’).stat(): 跟随链接。Path(‘symlink’).lstat(): 不跟随链接,获取链接本身信息。
优点:
- API设计优雅:面向对象,方法链式调用,代码可读性高,更符合现代编程习惯。
- 路径操作安全方便:拼接路径使用
/操作符,避免了字符串拼接可能带来的错误(如忘记分隔符)。# 传统方式 dir_path = ‘/home/user’ file_name = ‘data.txt’ full_path = os.path.join(dir_path, file_name) # pathlib方式 dir_path = Path(‘/home/user’) full_path = dir_path / ‘data.txt’ # 更直观 - 跨平台性更好:
pathlib内部自动处理了Windows和Unix路径分隔符的差异(\vs/),让你可以用统一的/风格写代码。 - 功能集成度高:
Path对象集成了很多常用操作,如exists(),is_file(),is_dir(),mkdir(),rename()等,无需再导入os或os.path中的多个函数。
缺点与注意事项:
- 性能微开销:由于是面向对象的封装,相比直接调用
os.stat,可能会有极其微小的性能开销,但在99.9%的应用场景中完全可以忽略不计。不要过早优化,代码的清晰度和可维护性更重要。 - Python版本要求:需要Python 3.4+。虽然现在新项目基本都满足,但维护一些非常古老的系统时需要注意。
- 习惯转变:对于习惯了
os.path的开发者,需要一点时间来适应新的范式。
实操心得:对于所有新的Python项目,我强烈推荐使用
pathlib。它不仅仅是获取文件大小,而是整体上让文件系统操作变得更安全、更愉悦。当你需要做的不仅仅是获取大小,而是涉及路径拼接、文件检查、遍历目录等一系列操作时,pathlib的优势会非常明显。把它看成是文件系统操作的“现代化改造”。
2.4 方法四:通过文件对象(seek/tell)– 特定场景的“偏方”
这种方法比较“原始”:打开文件,将文件指针移动到末尾,然后查询指针的位置,这个位置就是文件的大小。
原理剖析:open()函数返回一个文件对象。文件对象内部维护着一个指向文件中某个位置的“指针”。f.seek(offset, whence)用于移动这个指针。whence=2表示“相对于文件末尾”。f.seek(0, 2)就是将指针移动到距离文件末尾0字节的位置,也就是文件末尾。f.tell()则返回指针当前在文件中的位置(字节偏移量)。因此,在文件末尾调用tell(),得到的就是文件的总大小。
使用示例与细节:
def get_size_by_seek(filepath): “”” 通过 seek/tell 方法获取文件大小。 注意:此方法会以读取模式打开文件。 “”” try: with open(filepath, ‘rb’) as f: # 必须用二进制模式 ‘rb’ f.seek(0, 2) # 移动到文件末尾 size = f.tell() return size except FileNotFoundError: return None except IOError as e: print(f“无法打开文件 {filepath}: {e}“) return None size = get_size_by_seek(‘large_data.bin’) if size is not None: print(f“文件大小: {size}“)为什么必须用二进制模式(‘rb’)?在文本模式(‘r’)下,由于存在字符编码转换(如UTF-8的BOM头、换行符转换等),文件指针的位置可能与实际的字节数不对应。seek()和tell()在文本模式下的行为是未定义的(platform-dependent)。二进制模式(‘rb’)保证了我们操作的是原始的字节流,seek和tell的数值就是精确的字节偏移量。
优点:
- 原理直观:对于理解文件操作的基础(文件指针)有帮助。
- 在某些极端情况下有用:如果你已经因为其他原因(比如要读取内容)打开了文件,那么顺便用这个方法获取大小可以避免额外的
stat系统调用。但这种情况很少见。
缺点与重大注意事项:
- 性能最差:它需要打开文件,这是一个相对昂贵的I/O操作,比直接查询元数据的
stat调用要慢得多,尤其是对于网络文件系统或慢速存储设备。 - 资源占用:它占用了一个文件描述符。虽然在
with语句中会自动关闭,但在高并发或需要处理大量文件时,无谓地打开文件会增加系统负担。 - 可能引发副作用:打开文件可能会触发文件锁(在某些操作系统和文件系统上),或者改变文件的“最近访问时间”(
st_atime),这有时是你不想看到的。 - 并非通用:对于某些特殊文件(如管道、设备文件
/dev/null),这种方法可能无法工作或返回无意义的结果。
实操心得:这是一个“反模式”,除非有非常特殊的理由,否则不要用。我几乎从未在生产代码中使用这种方法来专门获取文件大小。它的主要价值在于教学,用于解释文件指针的概念。如果你看到别人的代码里用这个来获取文件大小,可以考虑将其重构为使用
os.stat或pathlib。记住一个黄金法则:获取元数据,就用元数据接口(stat);读取内容,才去打开文件。
3. 性能实测与场景化选型指南
纸上谈兵不如实际跑一跑。我们来设计一个简单的性能测试,看看这几种方法在效率上到底有多大差异。同时,结合不同的应用场景,给出我的选型建议。
3.1 性能基准测试
我们创建一个中等大小的文件(比如10MB),然后分别用四种方法获取其大小,重复多次(如1000次),计算平均耗时。
import os import timeit from pathlib import Path def create_test_file(size_mb=10): “””创建一个指定大小的测试文件“”” file_name = ‘perf_test.dat’ with open(file_name, ‘wb’) as f: f.write(os.urandom(size_mb * 1024 * 1024)) # 写入随机字节 return file_name def cleanup_test_file(file_name): “””清理测试文件“”” if os.path.exists(file_name): os.remove(file_name) def method_os_getsize(file_name): return os.path.getsize(file_name) def method_os_stat(file_name): return os.stat(file_name).st_size def method_pathlib(file_name): return Path(file_name).stat().st_size def method_seek_tell(file_name): with open(file_name, ‘rb’) as f: f.seek(0, 2) return f.tell() if __name__ == ‘__main__’: test_file = create_test_file(10) # 10MB文件 number = 1000 # 执行次数 print(f“性能测试 (文件: {test_file}, 循环 {number} 次)“) print(“-” * 40) for func, name in [(method_os_getsize, ‘os.path.getsize’), (method_os_stat, ‘os.stat’), (method_pathlib, ‘pathlib.Path.stat’), (method_seek_tell, ‘seek/tell’)]: # 使用 timeit 进行精确计时,避免第一次打开文件等开销的影响 timer = timeit.Timer(lambda: func(test_file)) # 执行一次预热,然后计算多次执行的平均时间 elapsed = timer.timeit(number=number) / number print(f“{name:25} 平均耗时: {elapsed * 1e6:8.2f} 微秒”) cleanup_test_file(test_file)(注意:实际耗时因硬件、操作系统、Python版本而异,以下为相对趋势分析)
在我的测试环境(SSD, Python 3.9)下,结果趋势通常是:
os.stat和os.path.getsize最快且速度几乎相同(因为后者是前者的封装)。pathlib.Path.stat()稍慢一点点,差距在微秒级别,可忽略不计。seek/tell方法显著慢一个数量级以上,因为它涉及真正的文件I/O操作。
这个测试印证了之前的理论分析:基于stat的系统调用是最高效的方式。
3.2 不同场景下的最佳实践选择
知道了原理和性能,我们来看看具体怎么选。没有绝对最好的,只有最适合当前场景的。
场景一:写快速脚本或临时任务,只需要文件大小
- 推荐:
os.path.getsize - 理由:代码最简短,意图最清晰。你不需要导入
pathlib,也不需要处理stat_result对象。对于一次性脚本、命令行工具或简单的自动化任务,这是最省脑细胞的选择。 - 示例:写一个脚本清理
/tmp目录下超过100MB的旧文件。
场景二:开发生产级应用程序,需要文件大小及其他元数据(如修改时间、权限)
- 推荐:
os.stat或pathlib.Path.stat() - 理由:一次调用,获取全部信息,性能最优。这是最专业、最通用的做法。
- 如果项目是新的,且广泛使用面向对象风格,首选
pathlib。file_path.stat().st_size的写法清晰且易于集成到更大的Path对象操作链中。 - 如果项目是旧的,或者你更习惯过程式编程,或者你只需要在某个函数内部做一次简单的检查,使用
os.stat完全没问题。
- 如果项目是新的,且广泛使用面向对象风格,首选
- 示例:实现一个文件变化监控服务(Watchdog),需要持续检查文件的
st_mtime和st_size。
场景三:需要处理符号链接本身
- 推荐:
os.lstat()或Path.lstat() - 理由:这是唯一能正确获取符号链接自身信息(其大小是路径字符串长度)的方法。
os.path.getsize和默认的stat()都会跟随链接。 - 示例:写一个工具扫描目录,统计其中所有实体(包括链接文件)的磁盘占用情况。
场景四:你已经以二进制模式打开了文件,并且“顺便”需要知道大小
- 可以考虑:
seek/tell - 理由:虽然这种情况极少,但如果你已经因为读取内容而打开了文件(例如在解析文件头结构),那么用
f.seek(0,2); size = f.tell()获取大小,然后f.seek(0)回到开头,可以避免一次额外的stat调用。但要非常小心,确保文件是以二进制模式(‘rb’)打开的。 - 示例:解析一个自定义格式的二进制文件,文件头没有存储大小信息,你需要先知道总长度才能计算数据区偏移。
场景五:计算整个目录树的总大小
- 推荐:
pathlib结合递归 - 理由:
pathlib的Path.rglob()或Path.glob()非常适合进行递归文件遍历,代码非常简洁。 - 示例:
from pathlib import Path def get_dir_size(directory: Path) -> int: total_size = 0 for file_path in directory.rglob(‘*’): # rglob 递归遍历所有文件 if file_path.is_file() and not file_path.is_symlink(): # 跳过符号链接 try: total_size += file_path.stat().st_size except OSError: # 忽略权限错误等问题 pass return total_size project_size = get_dir_size(Path(‘./my_project’)) print(f“项目目录总大小: {project_size / 1e6:.2f} MB”)
绝对不应该使用seek/tell的场景:
- 专门为了获取文件大小而写一个函数。
- 在高频循环中获取大量文件的大小。
- 处理可能被锁定的文件(如某些数据库文件)。
- 处理特殊文件(如设备文件)。
4. 常见问题、坑点排查与高级技巧
即使知道了方法,在实际编码中还是会遇到各种问题。下面是我总结的一些典型“坑”和解决技巧。
4.1 文件不存在与权限问题
这是最常遇到的运行时错误。
问题表现:
FileNotFoundError: 路径错误,或文件确实不存在。PermissionError: 当前用户对文件或父目录没有读取权限。
解决方案:永远进行防御性编程。使用try…except块捕获异常,并提供友好的错误处理或回退方案。
import os from pathlib import Path def safe_get_size(path): “”” 安全地获取文件大小,处理常见异常。 返回文件大小(字节),如果出错则返回None。 “”” # 方法1: 使用 os.path.getsize 或 os.stat try: return os.path.getsize(path) except FileNotFoundError: print(f“错误:文件 ‘{path}’ 不存在。”) return None except PermissionError: print(f“错误:没有权限读取文件 ‘{path}’。”) return None except OSError as e: # 捕获其他操作系统错误 print(f“访问文件 ‘{path}’ 时发生未知错误: {e}“) return None # 方法2: 使用 pathlib (更推荐,因为Path对象方法本身会抛出异常) # try: # return Path(path).stat().st_size # except FileNotFoundError: # … # except PermissionError: # … # 使用示例 size = safe_get_size(‘/etc/shadow’) # 普通用户无权限 if size is None: print(“无法获取文件大小,已记录错误。”) else: print(f“文件大小: {size}“)4.2 处理符号链接与硬链接
这是一个容易混淆的概念,对获取大小有直接影响。
- 符号链接(软链接):是一个独立的文件,内容是指向目标文件的路径。
os.path.getsize(link)返回的是目标文件的大小。os.lstat(link).st_size返回的是链接文件本身(即存储的路径字符串)的大小,通常很小。 - 硬链接:是同一个inode的多个目录条目。
os.path.getsize(hardlink)和os.stat(hardlink).st_size返回的都是原始文件的大小,因为所有硬链接都指向同一个数据块。st_nlink属性可以告诉你有多少个硬链接指向这个inode。
技巧:如何判断并正确处理?
import os from pathlib import Path def analyze_link(filepath): path = Path(filepath) if path.is_symlink(): print(f“{filepath} 是一个符号链接。”) link_target = path.readlink() print(f“它指向: {link_target}“) print(f“链接本身大小: {path.lstat().st_size} bytes”) print(f“目标文件大小: {path.stat().st_size} bytes”) elif os.stat(filepath).st_nlink > 1: print(f“{filepath} 有 {os.stat(filepath).st_nlink} 个硬链接。”) print(f“文件大小: {os.path.getsize(filepath)} bytes”) else: print(f“{filepath} 是普通文件。”) print(f“文件大小: {os.path.getsize(filepath)} bytes”)4.3 大文件与整数溢出(历史问题)
在早期32位系统上,st_size是一个有符号的32位整数,最大只能表示2GB左右的文件。超过这个大小会导致溢出或错误。但在现代的64位系统和Python 3中,st_size通常是64位整数(Python的int类型是任意精度的),所以理论上可以处理任意大小的文件。
但是,需要注意一个潜在问题:某些旧的或嵌入式文件系统(如FAT32)本身有文件大小限制(如4GB)。当文件达到这个限制时,操作系统会阻止它继续增长,所以stat调用本身不会出错,但你无法创建或写入超过该限制的文件。
实践建议:对于处理潜在的大文件(如视频、数据库、科学数据集),你的代码逻辑应该能处理很大的int值。在进行单位转换时(如字节转GB),注意使用浮点数除法以避免整数除法截断。
size_bytes = some_file.stat().st_size # 正确的单位转换 size_gb = size_bytes / (1024 ** 3) # 使用浮点数除法 print(f“Size: {size_gb:.2f} GB”) # 如果需要整数GB(向下取整) size_gb_int = size_bytes // (1024 ** 3) print(f“Size: {size_gb_int} GB (approx)”)4.4 跨平台兼容性考量
虽然Python尽力提供一致的API,但不同操作系统(主要是Windows和Unix-like系统)在文件系统细节上仍有差异。
- 路径分隔符:这是
pathlib解决得最好的问题。使用Path对象和/操作符,可以完全忽略这个差异。 st_ctime的含义:- 在Unix/Linux/macOS上,
st_ctime表示“inode变更时间”,包括权限、所有者等元数据的修改。 - 在Windows上,
st_ctime表示“创建时间”。 - 因此,如果你写的代码需要依赖
st_ctime并在多平台运行,务必明确你的需求,并考虑使用st_mtime(修改时间)作为跨平台的标准,或者根据平台做条件判断。
- 在Unix/Linux/macOS上,
- 文件权限(
st_mode):Windows的权限模型与Unix不同。在Windows上,os.chmod()的功能有限,stat返回的权限位可能也不像Unix那样丰富。如果你的代码涉及复杂的权限判断,需要为Windows编写备用逻辑。
4.5 性能优化:批量获取与缓存
当需要获取成千上万个文件的大小时(例如扫描整个磁盘),即使是高效的stat调用,累积起来也可能成为瓶颈。
优化策略1:使用os.scandir()(Python 3.5+)os.scandir()比os.listdir()更快,因为它可以在一次系统调用中返回更多的文件信息(在某些操作系统上包括大小),避免了为每个文件单独调用stat。
import os def get_total_size_fast(directory): total_size = 0 with os.scandir(directory) as it: for entry in it: if entry.is_file(): # entry.stat() 在这里可能更快,因为部分信息已预取 total_size += entry.stat().st_size elif entry.is_dir(): # 递归处理子目录(注意:对于软链接,is_dir()会跟随链接) total_size += get_total_size_fast(entry.path) return total_size优化策略2:缓存stat结果如果文件大小不常变化,而你频繁需要读取,可以考虑缓存结果。
from functools import lru_cache from pathlib import Path import time @lru_cache(maxsize=1024) def get_cached_size(path: Path) -> int: “””带缓存的获取文件大小,适用于文件不常变的场景。“”” # 可以在这里加入更复杂的逻辑,比如检查文件是否被修改过 # 如果文件被修改了,应该使缓存失效(这里简化处理) return path.stat().st_size # 使用 p = Path(‘config.json’) size1 = get_cached_size(p) # 第一次调用,会执行 stat time.sleep(1) size2 = get_cached_size(p) # 第二次调用,直接返回缓存结果,极快 print(size1 == size2) # True注意:缓存策略需要根据文件的实际变化频率来设计。对于频繁写入的日志文件,缓存就没什么用,甚至会导致数据不一致。
4.6 一个综合案例:实现一个健壮的文件大小获取工具
最后,我们把所有知识点串起来,写一个考虑相对周全的实用函数。
import os from pathlib import Path from typing import Union, Optional def get_file_size( filepath: Union[str, Path], follow_symlinks: bool = True, human_readable: bool = False ) -> Optional[Union[int, str]]: “”” 安全、灵活地获取文件大小。 参数: filepath: 文件路径,可以是字符串或Path对象。 follow_symlinks: 如果为True且路径是符号链接,则返回目标文件大小; 如果为False,则返回链接本身的大小。 human_readable: 如果为True,返回带单位(B, KB, MB, GB)的字符串; 如果为False,返回字节数(整数)。 返回: 成功时返回大小(整数或字符串),失败时返回None。 “”” path = Path(filepath) if isinstance(filepath, str) else filepath try: if follow_symlinks: stat_func = path.stat else: stat_func = path.lstat size_in_bytes = stat_func().st_size except FileNotFoundError: print(f“[错误] 路径不存在: {path}“) return None except PermissionError: print(f“[错误] 权限不足,无法访问: {path}“) return None except OSError as e: print(f“[错误] 访问文件时发生系统错误 ({path}): {e}“) return None if not human_readable: return size_in_bytes else: # 转换为人类可读格式 for unit in [‘B’, ‘KB’, ‘MB’, ‘GB’, ‘TB’]: if size_in_bytes < 1024.0: return f“{size_in_bytes:.2f} {unit}“ size_in_bytes /= 1024.0 return f“{size_in_bytes:.2f} PB” # 理论上可能更大 # 使用示例 if __name__ == ‘__main__’: # 示例1: 获取字节数 size = get_file_size(‘/etc/passwd’, follow_symlinks=True, human_readable=False) print(f“Size in bytes: {size}“) # 示例2: 获取人类可读格式,不跟随符号链接 size_str = get_file_size(‘/usr/bin/python3’, follow_symlinks=False, human_readable=True) print(f“Size (link itself): {size_str}“) # 示例3: 处理不存在的文件 size_none = get_file_size(‘/non/existent/file’) if size_none is None: print(“正确处理了错误情况。”)这个函数集成了错误处理、符号链接控制、输出格式选择,并且同时兼容str和Path输入,算是一个比较生产就绪的工具函数了。你可以根据实际需求,继续扩展它,比如加入对目录大小的递归计算、支持exclude_patterns参数等。