Python ログ、標準の logging

以前、
Python ログ出力 logging iniファイルを使用しない - Oboe吹きプログラマの黙示録
を書いたが、隣接のサブディレクトリから使用する場合、呼出し側をきちんとインポートしないと、
 ValueError: attempted relative import beyond top-level package
になる。
例えば、
 blue/logger.py
に対して blue と red が隣接、同じ階層ディレクトリであるとき、
red/some.py
から、logger.py をインポートするので、

from ..blue.logger import Logger

と書くが ValueError: attempted relative import beyond top-level package になってしまうことがある。
この場合は、スクリプトがフルパス指定で実行されるのが前提で!

import sys
from pathlib import Path
sys.path.append('%s' % Path(__file__).parent.parent.resolve())
from blue.logger import Logger

と、__file__を利用する
Linux で実行するとき、うっかり、、

$ python  some.py 

と書いてしまうとダメだ、ちゃんとフルパスでスクリプトを指定する必要がある。
以前、書いた logger.py も、あまり良くないので改めて書き直す。

# -*- coding: UTF-8 -*-
from logging import Formatter, handlers, StreamHandler, getLogger, DEBUG, WARN, INFO
import inspect

class Logger:
    def __init__(self, name=__name__):
        if name=="blue.logger":
            fsplits = inspect.stack()[1].filename.split('/')
            name = fsplits[len(fsplits)-1]
        # ロガー生成
        self.logger = getLogger(name)
        self.logger.setLevel(DEBUG)
        formatter = Formatter(fmt="%(asctime)s.%(msecs)03d %(levelname)7s %(message)s [%(name)s  %(processName)s - %(threadName)s]",
                              datefmt="%Y/%m/%d %H:%M:%S")

        # 時刻ローテーション
        handler = handlers.TimedRotatingFileHandler(filename='/var/log/test.log',
                                                    encoding='UTF-8',
                                                    when='D',
                                                    backupCount=7 )
        # サイズローテーション
        ''' 
        handler = handlers.RotatingFileHandler(filename='/var/log/test.log',
                                               encoding='UTF-8',
                                               maxBytes=1048576,
                                               backupCount=3)
        '''
        # ログファイル設定
        handler.setLevel(INFO)
        handler.setFormatter(formatter)
        self.logger.addHandler(handler)
        # 標準出力用 設定: DEBUG レベルまで標準出力する
        sthandler = StreamHandler()
        sthandler.setLevel(DEBUG)
        sthandler.setFormatter(formatter)
        self.logger.addHandler(sthandler)

    def debug(self, msg):
        self.logger.debug(msg)
    def info(self, msg):
        self.logger.info(msg)
    def warn(self, msg):
        self.logger.warning(msg)
    def error(self, msg):
        self.logger.error(msg)
    def critical(self, msg):
        self.logger.critical(msg)

Logger生成で引数を省略すれば、呼出し元スクリプト名が %(name) で 出力

import sys
from pathlib import Path
# sys.path.append('%s' % Path(__file__).parent.parent.resolve())
from ..blue.logger import Logger

logger = Logger()
logger.error("メッセージ  ERROR")
logger.warn("メッセージ  WARN")
logger.info("メッセージ  INFO")
logger.debug("メッセージ  DEBUG")

Logger生成で引数を指定すれば、%(name) は指定した文字列になる

import sys
from pathlib import Path
# sys.path.append('%s' % Path(__file__).parent.parent.resolve())
from ..blue.logger import Logger

logger = Logger('gold')
logger.error("メッセージ  ERROR")
logger.warn("メッセージ  WARN")
logger.info("メッセージ  INFO")
logger.debug("メッセージ  DEBUG")

Java で JWT の利用

JSON Web Tokens - jwt.io
Java で利用する。
利用環境、pom.xml  Maven

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>

JWTの生成

// 有効期間  3分
int minutes = 3;
// 秘密鍵
String secret = "123abc";
// 期間設定の為の計算
LocalDateTime nowtime = LocalDateTime.now();
long nowmilisec = nowtime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
java.util.Date before = Date.from(ZonedDateTime.of(nowtime, ZoneId.systemDefault()).toInstant());
java.util.Date expiretime = new Date(nowmilisec + (60 * minutes * 1000));

// ヘッダに任意のデータセットは Mapで
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("ha", "HA");
headerClaims.put("point", 12);

// アルゴリズムを秘密鍵で生成
Algorithm algorithm = Algorithm.HMAC256(secret);

String token = JWT.create()
   .withJWTId("222")
   .withKeyId("aaaa")
   .withAudience("A", "B", "C")
   .withIssuer("issuer")
   .withSubject("subjects")
   .withIssuedAt(Date.from(ZonedDateTime.of(nowtime, ZoneId.systemDefault()).toInstant()))
   .withNotBefore(before)
   .withExpiresAt(expiretime)
   .withClaim("X-EMAIL", "yip@xxx.xxx.com")
   .withClaim("X-SIZE", 1024)
   .withHeader(headerClaims)
   .sign(algorithm);

ヘッダのJSONが、以下のとおりにできて、

{"kid":"aaaa","ha":"HA","typ":"JWT","alg":"HS256","point":12}

ペイロードJSON が、以下のとおりにできて、

{"aud":["A","B","C"],"sub":"subjects","nbf":1602924613,"iss":"issuer","X-EMAIL":"yip@xxx.xxx.com","exp":1602924793,"iat":1602924613,"X-SIZE":1024,"jti":"222"}

署名が作られて、Base64 エンコードした文字列、String token が作成できます。
これは、それぞれ

String headerDecoded = new String(Base64.getUrlDecoder().decode(JWT.decode(token).getHeader().getBytes()), StandardCharsets.UTF_8);
String payloadDecoded = new String(Base64.getUrlDecoder().decode(JWT.decode(token).getPayload().getBytes()), StandardCharsets.UTF_8);

JSON文字列化することです。

JWT発行時間検査の為の時刻を生成するのに、com.auth0.jwt は、java.util.Date
withIssuedAt , withNotBefore , withExpiresAt で生成しなくてはならず、
java.time.LocalDateTime を使用するのスタンダードな今の時代ではちょっと使いにくいです。
次のJWTトークンの読込み&検証でも使用する以下のユーティリティを用意しておくと便利です。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Locale;
import com.auth0.jwt.exceptions.TokenExpiredException;

public final class JwtUtil {
   private JwtUtil() {}
   public static LocalDateTime getExpiredDate(TokenExpiredException x){
      try{
         java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH)
               .parse(x.getMessage().replaceFirst("The Token has expired on ", ""));
         return Instant.ofEpochMilli(dt.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
      }catch(ParseException e){
         throw new RuntimeException(e);
      }
   }
   public static long getExpireOverTime(TokenExpiredException x) {
      try{
         long epocmili = Instant.now().toEpochMilli();
         java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH)
                     .parse(x.getMessage().replaceFirst("The Token has expired on ", ""));
         return epocmili - dt.getTime();
      }catch(ParseException e){
         throw new RuntimeException(e);
      }
   }
   public static java.util.Date toDate(LocalDateTime at){
      return java.util.Date.from(ZonedDateTime.of(at, ZoneId.systemDefault()).toInstant());
   }
   public static LocalDateTime toLocalDateTime(java.util.Date date) {
      return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
   }
}

JWTトークンの読込み&検証

受信したトークン(Base64エンコード済)は、
 JWT.require(アルゴリズム).build()
で生成した JWTVerifier で、verify(token)実行することで、署名検証&デコードまでするのですが、
以下のように、ラムダ実行できるものを用意すると便利なはずです。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Locale;
import java.util.function.Consumer;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;

public final class JWTVerifyAction{
   private Algorithm algorithm;

   private JWTVerifyAction(Algorithm algorithm){
      this.algorithm = algorithm;
   }
   public static JWTVerifyAction of(Algorithm algorithm){
      return new JWTVerifyAction(algorithm);
   }

   public boolean verifyRead(String token, Consumer<DecodedJWT> decoded, Consumer<Throwable> error){
      JWTVerifier verifier = JWT.require(algorithm).build();
      try{
         DecodedJWT jwt = verifier.verify(token);
         decoded.accept(jwt);
         return true;
      }catch(Throwable e){
         error.accept(e);
      }
      return false;
   }

   public LocalDateTime getExpiredDate(TokenExpiredException x){
      try{
         java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH)
               .parse(x.getMessage().replaceFirst("The Token has expired on ", ""));
         return Instant.ofEpochMilli(dt.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
      }catch(ParseException e){
         throw new RuntimeException(e);
      }
   }
   public long getExpireOverTime(TokenExpiredException x) {
      try{
         long epocmili = Instant.now().toEpochMilli();
         java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH)
                     .parse(x.getMessage().replaceFirst("The Token has expired on ", ""));
         return epocmili - dt.getTime();
      }catch(ParseException e){
         throw new RuntimeException(e);
      }
   }
}

検証で発生する例外Exception は、

SignatureVerificationException secretKeyや署名にエラーがある場合
AlgorithmMismatchException 使用 Algorithm が不一致
JWTDecodeException token 改竄エラー
TokenExpiredException 有効期限切れエラー
InvalidClaimException トークン使用する前にトークン生成時刻になっているエラー


になります。

使用例

Algorithm algorithm = Algorithm.HMAC256("123abc");

JWTVerifyAction action = JWTVerifyAction.of(algorithm);
action.verifyRead(token,
   d->{
      System.out.println("aud        = "+ d.getAudience()  );
      System.out.println("iss        = "+ d.getIssuer()  );
      System.out.println("sub        = "+ d.getSubject()  );
      System.out.println("expired    = "+ d.getExpiresAt() );
      System.out.println("expired    = "+ JwtUtil.toLocalDateTime(d.getExpiresAt()) );
      System.out.println("nbf        = "+ d.getNotBefore() );
      System.out.println("nbf        = "+ JwtUtil.toLocalDateTime(d.getNotBefore()) );
      System.out.println("iat        = "+ d.getIssuedAt() );
      System.out.println("iat        = "+ JwtUtil.toLocalDateTime(d.getIssuedAt()) );
      System.out.println("X-EMAI     = "+ d.getClaim("X-EMAIL").asString()  );
      System.out.println("X-SIZE     = "+ d.getClaim("X-SIZE").asInt()  );
      System.out.println("jti        = "+ d.getId()  );
      System.out.println("Algorithm  = "+ d.getAlgorithm()  );
      System.out.println("header typ = "+ d.getType()  );
      System.out.println("header kid = "+ d.getKeyId() );
      System.out.println("headerClaim ha    = "+ d.getHeaderClaim("ha").asString()  );
      System.out.println("headerClaim point = "+ d.getHeaderClaim("point").asInt()  );

      System.out.println("signature  = "+ d.getSignature() );
      System.out.println("payload    = "+ d.getPayload() );
      System.out.println("header     = "+ d.getHeader() );

      String payloadJson = new String(Base64.getUrlDecoder().decode(d.getPayload().getBytes()), StandardCharsets.UTF_8);
      System.out.println(payloadJson);
      String headerJson = new String(Base64.getUrlDecoder().decode(d.getHeader().getBytes()), StandardCharsets.UTF_8);
      System.out.println(headerJson);
   },
   e->{
      if (e instanceof TokenExpiredException){
         LocalDateTime expAt = JwtUtil.getExpiredDate((TokenExpiredException) e);
         System.out.print("TokenExpiredException !! expAt = "+expAt);
         long overtime = JwtUtil.getExpireOverTime((TokenExpiredException) e);
         System.out.println("  over : " + overtime + " msec");

      }else if(e instanceof SignatureVerificationException){
         System.out.print("SignatureVerificationException !! secretKey or 署名にエラー ");

      }else if(e instanceof AlgorithmMismatchException){
         System.out.print("AlgorithmMismatchException !! アルゴリズムが一致しない ");

      }else if(e instanceof JWTDecodeException){
         System.out.print("JWTDecodeException !! トークンが改竄 ");

      }else if(e instanceof InvalidClaimException){
         System.out.print("InvalidClaimException !! nbf > iat のトークン使用する前にトークン生成時刻になっているエラー");

      }else{
         System.out.println("その他のエラー:" + e.getMessage());
      }
      // TODO

   }
);

com.auth0.jwt は、JSON を処理するのに、Jackson を使用しています。
使用する依存ライブラリは気をつけたほうが良いでしょう。

日付のリスト

timedelta を使って作成する日付リスト

from datetime import datetime, timedelta

# 指定日から7日間のリスト
dlist = [datetime.strptime('2020-10-28', '%Y-%m-%d') + timedelta(days=i) for i in range(7)]
# 検証
list = [d.strftime('%Y-%m-%d') for d in dlist]
print(list)

指定日から7日間のリスト を map(ラムダ関数, シーケンス)で作る

dlist = [d for d in map(lambda a, b=datetime.strptime('2020-10-28', '%Y-%m-%d'): b + timedelta(days=a), range(7))]
# 検証
list = [d.strftime('%Y-%m-%d') for d in dlist]
print(list)

timedelat( days= )を 、minutes に変えれば、分毎のリストも作れる

# 5分前からのリスト
dlist = [d for d in map(lambda a, b=datetime.now(): b + timedelta(minutes=-a), range(6))]
# 検証
list = [d.strftime('%Y-%m-%d %H:%M:%S') for d in dlist]
print(list)

でも、これは時刻の逆順になってしまうので、昇順にするには、

# リストにしてから、reverse()
dlist = [d for d in map(lambda a, b=datetime.now(): b + timedelta(minutes=-a), range(6))]
dlist.reverse()
# 検証
list = [d.strftime('%Y-%m-%d %H:%M:%S') for d in dlist]
print(list)

ちょっと、↑これだと脳がない。 ラムダの前に、逆順にする

dlist = [d for d in map(lambda a, b=datetime.now(): b + timedelta(minutes=-a), reversed(range(6)))]
# 検証
list = [d.strftime('%Y-%m-%d %H:%M:%S') for d in dlist]
print(list)

今月の1日~末日のリスト
monthrange( 年, 月 ) を使用する

from datetime import datetime, timedelta
import calendar

today = datetime.now();
dlist = [d for d in map(lambda a, b=datetime(today.year, today.month, 1): b + timedelta(days=a), range(calendar.monthrange(today.year, today.month)[1]))]
# 検証
list = [d.strftime('%Y-%m-%d') for d in dlist]
print(list)

re の flags

Python の標準正規表現操作 re で、
 先頭:^
 末尾:$
を想定どおりに働かせる場合は、フラグ MULTILINE を指定する。
 flags=re.MULTILINE

例えば、末尾に、カンマ文字 ',' と数字、カンマと数字の間に空白がある可能性があるものを
除去したい時は、

res = re.sub(r',( )*[0-9]+$', '', target, flags=re.MULTILINE)

Python でCSVを読む時の注意

Python では、CSVを読む時、カンマ区切りの後に空白があると読込んだ後に列がズレたり、
(最終列の前のカンマの後に空白が存在して最終列がダブルクォートで括って改行が含まれていると、
 次の行と一緒に列の認識が崩れる!)
想定しない障害になります。

カンマ区切りの後の空白をスキップさせる指定をすることで、想定しないエラーを回避できます。
skipinitialspace=True

Python 標準 csv で読込む時の指定

# -*- coding: UTF-8 -*-
import csv
import codecs

with codecs.open('test.csv', 'r', 'utf8') as f:
    reader = csv.reader(f, skipinitialspace=True)
    for row in reader:
        print(row)

Python pandas で読込む時の指定

# -*- coding: UTF-8 -*-
import pandas

df = pandas.read_csv('test.csv', skipinitialspace=True)
print(df)

同じものをJavaで読込む時、
yipuran-csv
GitHub - yipuran/yipuran-csv: Java CSV read and write
で、読み込むなら、カンマ前後に空白文字があるからといって、
特別な指定をすることなく読込むことができます。

subprocess.Popen で Java System.in にデータ渡す。

subprocess.Popencommunicate に文字列をセットして実行する
Java Class の System.in に入力させてみる。

Java のクラス(実験用なので simple)

package org.talking;

import java.util.Arrays;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger;

public class TalkMain {
   public static void main(String[] args) {
      AtomicInteger x = new AtomicInteger(0);
      Arrays.stream(args).map(e->"args["+x.getAndIncrement()+"] = "+e)
      .forEach(System.out::println);
      System.out.print("-->");
      try(Scanner scan = new Scanner(System.in)){
         System.out.println(scan.nextLine().toUpperCase());
      }
   }
}

java -jar で実行したいので、Maven pom.xml は、以下の plugin で
talkdemo.jar を作る。

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-shade-plugin</artifactId>
   <version>2.4.1</version>
   <executions>
      <execution>
        <phase>package</phase>
        <goals><goal>shade</goal></goals>
        <configuration>
           <finalName>talkdemo</finalName>
           <transformers>
               <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>org.talking.TalkMain</mainClass>
               </transformer>
           </transformers>
           <filters>
               <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                     <exclude>META-INF/*.SF</exclude>
                     <exclude>META-INF/*.DSA</exclude>
                     <exclude>META-INF/*.RSA</exclude>
                  </excludes>
               </filter>
           </filters>
        </configuration>
     </execution>
   </executions>
</plugin>

Pythonソース

# -*- coding: UTF-8 -*-
import subprocess
from subprocess import PIPE

msg = b'Hello'
p = subprocess.Popen('java -jar talkdemo.jar A B C', shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
(pout, perr) = p.communicate(input=msg)
print(pout.decode())

標準出力結果は、

args[0] = A
args[1] = B
args[2] = C
-->HELLO

ここで、注意しなければならないのは、
stdin を、 subprocess.PIPE することと、
communicate input= で渡す文字列引数で
バイトエンコードでなければならない。

msg = 'Hello' では、TypeError: a bytes-like object is required, not 'str'
エラーになるのである。

msg = "Hello".encode()

でも良い。

JWT ペイロードを解析(JavaScript)

JavaScript で、JWT のペイロードだけを Base64 で解析
以下のようなメソッドで充分

function parseJwt (token) {
    var base64Url = token.split('.')[1];
    var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
    return JSON.parse(jsonPayload);
}
let payload= parseJwt( JWTデータ );

console.log( JSON.stringify(payload, null, 4) );

結果

{
    "sub": "1234567890",
    "name": "John Doe",
    "iat": 1516239022
}