mod_wsgi を利用する

28 10月

python を apache で実行することを試しています。php から python を exec 等で呼び出したときに,どうも時間がかかりすぎます。それで,他のやり方として,apache の mod_wsgi を試してみます。OS は ubuntu 20.04 です。synaptic から libapache2-mod-wsgi-py3 を導入します(似たものが二つありますが,py3 が付いている方が良いと何処かに書いてあった)。

ネットにある記事があてはまらなくて試行錯誤しました。conf-available/mod-wsgi.conf に

WSGIScriptAlias /test_wsgi /var/www/scripts/test_wsgi_script.py

上記ようなコードを記入しろという記事が多くあるのですが,この名前のファイルはありません。このページによると,wsgi.conf というファイルが相当するようです。どこに何を書いてよいのか分からなかったのですが,下図のように記入してみました。

/var/www/script/ というフォルダーを用意して,そこに hello.py というファイルを置きました。その中身は下記のようなものです。

コードはこちらから。ファイルに実行権限を与えました。apache を再起動した後,apache の URL は 192.168.100.4 なのですが,http://192.168.100.4/hello にアクセスをしてみると下記のように表示されました。

分からないことが多いのですが,動いた!

(20211105)
wsgi の利用は直接呼び出すのではなくて,PHP の処理の途中で PHP から呼び出します。これには file_get_contents を使用します(参考サイト)。python が実行するコードを文字列で渡して色々処理を実行したい。文字列は GET で渡します。python を呼び出す PHP のコードは下記のような感じです。1行だけの python コードを渡します。変数 a を定義するだけの内容です。

<?php
$starttime = microtime(true);
$tmp = <<< end_of_quote
a = 'python!'
end_of_quote;
$tmpBase64 = base64_encode($tmp);

$context = stream_context_create([
    'http' => [
	'ignore_errors' => true
    ]
]);

$res = file_get_contents("http://192.168.100.9/hello?$tmpBase64", false, $context);
echo $res . "<br>";
$now = microtime(true);
echo "Start: {$starttime}, End: {$now}, Taken = " .($now - $starttime)."\n";
?>

時間計測のコードもついています。GET で文字列を渡すので,そのままだと文字列中のスペースなどが変換されてしまいます。その為,文字列を Base64 変換して,python 側でもとに戻すようにしました。次に python 側のコードです。変数 a の値を返すだけのコードです。

def application(environ, start_response):    
    import sys, os
    import base64    
    global a 
    
    tmpStr = environ.get('QUERY_STRING')
    tmpStrBina = tmpStr.encode()    
    tmpDecodeStr = base64.b64decode(tmpStrBina).decode()
        
    exec(tmpDecodeStr, globals())
    output = a.encode()

    status = '200 OK'
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(output)))]
    start_response(status, response_headers)
    return [output]

environ.get(‘QUERY_STRING’) で GET での送信内容を受け取ります。利用している Base64 変換の関数は,バイナリーを受け付けます。出力もバイナリーです。それで,入力と出力の際に変換処理が入っています。PHP に値を返すところも エンコード処理をいれないといけないようです。
 ポイントは global a という宣言です。GET で受け取った python コードを exec で実行しますが,それはグローバルの領域のようで,宣言無しでは,関数内から a を参照しても下記のようなエラーとなります。

NameError: name 'a' is not defined

numpy とか pandas とかも利用したいのですが,GET で渡す文字列に import 文 を含めてしまえば利用できます。グローバルな領域に読み込んだことになるのかなと思いますけど,毎回 python を呼び出す際に import するのは,時間がかかるのではないかと思って,下記のような宣言をしてみました。

    global pd, np, sympy
    import pandas as pd
    import numpy as np
    import sympy

意外にこれで,GET で渡すコード内で numpy 等が利用できるようになりました。

(20211228) POST で渡す

GET で渡せるデータには大きさの制限があるので、動画を base64 に変換したデータなどは POST で渡す必要があります(参考サイト)。再び、 PHP から wsgi へアクセスする場合を考えます。以下では hello! という文字を渡します。

<?php
$mess = 'hello!';
$postdata = http_build_query(array('postData' => $mess));

$headers = array(
    'Content-Type: application/x-www-form-urlencoded',
    'Content-Length: '.strlen($postdata)
    );

$context = stream_context_create([	
	'http' => ['method'  => 'POST',
		'header'=>implode( "\r\n", $headers),
		'ignore_errors' => true,
		'content' => $postdata
	]	
]);

$res = file_get_contents("http://xxx.xxx.xxx.xxx/wsgi", false, $context);

if (strpos($http_response_header[0], '200') !== false) {
	echo base64_decode($res) . "\n";
} else {
	echo "file_get_contents false" . "\n";
}
?>

受ける方は下記。受け取った文字列 hello! を返すだけです。

def application(environ, start_response):
    
    import sys, os
    import base64
    
    import pandas as pd

    import urllib
    
    content_length = environ.get('CONTENT_LENGTH', 0)    
    body = environ.get('wsgi.input').read(int(content_length))    
    bodyString = body.decode()
    
    tmpObj = urllib.parse.parse_qs(bodyString)
    tmpStr = tmpObj['postData'][0]
   
    status = '200 OK'    
    output = base64.b64encode(tmpStr.encode())
    #output = base64.b64encode(bodyString.encode())

    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(output)))]
    start_response(status, response_headers)    

    return [output]

python の文字列には2種類あるようで(参考サイト)、コード中の body はバイト型のようです。バイト型は str 型に変換しないと、エンコードする場合など下記のようなエラーとなります。

'bytes' object has no attribute 'encode'

tmpObj の形がよくわかりません。とにかく tmpObj[‘postData’] は中身が1個の配列です。urllib を利用するために、pandas が必要だったような気がします。

(20220103) 外部プログラムを利用する

話が wsgi から離れるのですが、分けるほどの内容もないので、ここに書きます。最終的には wsgi の中で、ffmpeg などの外部プログラムを利用したいので、少し調べて見ました(参考サイト)。 こんな感じで利用しようと思います。ffmpeg で動画の情報を探ります。

import sys, os
import subprocess

try:
    result = subprocess.run('ffprobe -v quiet -hide_banner -show_streams -print_format json /var/www/html/temporary/tmp1641143261925888.mp4', shell=True, check=True,
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
    feedback = result.stdout
    print(feedback)
except subprocess.CalledProcessError:
    print('外部プログラムの実行に失敗しました', file=sys.stderr)

出力結果は下記。

{
    "streams": [
        {
            "index": 0,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "Main",
            "codec_type": "video",
            "codec_time_base": "5/299",
            "codec_tag_string": "avc1",
            "codec_tag": "0x31637661",
            "width": 960,
            "height": 540,
            "coded_width": 960,
            "coded_height": 544,
            "has_b_frames": 1,
            "sample_aspect_ratio": "1:1",
            "display_aspect_ratio": "16:9",
            "pix_fmt": "yuv420p",
            "level": 31,
            "color_range": "tv",
            "color_space": "bt709",
            "color_transfer": "bt709",
            "color_primaries": "bt709",
            "chroma_location": "left",
            "refs": 1,
            "is_avc": "true",
            "nal_length_size": "4",
            "r_frame_rate": "299/10",
            "avg_frame_rate": "299/10",
            "time_base": "1/19136",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 574080,
            "duration": "30.000000",
            "bit_rate": "1865648",
            "bits_per_raw_sample": "8",
            "nb_frames": "897",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "language": "und",
                "handler_name": "ISO Media file produced by Google Inc."
            }
        },
        {
            "index": 1,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/44100",
            "codec_tag_string": "mp4a",
            "codec_tag": "0x6134706d",
            "sample_fmt": "fltp",
            "sample_rate": "44100",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/44100",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 1323000,
            "duration": "30.000000",
            "bit_rate": "91250",
            "max_bit_rate": "128000",
            "nb_frames": "1293",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "language": "und",
                "handler_name": "ISO Media file produced by Google Inc."
            }
        }
    ]
}