Skip to content

Instantly share code, notes, and snippets.

@ginrou
Last active September 18, 2021 06:46
Show Gist options
  • Save ginrou/5787895 to your computer and use it in GitHub Desktop.
Save ginrou/5787895 to your computer and use it in GitHub Desktop.
パーフェクトPython 読書会 10章 コマンドラインユーティリティ

10章 コマンドラインユーティリティ

目次

  • 1 Pythonでのファイルの取扱と文字コード
  • 1 単純なファイルの読み込み
  • 2 少しずつ読み取る
  • 3 文字コードを指定して開く
  • 4 バイナリで開く
  • 5 bytesとstrの関係
  • 6 特定の文字列の出現回数を数える
  • 7 標準入出力をファイルのように扱う
  • 2 文字列のフォーマット
  • 1 formatメソッド
  • 2 formatメソッドで数値を表示する
  • 3 プロパティやサブスクリプションをフォーマット文字列で扱う
  • 4 フォーマットの注意点
  • 5 ユーザー定義クラスにフォーマットを定義する
  • 3 さらにテキストファイルを極める
  • 1 複数ファイルを扱うためのfileinput
  • 2 文字列検索ユーティリティ
  • 3 行の分割
  • 4 集計
  • 5 データを表示する
  • 4 Pythonオブジェクトでデータ処理
  • 1 TODOリスト
  • 2 データ構造を決める
  • 3 タスクを作成して登録する
  • 4 データを操作する
  • 5 タスクを保存する
  • 6 TODOリストファイル
  • 7 タスクをShelfに保存する
  • 8 タスクをすべて取り出す
  • 9 タスクをフィルタリングする
  • 5 コマンドラインアプリケーションとコマンドライン引数
  • 1 スクリプトファイルを実行可能にする
  • 2 コマンドライン引数
  • 3 コマンドラインオプション
  • 4 サブコマンド
  • 6 TODOアプリケーション
  • 1 アプリケーションの開始点
  • 2 サブコマンドごとの処理
  • 7 まとめ

1 Pythonでのファイルの取扱と文字コード

 

1 単純なファイルの読み込み

ファイルを開いて、読み込んで、閉じる

f = open('test.txt') ## ファイルをオープン
print( f.read(), end="") ## ファイルを読み込み
f.close()

f.read() とするとファイルを全部読み込むことができる

2 少しずつ読み取る

ファイルライクオブジェクト(f.openの戻り値)は一行分ずつstrを返すiteratorなので、for文で一行ずつループすることができる。

f = open('test.txt') ## ファイルをオープン
for line in f:
    print(line, end="")
f.close()

iteratorについてはここらへんを参考にしてください http://jutememo.blogspot.jp/2008/06/python.html

3 文字コードを指定して開く

open関数の引数で ** encoding ** を指定することでその文字コードで読み込むことができる

f = open('text.txt', encoding='utf-8')

例)

IHR ihr
foo bar
xhr xhr
presto (・w・)

こんなファイルをutf-8で保存して

>>> f = open('test.txt', encoding='utf-8')
>>> print(f.read(), end="")

とすると、きちんと表示される。EUC-JPで表示しようとすると

>>> f = open('test.txt', encoding='euc-jp')
>>> print(f.read(), end="")
UnicodeDecodeError: 'euc_jp' codec can't decode byte 0x88 in position 33: illegal multibyte sequence

と出てきて読み込むことが出来ない

4 バイナリで開く

open関数の引数で ** mode ** にbを追加するとバイナリ読み込みモードとなる。 上のtxtファイルを読み込んで表示すると次のようになる

>>> f = open('text.txt', mode='rb')
>>> f.read()
b'IHR ihr\nfoo bar\nxhr xhr\npresto \xef\xbc\x88\xe3\x83\xbb\xef\xbd\x97\xe3\x83\xbb)\n'

5 bytesとstrの関係

strオブジェクトは内部で文字列をユニコードで扱っています。strオブジェクトをコンソールやファイルに出力するには bytesオブジェクトのバイト列に戻す必要があります。

>>> s = 'あ'
>>> print(s)
あ
>>> print(s.encode('utf-8'))
b'\xe3\x81\x82'
>>> b = b'あ' ## syntax error になる
>>> b = b'\xe3\x81\x82' ## これなら大丈夫
>>> print( b.decode('utf-8') )
あ

6 特定の文字列の出現回数を数える

#!/usr/bin/env python3.3

with open('test.txt') as f:
    search = input() ## wait for input
    count = 0
    for line in f:
        if line.find(search) > -1: ## cannot use line.indexof 
            count += 1
    print(count)

input()は標準入力を待つメソッドっぽい。(2.7では使えなかった) 3.3だと書籍に書いてあったindexofがstrオブジェクトになかったので代わりに似たようなfindメソッドを使いました

7 標準入出力をファイルのように扱う

標準モジュールであるsysモジュールのsys.stdinsys.stdoutを使うことでファイルの入出力のように扱うことができます。

#!/usr/bin/env python3.3

import sys

def echo(in_, out):
    for line in in_ :
        out.write(line) ## そのまま出力
        out.write( "{0} {1}".format(len(line), line)) ## 先頭に文字数を表示

echo(sys.stdin, sys.stdout)

2 文字列のフォーマット

strオブジェクトを良い感じにフォーマットする。割と便利っぽいけど、

>>> print ("%d, %lf, %e" %(10, 20.004, 10.00))
10, 20.004000, 1.000000e+01

のように%でフォーマット指定子使うことができるし、pprintというモジュールもあるのでそっちを使うことも検討したほうがいいかもしれない。

1 formatメソッド

フォーマットするにはformatメソッドを利用します。

>>> "{0:04}".format(2)
0002

{0:04}は、formatメソッドの0番目の引数で入れ替えられます。今回は2が入れ替えられます。":"以降がフォーマットで、"04"としている今回は4桁で表示して0でで埋め合わせます.

>>> s = "{0:04} -> {1:04}"
>>> s.format(2,8)
'0002 -> 0008'

2 formatメソッドで数値を表示する

","をフォーマットに指定することで、3桁ごとに","で区切った文字列が表示されます。

>>> "---{0:,}---".format(1234567890)
'---1,234,567,890---'

3 プロパティやサブスクリプションをフォーマット文字列で扱う

プレースホルダの指定は引数だけでなくそのオブジェクトのプロパティを指定出来ます。

>>> class Point:
>>>     def __init__(self, x, y):
>>> 	self.x = x
>>> 	self.y = y
>>> 
>>> p = Point(10,20)
>>> str_ = "({0.x:04}, {0.y:04})".format(p)
>>> print(str_)
(0010, 0020)

listやdictも利用することができます。

>>> list = ['foo', 'bar', 'ihr', 'presto']
>>> "{0[0]}, {0[3]}".format(list)
'foo, presto'
>>> dict = { 'foo' : 'bar', 'ihr' : 'IHRIHR' }
>>> "{0[foo]}, {0[ihr]}".format(dict)
'bar, IHRIHR'

4 フォーマットの注意点

辞書のキーに指定する際に、ダブルクォーテーションやシングルクォーテーションは必要ないので注意が必要

>>> dict = { 'foo' : 'bar', 'ihr' : 'IHRIHR' }
>>> dict['foo']
'bar'
>>> "{0['foo']}".format(dict)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: "'foo'"
>>> "{0[foo]}".format(dict)
'bar'

辞書のキーで数字をしていすると取り出せないこともあり得る。

5 ユーザー定義クラスにフォーマットを定義する

__format__ メソッドをオーバーライドすることで format(obj)のようにしてフォーマットすることができる。

class Point:
    def __init__(self, x, y):
	self.x = x
	self.y = y

    def __format__(self, spec):
	return ("({0.x:"+spec+"}, {0.y:"+spec+"})").format(self)

p = Point(10,20)
print (format(p, "04"))

実行結果

(0010, 0020)

3 さらにテキストファイルを極める

もう少し実用的なテキストファイルの触り方です

1 複数ファイルを扱うためのfileinput

複数のファイルを一括で扱うためのモジュールとしてfileinputがあります。 複数のファイル名を入力として受け取り、それらのファイルの各行を順番にiteratorとして扱います。

with fileinput.FileInput(files=sys.argv[1:]) as f:
    for line in f:

としたとき、lineには1つ目のファイルの1行目, 2行目...最終行, 2つ目のファイルの1行目, 2行目... という風にはりいます。

-----------test1.txt----------
aaa
bbb
ccc
ddd
eee
-----------test2.txt----------
ihr
ihr
ihr
presto
shigeta
-----------test3.txt----------
satou haruki
asami yuma
ogura yuzu
julia
yoshizawa akiho
siina yuna
tsubomi
sakura mana

としたとき、

#!/usr/bin/env python3.3
import sys, fileinput

with fileinput.FileInput(files=sys.argv[1:]) as f:
    for line in f:
	print(line, end="")

の出力は

aaa
bbb
ccc
ddd
eee
ihr
ihr
ihr
presto
shigeta
satou haruki
asami yuma
ogura yuzu
julia
yoshizawa akiho
siina yuna
tsubomi
sakura mana

となります。

2 文字列検索ユーティリティ

いくつかのモジュールを使いながら文字列検索ユーティリティを作ります。

  • fileinput : ファイルの読み込み
  • argpase : コマンドライン引数を良い感じに扱ってくれる
#!/usr/bin/env python3.3
import sys, fileinput, argparse

parser = argparse.ArgumentParser()
parser.add_argument('-e', '--encoding', default=None)
parser.add_argument('search', help="search word")
parser.add_argument('files', nargs="+")

args = parser.parse_args()


search = args.search
def enc_open(filename, mode):
    return open(filename, mode=mode, encoding  = args.encoding )

with fileinput.FileInput(files=args.files, openhook=enc_open) as f:
    for line in f:
	if line.find(search) > -1:
            print("{0:20}:{1:4} {2}".format( f.filename(), f.lineno(), line), end="")

3 行の分割

各行を分割するにはsplitメソッドを使います。

>>> str = "hoge,fuga,piyo"
>>> str.split(',')
['hoge', 'fuga', 'piyo']

splitメソッドは引数としてセパレータ文字列と最大分割数を渡すことができます。デフォルトでは空白文字で分割します。

さらに、文字列の先頭や終端の文字を削除するにはstripメソッドを用います。こちらも削除対象の文字列を渡すことができますが、デフォルトでは空白文字です。

>>> "  ihrihr  ".strip() ## デフォルトでは空白文字が削除される
'ihrihr'
>>> "xxxxihrihrxxxx".strip('x') ## 削除する文字を指定する
'ihrihr'

先頭だけ削除したい場合はlstripを、終端だけ削除したい場合はrstripを用います。

4 集計

csvのようなカンマ区切りのテキストファイルのデータを辞書にまとめます。 女優名をキーに、出演タイトル数を返す辞書を作ります。

def sum_product(filename):
    with open(filename) as f:
        results = {}
        for line in f:
            parts = line.split(",")
            name = parts[0]
            count = int(parts[1])
            last_count = results.get(name, 0) ## 重複してファイルにあった場合は加算する
            results[name] = count + last_count

	return results

5 データを表示する

こんなデータファイルがあったときに、先ほどのsum_product関数を用います

// 女優名, 出演タイトル数, バスト, ウエスト, ヒップ
satou haruki, 456, 89, 59, 88
asami yuma, 363, 96, 58, 88
ogura yuzu, 68, 83, 59, 83
julia, 317, 101, 55, 84
yoshizawa akiho, 547, 86, 58, 86
siina yuna, 217, 88, 58, 85
tsubomi, 941, 84, 58, 85
sakura mana, 20, 89, 56, 89

実行内容

results = sum_product('test3.txt')

for key, value in results.items():
    print(key, value)

実行結果

ogura yuzu 68
sakura mana 20
tsubomi 941
asami yuma 363
satou haruki 456
julia 317
yoshizawa akiho 547
siina yuna 217

4 Pythonオブジェクトでデータ処理

TODOリストをテキストファイルで管理するアプリケーションを作ります。

使うモジュール

  • datetime
  • pickle
  • shelve

1 TODOリスト

spec:

  • TODOリストはタスクの一覧を持つ
  • タスクは、名前と締切日、予測時間を持つ

2 データ構造を決める

タスクのデータ構造は

  • 名前(name) : str
  • 締切日(due_date) : datetimeモジュールのdatetime (datetime.dateも使えるがパーサーなどの都合でこちらのほうが良い)
  • 予測時間(required_time) : datetime.time
  • 状態(finished) : Bool

これらを辞書で持つ

3 タスクを作成して登録する

タスクを作成する関数create_taskを以下のように定義します

def create_task(name, due_date, required_time):
    return dict( name=name, due_date=due_date, required_time=required_time, finished=False)

次のように使います。

#!/usr/bin/env python3.3
# -*- coding: utf-8 -*-

from datetime import datetime, time

name = "最初のタスク"
due_date = datetime(2013, 6, 16)
tm = time(0,20)

task = create_task(name, due_date, tm)
print(task) ## {'finished': False, 'due_date': datetime.datetime(2013, 6, 16, 0, 0), 'required_time': datetime.time(0, 20), 'name': '最初のタスク'}

フォーマットする関数を追加します

def format_task(task):
    state = "完了" if task['finished'] else "未完了"
    format = "{state} {task[name]} : {task[due_date]:%Y-%m-%d} まで 予定所要時間 {task[required_time]}分"
    return format.format(task=task, state=state)
>>> task = create_task(name, due_date, tm)
>>> print(format_task(task))
未完了 最初のタスク : 2013-06-16 まで 予定所要時間 00:20:00分

4 データを操作する

タスクを完了する関数を追加

def finish_task(task):
    task['finished'] = True

5 タスクを保存する

pickleモジュールを使うことでオブジェクトをファイルに保存することができます。

import pickle

def save_task(task, filename):
    pickle.dump(task, open(filename, "wb"))

def load_task(filename):
    return pickle.load(open(filename, "rb"))

使い方

task = create_task("最初のタスク", datetime(2013,6,16), time(0,20))
task2 = create_task( "4章を作る", datetime(2013, 6, 17), time(0, 30))
tasks = [task, task2]
save_task(tasks, "tasks.txt")
saved_tasks = load_task("tasks.txt")
print( saved_tasks )

タスクはリストに入れています。

出力結果

[{'name': '最初のタスク', 'required_time': datetime.time(0, 20), 'finished': False, 'due_date': datetime.datetime(2013, 6, 16, 0, 0)}, {'name': '4章を作る', 'required_time': datetime.time(0, 30), 'finished': False, 'due_date': datetime.datetime(2013, 6, 17, 0, 0)}]

注意点)

  • pickleモジュールはバイナリモードで開く必要があります
  • バイナリで保存されるのでそのままでは読めません

6 TODOリストファイル

キーを指定してオブジェクトを格納して、ファイルに保存してくれるshelveモジュールのShelfオブジェクトを利用します

>>> import shelve
>>> db = shelve.open("data_store", "c")  ## Shelfオブジェクトを生成. 保存ファイルとして data_store というファイルを利用
>>> len(db)
0
>>> db['test_data'] = {"message":"Hello, world!!"} ## test_data をキーとしてマップを登録
>>> db.close() ## data_store に書き込み
>>> db = shelve.open("data_store", "c")
>>> len(db)
1
>>> db['test_data']
{'message': 'Hello, world!!'}

shelveの保存にはpickleモジュールを利用します

7 タスクをShelfに保存する

先ほど用いたshelfを利用して、タスクを保存します。shelfにデータを保存するにはキーが必要となります。ここでは連番を生成して順次キーを生成する関数を作ります。連番の元になる値もshelfに保存します。

def next_task_key(db):
    id_ = db.get('next_id', 0)
    db['next_id'] = id_ + 1
    return "task:{0}".format(id_)

こんなかんじで使います

db = shelve.open("task.db", "c")
task = create_task("最初のタスク", datetime(2013,6,16), time(0,20))
task2 = create_task( "4章を作る", datetime(2013, 6, 17), time(0, 30))
add_task(db, task)
add_task(db, task2)

さらにこの関数を用いてshelfにタスクを追加する関数を追加します。

def add_task(db, task):
    key = next_task_key(db)
    db[key] = task

8 タスクをすべて取り出す

すべてのタスクを取り出して表示するには次のようにします。

db = shelve.open("task.db", "c")
for key in db:
    print (key,	db[key])

出力はこんな感じ

task:1 {'finished': False, 'required_time': datetime.time(0, 30), 'name': '4章を作る', 'due_date': datetime.datetime(2013, 6, 17, 0, 0)}
task:0 {'finished': False, 'required_time': datetime.time(0, 20), 'name': '最初のタスク', 'due_date': datetime.datetime(2013, 6, 16, 0, 0)}
next_id 2

こんな感じに条件を追加して、"task:" で始まるキーをまとめて返す関数を作ります。

def all_tasks(db):
    for key in db:
	if key.startswith('task:'):
            yield key, db[key]

yieldは必要になるまで処理を行わない、ジェネレータのための何からしいです。

こんな感じで使います。

db = shelve.open("task.db", "c")
for k, v in all_tasks(db):
    print(k,v)

(k,v)のループが実行されるまでall_tasksのループは実行されません。

9 タスクをフィルタリングする

未完了のタスクのみを取り出します。

def unfinished_task(db):
    return ((key, task) for (key, task) in all_tasks(db) if not task['finished'])

使い方と実行結果

>>> db = shelve.open("task.db", "c")
>>> for k, v in unfinished_task(db):
>>>     print(k,v)
task:1 {'required_time': datetime.time(0, 30), 'due_date': datetime.datetime(2013, 6, 17, 0, 0), 'finished': False, 'name': '4章を作る'}
task:0 {'required_time': datetime.time(0, 20), 'due_date': datetime.datetime(2013, 6, 16, 0, 0), 'finished': False, 'name': '最初のタスク'}

5 コマンドラインアプリケーションとコマンドライン引数

1 スクリプトファイルを実行可能にする

chmod +x してください

2 コマンドライン引数

コマンドライン引数をうけとるにはsysモジュールをインポートしてsys.argvの中に入っています

こんなプログラムを書いて

#!/usr/bin/env python3.3
import sys
for a in sys.argv:  print(a)

実行する

$ ./sample.py a b c d
./sample.py
a
b
c
d

3 コマンドラインオプション

もう少し複雑なコマンドライン引数を良い感じに扱ってくれるargparseで取り扱う

### arg_parse.py
#!/usr/bin/env python3.3
import argparse

parser = argparse.ArgumentParser() ## インスタンス生成
parser.add_argument('-n', '--name', default="world") ## -n, --name 引数を指定
args = parser.parse_args() ## 引数をパース

print("Hello, {name}!!".format(name=args.name)) ## args.name として利用可能
$ ./arg_parse.py --name IHR
Hello, IHR!!

デフォルトで -h, --help を入れるとusageが表示されます。

$ ./arg_parse.py -h
usage: arg_parse.py [-h] [-n NAME]

optional arguments:
  -h, --help            show this help message and exit
  -n NAME, --name NAME

4 サブコマンド

argpaseは git add, git commit のようなサブコマンドにも対応しています

#!/usr/bin/env python3.3
import sys
import argparse

def greeting(args):
    print("Hello, {name}!!".format(name=args.name))

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subparser_name')
subparser = subparsers.add_parser("greeting")
subparser.add_argument('-n', '--name', default="world")

args = parser.parse_args()

if not args.subparser_name:
    parser.print_help()
    sys.exit(1)

if args.subparser_name == "greeting":
    greeting(args)

実行結果

$ ./arg_parse.py greeting -n ihr
Hello, ihr!!
$ ./arg_parse.py
usage: arg_parse.py [-h] {greeting} ...

positional arguments:
  {greeting}

optional arguments:
  -h, --help  show this help message and exit

6 TODOアプリケーション

argparserとTODOアプリケーションを使ってTODOアプリケーションを作ります

1 アプリケーションの開始点

アプリケーションはmain()で始まることにします。__name____main__となっているとき、スクリプトが実行されています。

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('shelve')

    subparsers = parser.add_subparsers()

    add_parser = subparsers.add_parser('add')
    add_parser.set_defaults(func=cmd_add)

    list_parser = subparsers.add_parser('list')
    list_parser.add_argument('-a', '--all', action="store_true")
    list_parser.set_defaults(func=cmd_list)

    finish_parser = subparsers.add_parser('finish')
    finish_parser.add_argument('task')
    finish_parser.set_defaults(func=cmd_finish)

    args = parser.parse_args()

    db = shelve.open(args.shelve, 'c')

    try:
        args.db = db
        if hasattr(args, 'func'):
            args.func(args)
        else:
            parser.print_help()
    finally:
	db.close()


if '__name__' == '__main__':
    main()

2 サブコマンドごとの処理

def cmd_add(args):
    name = input('task name:')
    due_date = datetime.strptime(input('due date [Y-m-d]:'), '%Y-%m-%d')
    required_time = int(input('required time:'))
    task = create_task(name, due_date, required_time)
    add_task(args.db, task)

def cmd_list(args):
    if args.all:
	tasks =	all_task(args.db)
    else:
        tasks = unfinished_task(args.db)

    for key, tasks in tasks:
        print("{0} {1}".format( key, format_task(task)))


def cmd_finish(args):
    task = args.db[args.task]
    finish_task(task)
    args.db[args.task] = task

7 まとめ

Pythonは簡単に書くことができるため、ファイル処理やテキスト処理、簡単なデータ管理などの非常に役立ちます。WindowsやMac, Linuxなどのマルチプラットフォームで実行することができるユーティリティはアプリケーション開発の優秀な手助けとなるでしょう

#!/usr/bin/env python3.3
# -*- coding: utf-8 -*-
from datetime import datetime, time
import argparse
import shelve
def create_task(name, due_date, required_time):
return dict( name=name, due_date=due_date, required_time=required_time, finished=False)
def format_task(task):
state = "完了" if task['finished'] else "未完了"
format = "{state} {task[name]} : {task[due_date]:%Y-%m-%d} まで 予定所要時間 {task[required_time]}分"
return format.format(task=task, state=state)
def finish_task(task):
task['finished'] = True
def save_task(task, filename):
pickle.dump(task, open(filename, "wb"))
def load_task(filename):
return pickle.load(open(filename, "rb"))
def add_task(db, task):
key = next_task_key(db)
db[key] = task
def next_task_key(db):
id_ = db.get('next_id', 0)
db['next_id'] = id_ + 1
return "task:{0}".format(id_)
def all_tasks(db):
for key in db:
if key.startswith('task:'):
yield key, db[key]
def unfinished_task(db):
return ((key, task) for (key, task) in all_tasks(db) if not task['finished'])
def cmd_add(args):
name = input('task name:')
due_date = datetime.strptime(input('due date [Y-m-d]:'), '%Y-%m-%d')
required_time = int(input('required time:'))
task = create_task(name, due_date, required_time)
add_task(args.db, task)
def cmd_list(args):
if args.all:
tasks = all_task(args.db)
else:
tasks = unfinished_task(args.db)
for key, task in tasks:
print("{0} {1}".format( key, format_task(task)))
def cmd_finish(args):
task = args.db[args.task]
finish_task(task)
args.db[args.task] = task
def main():
parser = argparse.ArgumentParser()
parser.add_argument('shelve')
subparsers = parser.add_subparsers()
add_parser = subparsers.add_parser('add')
add_parser.set_defaults(func=cmd_add)
list_parser = subparsers.add_parser('list')
list_parser.add_argument('-a', '--all', action="store_true")
list_parser.set_defaults(func=cmd_list)
finish_parser = subparsers.add_parser('finish')
finish_parser.add_argument('task')
finish_parser.set_defaults(func=cmd_finish)
args = parser.parse_args()
db = shelve.open(args.shelve, 'c')
try:
args.db = db
if hasattr(args, 'func'):
args.func(args)
else:
parser.print_help()
finally:
db.close()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment