10. 特殊な処理メソッド

mcmdのメソッドが行っていることは、バイトストリームとして受け取ったデータに対して 何らかの「処理」を行い、その結果をバイトストリームとして出力しているだけである。 とすると、その「処理」をPythonの関数やOSのコマンドで実現できてもおかしくない。 これらの機能を担うのが以下で説明する、 runfunccmd メソッドである。

10.1. runfunc: 関数の実行

runfunc メソッドは、 Pythonの関数をあたかもmcmdのメソッドのように動作させるためのメソッドである。 基本的な利用方法を リスト 10.1 に示している。 bigAmount 関数の中で使われている、 mstdinmstdout メソッドがポイントで、 それぞれ、標準入力を読み込む、そして標準出力に書き出す機能を持っている。 このように、関数が、標準入力を入力データとして読み込み、標準出力を出力データとして書き出す機能さえ持っていれば、 runfunc 関数によって、mcmdの処理メソッドのように扱えるようになるのである。 リスト 10.1 では、 mcut の出力が、 runfunc により、 bigAmount 関数の標準入力に接続されており、 またbigAmount関数の標準出力が、 msum へと接続されている。

リスト 10.1 runfuncメソッドの基本的な利用方法
 1import nysol.mcmd as nm
 2dat=[
 3["customer","date","amount"],
 4["A","20180101",5200],
 5["B","20180101",800],
 6["B","20180112",3500],
 7["A","20180105",2000],
 8["B","20180107",4000]
 9]
10
11def bigAmount(lowerBound):
12  f = None
13  f <<= nm.mstdin()
14  f <<= nm.mselnum(f="amount",c="[%d,]"%lowerBound)
15  f <<= nm.mstdout()
16  f.run()
17
18sel=None
19sel <<= nm.mcut(f="customer,amount",i=dat)
20sel <<= nm.runfunc(bigAmount,lowerBound=4000)
21sel <<= nm.msum(k="customer",f="amount")
22print(sel.run())
23# [['A', '5200'], ['B', '4000']]

mcmdを用いずに関数を実装する

リスト 10.1 では、mcmdの処理メソッド mstdin mstdout をつかって実装したが、 Pythonで利用可能な一般的な標準入出力のライブラリを用いても全く問題ない。 リスト 10.2リスト 10.1bigAmount 関数のみを書き換えたものである。 中では、sysライブラリの stdin を標準入力として用い、print で標準出力に書き出している。 全てPythonのネイティブコードのため自由度は非常に高くなる。 ただし、この場合、CSVデータのparsingロジックを自分で書く必要があることに注意する。 単純なCSVであれば問題ないが、ダブルクオーテーションやカンマの入った文字列を扱うのは結構面倒である。

リスト 10.2 mcmdを利用しない実装
 1import sys
 2def bigAmount2(lowerBound):
 3  header = True
 4  for line in sys.stdin:
 5    if header:
 6      print(line.strip())
 7      header = False
 8    else:
 9      tokens = line.strip().split(",")
10      if int(tokens[1])>=lowerBound:
11        print(",".join(tokens))

mcmdのイテレータを用いた実装

最後に、リスト 10.1リスト 10.2 の中間的な書き方として、 リスト 10.3 に示すように、CSVのparsingはmcmdの mstdin にまかせて、 その後にmcmdのイテレーションを用いてPythonロジックを書く方法を紹介しておこう。 ポイントは、mcmdのイテレータは項目名ヘッダーを無視するため、 for 文の中でヘッダー行を意識しなくてもよい一方で、 次のメソッドへの出力には項目名ヘッダーを出力しなければならないという点である。 以下のコードでは、最初に項目名ヘッダーを出力している。

リスト 10.3 mstdinとイテレーションを組み合わせた例
1def bigAmount(lowerBound):
2  print("customer,amount")
3  for line in nm.mstdin():
4    if int(line[1])>=lowerBound:
5      print(",".join(line))

もしデータから項目名を取得したければ、 mstdin に続けて、 getline イテレータに接続し、 そこで項目名行を出力するオプション header=True を指定すれば良い。 項目名行の扱いは、 リスト 10.2 と同様である。

リスト 10.4 イテレーションの中で項目名をデータから取得する方法
1def bigAmount(lowerBound):
2  header = True
3  for line in nm.mstdin().getline(header=True):
4    if header:
5      print(",".join(line))
6      header = False
7    else:
8      if int(line[1])>=lowerBound:
9        print(",".join(line))

runfuncのデバッグ

runfunc() メソッドは指定された関数をPythonにまかせて実行するだけなので、 もし関数の中でエラーが生じても、エラーが生じたことは分かっても、その詳細については感知していない。 例えば、 リスト 10.5 は、上述の リスト 10.3 に文法エラーを加えたコードで、 これを実行した時のエラーメッセージは リスト 10.6 に示すとおりである。 このように、runfuncでエラーが起こっていることは分かってもそれ以上の詳細はわからない。

リスト 10.5 関数内にエラーを入れた例(debug1.py)
 1import sys
 2import nysol.mcmd as nm
 3
 4def bigAmount(lowerBound):
 5  print("customer,amount")
 6  for line in nm.mstdin():
 7    if int(line)>=lowerBound: # lineの要素を指定していないエラー
 8      print(",".join(line))
 9
10sel=None
11sel <<= nm.mcut(f="customer,amount",i=dat)
12sel <<= nm.runfunc(bigAmount,lowerBound=4000)
13sel <<= nm.msum(k="customer",f="amount")
14print(f.run(msg="on"))
リスト 10.6 リスト 10.5 の実行結果。
1$ python debug1.py
2#END# kgload; IN=0 OUT=5; 2018/09/05 10:18:51; 2018/09/05 10:18:51
3#END# kgcut f=customer,amount; IN=5 OUT=5; 2018/09/05 10:18:51; 2018/09/05 10:18:51
4#ERROR# error occured in the function, check the detail error message using try-exception in the function. (kgpyfunc); kgpyfunc; ; 2018/09/05 10:18:51; 2018/09/05 10:18:51
5#ERROR# ; kgshell (script RUN KGERROR runmain on kgshell); 2018/09/05 10:18:51
6RuntimeError: runmain on kgshell
7[]

関数の中でのエラーの詳細を追跡するには、 tryexception を入れることで解決できる。 そのコードを リスト 10.7 に示す。 exception の中で、トレースバック関数を呼び出しているが、出力先を標準エラー出力にするのがポイントである。 標準入出力は、 runfunc メソッドによってデータとし扱われてしまうからである。 また、デバッグ目的で変数の内容を表示させるときも、標準出力ではなくエラー出力に出さなければならない。 以下のコードでは、 sys.stderr のメソッドを使って引数 lowerBound を標準エラー出力に出力している。

実行時のメッセージは リスト 10.8 に示す通りで、 7行目の int(line) に問題があることが示され、また最初に lowerBound の内容も表示されている。

リスト 10.7 エラーの追跡を可能とする関数の実装例(debug2.py)
 1def bigAmount(lowerBound):
 2  try:
 3    sys.stderr.write(str(lowerBound)+"\n")
 4    print("customer,amount")
 5    for line in nm.mstdin():
 6      if int(line)>=lowerBound:
 7        print(",".join(line))
 8  except Exception as e:
 9    with open('/dev/stderr', 'w') as fpe:
10      traceback.print_exc(file=fpe)
リスト 10.8 リスト 10.7 の実行結果。
 1$ python debug2.py
 24000
 3#END# kgcut f=customer,amount; IN=5 OUT=5; 2018/09/05 10:32:47; 2018/09/05 10:32:47
 4#END# kgload; IN=0 OUT=5; 2018/09/05 10:32:47; 2018/09/05 10:32:47
 5Traceback (most recent call last):
 6File "special_runfunc.py", line 7, in bigAmountBug
 7if int(line)>=lowerBound:
 8TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'
 9#END# kgpyfunc; ; 2018/09/05 10:32:47; 2018/09/05 10:32:47
10#END# kgsum f=amount k=customer; IN=0 OUT=0; 2018/09/05 10:32:47; 2018/09/05 10:32:47
11#END# kgload; IN=0 OUT=0; 2018/09/05 10:32:47; 2018/09/05 10:32:47

runfuncは試験運用

runfuncメソッドは非常に強力で、個人や企業がよく利用する処理機能をメソッド化することが可能となり、 プログラムのモジュール化を促進できる。 しかしながら、一方でrunfuncからrunfuncを実行することも可能で、このようなネストが深くなった時にも 内部的にはどうにか頑張って処理しようとするが、まだ十分な運用と検証ができていない。 現在のところ、このメソッドは試験運用と考えてもらいたい。

10.2. cmd: コマンドの実行

runfunc が処理をPythonの関数で実現していた一方で、 cmd メソッドは、OSのコマンドによって実現するものである。 UNIX系OSの多くは標準入力からデータを受け取り、コマンド内部で一定の処理を付し、標準出力に結果を書き込む。 基本的な利用方法を リスト 10.9 に示している。 ここでは、 customeramount 項目を選択したのち、 tr コマンドに接続している(あまり意味のある例ではない)。 tr コマンドは 入力のバイトストリームに対して1文字単位の置換を実行する。 以下の例では 文字 AC に置換している。 結果として、顧客 AC に置き換わった集計結果が計算されている。 ただし、cmd メソッドには、項目名もデータ本体も区別することはなく、 さらにはカンマ区切りの項目も区別なくデータストリームとして流されるだけであることに注意する。 例えば、以下の例では、もし項目名に A が含まれていれば、それも C に変換されてしまい、意図した動きにはならない。 項目名ヘッダーの問題だけであれば、 リスト 10.10 に示されるように、直前のメソッド mcut で項目名ヘッダーを抑制し( nfno=True ) そして、コマンド実行後に mcut メソッドにより項目名ヘッダー行を追加してやれば良い。

リスト 10.9 mcmdのインポートと入力データの設定
 1import nysol.mcmd as nm
 2dat=[
 3["customer","date","amount"],
 4["A","20180101",5200],
 5["B","20180101",800],
 6["B","20180112",3500],
 7["A","20180105",2000],
 8["B","20180107",4000]
 9]
10
11f=None
12f <<= nm.mcut(f="customer,amount",i=dat)
13f <<= nm.cmd("tr 'A' 'C'")
14f <<= nm.msum(k="customer",f="amount")
15print(f.run())
16# [['B', '8300'], ['C', '7200']]
リスト 10.10 項目名ヘッダーをスキップする例
1f=None
2f <<= nm.mcut(f="customer,amount",nfno=True,i=dat)
3f <<= nm.cmd("tr 'A' 'C'")
4f <<= nm.mcut(f="0:customer,1:amount",nfni=True)
5f <<= nm.msum(k="customer",f="amount")
6print(f.run(msg="on"))
7# [['B', '8300'], ['C', '7200']]

ファイル一覧の取得

UNIX系OSには多くの便利なコマンドが多く存在する。 例えば、 表構造データを柔軟に扱うawk、パターンマッチで行を選択するgrep、 正規表現による文字列置換のsedなどである。 UNIX系のコマンドの扱いに慣れた人にとっては、cmdメソッドを利用することで、 これらのコマンドをmcmdメソッドと連携して利用することができるようになる。 以下に、 ls tail sed の3つのコマンドを用いて、ファイルリストの一覧を処理するプログラムを紹介しておく。 リスト 10.11 はそのコードである。 ls -l でパーミッションやサイズ、ファイル名といった情報が標準出力に出力される。 最初の行にファイル数の情報が出力されるので tail コマンドでその行をスキップしている(2行目から読み込む)。 そして、 ls の出力の区切り文字である複数のスペース文字を sed コマンドによりカンマに変換している。 あとは、 mcut メソッドで項目名ヘッダーを付けてファイル一覧の出来上がりである。

リスト 10.11 lsコマンドを使ってファイル一覧を取得する例
1f=None
2f <<= nm.cmd("ls -l")
3f <<= nm.cmd("tail +2")
4f <<= nm.cmd("sed 's/  */,/g'")
5f <<= nm.mcut(nfni=True,f="0:permission,1:link,2:user,3:group,4:volume,5:month,6:day,7:time,8:filename")
6print(f.run())
7# [['-rw-r--r--', '1', 'foo', 'staff', '4997', '8', '3', '16:44', 'dat.csv'], ['-rw-r--r--', '1', 'foo', 'staff', '104', '9', '6', '10:56', 'dat2.csv'], ...]

マルチバイト文字の変換

最後に、データクリーニングでよく利用されるマルチバイトコードの変換コマンドである nkf の利用例を リスト 10.12 に示しておく。 ただし、これは動作させるためのコーディング例であるため、データ dat.csv にはマルチバイト文字は含まれていないが、 このファイルがShift_jisコードであることを想定している。 また、OSコマンドとして nkf をインストールしておく必要がある。

リスト 10.12 nkfによるShift_jisコードをutf-8コードに変換する例
 1>>> import nysol.mcmd as nm
 2>>> with open('dat.csv','w') as f:
 3>>>   f.write(
 4'''customer,quantity,amount
 5A,20180101,5200
 6B,20180101,800
 7B,20180112,3500
 8A,20180105,2000
 9B,20180107,4000
10''')
11
12>>> f=None
13>>> f <<= nm.cmd("nkf -Sw dat.csv")
14>>> f <<= nm.mcut(f="customer,amount")
15>>> f <<= nm.msum(k="customer",f="amount")
16>>> print(f.run())
17[['A', '7200'], ['B', '8300']]