7. スクリプトとしてのPython

これまで見てきたようにPythonの大きな強みは対話的に実行ができることである.一方で対話的な実行では,毎回必ず人間が何らかの操作をしなければならない.Pythonでも最低限の人間の操作でまとまった処理を自動化することができると大変便利である.Pythonではbashなどを用いたシェルスクリプトの代替のような使い方から,GUIアプリケーションや,より高度なプログラム開発まで様々なことができる.ここではコマンドラインで実行できるPythonスクリプト開発の基本を学ぼう.

参考

7.1. スクリプトの基本

7.1.1. 実行方法(復習)

ここ で既に見たように, *.py という名前で保存したPythonのソースコード(スクリプト)はコマンドラインで実行することができる.ここではもう一度 hello.py を見直してみよう.

hello.py (再掲)
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4print('Hello, World !')

この一行目の #!/usr/bin/env python はUnix-likeなOSで

1$ ./hello.py

のように実行するときにこのスクリプトをPythonで解釈するためのオマジナイである.最近は諸々の事情でシステム内にPython環境が複数存在することも多いので,混乱を避けるために以下では

1$ python hello.py

のように実行することにしよう.例えばバージョンの違う複数のPython環境がシステムに存在するときには陽に自分が使いたいPythonのコマンドを指定すればよい.例えば,最近では流石に少なくなったが,古いPython-2.xでしか動かないコードを実行するときには,明示的にそれを指定する必要がある.Python-2.xのパスが /usr/local/bin/python2 であるときに,これを用いてスクリプトを実行したい場合には

1$ /usr/local/bin/python2 hello.py

のように実行すればよい.

7.1.2. 作法

Pythonのスクリプトでは以下のような書き方がよく見られる.

 1#!/usr/bin/env python
 2# -*- coding: utf-8 -*-
 3
 4n = 10
 5x = 3.14
 6
 7
 8def main():
 9    print('main() is called')
10    print('n = {}'.format(n))
11    print('x = {}'.format(x))
12
13
14if __name__ == '__main__':
15    print('This will be printed only if run as a script')
16    main()

12行目以降の if ブロックはこのプログラムがスクリプトとして実行された場合にのみ実行される.これは,このファイルがスクリプトとして実行されたとき(トップレベルスクリプトと呼ぶ)のみ __name__ という特殊な変数に __main__ という名前が(Pythonによって自動的に)与えられるようになっているためである.したがって,このファイルを実行すると以下のような結果が得られる.

1$ python sample1.py
2This will be printed only if run as a script
3main() is called
4n = 10
5x = 3.14

11行目までのコードは変数や関数の宣言のみであり,実行は12行目以降で行われていることが分かる.このような書き方がよく用いられるのは,このファイルが他のファイルや,Jupyter Notebookなどの対話的環境から import される可能性を考慮しているからである.例えばコマンドラインで以下のように実行してみよう.

1$ python -c 'import sample1; sample1.main();'
2main() is called
3n = 10
4x = 3.14

ここで -c オプションで与えられた文字列をPythonが解釈して実行している.まず import sample1 でこのファイルをモジュールとして import し,このモジュールで定義された関数 sample1.main() を実行している.この場合は12行目以降の if ブロックは実行されていないことが分かる.このように単なるスクリプトとしての実行だけでなく,自分が import されたときの場合も考えて

1if __name__ == '__main__':
2    # トップレベルスクリプトとして実行される

のような形でトップレベルスクリプトとしての実行文とそれ以外を分離しておくのが作法になっている.

注釈

スクリプトを開発している際にも対話型の環境を用いると便利である.例えば,ある機能を実装する関数を実装するときにも,対話型の実行環境で簡単に動作確認をしながら実装すると効率が良い.ここで紹介した作法でスクリプトに関数を実装していれば,ソースコードを修正し,その都度Jupyter Notebookなどで importimportlib.reload しながら関数単体の動作確認ができる(トップレベルスクリプトとしては実行されない). 例えばJupyter Notebookのセルで

1>>> import sample1
2>>> sample1.main()

した後に sample1.py を修正したとき,同じJupyterセッション中であっても

1>>> import importlib
2>>> importlib.reload(sample1)
3>>> sample1.main()

のように importlib.reaload することで,その修正を反映させることができる. importlib.reload については 公式ドキュメント を参照のこと.

7.2. ファイルの読み書きと文字列処理

7.2.1. ファイルの読み書き

Pythonによるファイル読み書きはC言語によるものと非常に似ている.まずファイルを開くには以下のように open() を使う.

1>>> f = open('testfile.txt', 'w')
2>>> f.write('hello')
35
4>>> f.close()

ここで open() には開きたいファイル名および読み書きのモードを与える.この例では書き込み w モードでファイルを開く.これによってファイルオブジェクトが返されるので,write() メソッドを呼び出してで文字列を書き込み, close() で最後にファイルを閉じる.次にこのファイルの中身を読み込んでみよう.これにはモードを r としてファイルを開き, read() メソッドを呼び出せばよい.

1>>> f = open('testfile.txt', 'r')
2>>> f.read()
3'hello'
4>>> f.close()

ここで read() はデフォルトでファイルの中身を全て読み込むが,引数で読み込むデータサイズ(バイト単位)を指定することもできる.また readline() は1行を読み込み, readlines() はファイルの中身を全て読み込み,各行をlistとして返す.

また,一度開いたファイルは必要が無くなったら閉じるのが作法であるが,これを忘れるのを防ぐために with が使われることが多い.以下の例を見てみよう.

1>>> # 書き込み
2>>> with open('testfile.txt', 'w') as f:
3>>>     f.write('hello\npython')
4>>> # 読み込み
5>>> with open('testfile.txt', 'r') as f:
6>>>     print(f.readlines())
7['hello\n', 'python']

ここでは with で開いたファイルを f として,ファイルの読み書きを行っているが with のブロックを抜けると,自動的に close() が呼ばれるので,閉じ忘れの心配がない. open() については 公式ドキュメント も参照しよう.

7.2.2. str

Pythonによる文字列処理には組み込みの文字列オブジェクトである str の種々なメソッドを駆使して行えばよい.例えば split() は引数で与えられた文字(デフォルトではホワイトスペース)を区切りとして,元の文字列を分割したlistを返す.また, join() は引数で与えられた文字列のシーケンス(listやtuple)を結合する.使い方は以下の例を見れば明らかであろう.

1>>> 'I love python'.split()
2['I', 'love', 'python']
3>>> '_'.join(['I', 'hate', 'fortran'])
4'I_hate_fortran'

他にも以下の例のように find() メソッドを使って文字列を検索したり,配列のように添字 [] を用いて部分文字列を取り出したりすることができる.

1>>> text = 'Earth and Planetary Physics'
2>>> text.find('Planet')
310
4>>> text[10:]
5'Planetary Physics'

より詳細については 公式ドキュメント を参照のこと.

7.2.3. re

先の例の find() は厳密に一致する文字列を検索するのには十分であるが,より柔軟な文字列の検索・置換には正規表現が用いられる.Pythonでは標準ライブラリの re (regular expression)が正規表現を扱うためのモジュールになっている.正規表現そのものの詳細についてはここでは立ち入らないが,ツールによって微妙に正規表現のシンタックス(文法)が異なることがあるので注意しよう.Pythonの正規表現については 公式ドキュメント を参照して欲しい.

ここでは re モジュールの使い方を簡単に見てみよう.

 1>>> url1 = 'https://www.eps.s.u-tokyo.ac.jp'
 2>>> url2 = 'https://www.eps.s.u-tokyo.ac.jp/epp/'
 3>>> https_pattern = r'https://([a-zA-Z0-9\-\.]+)/?.*'
 4>>> # 検索
 5>>> re.search(https_pattern, url1)
 6<re.Match object; span=(0, 31), match='https://www.eps.s.u-tokyo.ac.jp'>
 7>>> re.search(https_pattern, url2)
 8<re.Match object; span=(0, 36), match='https://www.eps.s.u-tokyo.ac.jp/epp/'>
 9>>> # グループを取り出す
10>>> re.search(https_pattern, url1).groups()
11>>> ('www.eps.s.u-tokyo.ac.jp',)
12>>> re.search(https_pattern, url2).groups()
13>>> ('www.eps.s.u-tokyo.ac.jp',)

上のコードでは re.saerch() を使って文字列 url1 および url2 からhttpsプロトコルのURLを表すパターン https_pattern を検索している.パターンが見つかると re.search()re.Match オブジェクトを返す.ここで謎の呪文のようにも見える https_pattern が正規表現である.ここで [a-zA-Z0-9\-\.]+ がアルファベット(大文字・小文字),数字, - および . の1回以上の繰り返しを意味している.このパターンが () で囲まれているが,これはグループ化することを意味しており, re.Match オブジェクトの groups() メソッドを呼ぶことでグループを取り出すことができる. re.Match そのものはURL全体にマッチしているが,この場合はグループの最初の要素が / で区切られる前のURLのドメイン部分だけを示している.

以下の例では re.sub() を用いてURLの httpshttp に置換している.

1>>> re.sub(r'https://([a-zA-Z0-9\-\.]/?.*)', r'http://\1', url1)
2>>> 'http://www.eps.s.u-tokyo.ac.jp'
3>>> re.sub(r'https://([a-zA-Z0-9\-\.]/?.*)', r'http://\1', url2)
4>>> 'http://www.eps.s.u-tokyo.ac.jp/epp/'

ここで re.sub() の第2引数の \1 は第1引数で指定したパターンの () で囲まれたグループが代入される.

re.finditer() は検索したパターンを順に処理するループを記述するのに便利である. sample2.py はPythonのソースコードから関数定義を検索するサンプルで,以下には re.finditer() を用いたループを抜粋している.第1引数 patern にマッチする部分を第2引数 text から検索して,見つかった順にループ内で処理をしている.

sample2.py (抜粋)
13    for m in re.finditer(pattern, text, re.MULTILINE):
14        groups = m.groups()
15        if len(groups) >= 1:
16            # 関数名
17            name = groups[0]
18            # 引数リスト
19            args = groups[1].split(', ')
20            while args.count('') > 0:
21                args.remove('')
22            narg = len(args)
23            print('found function named {} with {} argument(s)'.format(name, narg))

このように re モジュールを使うことで柔軟な検索や置換処理が実装できる.

7.3. システムインターフェース

7.3.1. ファイルやディレクトリの操作

ファイルやディレクトリの操作をするための便利な関数群がPythonの標準ライブラリで提供されている.bashなどのシェルスクリプトは基本的にはUnix-likeなOSでの動作が前提となるが,Pythonのライブラリをうまく使うことでOSなどの環境に依らないプログラムを作ることが可能である [1]

これには標準ライブラリの os モジュール(およびそのサブモジュール)を import して使えばよい.例えば os.listdir() は与えられたパス内の全てのファイルとディレクトリをlistオブジェクトとして返す.例えばシェルで

1$ ls
2test.txt  testdir/

となる環境であれば,

1>>> os.listdir('.')
2['testdir', 'test.txt']

なる結果が得られる.例えばこの結果として返されるlistの各要素がファイルかディレクトリか調べるには os.path.isfile()os.path.isdir() を使えばよい.例えば

1>>> for f in os.listdir('.'):
2>>>     if os.path.isfile(f):
3>>>         print('"{}" is a file'.format(f))
4>>>     if os.path.isdir(f):
5>>>         print('"{}" is a directory'.format(f))
6"testdir" is a directory
7"test.txt" is a file

のような結果が得られる.また,以下は指定されたディレクトリのファイルのうちPythonのソースコードだけを抽出し,各ファイルの行数を数える関数 count_py_lines1() の実装例である.

sample3.py (抜粋)
12def count_py_lines1(dirname):
13    print('*** count_by_lines1')
14    # ファイルとディレクトリのリスト
15    files = os.listdir(dirname)
16    files.sort()
17    for f in files:
18        # 拡張子をチェックしてpythonのソースであれば行数を数える
19        if os.path.splitext(f)[1] == '.py':
20            with open(f, 'r') as fp:
21                c = len(fp.readlines())
22            print('{} : number of lines = {}'.format(f, c))

ファイルやディレクトリの操作で注意しなければならないのはパスの区切り文字である.Windowsではパスの区切り文字として \ が使われているのに対してMacやLinuxなどでは / が使われているので,どちらの環境でも動くスクリプトにするためにはプログラム側でこの違いを吸収してやらなければならない.Pythonでは os.sep (または os.path.sep )がパスの区切り文字として定義されているので,これを使うことでプラットフォームに依存しないプログラムとすることができる.例えば os.getcwd() で現在の作業ディレクトリを取得し,これにファイル名を文字列として結合して絶対パスとするには os.path.join() を用いて

1>>> os.path.join(os.getcwd(), 'test.txt')
2/home/hoge/test.txt

のようにすればよい.ここでは os.getcwd() で得られる /home/hogetest.txt が区切り文字 / で結合されているが,同じコードをWindowsで実行すれば自動的に区切り文字として \ が選ばれる.同じことが

1>>> os.sep.join([os.getcwd(), 'test.txt'])
2/home/hoge/test.txt

でも可能である.これは str オブジェクトである os.sepjoin メソッドを使った例である.

他にもファイルの作成やファイル名変更,削除などの一通りの作業ができる.詳細については 公式ドキュメント を確認して欲しい.ただし,ファイルのコピーや削除などのより高レベルの(より簡単な)ファイル操作には標準ライブラリの shutil モジュール( 公式ドキュメント )を用いる方が便利である.

7.3.2. シェルコマンドの実行

os モジュールの os.system() を使うとPythonプログラムからシェルコマンド(例えば lscat など)を実行することができる.使い方は以下の通りである.

1>>> r = os.system('echo "hello shell"')
2hello shell
3>>> print(r)
40

ここでシェルで実行されたコマンドの終了ステータスが返値となる.

単にコマンドを実行するだけであればこれでも十分だが,実用的にはシェルコマンドに何らかの入力を与えたり,また実行結果を受け取って処理をしたくなってくる.これには subprocess モジュールを使うのがよい.例えば,先ほどの例と同じことは subprocess.run() を使って

1>>> r = subprocess.run('echo "hello shell"', shell=True)
2hello shell
3>>> print(r.returncode)
40

のように実現できる.ここで subprocess.run()shell=True としているのは第1引数で与えた文字列をシェルで解釈するという意味である.このオプションを指定しないときには

1>>> r = subprocess.run(['echo', 'hello shell'])
2hello shell

のようにlistとしてコマンドラインに与える引数を与える必要がある.(シェルとして解釈させるのはセキュリティ的にあまり好ましくないので注意する必要がある.)

シェルのパイプ( | )のように標準出力をPythonで受け取って処理をするには capture_output=Truetext=True を指定すればよい [2]

1>>> r = subprocess.run(['echo', 'hello shell'], capture_output=True, text=True)
2>>> print(r.stdout)
3hello shell

基本的にはシェルコマンドを使わないでもPythonだけで実現できることがほとんどであるが,シェルなら簡単にできるようなことであれば,このようにその実行結果を受け取って処理する方が話が早いこともあるだろう.例えば以下は先に示した関数 count_py_lines1() と全く同じ機能をシェルコマンドで実現する関数 count_py_lines2() の実装例である.

sample3.py (抜粋)
25def count_py_lines2(dirname):
26    print('*** count_by_lines2')
27    # wcコマンドで行数を数える
28    cmd = 'wc -l {}/*.py'.format(dirname)
29    r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
30    # コマンドの出力からファイル名と行数を取り出す
31    for line in r.stdout.split('\n'):
32        l = line.strip().split()
33        if len(l) == 2 and os.path.splitext(l[1])[1] == '.py':
34            c = l[0]
35            f = os.path.split(l[1])[1]
36            print('{} : number of lines = {}'.format(f, c))

7.4. コマンドライン引数の処理

Unixのコマンドは多くの引数やオプションを受け取り,それに応じて異なる処理を実行する.Pythonでは標準ライブラリの argparse を使うことで,コマンドラインで与えられた引数を簡単に処理することができる.使い方は簡単で, argparse.ArgumentParser() でオブジェクトを生成し, add_argument() でオプションを順次定義していけばよい.以下の例は sample4.py の抜粋である.

sample4.py (抜粋)
 8def parse_args():
 9    parser = argparse.ArgumentParser(description='Print Greetings')
10    # 整数
11    parser.add_argument('-i', '--integer', type=int,
12                       help='number of output')
13    # 文字列
14    parser.add_argument('-g', '--greeting', type=str,
15                       help='greeting')
16    # 真偽値(デフォルトはFalse)
17    parser.add_argument('-c', '--capitalize', action='store_true', default=False,
18                        help='capitalize or not')
19
20    # デフォルトではsys.argvをパース
21    return parser.parse_args()

このサンプルでは --integer で指定された回数だけ --greeting で指定された文字列を出力する.また --capitalize を指定されると,指定された文字列の各単語の先頭文字を大文字に変換する.以下はコマンドの具体的な実行例である.

 1$ python sample4.py --integer 3 -c -g 'hello python scripting'
 2*** command-line arguments
 3['sample4.py', '--integer', '3', '-c', '-g', 'hello python scripting']
 4
 5*** parse results
 6option: integer => 3
 7option: greeting => hello python scripting
 8option: capitalize => True
 9
10*** show greetings
11Hello Python Scripting
12Hello Python Scripting
13Hello Python Scripting

Unixの多くのコマンドでは長いオプションと,その省略形のオプションを使うことができるが,それはこの例でも同様である.例えば,11-12行目は --integer とその省略形 -itype=int で整数型として定義している.また17-18行目は --capitalize で指定される真偽値のオプションを定義しており,デフォルトでは False だが,オプションが指定されると True となるよう action='store_true' で指定されている.オプションの指定が終わった後にパーサーオブジェクトの parse_args() メソッドを呼ぶと自動的に sys.argv に格納されているコマンドラインオプションがパースされるので,後はこれを適宜用いればよい. parse_args() で返されるオブジェクトにはそれぞれのオプションがアトリビュートとして保存されているので,例えば --integer オプションであれば .integer アトリビュートを参照すればよい.

なお,この argparse を使うと自動的にヘルプメッセージが生成され,オプションとして -h--help を指定すると以下のようにヘルプメッセージが表示される.

 1$ python sample4.py -h
 2usage: sample4.py [-h] [-i INTEGER] [-g GREETING] [-c]
 3
 4Print Greetings
 5
 6optional arguments:
 7  -h, --help            show this help message and exit
 8  -i INTEGER, --integer INTEGER
 9                        number of output
10  -g GREETING, --greeting GREETING
11                        greeting
12  -c, --capitalize      capitalize or not

7.5. 第7章 演習課題

7.5.1. 課題1

サンプルを実行して動作を確認せよ.

7.5.2. 課題2

コマンドラインで与えられた(HTTPまたはHTTPSプロトコルの)URLにアクセスして得られた結果(html)からtitleタグの内容を抽出して表示するプログラムを作成せよ. urllib.request.urlopen() を使って得られたバイト列を文字列に変換(デコード)し,正規表現を用いてtitleタグの中身を抽出すればよい.サイトによって様々な文字コードが使われているので, chardet.detect() で推測したエンコーディングでデコードするのがよいだろう.例えば,以下のような実行結果が得られればよい.

1$ python kadai2.py https://www.eps.s.u-tokyo.ac.jp/
2東京大学 理学部 地球惑星物理学科・地球惑星環境学科/大学院理学系研究科 地球惑星科学専攻 |
3$ python kadai2.py https://www.yahoo.co.jp/
4Yahoo! JAPAN

7.5.3. 課題3

シェルの ls コマンドのようにカレントディレクトリに存在するファイルやディレクトリのリストを表示するプログラムを作成せよ.ただし,デフォルトではファイル名のアルファベット順( ls のデフォルト)で, -S オプションが指定された場合はファイルサイズの降順で( ls -S と同じ順で)表示するものとする.また,どちらの場合も -r が指定された場合には表示順を逆にせよ( ls と同じ仕様である).例えば以下のような実行結果が得られればよい.

1$ python kadai3.py -S
2    2844 Fri Apr 22 13:42:47 2022 kadai4.py
3    1707 Fri Apr 22 13:42:47 2022 kadai3.py
4     763 Fri Apr 22 13:42:47 2022 kadai2.py
5$ python kadai3.py -Sr
6     763 Fri Apr 22 13:42:47 2022 kadai2.py
7    1707 Fri Apr 22 13:42:47 2022 kadai3.py
8    2844 Fri Apr 22 13:42:47 2022 kadai4.py

なお,この例ではファイル名だけでなく,ファイルサイズや更新日付も表示している.

7.5.4. 課題4

Open-Meteo というサイトでは,無料で天気予報データをダウンロードできるサービスを提供している.決められた形式でHTTPSアクセスをするとデータがJSON形式でダウンロードされる仕組みである.このサイトの東京の天気予報データにアクセスする URL を使って,ダウンロードしたデータを自動でプロットするプログラムを作成せよ.ただし,オプション無しで実行した場合には画面にプロットを表示し, --save オプションでファイル名を指定した場合には,指定されたファイル名(例えばpng形式など)でプロットをファイルに保存するようにせよ.例えば以下のようなプロットが得られればよい.この例のURLでは温度,降水量,風速と風向きをプロットしている.(Open-Meteoのサイトではダウンロードするデータを自分でカスタマイズすることができる.)

_images/chap07_openmeteo.png

ここで, matplotlib を使うのであれば,時間が numpy.datetime64 形式の配列になっていれば,横軸は自動でこの例のようにフォーマットされる.なお,このようなスクリプトで実行する際にはプロットした後に matplotlib.pyplot.show() を呼ぶと画面にウインドウが表示される.画面に表示せずにファイルに保存したい場合は matplotlib.pyplot.savefig() を使えばよい.