Featured image of post Python系列:使用 os 成為系統操作大師

Python系列:使用 os 成為系統操作大師

Background photo by Artturi Jalli on Unsplash

大家在撰寫 python 是否有遇到以下問題/需求:

  • 目標檔案與路徑需要不停切換
  • 想自動化的遍歷目標路徑下是否有目標檔案,若檔案存在才進行後續處理
  • 在 python 環境下想跳轉至 linux 執行系統命令

以上這些問題都可以使用 python 強大的內建模組 os (operating system) 來完成! 本文將介紹常用的 os 函式,特別是文件與路徑操作相關的內容,以及其實際應用。

引入模組

os 屬於內建模組,直接引入即可使用

1
import 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:
                ...

和 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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus