2026年5月23日

2026年5月23日

Pythonのosモジュールでシステム操作する方法(ファイル・ディレクトリ・環境変数・権限)

Python の os モジュールを使うと、パスの組み立て、ディレクトリの作成と走査、ファイルの移動や削除、環境変数の読み取り、パーミッションの変更といったシステム操作を、追加ライブラリなしで書けます。この記事では、サーバー運用でよく使う関数を目的別に整理し、最後に「古い WordPress バックアップを安全に整理するスクリプト」を通して、削除系の操作で事故を起こさない書き方まで説明します。

os モジュールで何ができるか

os は OS の機能を Python から呼ぶための標準モジュールです。大きく分けると次の役割があります。

  • パスの操作(os.path):結合、存在確認、サイズや更新日時の取得
  • ディレクトリとファイルの操作:作成、一覧、走査、移動、削除
  • 環境変数(os.environ)とプロセス情報(PID、CPU 数、ロードアベレージ)
  • パーミッションと所有者の変更

コマンドの実行もできますが、後述のとおり現在は subprocess モジュールを使うのが一般的です。また、ファイルのコピーやディレクトリごとの削除は os ではなく shutil の担当です。

以下のコードは Linux サーバー上の Python 3 を前提にしています(2026年10月現在、サポート期間内の 3.11 以降を推奨します)。Windows では使えない関数には都度ふれます。

パスを組み立てる・調べる

パスは文字列の足し算ではなく os.path.join() で組み立てます。区切り文字の重複や抜けを防げるうえ、Windows でもそのまま動きます。

import os

base = os.path.dirname(os.path.abspath(__file__))  # このスクリプトのあるディレクトリ
config = os.path.join(base, "config.ini")

print(os.getcwd())                 # 現在の作業ディレクトリ
print(os.path.exists(config))      # ファイルかディレクトリが存在するか
print(os.path.isfile(config), os.path.isdir(base))
if os.path.isfile(config):
    print(os.path.getsize(config), "bytes")
    print(os.path.getmtime(config))    # 更新日時(UNIX時刻)

つまずきやすいのは「作業ディレクトリ」です。cron や systemd から実行すると、作業ディレクトリはスクリプトの場所ではなくホームディレクトリやルートになります。相対パスで書いたファイルが見つからないときは、上のように __file__ を基準にした完全なパスへ直すのが早道です。

環境変数を読む

パスワードや API トークンをコードに直書きせず、環境変数から読むのが基本です。

import os

db_host = os.environ.get("DB_HOST", "localhost")  # 無ければ既定値
api_token = os.environ.get("API_TOKEN")
if not api_token:
    raise SystemExit("API_TOKEN が設定されていません")

os.environ["名前"] は未設定だと KeyError になり、os.environ.get() は None か既定値を返します。必須の値は上のように起動直後に確認して止めると、途中まで処理が進んでから失敗する事態を避けられます。なお、値をログに出力するのは避けてください。

os.environ を書き換えると、そのプロセスと、そこから起動する子プロセスにだけ反映されます。親のシェルの環境変数は変わりません。

ディレクトリを作る・一覧する・走査する

import os

os.makedirs("/var/tmp/myjob/output", mode=0o750, exist_ok=True)

os.makedirs() は途中のディレクトリもまとめて作り、exist_ok=True なら既にあってもエラーになりません。mode は umask の影響を受けるうえ、Python 3.7 以降では途中に作られるディレクトリには適用されません。権限を厳密に決めたいときは、作成後に os.chmod() で設定し直します。

一覧の取得には os.listdir() もありますが、種類や更新日時も一緒に見たいなら os.scandir() が効率的です。ディレクトリツリー全体をたどるなら os.walk() を使います。次の例は、WordPress のアップロードフォルダの容量を年ごとに集計します。

import os

uploads = "/var/www/html/wp-content/uploads"
for name in sorted(os.listdir(uploads)):
    year_dir = os.path.join(uploads, name)
    if not (name.isdigit() and os.path.isdir(year_dir)):
        continue
    total = 0
    for dirpath, dirnames, filenames in os.walk(year_dir):
        for f in filenames:
            fp = os.path.join(dirpath, f)
            if not os.path.islink(fp):
                total += os.path.getsize(fp)
    print(f"{name}: {total / 1024 / 1024:.1f} MB")

os.walk() は既定ではシンボリックリンク先のディレクトリをたどりません(followlinks=False)。たどるようにすると、リンクの循環で同じ場所を何度も数えることがあるので、必要なとき以外は既定のままにしておきます。走査を途中で打ち切りたいディレクトリがあれば、ループ内で dirnames リストから名前を取り除くと、その下には入らなくなります。

ファイルを移動・置き換え・削除する

操作 関数 注意点
名前変更・移動 os.rename(src, dst) Windows では移動先が既にあるとエラー
上書きを伴う置き換え os.replace(src, dst) 移動先があれば置き換える。同じファイルシステム内なら途中状態が見えない
ファイル削除 os.remove(path) ディレクトリには使えない
空のディレクトリ削除 os.rmdir(path) 中身があるとエラー

設定ファイルやキャッシュを書き換えるときは、一時ファイルに書いてから os.replace() で差し替えると、書き込み途中の壊れたファイルをほかのプロセスが読む事態を防げます。

中身ごとディレクトリを消す shutil.rmtree() は取り消しがききません。パスを変数で組み立てる場合、変数が空だったり想定外の値だったりすると、意図しない場所を消すおそれがあります。削除系の処理は、実行前に対象を表示する「ドライラン」を既定にするのが安全です。

パーミッションと所有者

import os
import stat

path = "/var/www/html/wp-config.php"
st = os.stat(path)
print(stat.filemode(st.st_mode))   # 例: -rw-r-----
print(st.st_uid, st.st_gid)

os.chmod(path, 0o640)              # 所有者は読み書き、グループは読み取りのみ

パーミッションは 8 進数で 0o640 のように書きます。640 と書くと 10 進数として解釈され、まったく別の権限になるので注意してください。所有者の変更 os.chown() は Unix 専用で、通常は root 権限が要ります。WordPress のファイル権限を変更する前には、元の状態を ls -l などで記録しておくと戻しやすくなります。

os.access() で「書き込めるか」を事前に調べる書き方もありますが、確認と実際の操作の間に状態が変わることがあります。実際に開いてみて、失敗したら PermissionError を捕まえるほうが確かです。

プロセスとシステムの情報

import os

print(os.getpid())         # 自分のプロセスID
print(os.cpu_count())      # 論理CPU数
print(os.getloadavg())     # (1分, 5分, 15分) の平均負荷 ※Windowsでは使えない
print(os.name)             # 'posix' や 'nt'
print(os.uname().nodename) # ホスト名 ※Unix専用

ロードアベレージが CPU 数を継続的に上回っていれば、処理待ちが溜まっているサインです。

コマンド実行は os.system ではなく subprocess で

os.system() は文字列をシェルに渡して実行するため、ファイル名などの外部入力が混ざるとコマンドインジェクションの原因になり、出力の取得やエラー処理もしにくい関数です。現在は subprocess モジュールの run() に、引数をリストで渡す書き方が推奨されています。

import subprocess

result = subprocess.run(
    ["wp", "plugin", "list", "--status=active", "--format=csv"],
    cwd="/var/www/html",
    capture_output=True, text=True, check=True, timeout=60,
)
print(result.stdout)

リストで渡せばシェルを経由しないので、引数に空白や記号が含まれても安全に扱えます。check=True にすると終了コードが 0 以外のとき例外になり、失敗を見逃しません。

実践:古いバックアップを安全に整理する

最後に、ここまでの関数を組み合わせて、古い WordPress のバックアップファイルを整理するスクリプトを作ります。誤削除を防ぐために、既定では候補を表示するだけで、--delete を付けたときだけ削除します。さらに、日数に関係なく最新の3件は残します。

#!/usr/bin/env python3
"""古いWordPressバックアップを整理する(既定は一覧表示のみ)"""
import argparse
import os
import time

BACKUP_DIR = "/var/backups/wordpress"
SUFFIXES = (".tar.gz", ".sql.gz")
KEEP_LATEST = 3


def find_backups(directory):
    files = []
    with os.scandir(directory) as it:
        for entry in it:
            if entry.is_file(follow_symlinks=False) and entry.name.endswith(SUFFIXES):
                files.append((entry.stat(follow_symlinks=False).st_mtime, entry.path))
    files.sort(reverse=True)  # 新しい順
    return files


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--days", type=int, default=30, help="この日数より古いものが対象")
    parser.add_argument("--delete", action="store_true", help="指定したときだけ実際に削除する")
    args = parser.parse_args()

    now = time.time()
    limit = now - args.days * 86400
    for mtime, path in find_backups(BACKUP_DIR)[KEEP_LATEST:]:
        if mtime >= limit:
            continue
        age = (now - mtime) / 86400
        if args.delete:
            os.remove(path)
            print(f"削除しました: {path} ({age:.0f}日前)")
        else:
            print(f"削除候補: {path} ({age:.0f}日前)")


if __name__ == "__main__":
    main()

ポイントは3つです。対象を拡張子で絞り、シンボリックリンクは対象外にしているので、リンク先の別の場所を消しません。最新の3件を残すため、バックアップの作成が止まっていても手元からすべて消える事態を避けられます。そして、まず --delete なしで実行し、候補の一覧が意図どおりかを確かめてから本番で使います。cron に登録するのは、その確認が済んでからにしてください。

まとめ

  • パスは os.path.join() で組み立て、cron などから動かすスクリプトは __file__ 基準の完全なパスを使う
  • 秘密情報は環境変数から os.environ.get() で読み、必須の値は起動直後に確認する
  • ディレクトリの走査は os.scandir() と os.walk()、置き換えは os.replace()、権限は 0o640 のように 8 進数で書く
  • コマンド実行は os.system() ではなく subprocess.run() に引数をリストで渡す
  • 削除系の処理はドライランを既定にし、残す件数の下限を決めてから自動化する

お気軽にご相談ください

お見積りへ お問い合わせへ
▲