LoginSignup
41
64

More than 5 years have passed since last update.

Python3.5〜でバッチスクリプトを書く

Last updated at Posted at 2017-08-12

この記事はBashスクリプトをPythonスクリプトで置き換えるためのノウハウをまとめていますが、今後加筆修正を行っていく予定です。

文字列の表示

print関数を使用します。これは可変長引数で、複数与えるとデフォルトではスペースで区切って出力します。

Bash
echo Hello, world!

Python3.5
print("Hello,", "world!")

標準出力以外に出力したい場合はfile=○○の形で指定します。例えば標準エラーの場合は次のようになります。

Bash
echo Error >&2

Python3.5
import sys

print("Error", file=sys.stderr)

ファイルに上書き・追記する場合は次のようになります。

Bash
echo foo > file1.txt
echo bar >> file1.txt
echo hoge >> file2.txt
echo fuga >> file2.txt

Python3.5
with open("file1.txt", "w") as f:
    print("foo", file=f)
    print("bar", file=f)
with open("file2.txt", "a") as f:
    print("hoge", file=f)
    print("fuga", file=f)

なお、表示する引数間のスペース、及び行末の改行文字は別の文字に変更できます。例えば、区切り文字をタブ、行末の文字をなしにする場合は次のようになります。

Python3.5
print("foo", "bar", sep="\t", end="")

オプションの解析

Bashなどでは色々大変なオプションの解析ですが、Pythonではargparseモジュールで簡単に解析できます。基本的には次のように使います。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--verbose", "-v", help="Verbose", action="store_true")
parser.add_argument("--quiet", "-q", help="Suppress messages", action="store_true")
parser.add_argument("target", help="target")
parser.add_argument("source", nargs="+", help="source")
args = parser.parse_args()

function(args.option)

各オプションにはargs.verboseargs.quietなどでアクセスできます。

詳しくは公式の解説を見てください。
https://docs.python.jp/3/library/argparse.html

コマンドの実行

Python3.5からsubprocess.runが使えます。過去バージョンとの互換性を考えないならこれを使います。

ただ実行するだけ

Bash
g++ sample.cpp -o sample -std=gnu++14 -march=native -O3

Python3.5
import subprocess

subprocess.run(["g++", "sample.cpp", "-o", "sample", "-std=gnu++14", "-march=native", "-O3"])

なお、リスト以外でも、iterableならOKです。(タプルなど)

コマンドが0以外で終了したら例外

check=Trueを指定します。例外はsubprocess.CalledProcessErrorです。

Python3.5
import subprocess

subprocess.run(["git", "push"], check=True)

標準出力・エラーを捕捉

stdout=subprocess.PIPEstderr=subprocess.PIPEを指定します。それぞれ返り値のstdoutstderr属性で取り出せます。byte型なので注意してください。

Python3
import subprocess

result = subprocess.run(["apt", "list", "--upgradable"], stderr=subprocess.PIPE, stdout=subprocess.PIPE, check=True)
process(result.stdout)

標準出力・エラーを捨てる

subprocess.PIPEの代わりにsubprocess.DEVNULLを指定します。

標準入力を指定

subprocess.runの引数にinput=を加えます。byte型で指定してください。

Python3.5
import subprocess

assert subprocess.run(["cat"], input=b"aaa\n", stdout=subprocess.PIPE).stdout == b"aaa\n"

リダイレクト

subprocess.runの引数stdin(inputではない)・stdoutstderrの各引数にopen関数などで開いたファイルオブジェクトを指定します。入出力がテキストデータである限りopenの第2引数はbなしでも大丈夫ですが、あった方が確実です。

ディレクトリ関連

追記: pathlib を使った方が楽です。

from pathlib import Path

python_path = Path("/usr/bin/python3")

以下ではpathlibを使わないレガシーな方法を紹介しています。

カレントディレクトリの取得

os.getcwd()で取得できます。または、pathlibを使う場合はPath()(相対パス)・Path.cwd()またはPath().resolve()(絶対パス)で取得できます。

ディレクトリの変更

os.chdirを使用します。

Bash
cd directory

Python3.5
import os

os.chdir("directory")

ディレクトリの中身を表示

os.listdirを使用します。カレントディレクトリの中身を表示する場合には引数は不要です。

Bash
foo=$(ls)

Python3.5
import os

foo = os.listdir()

特定のディレクトリの中身を表示したい時にはそのパスを与えます。

Bash
foo=$(ls bar)

Python3.5
import os

foo = os.listdir("bar")

pathlib.Pathを使う場合にはメソッドiterdir()を使います。返ってくるのはジェネレータなので、forループを使うなり、list()でリスト化するなりしてください。

Python3.5+pathlib
from pathlib import Path

foo = Path().iterdir() # foo=$(ls)
bar = Path("baz").iterdir() # bar=$(ls baz)

# ls qux
for path in Path("qux").iterdir():
  print(path)

パターンマッチングを使用したい場合は、代わりにglob.glob、またはglob.iglobを使用します。前者はリスト、後者はイテレータを返すので、後者はfor文やmapfilterなどに渡すために使うといいでしょう。

Bash
audio_files=$(ls *.wav)

ls *.tmp | xargs rm -f

Python3.5
import glob
import os

audio_files = glob.glob("*.wav")

for tmp_file in glob.iglob("*.tmp"):
    os.remove(tmp_file)

pathlib.Pathを使う場合はメソッドglobを使用します。

Python3.5+pathlib
from pathlib import Path

for tmp_file in Path().glob("*.tmp"):
    tmp_file.unlink()

ディレクトリの作成

単一のディレクトリを作成する場合、os.mkdirを使用します。

Bash
mkdir test_dir

Python3.5
import os

os.mkdir("test_dir")

pathlib.Pathの場合はメソッドmkdirを使用します。

Python3.5+pathlib
from pathlib import Path

Path("test_dir").mkdir()

パーミッションをデフォルト値(777 & umask値)より制限したい場合は、modeを使用します。

Bash
mkdir -m 700 secret_dir

Python3.5
import os

os.mkdir("secret_dir", mode=0o700)

なお、この関数でディレクトリを作成しようとしたもののすでに存在した場合には、例外FileExitsErrorが投げられます。また、この引数はpathlib.Path.mkdirメソッドでも使用できます。

ディレクトリを再帰的に作成したい場合はos.makedirsを使用します。ディレクトリがすでに存在しても構わない場合には、exist_okオプションを使用します。また、os.mkdirと同様modeオプションを使用することもできます。

Bash
mkdir -p /usr/local/my_software

Python3.5
import os

os.makedirs("/usr/local/my_software", exist_ok=True)

なお、exist_ok=Trueを指定しない場合にディレクトリがすでに存在した場合には、例外OSErrorが投げられます。pathlibの場合はmkdirメソッドのparentsオプションを使用します。

Python3.5+pathlib
from pathlib import Path

Path("/usr/local/my_software").mkdir(parents=True, exist_ok=True)

ディレクトリ内部の探索

Bash
find . -type d -exec chmod 700 {} \;
find . -type f -exec chmod 600 {} \;

Python
import os

for root, dirs, files in os.walk("."):
    for dir in dirs:
        os.chmod(0o700, os.path.join(root,dir))
    for file in files:
        os.chmod(0o600, os.path.join(root, file))

引数にPathクラスのインスタンスを渡す場合、Python3.6以降ならそのまま渡して構いません。3.5の場合はstrを利用して一旦文字列化します。

パーミッション・所有権・UID・GID

パーミッションの変更

os.chmodを使用します。

Bash
chmod 700 directory

Python3.5
import os

os.chmod("directory", 0o700)

Pathクラスにも同名のメソッドがあります。

Python3.5+pathlib
from pathlib import Path

Path("directory").chmod(0o700)

また、実行ビットを付与する場合など、元のパーミッション+αの形式で指定する場合には、os.stat・ビット演算と併用してください。

Bash
chmod +x script.sh

Python3.5
import os

os.chmod("script.sh", os.stat("script.sh").st_mode | 0o111)

Pathクラスの場合はos.statの代わりにstatメソッドを使用します。

Python3.5+pathlib
from pathlib import Path

script_path = Path("script.sh")
script_path.chmod(script_path.stat().st_mode | 0o111)

UID・GIDの取得

現在のユーザのUID・GIDを取得するにはそれぞれos.getuid()os.getgid()を使用します。

Python3.5
import os, sys

if os.getuid() != 0:
    print("このスクリプトはrootで実行してください", file=sys.stderr)
    exit(1)

また、指定された名前のユーザのUID・グループのGIDを取得するにはそれぞれpwd.getpwnamgrp.getgrnamを使用します。それぞれの返り値のpw_uidgr_gid属性がそれぞれUID・GIDとなります。なお、指定された名前のユーザ・グループがない場合には例外KeyErrorが返ってきますので、適宜捕捉してください。

Python3.5
import grp
import pwd

assert pwd.getpwnam("root").pw_uid == 0
assert grp.getgrnam("root").gr_gid == 0

所有権の変更

shutil.chownコマンドを使用します。

Bash
chown root:www /var/www/html

Python3.5
import shutil

shutil.chown("/var/www/html", "root", "www")

注: これらの関数はWindowsには実装されていません。また、pwdgrpモジュールもWindowsからは利用できません。

ファイル・ディレクトリのコピー・移動・削除

コピー

ファイルを普通にコピーする場合はshutil.copyを使用します。cpコマンドと同じ感覚で使用します。

import shutil

shutil.copy("old.txt", "new.txt")
shutil.copy("old.txt", "directory")

メタデータもコピーしたい場合はshutil.copy2を使用します。使い方はcopyと同じです。ファイルオブジェクト同士でコピーしたい場合はshutil.copyfileobjを使用します。

import shutil

with open("old.txt", "rb") as old:
    with open("new.txt", "wb") as new:
        shutil.copyfileobj(old, new)

ディレクトリをコピーしたい場合はshutil.copytreeを使用します。

移動

shutil.moveを使用します。os.renameもありますが、ファイル専用です。

削除

ファイルを削除するにはos.removeを使用します。

Bash
rm file

Python3.5
import os
from pathlib import Path

os.remove("file") # or Path("file").unlink()

空のディレクトリを削除する場合には、os.rmdirまたはPathクラスのメソッドunlinkを使用します。空でないディレクトリ全体を再帰的に削除する場合にはshutil.rmtreeを使用します。

Bash
rm -rf /

Python3.5
import shutil

shutil.rmtree("/")

シンボリックリンクの作成

os.symlinkまたはPathクラスのsymlink_toメソッドで作成できます。

Bash
ln -s foo bar

Python3.5
import os

os.symlink("foo", "bar") # or Path("bar").symlink_to("foo")

ファイルのダウンロード

requestsパッケージを使用するといいでしょう。
ファイルの内容を取得して後に直接Pythonで解析する場合は次のように書きます。

import requests

req = requests.get("http://server/page.html")
html = req.text

大きなファイルをダウンロードしたい場合は次のように書くとよいでしょう。

import requests
import posixpath
import os
import shutil

req = requests.get("http://server/largefile.deb", stream=True)

with open(os.path.join("~/ダウンロード",posixpath.basename(req.url)), "wb") as f:
    shutil.copyfileobj(req.raw, f)

万が一同パッケージが使えない場合には、urllib.requestモジュール(https://docs.python.jp/3/library/urllib.request.html)で代用します。

testコマンドの代替

パスの存在チェック

Bash
[[ -e "file.txt" ]] && echo "Exist" || echo "Not exist"

Python3.5
import os

print("Exist" if os.path.exists("file.txt") else "Not exist")
Python3.5+pathlib
from pathlib import Path

print("Exist" if Path("file.txt").exists() else "Not exist")

ファイル/ディレクトリのチェック

Bash
[[ -f "file.txt" ]] && echo "File" || echo "Not file"
[[ -d "directory" ]] && echo "Directory" || echo "Not directory"

Python3.5
import os

print("File" if os.path.isfile("file.txt") else "Not file")
print("Directory" if os.path.isdir("directory") else "Not directory")
Python3.5+pathlib
from pathlib import Path

print("File" if Path("file.txt").is_file() else "Not file")
print("Directory" if Path("directory").is_dir() else "Not directory")

権限のチェック

現在のユーザに対する権限をチェックする場合にはos.accessを利用します。

Bash
[[ -r 'file.txt' ]] && echo 'True' || echo 'False'
[[ -r 'file.txt' -a -w 'file.txt' ]] && echo 'True' || echo 'False'
[[ -r 'directory' -a -x 'directory' ]] && echo 'True' || echo 'False'

Python3.5
import os

print(os.access("file.txt", os.R_OK))
print(os.access("file2.txt", os.R_OK | os.W_OK))
print(os.access("directory", os.R_OK | os.X_OK))

更新日のチェック

更新日のタイムスタンプはos.statまたはPathクラスのメソッドstatで取得できる名前付きタプルのst_mtime属性で取得できます。

ファイルサイズのチェック

ファイルサイズは同じくos.statまたはPathクラスのメソッドstatの返り値のst_size属性で取得できます。ディレクトリの場合、中に入っているファイルの総サイズという訳ではないので注意してください。

アーカイブの圧縮・解凍

shutilの利用

解凍にはshutil.unpack_archiveを利用します。各種tarball・zipならばこれが一番楽です。

Bash
tar xf foo.tar.xz

Python
import shutil

shutil.unpack_archive("foo.tar.xz")

なお、この関数の第2引数で解凍先を指定できます。

Bash
tar xf foo.tar.gz -C bar

Python
import shutil

shutil.unpack_archive("foo.tar.gz", "bar")

あるディレクトリを丸ごと圧縮したい場合にはshutil.make_archiveを使用します。例えばbarという名前のディレクトリを圧縮してfoo.tar.xzという名前のアーカイブを作りたい場合は次のように書きます。

Bash
tar cf foo.tar.xz bar

Python
import shutil

shutil.make_archive("foo", "xztar", base_dir="bar")

gz・bz2・xz

これらのファイルはそれぞれgzipbz2lzmaモジュールを利用すると普通のファイルのように読み書きできます。

import gzip
import bz2
import lzma
import shutil

with gzip.open("file.gz", "rb") as gz:
    with bz2.open("file.bz2", "wb") as bzip2:
        shutil.copyfileobj(gz, bzip2)
    with lzma.open("file.xz", "wb") as xz:
        shutil.copyfileobj(gz, xz)

tar

tarfileモジュールを使います。解凍はextractall()を使用します。例えば、次のように利用します。

import tarfile

with tarfile.open("tarfile.tar.xz") as f:
    f.extractall()

なお、第1引数にディレクトリを指定すると、そこに展開します。tarファイルのファイル一覧は、members()で取得します。(リストが返ってきます)

tarファイルを作成する場合はadd()を使用しますが、open()で指定するファイルモードに注意が必要です。wだけだと非圧縮とみなされます。例えばtar.xz形式のアーカイブを作成したい場合は次のように書きます。

import tarfile

with tarfile.open("archive.tar.xz", "w:xz") as f:
    f.add("directory")

このようにwの後に:(圧縮形式名)を追加します。

zip

zipfile.ZipFileを使用します。(http://docs.python.jp/3.6/library/zipfile.html#zipfile-objects)
例えばZIPファイルの中身をカレントディレクトリ内に全て展開したい場合は次のように書きます。

import zipfile

with zipfile.ZipFile("foo.zip") as ar:
    ar.extractall()

zipファイルにはファイル名のエンコーディング問題があるので、OSのzipunzipコマンドをsubprocess.runで呼び出すといいかもしれません。(Windows・Ubuntu日本語Remix)

一時ファイル・ディレクトリ

一時ファイル・ディレクトリはtempfileモジュールを利用します。

一時ファイル

ファイルを外部から使用しない場合はtempfile.TemporaryFileを使用します。

import tempfile

with tempfile.TemporaryFile() as temp_file_handler:
  temp_file_handler.write(some_byte_array)
  # ...
  some_bytes = temp_file_handler.read(length)
  # ...

外部から使用する場合はtempfile.NamedTemporaryFileを使用します。name属性があり、これでファイル名を取得できます。

import tempfile
import subprocess

with tempfile.NamedTemporaryFile() as temp_file:
  temp_file.write(some_byte_array)
  # ...
  subprocess.run(("external-process", temp_file.name), check=True)
  # ...

拡張子・プレフィックスを指定したい場合はsuffixprefixを使用します。例えば、次のコード

import tempfile

with tempfile.NamedTemporaryFile(prefix="tempfile-", suffix=".deb") as temp_file:
  print(temp_file.name)

は、/tmp/tempfile-hz1u73cr.debのような文字列を出力して終了します。

なお、どちらもwith文を抜けるとファイルは削除されます。また、ファイルの規定のオープンモードはw+b、すなわちバイナリモードです。

一時ディレクトリ

一時ディレクトリはtempfile.TemporaryDirectoryで作成します。通常は次のようにwith文を使用します。

import tempfile
import os
import subprocess

with tempfile.TemporaryDirectory() as temp_dir_name:
  temp_conf_name = os.path.join(temp_dir_name, "hoge.conf")
  with open(temp_conf_name, "w") as temp_conf:
    print("hoge=hogehoge", file=temp_conf)
  subprocess.run(("sudo", "hoge", temp_conf_name), check=True)

この場合、with文を抜けると一時ディレクトリ及びその中のファイルはは削除されます。また、この例の場合temp_dir_nameの型はstrです。

環境変数

os.environという名前の辞書(のようなもの)で取得・設定できます。

Python3.5
import os

print("256色" if "TERM" in os.environ and "256color" in os.environ["TERM"] else "16色")
Python3.5
import os
import pip

if "CUDA_ROOT" not in os.environ:
    os.environ["CUDA_ROOT"] = "/usr/local/cuda"
pip.main(["install", "-U", "chainer"])

文字列の中に環境変数を埋め込みたい場合はos.path.expandvarsを利用します。

Python3.5
import os

WORKING_DIR = os.path.join(COMMON_ROOT, os.path.expandvars("work-$USER"))

Pathクラスのメソッドによるパスの文字列操作

パス連結

Python3.5+pathlib
from pathlib import Path

# 全て Path("/bin/bash")を返す
Path("/bin","bash")
Path("/bin") / "bash"
Path("/bin") / Path("bash")
Path("/bin").joinpath("bash")
Path("/bin").joinpath(Path("bash"))

パス分割

Python3.5+pathlib
from pathlib import Path

p = Path("dir/dir2/file.tar.gz")
print(p.name) # file.tar.gz
print(p.parent) # dir/dir2
print([str(dir) for dir in p.parents]) # ['dir/dir2', 'dir', '.']

print(p.stem) # file.tar
print(p.suffix) # .gz
print(p.suffixes) # ['.tar', '.gz']

親ディレクトリはそのままで名前・拡張子の変更

Python3.5+pathlib
from pathlib import Path

ico = Path("img/favicon.ico")

gitignore = ico.with_name(".gitignore") # img/.gitignore
png = ico.with_suffix(".png") # img/favicon.png

文字列操作

パス操作 (非推奨; Pathクラスの使用を推奨)

各種区切り文字はos.sep(ディレクトリ)・os.extsep(拡張子)・os.pathsep(環境変数PATH)で取得できます。

Python3.5
import os

print(os.sep, os.extsep, os.pathsep)
# Windows: \ . ;
# それ以外: / . :

パスの連結にはos.path.joinを使用します。

Python3.5
import os

os.makedirs(os.path.join(PROJECT_ROOT, "build"), exist_ok=True)

パスをbasename・dirnameに分割するにはos.path.splitos.path.basenameos.path.dirnameを使用します。

Python3.5
import os

dirname, basename = os.path.split(FILE_PATH)
assert dirname == os.path.dirname(FILE_PATH)
assert basename == os.path.basename(FILE_PATH)

パスの拡張子・拡張子以外の部分を取り出す場合はos.path.splitextを使います。

Python3.5
import os

root, ext = os.path.splitext("/etc/nginx/nginx.conf")
print(root, ext)
# /etc/nginx/nginx .conf

ホームディレクトリを表す~を利用したパスを取り扱うにはos.path.expanduserを使用します。

Bash
echo "Your home directory is: $HOME"

Python3.5
import os

print("Your home directory is:", os.path.expanduser("~"))

Windowsとの互換性を意識する場合にはこのようなホームディレクトリを取得する使い方のみをし、os.path.joinを併用してください。

また、環境変数を含んだパスを扱うにはos.path.expandvarsを使用します。

Python3.5
import os

os.makedirs(os.path.join(PROJECT_ROOT, "build", os.path.expandvars("python-$PYTHON_VERSION")), exist_ok=True)

pathlibを使う方法もありますが、現在のところは解説しません。
https://docs.python.jp/3/library/pathlib.html

その他

他にはsedawkgrep相当のことを書く予定です

参考

41
64
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
64