順番に読み進めながら学べます

os と pathlib — ファイルパスとディレクトリ操作

os.path.joinでWindows/Linuxのパス区切り差を吸収し、listdir/walk/globでディレクトリを再帰探索、pathlib.Pathの / 演算子と .stem / .suffixでパスを分解する書き方を実行で学びます。

ファイルパスとディレクトリを扱う 2 つのモジュール — 古くから使われるos.pathと、より新しく書きやすいpathlibを整理します。OS の差を吸収するパス組み立て、ディレクトリの一覧と再帰探索、Path オブジェクトでのパス分解までを順に見ていきます。

os.path — OS の差を吸収するパス組み立て

ファイルパスの区切り文字は OS によって違います。Windows は\(バックスラッシュ)、Linux と macOS は/です。コードに直接 "data/sales/2024.csv" と書くと Linux と Mac では動きますが、Windows 上で実行したときにパスを誤認することがあります。

os.path.join("data", "sales", "2024.csv")のように要素ごとに区切って渡すと、Python が動いている OS に合わせて適切な区切り文字をその場で選んでくれます。

os.path.join が OS ごとに区切りを切り替える
os.path.join( 'data', 'sales', '2024.csv')Linux / Macdata/sales/2024.csvWindowsdata\sales\2024.csv
同じ Python コードでも、Linux / Mac では/区切り、Windows では\区切りに展開される。手書きで区切り文字を埋め込まないことが、移植性を保つコツ。
関数意味
os.path.join(*parts)OS の区切りでパスを連結するjoin('data', 'sales') → 'data/sales'
os.path.exists(p)パスが存在するかTrue / False
os.path.isfile(p)ファイルか(ディレクトリではない)True / False
os.path.isdir(p)ディレクトリかTrue / False
os.path.basename(p)末尾のファイル名・フォルダ名basename('data/x.csv') → 'x.csv'
os.path.dirname(p)末尾を取り除いた親パスdirname('data/x.csv') → 'data'
os.path.splitext(p)拡張子を分離するsplitext('x.csv') → ('x', '.csv')

売上 CSV ファイルのパスos.pathで組み立てて、ファイルの中身を読み出します(左の 📂 ファイルからdata/sales/2024_q1.csvなどが見えます)。

① os を読み込み、3 つのパス要素data / sales / 2024_q1.csvを OS の区切り文字でつないで 1 つの文字列にしてください

② パス全体をそのまま表示してください

③ プレロードしたdata/sales/2024_q1.csvopen(path, "r")で開き、先頭 3 行を表示してください

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

実践 2 — basename と splitext で名前と拡張子に分ける

1 行に詰め込まず、段階的に変数へ受け取りながらファイル名と拡張子を分離します。basenameでフルパスから末尾のファイル名を取り出し、splitextでその名前を(名前, 拡張子)のタプルに分けます。

1 行に書かず、段階的に変数を経由してファイル名と拡張子を取り出します。

① os をインポートし、path = "data/sales/2024_q1.csv"を用意してください

os.path.basename(path)で末尾のファイル名を取り出してfilenameに入れ、表示してください

os.path.splitext(filename)(名前, 拡張子)のタプルを取り出してpartsに入れ、表示してください

④ そのタプルを分割代入stemsuffixに分け、stem suffixの形でスペース区切りで表示してください

Python エディタ

コードを実行してください

os.listdir と os.walk — ディレクトリの一覧と再帰探索

フォルダの中身を Python から取り出すとき、1 階層だけ見るならos.listdirサブフォルダまで再帰的に見るならos.walkを使います。os.listdirは指定したフォルダの直下にある名前(ファイルとサブフォルダ両方)をリストで返し、os.walkは配下のすべての階層を再帰的に辿って(現在のパス, サブフォルダ名のリスト, ファイル名のリスト)のタプルを 1 階層ずつ生成します。

os.listdir と os.walk の違い
os.listdir直下の名前のリストos.walk再帰的に全階層を走査
os.listdirは直下の名前だけ、os.walkは配下を全階層再帰的に辿る。深さの欲しさで使い分ける。
import os

# 1 階層: 'data' 直下の名前
print(os.listdir("data"))
# → ['sales', 'inventory']

# 再帰: 'data' 配下を全部辿る
for dirpath, dirnames, filenames in os.walk("data"):
    print(dirpath, filenames)
# → data ['sales', 'inventory'] []
#    data/sales [] ['2024_q1.csv', '2024_q2.csv']
#    data/inventory [] ['items.json']

data/ディレクトリの中身を 2 通りの方法で表示します。まずは直下、次に再帰的にすべて。

① os を読み込んでください

os.listdir("data")data/直下にある名前を並べ替えて表示してください(順序を揃えるためsortedで囲みます)

os.walk("data")data/配下を再帰的に辿り、各階層について(現在のパス, ファイル名のリスト)を 1 行ずつ表示してください(ファイル名のリストもsortedで揃えます)

Python エディタ

コードを実行してください

glob — パターンマッチでファイルを集める

拡張子が.csvのものだけのように、条件に合うファイルだけ集めたいときはglobモジュールが最短です。*(任意の文字列)や**(任意の階層)といったワイルドカードで対象パターンを書くと、それに合うパスのリストが返ります。

glob のワイルドカード
*同じ階層内の任意の文字列**任意の階層をまたぐ(recursive=True)
*は同一階層内の任意の文字列、**は任意の階層をまたぐ(recursive=True必須)。
import glob

# data/sales 直下の CSV ファイル
print(glob.glob("data/sales/*.csv"))
# → ['data/sales/2024_q1.csv', 'data/sales/2024_q2.csv']

# data 配下を再帰的に検索(** + recursive=True)
print(glob.glob("data/**/*.csv", recursive=True))
# → ['data/sales/2024_q1.csv', 'data/sales/2024_q2.csv']

glob のワイルドカード**は recursive=True とセット

glob.glob("data/**/*.csv")のパターンにある二重アスタリスクは、何階層でも辿るワイルドカードです。ただしrecursive=Trueを渡さないと普通の*と同じ扱いになり、深いフォルダが見つかりません。再帰検索したいときは必ずこの引数を付けます。

data/sales/の CSV ファイルを glob でまとめて取り出します。

① glob を読み込んでください

glob.glob("data/sales/*.csv")で sales 配下の CSV を取り出し、sortedで並べ替えて変数csv_filesに入れてください

③ 取れたパスを 1 件ずつ表示してください

見つかったファイル数: ◯の形で件数を表示してください

Python エディタ

コードを実行してください

pathlib.Path — オブジェクト指向のパス操作

os.path文字列でパスを扱うライブラリでしたが、Python 3.4 以降はパス自体を 1 つのオブジェクトとして扱う pathlib.Pathが推奨されています。Path("data/sales/2024_q1.csv")のように作ると、.parent で親フォルダ.name で末尾.stem で拡張子なしの名前.suffix で拡張子といった属性で各部分にアクセスできます。

Path オブジェクトの属性
Path('data/sales/2024_q1.csv').parentPath('data/sales').name'2024_q1.csv'.stem'2024_q1'.suffix'.csv'
1 つの Path から.parent / .name / .stem / .suffixで各部分を取り出せる。os.path.dirname / basename / splitextを別々に呼ぶより読みやすい。
from pathlib import Path

p = Path("data") / "sales" / "2024_q1.csv"   # / 演算子で連結
print(p)               # data/sales/2024_q1.csv
print(p.parent)        # data/sales
print(p.name)          # 2024_q1.csv
print(p.stem)          # 2024_q1
print(p.suffix)        # .csv
print(p.exists())      # True

# 中身を読む(with open のラッパー)
print(p.read_text())   # CSV の中身

# サブフォルダの一覧(os.walk 相当)
for sub in Path("data").rglob("*.csv"):
    print(sub)

os.pathは文字列ベース、pathlib.Pathはオブジェクトベースで同じ操作を提供します。下の表でやりたいことごとに対応関係を整理します。

やりたいことos.path 系pathlib 系
連結os.path.join('data', 'x.csv')Path('data') / 'x.csv'
親フォルダos.path.dirname(p)p.parent
ファイル名os.path.basename(p)p.name
拡張子なしのファイル名os.path.splitext(p)[0] の処理p.stem
拡張子os.path.splitext(p)[1]p.suffix
存在確認os.path.exists(p)p.exists()
再帰検索glob.glob('**/*.csv', recursive=True)Path('.').rglob('*.csv')
読み込みwith open(p) as f: f.read()p.read_text()

新しいコードは pathlib を選ぶ

新規コードは pathlib を使うのが推奨です。古いライブラリの API が文字列パスを要求する場面(例: 一部の DB ドライバ)ではstr(p)で変換すれば渡せます。os.pathが消えるわけではないので、既存コードを読むためにも両方の対応関係を知っておくと安心です。

pathlib.Path で売上 CSV を扱います。文字列を直接結合するのではなく、Path オブジェクトの演算子と属性を使って同じ結果を得ます。

① pathlib からPathクラスを読み込んでください

data / sales / 2024_q1.csvの 3 要素から、Path の連結演算子で 1 つの Path オブジェクトを作ってください(os.path.joinの代わり)

③ Path オブジェクトの拡張子なしの名前拡張子を、属性アクセスで取り出してスペース区切りで表示してください

④ Path オブジェクトのメソッドで、ファイルの中身を一気に文字列として読み出して表示してください(with open(...)を書かなくて済む方法を使います)

Python エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1Windows でも Linux でも壊れないパス組み立てとして推奨される書き方はどれですか?

Q2次のうち、フォルダの中身を再帰的に全階層辿るのに最も向いているのはどれですか?

Q3p = Path("data/sales/2024_q1.csv")のとき、p.stemの値はどれですか?