Day After Day
tsurezure naru mamani...
ANOTHER DECADE

from 2022 when it's begining after/with CORONA Virus.

C#からPythonのスクリプトを動かす

6月
26
2024
Back
Alt+HOME



C#からPythonのスクリプトを動かそうとして暗中模索したが、幾つかの紆余曲折プロトタイプを経て何とか環境依存しない(Windows標準環境で動く)アプリケーションとすることが出来た。
v.2.0.5
下位フォルダに本体プログラムからPythonコードをファイルとして出力させて、手作業で必要なPythonの環境を作っていた。
v.2.1.0
上の手作業のものを、ちゃんとしたコマンドを使用して構築。
v.2.2.0
ここまででパラメータの渡し方が理解できたので、ではpython環境無しで、コンパイルしたEXEを使えないか。
βTest Version
Download for TEST(v.2.2.3)
IO-117, LEDSAT, KASHIWA, CubeBel-2, GRBAlpha

作り込む必要性は有るものの全体の挙動としてはそこそこ (v.2.0.5)


  1. C#やPythonの技量がまだまだ(どの言語もだけど)の私にとっては、Pythonのライブラリーである Requests をシミュレートするのは至難の技。

  2. そこで、思い付いたのが送信部だけPythonで書いたRequestsによるものとし、不慮の編集・削除を防ぐ様な方法は無いかと調べていたところ次のようなものが見つかった。 jsonデータの渡し方は HTTPClient とは随分違い用途に合わせている。

    /* C# */
    
    // 接続するデータベースのURL
    string url	= "https://database.url/api";
    string data	= "jsonにすべき元データ,コンマ区切り,・・・ ";
    
    // 実行するpython scriptとパラメータを定義
    string program = @".\python\python.py";
    string args = $"{data} {url}";
    
    // Python.exeを起動してスクリプトを実行
    foreach (var outPut in PythonCall(@".\python\python", $"{program} {args}"))
    {
    	// 標準出力をリターンとして読み取る
    	string statusCode = $"{outPut}";
    }	
    		

  3. PythonのコードをC#から生成する。同じものを .\python\python.py として直接書いても問題は無いが動作を保証するために本体と一体化する。 自分自身の環境内で呼び出す場合は直書きの方が良いだろう。

  4. // PythonプログラムをC#内で記述してファイル化する
    private static void PythonScript()
    {
    	StringBuilder sb = new();
    	sb.AppendLine("import sys");
    	sb.AppendLine("import requests");
    	sb.AppendLine("");
    	sb.AppendLine("def post_request(json_data, url):");
    	sb.AppendLine("	items = json_data.split(',')");
    	sb.AppendLine("	payload = {");
    	sb.AppendLine("		'noradID': int(items[0]),");
    	sb.AppendLine("		'source': items[1],");
    	sb.AppendLine("		'timestamp': items[2],");
    	sb.AppendLine("		'frame': items[3],");
    	sb.AppendLine("		'locator': items[4],");
    	sb.AppendLine("		'longitude': items[5],");
    	sb.AppendLine("		'latitude': items[6]");
    	sb.AppendLine("		}");
    	sb.AppendLine("	print(payload)");		// 標準出力
    	sb.AppendLine("	#print(url)");
    	sb.AppendLine("");
    	sb.AppendLine("	try:");
    	sb.AppendLine("		res = requests.post(url, data = payload, timeout = int(1))");
    	sb.AppendLine("		return res.status_code");
    	sb.AppendLine("	except Exception as exc:");
    	sb.AppendLine("		print(f\"Error: {exc}\", file=sys.stderr)");
    	sb.AppendLine("		return 400");
    	sb.AppendLine("");
    	sb.AppendLine("if __name__ == \"__main__\":");
    	sb.AppendLine("	json_data = sys.argv[1]");
    	sb.AppendLine("	url = sys.argv[2]");
    	sb.AppendLine("	stat_code = post_request(json_data, url)");
    	sb.AppendLine("	print(stat_code)");		// 標準出力
    			
    	string pyScript = sb.ToString();
    	string pyPath = @".\python\python.py";		// カレントフォルダの下、pythonフォルダとファイル名を指定
    
    	//Pythonプログラムソースをファイルに保存
    	File.WriteAllText(pyPath, pyScript, Encoding.GetEncoding("utf-8"));
    }
    		

    この PythonScript() 関数をフォーム起ち上げ時に実行しておく。

  5. 次に最初のコードで呼び出される PythonCall() を記述する

  6. public static IEnumerable PythonCall(string program, string args = "")
    {
    	// プログラム開始条件の設定
    	ProcessStartInfo startInfo = new()
    	{
    		FileName = program,                 // 実行するファイルをセット
    		Arguments = args,                   // 引数があればセット
    		RedirectStandardOutput = true,      // 標準出力をリダイレクトする
    		UseShellExecute = false,            // シェル機能を使用しない
    		CreateNoWindow = true,              // コンソール・ウィンドウを開かない
    	};
    
    	// プロセスを開始
    	using var process = new Process { StartInfo = startInfo };
    	process.Start();
    
    	// プロセスの返り値を標準出力として返す
    	while (!process.StandardOutput.EndOfStream)
    	{
    		yield return process.StandardOutput.ReadLine();		// Pythonコードの2つの標準出力が返される
    	}
    }	
    		

    これでそこそこに動作するのだが、仮想環境(python.exeやライブラリー)は手動で集めて、最小限動くように構築していた。

requestsモジュールを含む仮想環境をコマンドで作れる事を発見 (v.2.1.0)


IronPython、Pythonnet、Cythonなどを試す中で Pythonの仮想環境をコマンドで作れる事を知る。 そこで、早速その場合のPythonスクリプトの変更部分と環境の可搬性を試してみる。

  1. プログラムの直下(pyenv)に仮想環境を構築

  2. md pyenv
    cd pyenv
    python -m venv venv
    		

  3. 仮想環境をアクティベートする

  4. .\venv\Scripts\activate			Windowsの場合
    source venv/bin/activate		Linuxの場合
    		

  5. requestsモジュールをインストールする

  6. pip install requests

  7. フォルダ名の変更とpython.exeのパスをC#側で変更する

  8. // 実行するpython scriptとパラメータを定義
    string program = @".\pyenv\python.py";
    string args = $"{data} {url}";
    
    // Python.exeを起動してスクリプトを実行
    foreach (var outPut in PythonCall(@".\pyenv\venv\Scripts\python.exe", $"{program} {args}"))
    			:
    			:		
    		

  9. 同様にPythonスクリプトも仮想環境を読み込むように変更

  10. private static void PythonScript()
    {
    	StringBuilder sb = new();
    	sb.AppendLine("import os");
    	sb.AppendLine("import sys");
    	sb.AppendLine("");
    	sb.AppendLine("venv_path = os.path.join(os.path.dirname(__file__), 'venv', 'lib', 'site-packages')");
    	sb.AppendLine("sys.path.append(venv_path)"); 
    	sb.AppendLine("");
    	sb.AppendLine("import requests");
    			:
    			:
    		

  11. 今までのプログラムフォルダ内に有る手作業で作った環境フォルダ(.\python)を削除して、先ほど作った仮想環境を単純にコピーしてみた。
  12. 結果はOK、但し劇的に反応が早くなる訳ではないし、容量が大きく減る訳でもなかった。 しかし、IronPython、Pythonnet、Cythonなどに比較して劇的に楽に移行できた。

今までの作業でC#とPythonの関係が少し理解できてきた (v.2.2.0)


まず、上述のコマンドによる仮想環境の作成で気づいた事をテストしてみた。

  1. C#から書き出していたスクリプトpython.pyをコンパイル(若干変更した)。

  2. # python.py (コンパイル後の名前 post_request.exe)
    import sys
    import requests
    
    def post_request(json_data, url):
            items = json_data.split(',')
            payload = {
                    'noradID': int(items[0]),
                    'source': items[1],
                    'timestamp': items[2],
                    'frame': items[3],
                    'locator': items[4],
                    'longitude': items[5],
                    'latitude': items[6]
                    }
            #print(url) # Debug
    
            try:
                    res = requests.post(url, data = payload, timeout = int(1))
                    print(payload)					# ログ用のペイロードはリクエストしたときのみとした。
                    return str(res.status_code)
    
            except Exception as exc:
                    print(f"Error: {exc}", file=sys.stderr)
                    return str(400)
    
    if __name__ == "__main__":
            if len(sys.argv) != 3:
                    print("Usage: post_request.exe  ", file=sys.stderr)
                    sys.exit(1)
            json_data = sys.argv[1]
            url = sys.argv[2]
            stat_code = post_request(json_data, url)
            print(stat_code)
    		

    pyinstaller python.py --onefile --clean --icon=icon.ico --name=post_request
    混乱を防ぐために python.py --> python.exe ではなく(python.exeはPythonの本体と同名)post_request.exe になるよう指定している。 出来上がった post_request.exe をフォルダ pyenv に保存する。

  3. Python仮想環境を削除する。pyenv内にpost_request.exeのみ残し、他のフォルダをすべて削除。

  4. プログラム名の変更やPythonスクリプトの書き出しが不要になった点を、C#に反映させる。

  5. foreach (var outputLine in ExecuteTestExe(payloadString, database))
    {
    	// 返り値は改行されたpayloadとstatus Codeで構成されている
    	string[] results = $"{outputLine}".Split('\n');
    			:
    			:
    		

  6. PythoCall()に当たる部分をEXEファイルを起ち上げる記述に変更(関数名も変更している)

  7. static IEnumerable ExecuteTestExe(string arg1, string arg2)
    {
    	// プロセスの設定
    	ProcessStartInfo startInfo = new()
    	{
    		FileName = @".\pyenv\post_request.exe",		// 実行するexeのパス --> 変更
    		Arguments = $"{arg1} {arg2}",   	        // 引数を指定
    		RedirectStandardOutput = true,      	    	// 標準出力をリダイレクト
    		UseShellExecute = false,                	// シェル機能を使わない
    		CreateNoWindow = true                   	// コンソールウィンドウを表示しない
    	};
    
    	// プロセスを開始
    	using var process = new Process { StartInfo = startInfo };
    	process.Start();
    
    		// 標準出力から結果を取得
    	while (!process.StandardOutput.EndOfStream) 
    	{
    		yield return process.StandardOutput.ReadToEnd();
    	}
    }
    		

    これで post_request.exe をC#から DLL のように使用することが、Python仮想環境を引きずらずに構築できた。一応配布も可能となった。 しかし、それ程スピードは速くなく送信に対する status_code が返るまでかなり息をつく。
    次は この部分を出来れば Cythonでラッピングして C++で完全な DLL化してみたい。

    と言うより、PythonのRequests と同じ事が C# で書ければ全く問題ないのだけど・・・。


Back
Alt+HOME