5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PHPのexec()で正しくPythonを実行してるのに、なんかModuleNotFoundErrorになるんですけど!?

Last updated at Posted at 2022-04-07

はじめに

みなさま、PHP使っていますか。

PHPからPythonを呼び出す用事があり、やむなくPHPで外部プログラムを実行できるexec()を使いました。
自称PHPerとしては断腸の思いの決断です。

cronやシェルスクリプトなど、こういう系ではフルパスを使うのが定石と存じ上げています。
なので

$path="/usr/bin/python3 /...フルパス/.py";

として、exec()に渡しました。

exec($path,$output);

パスは正しいのに、動かないときがある?

ちゃんとPythonのコードは実行されていて、print文だけみたいな簡単なコードなら動くんです。
ちゃんと$outputにprint()などで入れた標準出力が帰ってきています。

でも本来動かしたい、少し規模の大きいコードを動かすと$outputがすっからかんに……

これはどこかでエラーが出ている可能性大なので、エラーを出させてみます。

このような形で

$path="/usr/bin/python3 /...フルパス/.py 2>&1";

2>&1を末尾につけると、$outputにエラーメッセージも含ませることができます。

ModuleNotFoundErrorが何故か出ていた……

エラーを見てみると

ModuleNotFoundError: No module named "モジュール名"

です。
もちろんpipし忘れていたてへぺろ☆的なことではありません。

間違いなく

pip3 install モジュール名

しました。

で、最大の厄介はSSHしたターミナルから同じPythonソースファイルを実行するとエラー無く動くことです。
最初に挙げた通りフルパスで指定していますから実行されるPythonは同じです……

同じPythonでも、呼び出されるモジュール先が違った

タイトルのとおりです。
と、その前にPythonがどこからモジュールを呼び出しているか確認してみましょう。

確認方法

下記のようなコードを書き連ねて、

import sys
import pprint

pprint.pprint(sys.path)

このPythonファイルをexec経由で実行すると
image.png
モジュール検索パスが出力されます。

で、同じPythonファイルをターミナルから実行すると
image.png

exec()で実行したときにはモジュール検索パスにsite-packageが含まれていないのです!

ユーザーによる違い

PHPからexec()経由だとwww-dataユーザーで実行され、SSHから実行すると自分でログインしたユーザーで実行されます。
私はsudo pip3 install は非推奨とどこかで聞いていたので、自分でログインした一般ユーザーでpip3 installしていました。
これが元凶です。

Ubuntuを初めとしたDebian系だと起きるらしいのですが

pip3 install mysqlclient

sudo pip3 install mysqlclient

ではインストール先が違うんです!!
前者だとsite-packages、後者だとdist-packagesにインストールされるみたいです。
で、普段一般ユーザーでPythonを実行する時はsite-packagesdist-packagesどちらも含めてPythonがモジュールを探し出してくれます。
なので、どちらのコマンドで実行しようがModuleNotFoundErrorにはなりません。

しかし、exec()での実行だとwww-dataユーザーでの実行扱いになり、site-packagesにインストールされたモジュールを探し出してくれないのです。

そして解決へ

exec()でPythonを実行するとdist-packagesを読みに行くので、そこにモジュールがインストールされる形でpipコマンドを走らせます。
sudoするだけですが(これで良いのか……)

sudo pip3 install mysqlclient

こういった要領で、必要なモジュールを改めてインストールしていきました。

そして、解決。
モジュール検索パス側を変えるという手もあるみたいですが、サーバー上のPythonでは他に用途がないので、ワイルドにsudoしちゃえってことにしました。簡単ですから……
Pythonの仮想環境ことvenvを使えばいいじゃんという意見もあるのですが、execコマンドで非同期的な実行をさせたい場合には、仮想環境立ち上げと破棄を&で繋いだものをexecに渡す形となってしまい、うまく非同期的な実行できないんですよ……

編集後記

エラーのハンドリング、めちゃ大事ですね……
私はそもそもModuleNotFoundErrorであることに気づくまで、めちゃくちゃ時間かかりました。
2>&1をつければすぐ出来るのに、それそら忘れていました。
ちゃんとフルパスで指定してる上、ターミナルからなら実行できるの謎だ……世の中は不条理だって諦めかけていたのも良くなかったという反省をしています……
さらにDockerによるローカル開発環境では正常動作しており、それがまた厄介さを極めました……本番と完全同じなテスト環境が欲しくなります。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?