Background photo by Artturi Jalli on Unsplash
大家在撰寫 python 是否有遇到以下問題/需求:
- 目標檔案與路徑需要不停切換
- 想自動化的遍歷目標路徑下是否有目標檔案,若檔案存在才進行後續處理
- 在 python 環境下想跳轉至 linux 執行系統命令
以上這些問題都可以使用 python 強大的內建模組 os (operating system) 來完成! 本文將介紹常用的 os 函式,特別是文件與路徑操作相關的內容,以及其實際應用。
引入模組
os 屬於內建模組,直接引入即可使用
os
在路徑名稱的操作函式為 os.path()
,如等下會介紹的 os.path.join()
, os.path.exists()
等,要寫完整才不會報錯
目標1: 操作目標路徑與檔案
os.getcwd()
取得當前路徑,等同 linux 的 pwd
os.chdir(path)
變換預設的工作路徑,等同於 linux 的 cd /to/path
- 在路徑的操作,除了直接打出絕對路徑,也可以使用
os.path.join()
來更靈活的操作路徑,下面舉一個例子:
1
2
3
4
5
6
7
8
9
10
|
# 假設在 /home/dirs/ 下有兩個資料夾 a, b,我想要分別以這兩資料夾為工作路徑來做事,
# 直接設兩個變數分別存 a, b路徑,有點浪費
a_path = '/home/dirs/a'
b_path = '/home/dirs/b'
# 設相同的路徑為變數,並以 os.path.join 產生a, b 的路徑
root_dir = '/home/dirs'
os.path.join(root_dir, 'a') # 等價於 a_path
os.path.join(root_dir, 'b') # 等價於 b_path
|
os.path.basename(path)
可取出目標路徑中最底層的資料夾/檔案名稱;反之 os.path.dirname(path)
可取出最底層以上的路徑,下面舉個例子比較
1
2
3
4
|
paths = '/home/dirs/a/a.txt'
print(os.path.basename(paths)) # >> a.txt
print(os.path.dirname(paths)) # >> /home/dirs/a
|
os.path.abspath(file_name)
和 os.path.realpath(file_name)
都可以找出目標檔案的絕對路徑
1
2
3
4
5
6
7
8
|
# 假設有一個檔案位於 /home/dirs/a/a.txt
file_name = 'a.txt'
print(os.path.abspath(file_name))
# >> /home/dirs/a/a.txt
print(os.path.realpath(file_name))
# >> /home/dirs/a/a.txt
|
注意: 兩者的差別在於,若是 soft link 指向的目標檔案,os.path.realpath()
會印出原始檔案的路徑! 請看下面例子
1
2
3
4
5
6
7
8
|
# 假設目標檔案是 ln -s /home/dirs/a.txt /home/dirs/b.txt
print(os.path.abspath('b.txt'))
# >> /home/dirs/b.txt
print(os.path.realpath('b.txt'))
# >> /home/dirs/a.txt
|
os.path.realpath(__file__)
為常見寫法,用來獲取當前執行 python script 的絕對路徑,其中 __file__
指的是目前引用的 python script
1
2
3
4
5
6
7
|
# 假設我的 python script 名為 test.py、位於 /home/dirs 底下
print(os.path.realpath(__file__))
# >> /home/dirs/test.py
# 若只想要獲取路徑,可搭配 os.path.dirname() 來操作
print(os.path.dirname(os.path.realpath(__file__)))
# >> /home/dirs
|
注意: 若 python script 存在的路徑與執行 script 路徑不同,執行 os.path.realpath()
與os.getcwd()
的結果會不同!
1
2
3
4
5
6
7
8
|
# 假設我的 python script 名為 test.py、位於 /home/dirs 底下
# 現在我到 /home 路徑下執行 python script: python3 test.py
print(os.path.dirname(os.path.realpath(__file__)))
# >> /home/dirs
# print(os.getcwd())
# >> /home
|
補充: os.path.abspath(os.path.dirname(__file__))
和 os.path.dirname(os.path.abspath(__file__))
順序有差嗎? 參考網站
總結一下,在印出路徑這件事上,受到相對路徑、絕對路徑與鏈結等多重因素大亂鬥,請小心使用測試
目標2: 檢查目標路徑/檔案的存在性
如果想確認目標路徑是否存在,os
能協助確認並返回boolean值;以下列出常用的函數與使用區別:
os.path.isdir()
可以確認資料夾路徑是否存在
os.path.exists()
可以確認資料夾路徑和檔案是否存在
os.path.isfile()
可以確認檔案是否存在
舉一個實例比較:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 假設在 /home/dirs 路徑下存在一份名為 file.txt 的檔案
# dirs/
# └── file.txt
exist_path = '/home/dirs'
exist_file = os.path.join(exist_path, 'file.txt')
# 確認路徑是否存在
os.path.isdir(exist_path) # >> True
os.path.exists(exist_path) # >> True
os.path.isfile(exist_path) # >> False isfile 不能判斷路徑是否存在!!
# 確認檔案是否存在
os.path.isdir(exist_file) # >> False isdir 不能判斷檔案是否存在!!
os.path.exists(exist_file) # >> True
os.path.isfile(exist_file) # >> True
|
注意: 從上面的例子可知,即使路徑檔案都存在,os.path.isdir() / os.path.isfile()
都有各自的判斷侷限,建議多使用 os.path.exists()
來減少麻煩,更多討論請參考這個網址
目標3: 創建資料夾
如果想在目標路徑中創建資料夾,os
有兩個相當於 linux mkdir
的函數:
os.mkdir()
: 只能創立一層(最底層)資料夾,若上層資料夾不存在會報錯
os.makedirs()
: 可以創立多層,若想忽略已創建資料夾可加上 exist_ok=True
(這樣就相當於linux的 mkdir -p
)。為了保險起見建議多使用os.makedirs
減少報錯麻煩
另外有點要注意,若路徑已經存在 os 也會報錯,因此常見的寫法是先用 os.path.exists()
判斷目標路徑是否存在,再來創建資料夾,下面舉一個程式實例:
1
2
3
4
5
6
7
8
|
# 假設我們想將 /home/dirs/folder1 設為當前的工作目錄,但又不確定 dirs/folder1 是否存在
work_path = '/home/dirs/folder1'
if os.path.exists(work_path): # if return True
os.chdir(work_path)
else: # if return False
os.makedirs(work_path) # create dirs
os.chdir(work_path)
|
目標4: 遍歷目標路徑下的所有檔案
如果目標路徑下包含多層子路徑與檔案,使用者不可能一層層去探訪篩選,模組 os
提供強大的函數 os.walk
協助走訪遍歷路徑下所有角落,如同 linux tree
一樣走訪(並印出)路徑下所有內容
os.walk
會輸出三數值為一組的tuple (dirpath, dirnames, filenames),一般會搭配迴圈來寫,並將三個輸出分別命名為 root
, dirs
, 以及 files
:
假設我們想走訪 /home/dirs
路徑下的內容,透過 tree 已知有這些路徑/檔案
1
2
3
4
5
6
7
|
├── a
│ └── a.txt
├── b
│ └── b.txt
├── c
│ └── c.txt
└── file.txt
|
基本的 python code:
1
2
3
|
path = '/home/dirs'
for root, dirs, files in os.walk(path):
...
|
print(root)
1
2
|
for root, dirs, files in os.walk(path):
print(roots)
|
得到以下輸出,可以看到 root 會隨深入下層資料夾而改變:
1
2
3
4
|
/home/dirs
/home/dirs/a
/home/dirs/b
/home/dirs/c
|
print(dirs)
1
2
|
for root, dirs, files in os.walk(path):
print(dirs)
|
得到以下輸出,和 root 輸出順序相對應,可看到 dirs 列出的是當前 root 路徑下包含的資料夾名稱
1
2
3
4
|
['a', 'b', 'c'] # /home/dirs 下有三個資料夾
[] # /home/dirs/a 下沒有資料夾
[]
[]
|
print(files)
1
2
|
for root, dirs, files in os.walk(path):
print(files)
|
得到以下輸出,和 root, dirs 輸出順序相對應,可看到 files 的是當前 root 路徑下包含的檔案名稱
1
2
3
4
|
['file.txt'] # /home/dirs 下有一個檔案
['a.txt'] # /home/dirs/a 下有一個檔案
['b.txt']
['c.txt']
|
因為 files 只有檔案名稱,常利用 os.path.join(root, f)
來取得檔案的絕對路徑
實例
了解基本構造後,透過實例來了解 os.walk()
的可以如何使用。
假設我想取出每個樣本名稱folder中,結尾包含 .tab.gz
的檔案來操作
目標路徑大致的結構如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/home/dirs/folders/
├── annotation
│ ├── AD_sample1
│ ├── AD_sample1.annotation.tab.gz
│ ├── AD_sample2
│ ├── AD_sample2.annotation.tab.gz
│ ├── AD_sample3
│ ├── AD_sample3.annotation.tab.gz
│ ├── BS_sample12
│ ├── BS_sample12.annotation.tab.gz
│ ├── BS_sample14
│ ├── BS_sample14.annotation.tab.gz
│ ├── BS_sample17
│ ├── BS_sample17.annotation.tab.gz
│ ├── BS_sample18
│ ├── BS_sample18.annotation.tab.gz
│ └── BS_sample19
│ ├── BS_sample19.annotation.tab.gz
├── csv
├── multiqc
├── logs
└── reports
|
python code:
1
2
3
4
5
6
7
|
work_path = '/home/dirs/folders/'
for root, dirs, files in os.walk(work_path):
for f in files:
if f.endswith('.tab.gz'):
file_name = os.path.join(root, f)
with open(file_name, 'r') as fh:
...
|
目標5: 產生symlink
和 linux 中的 ln -s
相同,os 也可協助建立連結以節省空間使用
os.symlink(sourec_path, dest_path)
相當於 ln -s
os.islink()
相當於用 ls -alh
檢查
os.unlink()
相當於 unlink
,注意資料夾不能加 /
不然移除無效
目標6: 執行系統命令
os.system(cmd)
也可以在 python 環境下執行系統命令,和 subprocess
模組功能相似,但較為陽春
參考資料與補充
python 3.4 後有新的模組 pathlib
(物件導向的操作) 其功能和 os
相似,有興趣可以參考,由於目前 os
使用上沒太大問題,就沒有特別轉換模組
https://docs.python.org/zh-tw/3/library/os.html#files-and-directories