pythonで外部プロセス(subprocess)と、標準入出力を介したやりとりをしたいときがある。
目次
やってみること
今回は問題例として、形態素解析器MeCabをpythonから呼んでみる。MeCabはpythonバインディングがあるので、実用的には外部プロセスとして起動する必要はないのだが、わかりやすい例として敢えて挙げる。
なお、MeCabを知らない方のために説明すると、自然言語処理で使う形態素解析器というもので、次のような使い方をするものである。
$ mecab 吾輩は猫である。 # 入力行。この下の行から解析結果 吾輩 名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ は 助詞,係助詞,*,*,*,*,は,ハ,ワ 猫 名詞,一般,*,*,*,*,猫,ネコ,ネコ で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ ある 助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル 。 記号,句点,*,*,*,*,。,。,。 EOS
pythonスクリプトから外部プロセスとしてMeCabを起動し、入力をpythonスクリプトから送り込み、解析結果を得ることを目標にする。
簡単な方法
とりあえずやる
標準ライブラリのsubprocessを用いる。次のように書く。
# coding: UTF-8 import subprocess def main(): input_string = "吾輩は猫である。" mecab_p = subprocess.Popen(["mecab"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) output_string = mecab_p.communicate(input_string.encode())[0].decode() print(output_string) if __name__ == "__main__": main()
結果
吾輩 名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ は 助詞,係助詞,*,*,*,*,は,ハ,ワ 猫 名詞,一般,*,*,*,*,猫,ネコ,ネコ で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ ある 助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル 。 記号,句点,*,*,*,*,。,。,。 EOS
できた。
解説
subprocess.Popenでプロセスを立ち上げパイプをつなぐ。後はmecab_p.communicateの引数で入力を、返り値で出力を得る。なお、返り値は
注意すべき点としては、端末から打ち込むのと基本的に同じなので文字コードを意識する必要がある。python内部で使われている文字列型は、(python3では)unicode文字列(str型)という「とりあえず文字コードを意識しなくても使える型」になっているのだが、外部プロセスに投げるときはencodeしてbytes型という要するにただのバイト列にしてやる必要があり、また外部プロセスから受け取った文字列もまたbytes型なのでdecodeしてstr型に戻してやらないといけない。
今回はlinux上の環境で実験しているので、文字コードはpython、システム、mecabすべてutf-8で統一されており、その場合は上のコードのように簡単に書ける。
windows環境だったりすると(けっきょくはpythonとmecabの設定に依存するが)、encodingが統一されていないこともあり得るので、そういうときはちゃんと自分で環境を設定した通りにencodingを取り扱ってやらないと、たぶん動かない。
応用:複数回入出力を送りつけたい
subprocess.Popen.communicateを2回呼ぶと、次のようなエラーが出る。
ValueError: Cannot send input after starting communication
つまり1回しかやり取りできない。送りつけて出力を受け取った瞬間、パイプは閉じられてしまう。安全といえば安全だが、これは不便である。
この回避のためには、
- 複数回の入力を一つにまとめて送りつける
- stdin, stdoutを直接叩く
といった方法がある、らしい。
参考:python - Communicate multiple times with a process without breaking the pipe? - Stack Overflow
前者の方法はそれで済めば良いだろうけど、たまには対応しきれないときもある。後者の方法は、そんな見るからにおっかない(制御ミスるとすぐ死ねそうな)方法は使いたくない。というか、実際やってみても上手く動かせなかった。
なので、Pexpectというライブラリに頼る。これは擬似端末を立ち上げてプロセスとおしゃべりする系のもの。コマンドの自動化などに使うものらしい。
結論から言うと、これを使うのはけっこう辛かった。挙動のクセが強すぎる。正規表現に慣れててexpectの世界観も理解してる人には良いんだろうけど、僕みたいな素人にはただただ望む出力を得るまで試行錯誤を重ねる苦行だった。
def main2(): inputs = ["吾輩は猫である。", "名前はまだない。"] p = pexpect.spawn("/bin/bash") #p.logfile_read = sys.stdout.buffer p.sendline("mecab") p.readline() p.sendline("") p.expect("EOS") results = [] for s in inputs: p.sendline(s) p.expect("EOS") results.append(p.before.decode()) p.terminate() p.expect(pexpect.EOF) for r in results: print(r)
うまい正規表現が思いつかなかったのでMeCabのEOSを取ることに。
なんだか微妙な結果になってしまったけど、Pexpect自体はたぶん良いライブラリなので、然るべき場面で活用してあげてください。標準入出力を自動化したいときはたぶん使えます。ただ、今回の用途にはいまいち合ってなかった気がする。
まとめ
パイプで投げつけるだけだろと思ってたら、けっこう色々なことに気を遣う羽目になりました。