トップページに戻る

shやBashの代わりにPythonを使う

目次

  • はじめに
  • pwd: 作業ディレクトリのパスを表示する
  • ls: ファイルをリストする
  • cd: 作業ディレクトリを変更する
  • cp: ファイルをコピーする
  • mv: ファイルを移動する
  • rm: ファイルを消去する
  • mkdir: ディレクトリを作る
  • ファイル操作その他
  • プロセスの実行

はじめに

Pythonの標準ライブラリを用いると、シェルスクリプト(shやbash)と同等の操作がPython上で行えます。
Pythonを用いることには次のような利点があります。

  • Pythonの強力な文字列操作メソッドを用いられる
  • シェルスクリプトの働きとPythonのその他のライブラリ(NumPyなど)とをシームレスにつなぐことができる

特に後者は、Pythonの強みである「言語自体の汎用性とライブラリによる専門性」を表していると言えるでしょう。

本稿では、Pythonを用いてファイル操作を行う方法と、プロセスを実行する方法について、シェルのコマンドと対応させて簡単に紹介します。

pwd: 作業ディレクトリのパスを表示する

pwdやlsなど、多くのファイル操作コマンドは、標準ライブラリosモジュールを通して実現されています。
現在の作業ディレクトリのパスはos.getcwd()によって得られ、文字列型として返されます。

In [1]:
import os
working_dir = os.getcwd()

cd: 作業ディレクトリを変更する

cd path は os.chdir(path) です。
Python上での作業ディレクトリの変更は、Pythonのセッションが終了するまで(IPythonなどを閉じるまで)有効です。

In [2]:
os.chdir("./my-dir")

ls: ファイルをリストする

ls path は os.listdir(path) で、pathにおけるファイルなどを、文字列のリストとして返してくれます。

In [3]:
os.listdir('.')
Out[3]:
['mycsv1.csv', 'mycsv2.csv', 'mytext2.txt', 'child-dir', 'mytext1.txt']

os.listdirでは、任意の1文字を表す ? や任意の文字 * などのワイルドカード、または正規表現を使うことができません。
ワイルドカードや正規表現を用いて特定のファイル名のみをリストしたいときは、標準ライブラリのglobを用います。
glob.glob(path)で、pathに一致するファイルをリストとして抽出できます。ただし、os.listdirと異なり、ファイル名でなくそのパスを返していることに気をつけましょう。

In [4]:
import glob
ls2 = glob.glob("./*.txt")
for f in ls2:
    print(f)
./mytext2.txt
./mytext1.txt

再帰的なファイルリスト化

glob.glob() 関数に recursive=True を与えてやると再帰的にファイル検索をします。

In [5]:
glob.glob("./**/*.txt", recursive=True)[:10]
Out[5]:
['./mytext2.txt', './mytext1.txt', './child-dir/childtext.txt']

pathlib: 再帰的なファイル一覧

さらに高機能なPath操作をしたい場合は、pathlibライブラリが便利です。
Pathオブジェクトでディレクトリを指定し、glob関数をワイルドカードとともに呼び出すと、サブディレクトリまで再帰的にファイルを検索します。Pathオブジェクトはイテレータになっているため、最初から全部のファイルを探すのではなく、forループごとに次のファイルへ進んでいきます。普通にリストが欲しい場合は、list(path.glob('xxxx'))を呼び出してください。

In [6]:
from pathlib import Path

# Pathオブジェクト(ここでは、今いるディレクトリを基準に作る)
path = Path('.')

#  再帰的にサブディレクトリ内の '.txt'ファイルを全て列挙する
for p in path.glob('./**/*.txt'):
    print(p)
mytext2.txt
mytext1.txt
child-dir/childtext.txt

より詳細な情報は、pathlib公式ドキュメントを参照してください。

mkdir: ディレクトリを作る

mkdir dirname は os.makedirs(dirname) です。

os.mkdir もありますが、再帰的にディレクトリを作る時に少し面倒なので、 os.makedirs を使うと良いです。

参照: Pythonでディレクトリ(フォルダ)を作成するmkdir, makedirs

In [7]:
os.mkdir('./new-dir')
In [8]:
os.makedirs('./new-dir/dep1/dep2/dep3')

# os.mkdir('./new-dir/dep1/dep2/dep3') はエラー

ファイル・フォルダが存在しているか確かめる

os.path.exists 関数を使います。

In [9]:
os.path.exists('new-dir')
Out[9]:
True

cp: ファイルをコピーする

cpやmvなどのコマンドはshutilモジュールによって提供されています。
cp src dst は shutil.copy(src, dst) です。

In [10]:
import shutil
print(glob.glob("./*.txt"))
shutil.copy("mytext1.txt", "copied_mytext1.txt")
print(glob.glob("./*.txt"))
['./mytext2.txt', './mytext1.txt']
['./mytext2.txt', './mytext1.txt', './copied_mytext1.txt']

また、ディレクトリまるごとのコピー(cp -r)は、 shutil.copytree(src, dst) です。

mv: ファイルを移動する

mv src dst は shutil.move(src,dst) です。rename もできます。

In [11]:
print(glob.glob("./*.txt"))
shutil.move("copied_mytext1.txt", "moved_mytext1.txt")
print(glob.glob("./*.txt"))
['./mytext2.txt', './mytext1.txt', './copied_mytext1.txt']
['./mytext2.txt', './mytext1.txt', './moved_mytext1.txt']

rm: ファイルを消去する

rm path は os.remove(path) です。

In [12]:
print(glob.glob("./*.txt"))
os.remove("moved_mytext1.txt")
print(glob.glob("./*.txt"))
['./mytext2.txt', './mytext1.txt', './moved_mytext1.txt']
['./mytext2.txt', './mytext1.txt']

フォルダを削除する

os.rmdir() 関数を使います。空でないとエラーが出ます。

フォルダ内のファイルも丸ごと削除したいときは、 shutil.rmtree() 関数を使います。

In [13]:
os.rmdir('new-dir/dep1/dep2/dep3')
os.path.exists('new-dir/dep1/dep2/dep3')
Out[13]:
False
In [14]:
shutil.rmtree('new-dir')

ファイル操作その他

osのサブモジュールpathは、パスの操作に便利な関数を提供しています。

os.path.exists(path) pathで表されるファイル・ディレクトリが存在していればTrue、存在していなければFalseを返す。
os.path.isdir(path) pathがディレクトリならばTrue、ファイルやシンボリックリンクならばFalseを返す。
os.path.isfile(path) pathがファイルならばTrue、ディレクトリやシンボリックリンクならばFalseを返す。

In [15]:
for file in glob.glob('./**', recursive=True)[:10]:
    print(f'is_dir = {os.path.isdir(file)} :  is_path = {os.path.isfile(file)}')
is_dir = True :  is_path = False
is_dir = False :  is_path = True
is_dir = False :  is_path = True
is_dir = False :  is_path = True
is_dir = True :  is_path = False
is_dir = False :  is_path = True
is_dir = False :  is_path = True

次の2つは、パスの文字列を場所+ファイル名に分割したり、拡張子を取り出すのに使えます。
dir, file = os.path.split(path) pathのファイル等の場所をdirに、ファイル名をfileに文字列として返す。
root, ext = os.path.splitext(path) pathのファイル等の拡張子をextに、拡張子の手前までをrootに文字列として返す。

プロセスの実行 - シェルを呼ぶ場合

上記のファイル操作コマンドだけでなく、Pythonからシェルを呼ぶことで任意のコマンドを実行することができます。

subprocess モジュールを使いましょう( os.system は非推奨)。

subprocess.run - 公式ドキュメント によると、基本的にPythonから子プロセスを実行する場合は、 subprocess.run のみを使えば良いように作られているらしいです。

試しに、 ディレクトリ構成をみやすく表示してくれる tree というプログラムを実行してみます。

In [16]:
# 表示例
! tree -L 2 .
.
├── child-dir
│   └── childtext.txt
├── mycsv1.csv
├── mycsv2.csv
├── mytext1.txt
└── mytext2.txt

1 directory, 5 files

これを subprocess モジュールを使って実行してみましょう。

subprocess.run の引数には、空白で区切ったコマンドをリストで渡してあげます。

In [17]:
import subprocess
subprocess.run(['tree', '.', '-L', '2'])
.
├── child-dir
│   └── childtext.txt
├── mycsv1.csv
├── mycsv2.csv
├── mytext1.txt
└── mytext2.txt

1 directory, 5 files
Out[17]:
CompletedProcess(args=['tree', '.', '-L', '2'], returncode=0)

CompletedProcess というオブジェクトが返っており、 returncode=0 つまりコマンドが正常終了したことがわかります。

標準出力や標準エラーを受け取りたいときは、 capture_output=True をセットしてあげます。

In [18]:
proc = subprocess.run(['tree', '.', '-L', '2'], capture_output=True)
proc
Out[18]:
CompletedProcess(args=['tree', '.', '-L', '2'], returncode=0, stdout=b'.\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 child-dir\n\xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 childtext.txt\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 mycsv1.csv\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 mycsv2.csv\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 mytext1.txt\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 mytext2.txt\n\n1 directory, 5 files\n', stderr=b'')
In [19]:
# proc.stdoutはバイナリなので decode してやると文字列に変換できる
print(proc.stdout.decode('utf-8'))
.
├── child-dir
│   └── childtext.txt
├── mycsv1.csv
├── mycsv2.csv
├── mytext1.txt
└── mytext2.txt

1 directory, 5 files

In [20]:
# あえてエラーを起こしてみる (tree に存在しないオプション -hoge を与えてみる)
proc_failed = subprocess.run(['tree', '-hoge'], capture_output=True)
print(proc_failed.stderr.decode('utf-8'))
tree: missing argument to -o option.

コマンド実行結果の標準エラーもまとめて標準出力にする

stdout=PIPEstderr=STDOUTsubprocess.run に渡すと、エラーも標準出力と一緒に出力されます。

In [21]:
proc = subprocess.run(['tree', '-hoge'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
print(proc.stdout.decode('utf-8'))
tree: missing argument to -o option.

コマンド実行結果をファイルに保存する

In [22]:
# file objectをstdoutに渡す
with open('result.txt', 'w') as fp:
    proc = subprocess.run(['tree', '.', '-L', '2'], stdout=fp)

! cat result.txt
.
├── child-dir
│   └── childtext.txt
├── mycsv1.csv
├── mycsv2.csv
├── mytext1.txt
├── mytext2.txt
└── result.txt

1 directory, 6 files
In [23]:
! rm result.txt

IPythonマジックコマンド

Jupyter notebookやIPythonコンソールのみで使える非常に便利なマジックコマンドと呼ばれるものがあります。
%から始まるコマンド、例えば%pwd%cdなどです。実は、有名なコマンドはだいたい%がなくても実行できちゃいます。しかも、変数への代入もできるのでコマンドの結果を使ったプログラムを書くことができます。

In [24]:
my_wd = %pwd

# 他には %cd や %mkdir などがある

Jupyterからシェルのコマンドそのものを呼ぶ

Jupyterからシェルのコマンドそのものを呼ぶこともできます。
! から始まる行は、!以降の文字列はJupyterが起動しているOSのシェルで実行されます。
コマンド実行によって標準出力に吐かれるログを変数に代入ができます。

In [25]:
!which python
/Users/zawawahoge/.asdf/shims/python

ちなみにwhichコマンドはプログラムバイナリの位置を教えてくれるコマンドです。

In [26]:
python_location = !which python
python_location
Out[26]:
['/Users/zawawahoge/.asdf/shims/python']

「!」コマンドにPython変数を埋め込む

シェルコマンドにPythonの変数を埋め込みたい時ありませんか?実はこれもできます。Python変数の前に$をつけることで、シェルの変数のように扱えます。

In [27]:
!ls
child-dir   mycsv1.csv  mycsv2.csv  mytext1.txt mytext2.txt
In [28]:
a = 'child-dir'
In [29]:
!ls ./$a
childtext.txt

このコマンドはforループの中でも使えます。

In [30]:
langs = ['python', 'ruby', 'java']
for lang in langs:
    path_bin = !which $lang
    print(path_bin)
['/Users/zawawahoge/.asdf/shims/python']
['/usr/bin/ruby']
['/usr/bin/java']

ただし、!を使ったシェルの呼び出しはあなたの環境でしか動かない可能性があるので、他の人にプログラムを渡す場合は使ってはいけません。
osなどの標準ライブラリは環境依存しないように作られているので、こちらを使うようにしましょう。

基礎編

応用編