Java → Python 実行結果の文字コード

以前、Java から Python 実行した時の結果をPython標準出力で Java が受信する方法を投稿したが、
Javaからプロセス起動で実行するPython と文字列の受け渡し - Oboe吹きプログラマの黙示録

Python 標準出力→Java受け取り - Oboe吹きプログラマの黙示録
このような、Base64 コードに変換して仲介するようなことをしなくても良いことに気がついた。

Python から、Java へ正常時の結果出力(標準出力)は、JSONで出力して
Java側がJSONパースするのが綺麗であろう。
Python 実行中でエラー発生、エラー出力:標準エラー出力の方は、特殊なので後で記載)
Pythonスクリプトが以下のような断片コードを実行しているとする。
Pythonの断片コード

import json

#  class の __init__で記述するもの
self.mydict = dict()
# メソッドで記述するもの
self.mydict['A'] = 'A123'
self.mydict['B'] = 24
self.mydict['C'] = '漢字:氏名'
# 
result = json.dumps(self.mydict)
print(result)

以下を標準出力する。

{"A": "A123", "B": 24, "C": "\u6f22\u5b57\uff1a\u6c0f\u540d"}

受信するJava側は、Jackson または、Google gson で読み取れば、

{"A": "A123", "B": 24, "C": "漢字:氏名"}

として読み込める。

自分が作った ScriptExecutor
https://github.com/yipuran/yipuran-core/wiki/Script_exec#orgyipuranutilprocessscriptexecutor
で、以下のように、コードを書いて確認できる。

Jackson 使用の場合、先日公開した以下を使って、、
https://github.com/yipuran/yipuran-jack/wiki

StringBuilder sb = new StringBuilder();
int sts = ScriptExecutor.run(()->"python c:/work/forJava/resmain.py"
, t->{
    sb.append(t);
}, (t, e)->{
    // エラー捕捉
    pythonErrorTrace(t).forEach(s->{
       System.out.println(s);
    });
});
String jsonstr = sb.toString();
// Jackson JsonNode を ObjectMapper readTree でJsonNode を求めて解析する処理
JsonNodeParse jp = new JsonNodeParse();
jp.stream(jsonstr).forEach(e->{
   System.out.println(e.getKey() + " --> " + e.getValue() );
});

Python用 エラー捕捉→ Stream<String>

public Stream<String> pythonErrorTrace(String error) {
   String estr = error.replaceAll("\r", "").replaceAll("\n", "");
   estr = estr.substring(2, estr.length()-2);
   String[] ary = estr.split("', '");
   return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator<String>(){
      int x = -1;
      @Override
      public boolean hasNext(){
         return x < ary.length-1;
      }
      @Override
      public String next(){
         x++;
         return ary[x].replaceFirst("\\\\n$", "");
      }
   }, Spliterator.ORDERED), false);
}

Google gson であれば、以前作って公開した
https://github.com/yipuran/yipuran-gsonhelper/blob/master/src/main/java/org/yipuran/gsonhelper/util/JsonEntryParse.java
を使用すれば、JSONの解析は、

String jsonstr = sb.toString();
JsonEntryParse jp = new JsonEntryParse();
jp.read(jsonstr, (k, v)->{
   System.out.println(k + " --> "+ v);
});

このように確認できる。

問題は、Python処理内でエラー発生した時に、
・エラーメッセージをJavaで受信した時に文字化けしないこと。
Python エラースタックトレースを中途半端ではなく最後まで取得すること
であった。

Python スクリプト

     raise RuntimeWarning("警告エラー")

を発生するように任意にコーディングします。

スタックトレースをエラー発生まで採取するように、Python標準の traceback モジュールを import して
format_exception スタックトレース採取して、print オプション file=sys.stderr で
スタックトレース標準エラー出力します。

if __name__ == '__main__':
    try:
        main = Main()
        main.exec()
    except Exception as e:
        (etype, evalue, etb) = sys.exc_info()
        print(traceback.format_exception(etype, evalue, etb), file=sys.stderr)

このままでは、Java 側、ScriptExecutor#run() のエラー捕捉の BiConsumer でダンプすると、

Traceback (most recent call last):
  File "c:\work\forJava\resmain.py", line 21, in <module>\n    main.exec()
  File "c:\work\forJava\resmain.py", line 13, in exec\n    self.stool.func()
  File "c:\work\forJava\tools\jpress.py", line 14, in func\n    raise RuntimeWarning("�x���G���[")
RuntimeWarning: �x���G���[

と、文字化けしてしまいます。

標準エラー出力UTF-8で出力するように、Python側で最初に宣言します。

import sys

sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

すると、Java 側、ScriptExecutor#run() のエラー捕捉の BiConsumer でのダンプも
結果は以下のとおりになる

Traceback (most recent call last):
  File "c:\work\forJava\resmain.py", line 21, in <module>\n    main.exec()
  File "c:\work\forJava\resmain.py", line 13, in exec\n    self.stool.func()
  File "c:\work\forJava\tools\jpress.py", line 14, in func\n    raise RuntimeWarning("警告エラー")
RuntimeWarning: 警告エラー

標準出力は、同様に sys.stdout を以下のように設定していても

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

Python が出力する全角文字は、 \uXXXX の書式で出力されるので、

{"A": "A123", "B": 24, "C": "\u6f22\u5b57\uff1a\u6c0f\u540d"}

Java側は、JSON として String値を読み込むロジックであれば、sys.stdout の設定は関係ない。

Java Unicode文字列を通常の文字列(utf-8)に変換する(他の文字が混合しても変換する)

文字列書式、\uXXXX のままの String インスタンスなら、変換処理の必要性なくそのまま
インスタンスを扱うのであるが、文字列として \uXXXX
通常の文字列(utf-8)に変換する場合の問題です。
文字列書式、\uXXXX

String  str = "\u6f22\u5b57\uff1a\u6c0f\u540d";
// str = 漢字:氏名

\u → \\u になっている文字列を通常の文字列(utf-8)に変換する問題

String  str = "\\u6f22\\u5b57\\uff1a\\u6c0f\\u540d";

\\u の後ろの Hex4文字を char 型に変換して読み込めば良いのだが、
間にASCII文字や他の文字が入っても Unicodeだけを変換する問題

String  str = "\\u6f22\\u5b57_\\uff1a_\\u6c0f\\u540d";
// 期待値 = 漢字_:_氏名

という期待値を求めるには、単純に1文字ループや、\\\\u で区切ったループ処理ではとても辛い。
・\\uXXXX の正規表現 Matcher を生成
・Matcher をイテレータ処理で一致に対してUTF-8に変換
イテレータを StremSupport でStream生成して、非変換と集約
ということをする必要がある。
以下のメソッドのとおりである。
・必要なインポート

import java.util.Iterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

・引数に、\\uXXXX が混ざった文字列を指定して変換

public String unicodeToUtf8(String ustr) {
   if (ustr==null) return null;
   AtomicInteger i = new AtomicInteger(0);
   Matcher m = Pattern.compile("\\\\u[0-9a-fA-F]{4}").matcher(ustr);
   return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator<String>(){
      @Override
      public boolean hasNext(){
         return m.find();
      }
      @Override
      public String next(){
         return ustr.substring(i.getAndSet(m.end()), m.start())
               + (char)(Integer.parseInt(m.group().substring(2), 16));
      }
   }, Spliterator.ORDERED), false).collect(Collectors.joining()) + ustr.substring(i.get());
}

総称型のクラスを認識する

総称型のインスタンスを与える場合、当たり前だが総称型のクラスは認識できる。

public class Some<T> {
   private Class<T> genericClass;
   
   public Some(T t) {
      genericClass = t.getClass();
   }

このようにコンストラクタで T インスタンスを渡すのではなく、渡さずに genericClass を求めたい。
つまり、

Some<Foo> some = new Some<>();

でも、総称型のクラスを、Some 内部だけの実行範囲で認識したい。(めちゃくちゃな要求だとは思う)

可変長引数のコンストラクタにすることで、解決する。

public class Some<T> {
   private Class<T> genericClass;
   
   @SuppressWarnings("unchecked")
   public Some(T...t) {
      genericClass = (Class<T>)t.getClass().getComponentType();
   }

可変長引数のコンストラクタが気にいらず、インスタンス取得を static メソッドから
生成するようにして、可変長引数のコンストラクタを private コンストラクタにすれば、
利用する側で、可変長引数として使用しないからと思うが、

その方法では、うまくいかず、java.lang.Object しか認識できない。</b>
   ダメな方法!!
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

public class Some<T> {
   private Class<T> genericClass;
   
   @SuppressWarnings("unchecked")
   private Some(T...t) {
      genericClass = (Class<T>)t.getClass().getComponentType();
   }
   @SuppressWarnings("unchecked")
   public static <T> Some<T> getInstance() {
       return new Some<>();
   }

ディレクトリ内のファイルリストを取得

ディレクトリ内のファイルリストは、最も安易な方法は、glob を使うことであるが、
再帰的に全てのファイルリストを得るには、recursive=True が必要である。

import glob

files = glob.glob("/var/tmp/**", recursive=True)
for f in files:
     print(f)

再帰的に全てのファイルリストで、尚且つディレクトリではなくファイルに限定するならば、
pathlib の Path から、glob を使うのが良い。
is_file() で、ファイルに限定できる。
is_dir() なら、ディレクトリに限定

from pathlib import Path

path = Path("/var/tmp")
files = [ v for v in path.glob("**/*") if v.is_file() ]
for f in files:
    print(f)

rglob なら、glob のパターンとして、先頭の "**/" を省略していることになり、

from pathlib import Path

path = Path("/var/tmp")
files = [ v for v in path.rglob("*.txt") if v.is_file() ]
for f in files:
    print(f)

とすれば、さらに拡張子 .txt だけに絞れる

Bean の lenient なコピー

親クラスを全て参照する方法を応用すれば、
以前書いた、yipuran-coreFieldUtil なるものを作り、
yipuran-core/FieldUtil.java at master · yipuran/yipuran-core · GitHub
 public static <R, T> R copy(T t, Supplier<R> s)
 public static <R, T> R copylenient(T t, Supplier<R> s)

も、以下のように書けるはずだ。

public static<T,U> U copylenient(T t, U u){
   UnaryOperator<Class<?>> superFind = c->c.getSuperclass();
   UnaryOperator<String> topUpper = s->s.substring(0, 1).toUpperCase() + s.substring(1);
   Class<?> c = t.getClass();
   try{
      do{
         for(Field f : c.getDeclaredFields()){
            String n = f.getName();
            String name = topUpper.apply(n);
            Method getter = c.getDeclaredMethod(
               (c.getDeclaredField(n).getType().equals(boolean.class) ? "is" : "get")
               + name);
            try{
               Method setter = u.getClass().getDeclaredMethod(
                  "set"+ name, getter.getReturnType());
               setter.invoke(u, getter.invoke(t));
            }catch(NoSuchMethodException e){
            }
         }
      }while(!(c=superFind.apply(c)).equals(Object.class));
   }catch(SecurityException | NoSuchFieldException | NoSuchMethodException
         | IllegalAccessException | IllegalArgumentException | InvocationTargetException e){
      throw new RuntimeException(e);
   }
   return u;
}

PyCharm の日本語化の方法

2020年版までの JetBrains PyCharm の日本語化は、
Pleiades日本語化プラグイン
  https://mergedoc.osdn.jp/
を使ってましたが、
JetBrains社の公式の言語パックで日本語化するのが正しい方法のようです。

PyCharm をインストールして起動直後
f:id:posturan:20210704153132j:plain

Plugins で Japanese と打ち込んで、Japanese Language Pack / 日本語パック を見つけます
f:id:posturan:20210704153401j:plain

[install] を実行します。
f:id:posturan:20210704153443j:plain

[Restart IDE] 再起動をクリックして再起動すれば終わりです。
f:id:posturan:20210704153557j:plain

メモ:Windows においての Python PATH

pip 実行の為のPATH
C:\Users\Xxxxxxx\AppData\Local\Programs\Python\Python39\Scripts\

python 実行の為のPATH
C:\Users\Xxxxxxx\AppData\Local\Programs\Python\Python39\

XxxxxxxWindows ユーザ

ーーーー
python-daemon · PyPI