CHAPTER 5 ・ ファイルとテキストの実践 / Lesson 18
パスを安全に扱う(pathlib)
フォルダをたどって対象ファイルを探し、名前や拡張子を取り出す。文字のつなぎ合わせより、安全な方法です。
このレッスンが終わったら
「パスは文字ではなく Path で持つ。glob で探し、stem や suffix で名前を分けられる」と言える。
こんな場面、ありませんか?
invoices フォルダに請求書のファイルが入っています。「誰の請求書か」が分かるように、ファイル名から拡張子(.txt)を外した名前を一覧にしたい、という場面を考えます。基礎編の書き方(基礎編 Lesson 13 の os と、文字の操作)では、こうなります。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
動きはしますが、危ない点が2つあります。
- 文字で
"/"をつなぐやり方は、Windows(区切りが\)とそれ以外で食い違います。 split(".")[0]は、名前の途中に.があると、最後の行のように2025だけが取れてしまいます。
さらに、Windows のパスをそのまま文字として書くと、こんなエラーにもなります。
▶ 実行を押すと、ここに結果が出ます
\U が「特別な文字」と読み取られるためです(\n は改行、\t はタブになります)。パスを文字で書くと、こうしたつまずきが続きます。これをまとめて片づけてくれるのが、この回の主役の pathlib です。
Path で書いてみる
同じことを、pathlib の Path で書きます。「実行」を押してください。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
.txt のファイルだけが対象になり、最後の行も 2025.10_invoice_tanaka と、正しく取れました。フォルダに / をつなぐこともできます。ファイルの中身を読むのも、1行です。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
/ は割り算ではなく、「パスをつなぐ記号」です。Windows で動かせば、自動で \ が使われます。
仕組みを読む
Path は、パスをただの文字ではなく、部品を持ったものとして扱います。1つのファイルのパスは、次の部品に分かれます。
invoices.parent(入っているフォルダ)/区切りinvoice_tanaka.stem(拡張子を除いた名前).txt.suffix(拡張子)invoice_tanaka.txt の部分(.stem と .suffix を合わせたもの)は .name で取れます。先ほどのコードを、1行ずつ日本語にします。
from pathlib import Pathpathlib から、Path という道具を借りるfolder = Path("invoices")invoices フォルダを、Path として持つ(文字ではない)for p in sorted(folder.glob("*.txt")):フォルダの中から、.txt で終わるものを探し、名前順に1つずつ「p」と呼ぶ print(p, "→", p.stem)パス全体と、拡張子を除いた名前を表示する日本語にすると
*.txt の * は「何でもよい(0文字以上)」という意味の記号で、これを使ってファイルを探す書き方をglob(グロブ)と呼びます。基礎編の endswith(".txt") で選ぶ代わりに、探す段階で絞り込めます。
基礎編(os と文字の操作)
import os
for name in sorted(os.listdir("invoices")):
if name.endswith(".txt"):
path = os.path.join("invoices", name)
print(name.split(".")[0])pathlib
from pathlib import Path
for p in sorted(Path("invoices").glob("*.txt")):
print(p.stem).suffix が返す .txt のような、名前の最後の部分を拡張子と呼びます。ドット(.)を含めた形で返ります。
変えて、試す
① 深いところまで探す。 glob は、指定したフォルダの直下だけを探します。サブフォルダの中まで探すときは rglob を使います。ここでは、年ごと・月ごとのフォルダを作ってから探します。mkdir(parents=True, exist_ok=True) は、「途中のフォルダもまとめて作る。もうあってもエラーにしない」という意味です。
▶ 実行を押すと、ここに結果が出ます
直下には1つもないので glob は 0 件、rglob は 2 件です。(as_posix() は、/ 区切りの文字にして表示する指定です。)
② 名前の変更は、まず「予定」を表示する。 ファイル名の変更は、元に戻せないことがあります。いきなり変更せず、変更後の名前を表示するだけにして確かめます。このやり方を試し実行(ドライラン)と呼びます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
予定が正しいと確かめられたら、次のように rename を足します。with_name は、フォルダはそのままで、名前の部分だけを入れ替えた新しい Path を作ります。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
iterdir() は、フォルダの中身を1つずつ取り出します。
③ ないファイルを読むと? 存在しないパスを read_text で読むと、FileNotFoundError(ファイルが見つかりません)で止まります。読む前に p.exists() で調べるか、try / except(Lesson 09 で学びます)で備えます。
自分で書く
お題① invoices フォルダにあるファイルを、拡張子ごとに数えて、拡張子 件数 の形で表示してください。拡張子の順番は、sorted で並べます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
for p in Path("invoices").iterdir(): で1つずつ取り出し、counts[p.suffix] = counts.get(p.suffix, 0) + 1 と数えます。.get は Lesson 06 で学ぶ「なければ0」の書き方です。
答えの例を見る
from pathlib import Path
counts = {}
for p in Path("invoices").iterdir():
counts[p.suffix] = counts.get(p.suffix, 0) + 1
for suffix, n in sorted(counts.items()):
print(suffix, n)お題② 請求書(.txt)の、名前の変更案を作ります。invoice_tanaka.txt を 2025-10-31_tanaka.txt のように、先頭の invoice_ を日付に替えた名前にして、今の名前 → 新しい名前 の形で表示してください。(名前は変更しません)
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
p.name.replace("invoice_", "2025-10-31_") で、名前の一部を入れ替えられます。print(p.name, "→", new_name) と書きます。
答えの例を見る
from pathlib import Path
for p in sorted(Path("invoices").glob("*.txt")):
new_name = p.name.replace("invoice_", "2025-10-31_")
print(p.name, "→", new_name)お題③ reports/2025-10 というフォルダを作り(途中の reports もまとめて)、その中の memo.txt に「確認済み」と書いて、書いた内容を読み直して表示してください。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
folder = Path("reports") / "2025-10" を作り、folder.mkdir(parents=True, exist_ok=True)。書くのは (folder / "memo.txt").write_text(...)、読むのは read_text(...) です。
答えの例を見る
from pathlib import Path
folder = Path("reports") / "2025-10"
folder.mkdir(parents=True, exist_ok=True)
memo = folder / "memo.txt"
memo.write_text("確認済み", encoding="utf-8")
print(memo.read_text(encoding="utf-8"))仕事ならどう使う?
理解チェックPath("invoices/2025.10_tanaka.txt").suffix の結果は、どれでしょう?
理解チェックサブフォルダの中にある .csv も含めて、すべて探したいときは?