Python os.path 完全指南
本文档面向零基础新手,目标是让你真正理解:
os.path.dirname()是什么,怎么用os.path.abspath()是什么,怎么用__file__是什么魔法变量os.path.dirname(os.path.abspath(__file__))为什么是 Python 项目的”万能定位公式”os.path其他常用函数(join、exists、basename、split等)- Windows / Linux / macOS 路径差异与跨平台写法
pathlib:更现代的路径写法(Python 3.4+)
配有大量可运行示例,全部从最基础讲起。
第一部分:路径是什么?为什么要处理路径?
1.1 文件路径的基本概念
文件路径就是"文件在磁盘上的地址",分两种:
绝对路径(Absolute Path):
从磁盘根目录开始的完整地址
Windows:C:UsersAdministratorDesktopprojectmain.py
Linux: /home/user/project/main.py
macOS: /Users/user/project/main.py
相对路径(Relative Path):
相对于"当前工作目录"的地址
./config.json → 当前目录下的 config.json
../data/input.csv → 上一级目录的 data 文件夹里的 input.csv
data/output.txt → 当前目录的 data 子目录下
为什么新手经常遇到”找不到文件”的错误?
# 假设你的项目结构是:
# project/
# ├── main.py
# └── data/
# └── input.txt
# main.py 里这样写:
with open('data/input.txt') as f:
content = f.read()
# 问题来了:这里的 'data/input.txt' 是相对路径
# 它相对的是你"运行 python 命令时所在的目录"
# 而不是 main.py 文件所在的目录!
# 在 project/ 下运行 python main.py ✅ 成功
# 在 /home/user/ 下运行 python project/main.py ❌ 报错!
# 因为此时当前目录是 /home/user/,找不到 data/input.txt
这就是为什么要用 os.path.dirname(os.path.abspath(__file__))——它总能找到脚本自己的位置!
1.2 三个核心角色
os.path.abspath(__file__) → 当前脚本的绝对路径(含文件名)
os.path.dirname(os.path.abspath(__file__)) → 当前脚本所在的目录
os.path.join(当前目录, '相对子路径') → 拼接成完整路径
举个例子:
脚本位置:C:projectsrcutils.py
__file__ = 'utils.py'(或相对路径)
os.path.abspath(__file__) = 'C:projectsrcutils.py'
os.path.dirname(os.path.abspath(...)) = 'C:projectsrc'
然后就可以:
os.path.join('C:projectsrc', '..', 'data', 'input.txt')
= 'C:projectdatainput.txt'
第二部分:os.path.abspath()
2.1 基本用法
os.path.abspath(path) 将任意路径转换成绝对路径。
import os
# ===== 基础示例 =====
# 当前工作目录假设是 C:UsersAdminDesktop
# 相对路径 → 绝对路径
print(os.path.abspath('test.txt'))
# C:UsersAdminDesktoptest.txt
print(os.path.abspath('./data/input.csv'))
# C:UsersAdminDesktopdatainput.csv
print(os.path.abspath('../other_folder'))
# C:UsersAdminother_folder (上一级目录)
print(os.path.abspath('.'))
# C:UsersAdminDesktop (当前目录)
print(os.path.abspath('..'))
# C:UsersAdmin (上一级目录)
# 已经是绝对路径 → 原样返回(只是规范化格式)
print(os.path.abspath('C:/Users/Admin/Desktop'))
# C:UsersAdminDesktop (正斜杠变反斜杠,Windows 上规范化)
2.2 abspath 的工作原理
import os
# abspath 的等价实现(理解原理):
# os.path.abspath(path) ≈ os.path.normpath(os.path.join(os.getcwd(), path))
cwd = os.getcwd() # 获取当前工作目录
path = '../data'
# 方式1:abspath(推荐)
result1 = os.path.abspath(path)
# 方式2:手动实现(效果相同)
result2 = os.path.normpath(os.path.join(cwd, path))
print(result1)
print(result2)
print(result1 == result2) # True
重要:abspath 基于的是”当前工作目录”,不是脚本所在目录!
import os
# 假设当前工作目录 = C:UsersAdmin(从这里启动 Python)
# 脚本位置 = C:projectsrcutils.py
# ❌ 这个结果取决于你从哪里运行脚本,不可靠
result = os.path.abspath('data/input.txt')
# 结果:C:UsersAdmindatainput.txt
# 而不是:C:projectsrcdatainput.txt ← 你想要的
# ✅ 正确做法:先获取脚本所在目录,再拼接
script_dir = os.path.dirname(os.path.abspath(__file__))
result = os.path.join(script_dir, 'data', 'input.txt')
# 结果:C:projectsrcdatainput.txt ← 无论从哪里运行都对!
第三部分:os.path.dirname()
3.1 基本用法
os.path.dirname(path) 返回路径中的目录部分(去掉最后一个文件名或目录名)。
import os
# ===== Windows 路径示例 =====
print(os.path.dirname(r'C:UsersAdminDesktopmain.py'))
# C:UsersAdminDesktop
print(os.path.dirname(r'C:UsersAdminDesktop'))
# C:UsersAdmin
print(os.path.dirname(r'C:Users'))
# C:
print(os.path.dirname(r'C:\'))
# C:
# ===== Linux/macOS 路径示例 =====
print(os.path.dirname('/home/user/project/main.py'))
# /home/user/project
print(os.path.dirname('/home/user/project'))
# /home/user
print(os.path.dirname('/home'))
# /
print(os.path.dirname('/'))
# / (根目录的父目录还是根目录)
# ===== 只有文件名(无目录)=====
print(os.path.dirname('main.py'))
# ''(空字符串!)
print(repr(os.path.dirname('main.py')))
# ''
# ===== 结尾有斜杠的目录 =====
print(os.path.dirname('/home/user/project/'))
# /home/user/project (去掉了结尾的斜杠)
3.2 dirname 的直觉理解
把路径想象成一棵树,dirname 就是"找父目录":
C:projectsrcutils.py
↑ ↑ ↑
根 目录 文件名
os.path.dirname('C:\project\src\utils.py')
= 'C:\project\src' ← 去掉了最后的 utils.py
再来一次:
os.path.dirname('C:\project\src')
= 'C:\project' ← 去掉了最后的 src
再来一次:
os.path.dirname('C:\project')
= 'C:\' ← 去掉了 project,只剩根目录
3.3 多级目录向上走
import os
# 从脚本路径逐级向上
path = r'C:projectsrcutilshelper.py'
print("原始路径:", path)
print("上一级: ", os.path.dirname(path))
print("上两级: ", os.path.dirname(os.path.dirname(path)))
print("上三级: ", os.path.dirname(os.path.dirname(os.path.dirname(path))))
print("上四级: ", os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(path)))))
# 输出:
# 原始路径: C:projectsrcutilshelper.py
# 上一级: C:projectsrcutils
# 上两级: C:projectsrc
# 上三级: C:project
# 上四级: C:
嵌套太多?用循环代替:
import os
def go_up(path, levels=1):
"""从路径向上走 levels 级"""
result = path
for _ in range(levels):
result = os.path.dirname(result)
return result
path = r'C:projectsrcutilshelper.py'
print(go_up(path, 1)) # C:projectsrcutils
print(go_up(path, 2)) # C:projectsrc
print(go_up(path, 3)) # C:project
print(go_up(path, 4)) # C:
第四部分:file 魔法变量
4.1 file 是什么?
__file__ 是 Python 自动设置的模块级别内置变量,代表当前脚本文件的路径。
# 在某个脚本文件(如 utils.py)中:
print(__file__)
# 可能的输出(取决于运行方式):
# utils.py ← 在脚本所在目录运行
# src/utils.py ← 从上级目录运行
# /home/user/project/src/utils.py ← 某些环境的完整路径
关键点:__file__ 的值不稳定,可能是相对路径或绝对路径!
运行方式不同,__file__ 不同:
场景1:在 C:project 下运行 python srcutils.py
→ __file__ = 'src\utils.py'(相对路径)
场景2:用 IDE(如 PyCharm、VS Code)运行
→ __file__ = 'C:\project\src\utils.py'(绝对路径)
场景3:在 C:projectsrc 下运行 python utils.py
→ __file__ = 'utils.py'(只有文件名)
这就是为什么要先用 abspath() 把它转成绝对路径!
4.2 file 在不同情况下的行为
# ===== 普通脚本文件中 =====
# 文件:C:projectmain.py
print(__file__) # 可能是 main.py 或 C:projectmain.py
# ===== 作为模块被导入时 =====
# 文件:C:projectutils.py,被 main.py 导入
import utils
print(utils.__file__) # C:projectutils.py(导入时通常是绝对路径)
# ===== 在交互式解释器中 =====
# 在 Python REPL (>>>提示符) 中:
# print(__file__) → NameError: name '__file__' is not defined
# 交互式环境没有 __file__!
# ===== 在 Jupyter Notebook 中 =====
# print(__file__) → 也会报错,因为 Notebook 不是普通脚本文件
第五部分:核心组合——dirname(abspath(file))
5.1 这行代码的完整解析
import os
# 分步理解每一层:
# 假设脚本是 C:projectsrcapp.py
# 第1步:__file__ 获取当前文件路径(可能不稳定)
step1 = __file__
print(f"__file__ = {step1!r}")
# 可能是:'app.py' 或 'src\app.py' 或 'C:\project\src\app.py'
# 第2步:abspath() 将其转为绝对路径(稳定!)
step2 = os.path.abspath(__file__)
print(f"abspath(__file__) = {step2!r}")
# 一定是:'C:\project\src\app.py'
# 第3步:dirname() 去掉文件名,只保留目录
step3 = os.path.dirname(os.path.abspath(__file__))
print(f"dirname(abspath()) = {step3!r}")
# 一定是:'C:\project\src'
# 结论:step3 就是"无论怎么运行,都能正确找到脚本所在目录"
这是 Python 项目中最常见的路径定位方式,务必牢记!
5.2 实际项目应用——构建跨平台路径
import os
# ==================================================
# 典型项目结构:
# myproject/
# ├── src/
# │ ├── app.py ← 当前文件
# │ └── utils.py
# ├── config/
# │ └── settings.json
# ├── data/
# │ └── input.csv
# └── logs/
# └── app.log
# ==================================================
# 当前脚本所在目录(myproject/src/)
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
# 项目根目录(myproject/)
PROJECT_DIR = os.path.dirname(THIS_DIR)
# 常用目录的绝对路径
CONFIG_DIR = os.path.join(PROJECT_DIR, 'config')
DATA_DIR = os.path.join(PROJECT_DIR, 'data')
LOG_DIR = os.path.join(PROJECT_DIR, 'logs')
# 具体文件的绝对路径
SETTINGS = os.path.join(CONFIG_DIR, 'settings.json')
INPUT_CSV = os.path.join(DATA_DIR, 'input.csv')
LOG_FILE = os.path.join(LOG_DIR, 'app.log')
# 无论从哪里运行脚本,这些路径永远正确!
print(f"脚本目录:{THIS_DIR}")
print(f"项目根目录:{PROJECT_DIR}")
print(f"配置文件:{SETTINGS}")
print(f"数据文件:{INPUT_CSV}")
print(f"日志文件:{LOG_FILE}")
# 使用这些路径读写文件(永远不会因为"从哪里运行"而出错)
import json
with open(SETTINGS, 'r', encoding='utf-8') as f:
settings = json.load(f)
print(f"读取配置:{settings}")
5.3 完整的项目路径配置模板
"""
paths.py —— 项目路径配置文件
放在项目根目录,在其他模块中导入使用
"""
import os
# ==================== 目录定位 ====================
# 本文件(paths.py)所在目录 = 项目根目录
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
# 各功能目录
SRC_DIR = os.path.join(ROOT_DIR, 'src')
CONFIG_DIR = os.path.join(ROOT_DIR, 'config')
DATA_DIR = os.path.join(ROOT_DIR, 'data')
OUTPUT_DIR = os.path.join(ROOT_DIR, 'output')
LOG_DIR = os.path.join(ROOT_DIR, 'logs')
TEST_DIR = os.path.join(ROOT_DIR, 'tests')
# ==================== 常用文件 ====================
CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.yaml')
DATABASE_URL = os.path.join(DATA_DIR, 'database.db')
LOG_FILE = os.path.join(LOG_DIR, 'app.log')
# ==================== 自动创建必要目录 ====================
for d in [OUTPUT_DIR, LOG_DIR]:
os.makedirs(d, exist_ok=True) # exist_ok=True:已存在时不报错
# ==================== 在其他模块中使用 ====================
# from paths import ROOT_DIR, DATA_DIR, LOG_FILE
# with open(LOG_FILE, 'a') as f:
# f.write('程序启动n')
5.4 为什么不直接用 os.getcwd()?
import os
# os.getcwd():获取"当前工作目录"(你在哪个目录下运行 Python)
# os.path.dirname(os.path.abspath(__file__)):脚本文件所在目录
# 两者的区别:
# ┌─────────────────┬──────────────────────────────────────────────┐
# │ 场景 │ os.getcwd() vs dirname(abspath(__file__))│
# ├─────────────────┼──────────────────────────────────────────────┤
# │ 在 C:project │ C:project C:projectsrc │
# │ 下运行 │ (运行时目录) (脚本文件位置,稳定!) │
# │ python srcapp.py│ │
# ├─────────────────┼──────────────────────────────────────────────┤
# │ 在 C:projectsrc│ C:projectsrc C:projectsrc │
# │ 下运行 python app│ (这次碰巧一样) (总是正确) │
# └─────────────────┴──────────────────────────────────────────────┘
# 结论:
# os.getcwd() → 取决于运行时的工作目录,不稳定
# dirname(abspath(__file__)) → 永远是脚本自己所在的目录,稳定可靠
print("当前工作目录:", os.getcwd())
print("脚本所在目录:", os.path.dirname(os.path.abspath(__file__)))
第六部分:os.path 其他常用函数
6.1 os.path.join()——路径拼接(跨平台神器)
import os
# ===== 基础用法 =====
# join 会自动用当前操作系统的路径分隔符拼接
# Windows:,Linux/macOS:/
path = os.path.join('home', 'user', 'project', 'main.py')
print(path)
# Windows:homeuserprojectmain.py
# Linux: home/user/project/main.py
# 从绝对路径开始
path2 = os.path.join('C:\', 'Users', 'Admin', 'Desktop')
print(path2)
# C:UsersAdminDesktop
# ===== 重要陷阱:遇到绝对路径会"截断"前面的部分 =====
path3 = os.path.join('/home/user', 'project', '/etc/config')
print(path3)
# /etc/config ← 前面的 /home/user/project 被丢弃了!
# 原因:/etc/config 是绝对路径,join 认为它就是新的根
# 记住:join 中如果某个部分是绝对路径,前面的都会被忽略
# ===== 正确做法:相对路径拼接 =====
base = '/home/user/project'
rel = 'data/input.csv' # 不要以 / 开头
print(os.path.join(base, rel))
# /home/user/project/data/input.csv ✅
# ===== 拼接多级 =====
print(os.path.join('/var', 'log', 'nginx', 'access.log'))
# /var/log/nginx/access.log
# ===== 实际应用:构建多个文件路径 =====
base_dir = os.path.dirname(os.path.abspath(__file__))
files = ['config.json', 'data.csv', 'output.txt']
for filename in files:
full_path = os.path.join(base_dir, filename)
print(f" {filename:15s} → {full_path}")
6.2 os.path.basename()——获取文件名
import os
# basename:返回路径中的"最后一部分"(文件名或最末级目录名)
print(os.path.basename('/home/user/project/main.py'))
# main.py
print(os.path.basename('C:\Users\Admin\data.csv'))
# data.csv
print(os.path.basename('/home/user/project'))
# project(目录名)
print(os.path.basename('/home/user/project/'))
# ''(空字符串!注意末尾有斜杠)
print(os.path.basename('main.py'))
# main.py(本来就只有文件名)
# ===== 实际应用:获取文件名(不含目录) =====
files = [
'/var/log/nginx/access.log',
'/home/user/documents/report.pdf',
'C:\Downloads\photo.jpg'
]
for f in files:
print(f"{os.path.basename(f):20s} ← 来自 {os.path.dirname(f)}")
6.3 os.path.split()——同时获取目录和文件名
import os
# split:将路径拆分为 (目录, 文件名) 两部分
# 等价于:(dirname(path), basename(path))
path = '/home/user/project/main.py'
directory, filename = os.path.split(path)
print(f"目录部分:{directory}") # /home/user/project
print(f"文件名部分:{filename}") # main.py
# 一次赋值给两个变量
head, tail = os.path.split(r'C:projectsrcutils.py')
print(head) # C:projectsrc
print(tail) # utils.py
# 注意:末尾有斜杠的情况
head2, tail2 = os.path.split('/home/user/project/')
print(head2) # /home/user/project
print(tail2) # ''(空字符串)
# ===== 实际应用:批量重命名前获取路径信息 =====
files = [
'/data/jan/report.csv',
'/data/feb/report.csv',
'/data/mar/report.csv'
]
for f in files:
folder, name = os.path.split(f)
new_name = os.path.join(folder, 'backup_' + name)
print(f"{f:30s} → {new_name}")
6.4 os.path.splitext()——分离文件名和扩展名
import os
# splitext:将路径拆分为 (路径去掉扩展名, 扩展名) 两部分
name, ext = os.path.splitext('document.pdf')
print(name, ext) # document .pdf(注意扩展名含点)
name2, ext2 = os.path.splitext('/home/user/photo.jpg')
print(name2, ext2) # /home/user/photo .jpg
# 没有扩展名
name3, ext3 = os.path.splitext('/etc/hosts')
print(name3, ext3) # /etc/hosts ''(空字符串)
# 多个点
name4, ext4 = os.path.splitext('archive.tar.gz')
print(name4, ext4) # archive.tar .gz(只取最后一个点后的内容)
# 隐藏文件(Linux 以点开头)
name5, ext5 = os.path.splitext('.bashrc')
print(name5, ext5) # .bashrc ''(没有扩展名!)
name6, ext6 = os.path.splitext('.config.json')
print(name6, ext6) # .config .json
# ===== 实际应用:批量转换文件格式 =====
files = ['image.jpg', 'photo.png', 'avatar.bmp', 'logo.gif']
for filename in files:
name, ext = os.path.splitext(filename)
new_file = name + '.webp' # 统一转为 webp 格式
print(f"{filename:15s} → {new_file}")
# ===== 按扩展名过滤文件 =====
all_files = ['a.py', 'b.txt', 'c.py', 'd.csv', 'e.py']
py_files = [f for f in all_files if os.path.splitext(f)[1] == '.py']
print(f"Python 文件:{py_files}") # ['a.py', 'c.py', 'e.py']
6.5 os.path.exists() / isfile() / isdir()——判断路径
import os
# ===== 判断是否存在 =====
print(os.path.exists('/etc/passwd')) # True(Linux)或 False(Windows)
print(os.path.exists('nonexistent')) # False
# ===== 判断是文件还是目录 =====
print(os.path.isfile('/etc/passwd')) # True(是文件)
print(os.path.isfile('/etc')) # False(是目录,不是文件)
print(os.path.isdir('/etc')) # True(是目录)
print(os.path.isdir('/etc/passwd')) # False(是文件,不是目录)
# 不存在的路径
print(os.path.isfile('no_such_file')) # False(不报错,返回False)
print(os.path.isdir('no_such_dir')) # False
# ===== 判断是否是符号链接(软链接)=====
print(os.path.islink('/usr/bin/python')) # 可能是 True(软链接到具体版本)
# ===== 实际应用:安全读取文件 =====
def safe_read(filepath):
if not os.path.exists(filepath):
print(f"❌ 文件不存在:{filepath}")
return None
if not os.path.isfile(filepath):
print(f"❌ 不是文件(可能是目录):{filepath}")
return None
with open(filepath, 'r', encoding='utf-8') as f:
return f.read()
content = safe_read('/etc/hosts')
if content:
print(f"文件内容(前100字符):{content[:100]}")
# ===== 确保目录存在再写文件 =====
def write_safely(filepath, content):
"""确保目录存在后再写文件"""
directory = os.path.dirname(filepath)
if directory and not os.path.exists(directory):
os.makedirs(directory, exist_ok=True)
print(f"📁 创建目录:{directory}")
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ 写入完成:{filepath}")
write_safely('/tmp/newdir/subdir/output.txt', '这是内容')
6.6 os.path.getsize()——获取文件大小
import os
# 返回文件大小(字节数)
size = os.path.getsize('/etc/passwd')
print(f"文件大小:{size} 字节")
# 格式化显示文件大小
def format_size(size_bytes):
"""将字节数转换为人类可读的格式"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if size_bytes < 1024:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024
return f"{size_bytes:.2f} PB"
files = ['/etc/passwd', '/var/log/syslog', '/boot/vmlinuz']
for f in files:
if os.path.exists(f):
print(f"{f:30s} {format_size(os.path.getsize(f))}")
6.7 os.path.normpath()——规范化路径
import os
# normpath:消除路径中的多余斜杠、. 和 .. 等
print(os.path.normpath('/home/user/../user/./project'))
# /home/user/project(消除了 .. 和 .)
print(os.path.normpath('C://Users//Admin//Desktop'))
# C:UsersAdminDesktop(重复斜杠 → 单斜杠;正斜杠 → 反斜杠)
print(os.path.normpath('./data/../output/./result.txt'))
# outputresult.txt(化简后的相对路径)
print(os.path.normpath('/'))
# (Windows)或 /(Linux)
# ===== 实际应用:处理用户输入的路径 =====
user_input = input("请输入文件路径:") # 用户可能乱填斜杠
clean_path = os.path.normpath(user_input)
print(f"规范化后:{clean_path}")
6.8 os.path.relpath()——计算相对路径
import os
# relpath(path, start):计算从 start 到 path 的相对路径
# 从 /home/user 到 /home/user/project/main.py
rel = os.path.relpath('/home/user/project/main.py', '/home/user')
print(rel) # project/main.py
# 从 /home/user/project 到 /home/user/data
rel2 = os.path.relpath('/home/user/data', '/home/user/project')
print(rel2) # ../data(需要先上去一级)
# 从当前目录(默认)计算
os.chdir('/home/user')
rel3 = os.path.relpath('/home/user/project/main.py')
print(rel3) # project/main.py
# ===== 实际应用:生成 HTML 中的相对链接 =====
base_html = '/website/pages/about.html'
image = '/website/assets/images/logo.png'
css = '/website/assets/css/style.css'
base_dir = os.path.dirname(base_html)
print(f"图片链接:{os.path.relpath(image, base_dir)}")
# ../assets/images/logo.png
print(f"CSS链接:{os.path.relpath(css, base_dir)}")
# ../assets/css/style.css
6.9 os.path.commonpath() / commonprefix()——公共路径
import os
# commonpath:找多个路径的公共前缀(目录级别)
paths = [
'/home/user/project/src/main.py',
'/home/user/project/src/utils.py',
'/home/user/project/config/settings.json',
]
common = os.path.commonpath(paths)
print(common) # /home/user/project
# commonprefix(旧版,字符串级别,不推荐)
prefix = os.path.commonprefix(paths)
print(prefix) # /home/user/project/(字符串前缀,不是目录级别)
# 注意:commonprefix 可能返回不完整的目录名!
# ===== 实际应用:找出文件集合的共同根目录 =====
backup_files = [
'/data/2024/jan/report.csv',
'/data/2024/feb/report.csv',
'/data/2024/mar/report.csv',
]
root = os.path.commonpath(backup_files)
print(f"公共根目录:{root}") # /data/2024
6.10 os.path.expanduser()——展开用户目录
import os
# ~ 表示当前用户的主目录
print(os.path.expanduser('~'))
# Windows:C:UsersAdmin
# Linux: /home/user
# macOS: /Users/user
print(os.path.expanduser('~/.ssh/id_rsa'))
# Windows:C:UsersAdmin.sshid_rsa
# Linux: /home/user/.ssh/id_rsa
# 指定用户
print(os.path.expanduser('~root'))
# /root(Linux 的 root 用户主目录)
# ===== 实际应用:跨平台找配置文件 =====
config_path = os.path.expanduser('~/.myapp/config.json')
print(f"配置文件路径:{config_path}")
# 确保目录存在
os.makedirs(os.path.dirname(config_path), exist_ok=True)
第七部分:os.path 综合实战
7.1 实战一:扫描目录,按类型归类文件
import os
def classify_files(directory):
"""
扫描目录,按文件扩展名归类,统计文件数量和总大小
"""
result = {} # 格式:{扩展名: {'count': n, 'size': n, 'files': [...]}}
if not os.path.isdir(directory):
print(f"❌ 不是有效目录:{directory}")
return result
for item in os.listdir(directory):
full_path = os.path.join(directory, item)
if not os.path.isfile(full_path):
continue # 跳过子目录
_, ext = os.path.splitext(item)
ext = ext.lower() or '(无扩展名)'
if ext not in result:
result[ext] = {'count': 0, 'size': 0, 'files': []}
result[ext]['count'] += 1
result[ext]['size'] += os.path.getsize(full_path)
result[ext]['files'].append(item)
return result
def format_size(size):
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024:
return f"{size:.1f}{unit}"
size /= 1024
return f"{size:.1f}TB"
# 扫描某个目录
scan_dir = os.path.expanduser('~/Downloads') # 用户下载目录
classified = classify_files(scan_dir)
print(f"目录:{scan_dir}")
print(f"{'扩展名':12s} {'文件数':>6s} {'总大小':>10s}")
print("-" * 35)
for ext, info in sorted(classified.items(), key=lambda x: -x[1]['size']):
print(f"{ext:12s} {info['count']:>6d} {format_size(info['size']):>10s}")
7.2 实战二:递归搜索文件
import os
def find_files(root_dir, extension=None, pattern=None, min_size=None):
"""
递归搜索文件
参数:
root_dir - 搜索的根目录
extension - 文件扩展名(如 '.py',含点)
pattern - 文件名包含的关键词
min_size - 最小文件大小(字节)
"""
matches = []
for dirpath, dirnames, filenames in os.walk(root_dir):
# os.walk 递归遍历目录树
# dirpath = 当前目录路径
# dirnames = 当前目录下的子目录列表
# filenames= 当前目录下的文件列表
# 跳过 .git、__pycache__ 等隐藏/缓存目录
dirnames[:] = [d for d in dirnames if not d.startswith('.') and d != '__pycache__']
for filename in filenames:
full_path = os.path.join(dirpath, filename)
# 扩展名过滤
if extension and not filename.endswith(extension):
continue
# 关键词过滤
if pattern and pattern.lower() not in filename.lower():
continue
# 大小过滤
if min_size and os.path.getsize(full_path) < min_size:
continue
matches.append(full_path)
return matches
# 查找当前项目中所有 .py 文件
project_dir = os.path.dirname(os.path.abspath(__file__))
py_files = find_files(project_dir, extension='.py')
print(f"找到 {len(py_files)} 个 Python 文件:")
for f in py_files[:10]: # 只显示前10个
# 显示相对路径,更清晰
rel = os.path.relpath(f, project_dir)
size = os.path.getsize(f)
print(f" {rel:40s} {size:>8} bytes")
# 查找大于 1MB 的文件
large_files = find_files('/home', min_size=1024*1024)
print(f"n大于1MB的文件:{len(large_files)} 个")
7.3 实战三:路径处理工具函数集合
import os
import shutil
from datetime import datetime
def ensure_dir(path):
"""确保目录存在,不存在则创建(含所有父目录)"""
os.makedirs(path, exist_ok=True)
return path
def safe_filename(filename):
"""
把文件名中的非法字符替换掉(跨平台兼容)
Windows 不允许: / : * ? " < > |
"""
illegal = r'/:*?"<>|'
for ch in illegal:
filename = filename.replace(ch, '_')
return filename
def add_timestamp(filepath):
"""在文件名中插入时间戳(避免覆盖)"""
dir_part, filename = os.path.split(filepath)
name, ext = os.path.splitext(filename)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
new_name = f"{name}_{timestamp}{ext}"
return os.path.join(dir_part, new_name)
def make_unique_path(filepath):
"""如果文件已存在,自动编号避免覆盖"""
if not os.path.exists(filepath):
return filepath
dir_part, filename = os.path.split(filepath)
name, ext = os.path.splitext(filename)
counter = 1
while True:
new_path = os.path.join(dir_part, f"{name}_{counter}{ext}")
if not os.path.exists(new_path):
return new_path
counter += 1
def get_project_root(marker='setup.py'):
"""
从当前文件向上寻找项目根目录
(通过查找标志文件:setup.py、pyproject.toml、.git 等)
"""
current = os.path.dirname(os.path.abspath(__file__))
while True:
if os.path.exists(os.path.join(current, marker)):
return current
parent = os.path.dirname(current)
if parent == current: # 到达根目录
return None
current = parent
# ===== 使用示例 =====
# 1. 确保目录存在
log_dir = ensure_dir('/tmp/myapp/logs')
print(f"日志目录:{log_dir}")
# 2. 安全文件名
unsafe = 'report: 2024/01/01 < data >.csv'
safe = safe_filename(unsafe)
print(f"原始:{unsafe}")
print(f"安全:{safe}")
# 3. 带时间戳的文件名
ts_path = add_timestamp('/backup/data.csv')
print(f"带时间戳:{ts_path}")
# 4. 不覆盖现有文件
unique = make_unique_path('/tmp/output.txt')
print(f"唯一路径:{unique}")
第八部分:pathlib——更现代的路径写法
8.1 pathlib 简介(Python 3.4+)
pathlib 是 Python 3.4 引入的面向对象路径库,让路径操作更直观:
from pathlib import Path
# ===== 基础对比 =====
# os.path 的写法(函数式)
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
data_path = os.path.join(script_dir, 'data', 'input.csv')
# pathlib 的写法(面向对象,更简洁)
script_dir = Path(__file__).resolve().parent
data_path = script_dir / 'data' / 'input.csv' # 用 / 拼接路径!
8.2 pathlib 核心操作
from pathlib import Path
# ===== 创建 Path 对象 =====
p = Path('/home/user/project/main.py')
print(p.name) # main.py ← 等同于 os.path.basename
print(p.stem) # main ← 文件名(不含扩展名)
print(p.suffix) # .py ← 扩展名(含点)
print(p.suffixes) # ['.py'] ← 所有扩展名(如 ['.tar', '.gz'])
print(p.parent) # /home/user/project ← 等同于 os.path.dirname
print(p.parents[0]) # /home/user/project ← 直接父目录
print(p.parents[1]) # /home/user ← 上两级
print(p.parents[2]) # /home ← 上三级
# ===== 路径拼接(用 / 运算符)=====
base = Path('/home/user/project')
data_dir = base / 'data'
csv_file = base / 'data' / 'input.csv'
print(csv_file) # /home/user/project/data/input.csv
# ===== 绝对路径和解析 =====
p_rel = Path('data/input.csv')
p_abs = p_rel.resolve() # 等同于 os.path.abspath,但返回 Path 对象
print(p_abs)
# ===== 等同于 dirname(abspath(__file__)) =====
THIS_DIR = Path(__file__).resolve().parent
print(THIS_DIR)
# 向上多级
PROJECT_DIR = Path(__file__).resolve().parent.parent
print(PROJECT_DIR)
# ===== 检查路径 =====
p = Path('/etc/passwd')
print(p.exists()) # True
print(p.is_file()) # True
print(p.is_dir()) # False
# ===== 读写文件(pathlib 特有的便捷方法)=====
config = Path('/tmp/config.txt')
config.write_text('name=testnversion=1.0', encoding='utf-8')
content = config.read_text(encoding='utf-8')
print(content)
config.write_bytes(b'x89PNGrn') # 写二进制
data = config.read_bytes() # 读二进制
# ===== 遍历目录 =====
data_dir = Path('/var/log')
if data_dir.exists():
# 列出所有文件
for f in data_dir.iterdir():
print(f.name)
# 只列出 .log 文件(glob 模式)
for f in data_dir.glob('*.log'):
print(f"{f.name:20s} {f.stat().st_size} bytes")
# 递归搜索所有 .py 文件
for f in data_dir.rglob('*.py'):
print(f)
# ===== 创建/删除目录 =====
new_dir = Path('/tmp/myapp/data/2024')
new_dir.mkdir(parents=True, exist_ok=True) # 等同于 os.makedirs
# 重命名文件
old_file = Path('/tmp/old.txt')
old_file.rename('/tmp/new.txt')
# 删除文件
p = Path('/tmp/temp.txt')
if p.exists():
p.unlink() # 删除文件(unlink 是 Unix 术语)
8.3 os.path 和 pathlib 对照表
┌──────────────────────────────────────────────────────────────────────┐
│ os.path vs pathlib 对照 │
├──────────────────────────────┬───────────────────────────────────────┤
│ os.path 写法 │ pathlib 写法 │
├──────────────────────────────┼───────────────────────────────────────┤
│ os.path.abspath(__file__) │ Path(__file__).resolve() │
│ os.path.dirname(path) │ Path(path).parent │
│ os.path.basename(path) │ Path(path).name │
│ os.path.splitext(f)[0] │ Path(f).stem │
│ os.path.splitext(f)[1] │ Path(f).suffix │
│ os.path.join(a, b, c) │ Path(a) / b / c │
│ os.path.exists(p) │ Path(p).exists() │
│ os.path.isfile(p) │ Path(p).is_file() │
│ os.path.isdir(p) │ Path(p).is_dir() │
│ os.path.getsize(p) │ Path(p).stat().st_size │
│ os.makedirs(p, exist_ok=True)│ Path(p).mkdir(parents=True, │
│ │ exist_ok=True) │
│ open(p, 'r').read() │ Path(p).read_text() │
│ open(p, 'w').write(s) │ Path(p).write_text(s) │
│ os.listdir(p) │ [x.name for x in Path(p).iterdir()] │
│ glob.glob('**/*.py') │ Path(p).rglob('*.py') │
└──────────────────────────────┴───────────────────────────────────────┘
第九部分:Windows / Linux 跨平台注意事项
9.1 路径分隔符问题
import os
# ❌ 硬编码 Windows 反斜杠(移植到 Linux 会出错)
bad_path = 'C:\Users\Admin\data\file.txt'
# ✅ 方式1:用 os.path.join(推荐,最通用)
good_path = os.path.join('data', 'subdir', 'file.txt')
# Windows:datasubdirfile.txt
# Linux: data/subdir/file.txt
# ✅ 方式2:用正斜杠(在 Windows 上大多数函数也能识别)
path2 = 'data/subdir/file.txt'
# ✅ 方式3:用 pathlib(最现代)
from pathlib import Path
path3 = Path('data') / 'subdir' / 'file.txt'
# 获取当前平台的路径分隔符
print(os.sep) # Windows: Linux/macOS:/
print(os.pathsep) # Windows:;(PATH 分隔符) Linux::
print(os.altsep) # Windows:/ Linux:None
# 判断当前操作系统
import sys
print(sys.platform) # win32 / linux / darwin(macOS)
print(os.name) # nt(Windows)/ posix(Linux/macOS)
9.2 大小写问题
import os
# Windows 文件系统不区分大小写
# Linux/macOS 区分大小写(默认)
# 同一个文件:
# Windows:data.csv == DATA.CSV == Data.Csv(都能访问)
# Linux: data.csv ≠ DATA.CSV ≠ Data.Csv(是不同的文件!)
# 跨平台安全做法:统一用小写文件名
filename = user_input.lower()
# 在 Windows 上规范化大小写
if os.name == 'nt':
path = os.path.normcase(path) # 把路径转为小写
9.3 路径长度限制
# Windows:传统限制 MAX_PATH = 260 个字符
# Linux: 通常 4096 个字符
# 解决 Windows 路径过长的方法:在路径前加 \? 前缀
# ===== 检查路径长度 =====
path = r'C:verylong...pathfile.txt'
if os.name == 'nt' and len(path) > 260:
# 启用长路径支持(Windows 10 以上,需要修改注册表或组策略)
path = '\\?\' + path
第十部分:完整速查表
10.1 函数速查
📌 核心组合(最常用!)
os.path.dirname(os.path.abspath(__file__))
→ 当前脚本所在目录(无论从哪里运行,永远正确)
📌 路径转换
os.path.abspath(path) → 转为绝对路径(基于当前工作目录)
os.path.normpath(path) → 规范化路径(消除 . .. 和多余斜杠)
os.path.expanduser(path) → 展开 ~ 为用户主目录
os.path.relpath(path, start) → 计算相对路径
📌 路径拆解
os.path.dirname(path) → 目录部分
os.path.basename(path) → 文件名部分(最末级)
os.path.split(path) → (目录, 文件名) 元组
os.path.splitext(path) → (路径去扩展名, 扩展名) 元组
📌 路径拼接
os.path.join(a, b, c) → 跨平台路径拼接(推荐!)
📌 路径判断
os.path.exists(path) → 是否存在(文件或目录)
os.path.isfile(path) → 是否是文件
os.path.isdir(path) → 是否是目录
os.path.islink(path) → 是否是符号链接
os.path.isabs(path) → 是否是绝对路径
📌 文件信息
os.path.getsize(path) → 文件大小(字节)
os.path.getmtime(path) → 最后修改时间(时间戳)
os.path.getatime(path) → 最后访问时间(时间戳)
os.path.getctime(path) → 创建时间(时间戳,Windows)
📌 其他
os.path.commonpath(paths) → 多路径的公共目录前缀
os.getcwd() → 获取当前工作目录
os.chdir(path) → 改变当前工作目录
10.2 pathlib 替代写法(现代推荐)
from pathlib import Path
THIS_FILE = Path(__file__).resolve() # 当前文件绝对路径
THIS_DIR = Path(__file__).resolve().parent # 当前文件所在目录(最常用!)
# 向上N级
ROOT = Path(__file__).resolve().parents[2] # 上3级(parents[0]=直接父,以此类推)
# 拼接子路径
data_file = THIS_DIR / 'data' / 'input.csv'
# 读写
text = data_file.read_text(encoding='utf-8')
content = data_file.read_bytes()
data_file.write_text("hello", encoding='utf-8')
# 判断
data_file.exists()
data_file.is_file()
data_file.parent.is_dir()
# 遍历
for f in THIS_DIR.glob('*.py'): # 当前目录的 .py 文件
print(f.name, f.stat().st_size)
for f in THIS_DIR.rglob('*.txt'): # 递归搜索所有 .txt 文件
print(f)
总结
学完本章,你应该彻底理解:
__file__是当前脚本路径,但可能不稳定(相对路径)os.path.abspath(__file__)将其转为永远稳定的绝对路径os.path.dirname(...)去掉文件名,只保留目录部分os.path.dirname(os.path.abspath(__file__))是”定位脚本自身位置”的万能公式,生产项目必用os.path.join()是跨平台拼接路径的最佳方式,永远不要用字符串手动拼- 其他常用函数:
exists、isfile、isdir、basename、splitext、normpath等 pathlib.Path是更现代的写法,用/拼接路径,代码更优雅
记住这个项目模板:
import os
from pathlib import Path
# os.path 风格(兼容性最好)
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.dirname(THIS_DIR)
DATA_FILE = os.path.join(PROJECT_DIR, 'data', 'input.csv')
# pathlib 风格(Python 3.4+,更现代)
THIS_DIR = Path(__file__).resolve().parent
PROJECT_DIR = THIS_DIR.parent
DATA_FILE = PROJECT_DIR / 'data' / 'input.csv'