SQLの結合3パターン。クロスジョインはカラム列からレコード行への変換で使おう。

データベース

SQLのテーブル結合は、以下の3種類の方法があります。


  • 内部結合
  • 外部結合
  • クロス結合

取得したいデータの内容によって、結合の方法を使い分ける必要があります。


今回は、3種類の結合方法について、PostgreSQLを使って解説していきます。


環境情報


  • DB:PostgreSQL9.6

テーブル定義と格納データ


解説で使用するテーブル定義と格納データは以下になります。

役職テーブルと社員テーブルになります。


create table yakusyoku (
  yakusyoku_id serial not null
  , yakusyoku_name varchar(10) not null
  , constraint yakusyoku_pkey primary key (yakusyoku_id)
) ;

create table syain (
  syain_id serial not null
  ,yakusyoku_id integer
  , syain_name varchar(10) not null
  , constraint syain_pkey primary key (syain_id)
) ;
"yakusyoku_id"	"yakusyoku_name"
-----------------------------------
1				"部長"
2				"課長"
3				"係長"
4				"主任"
"syain_id"	"yakusyoku_id"	"syain_name"
----------------------------------------------
1			1				"山田部長"
2			2				"鈴木課長"
3			3				"佐藤係長"
4			3				"佐藤係長"

内部結合/依存関係ありのデータを取得する


内部結合は、依存関係(データ間のリレーション)があるデータのみを取得します。
今回の例では、役職が付いている社員のみが取得できることになります。


SELECT
    A.yakusyoku_id	
    A.yakusyoku_name	
    B.syain_id	
    B.syain_name
FROM
    yakusyoku as A INNER JOIN syain as B 
ON 
    A.yakusyoku_id = B.yakusyoku_id;
"yakusyoku_id"	"yakusyoku_name"	"syain_id"	"syain_name"
-----------------------------------------------------------
1				"部長"				1			"山田部長"
2				"課長"				2			"鈴木課長"
3				"係長"				3			"佐藤係長"
3				"係長"				4			"佐藤係長"

外部結合/依存関係なしのデータも取得する


内部結合は、依存関係がないデータのみも取得します。
今回の例では、主任の社員はいないのですが、役職テーブルのメインテーブルとして外部結合をおこなっているので、主任レコードも取得できます。


SELECT
    A.yakusyoku_id	
    A.yakusyoku_name	
    B.syain_id	
    B.syain_name
FROM
    yakusyoku as A LEFT OUTER JOIN syain as B 
ON 
    A.yakusyoku_id = B.yakusyoku_id;
"yakusyoku_id"	"yakusyoku_name"	"syain_id"	"syain_name"
-----------------------------------------------------------------
1				"部長"				1			"山田部長"
2				"課長"				2			"鈴木課長"
3				"係長"				3			"佐藤係長"
3				"係長"				4			"佐藤係長"
4				"主任"				""			""

クロス結合/組み合わせを取得する


クロス結合の基本文法


クロス結合は、レコード同士の組み合わせを取得します。
今回の例では、全ての役職と全ての社員の組み合わせを取得できます。


結合条件も指定しないので、役職テーブルの主任レコードに対する組み合わせについても取得できます。


SELECT
    A.yakusyoku_id	
    A.yakusyoku_name	
    B.syain_id	
    B.syain_name
FROM
    yakusyoku as A CROSS JOIN syain as B;
"yakusyoku_id"	"yakusyoku_name"	"syain_id"	"syain_name"
--------------------------------------------------------------
1				"部長"				1			"山田部長"
1				"部長"				2			"鈴木課長"
1				"部長"				3			"佐藤係長"
1				"部長"				4			"佐藤係長"
2				"課長"				1			"山田部長"
2				"課長"				2			"鈴木課長"
2				"課長"				3			"佐藤係長"
2				"課長"				4			"佐藤係長"
3				"係長"				1			"山田部長"
3				"係長"				2			"鈴木課長"
3				"係長"				3			"佐藤係長"
3				"係長"				4			"佐藤係長"
4				"主任"				1			"山田部長"
4				"主任"				2			"鈴木課長"
4				"主任"				3			"佐藤係長"
4				"主任"				4			"佐藤係長"

クロス結合の使いどころ。横→縦変換


クロスジョインはあまり用途がないように思いますが、よく使う用途としては横→縦変換になります。


横→縦変換とは、カラム毎に格納されている値を、レコード事に格納することです。
以下のようなイメージですね。


以下に具体的を使って説明しています。

以下テーブルの横→縦変換をおこないます。


create table col_test (
  col_test_id serial not null
  , name1 varchar(10) not null
  , name2 varchar(10) not null
  , name3 varchar(10) not null
  , name4 varchar(10) not null
  , name5 varchar(10) not null
  , constraint col_test_id_pkey primary key (col_test_id)
) ;
	"name1",	"name2",	"name3",	"name4",	"name5"
--------------------------------------------------------------
1,	"名称1_A",	"名称2_B",	"名称3_C",	"★名称4_D","名称5_E"
2,	"★名称1_B","名称2_B",	"名称3_B",	"名称4_B",	"名称5_B"
3,	"名称1_C",	"★名称2_C","★名称3_C","名称4_C",	"名称5_C"

このテーブルから、名称の先頭に「★」が付いている値を行データとして取得するとします。 この場合、縦→横変換をおこなうと便利です。


縦→横変換をおこなうために、テンポラリテーブルを使っています。
テンポラリテーブルで、1~5の値が入っているのみのテーブルを作成しています。


このテンポラリテーブルと対象テーブル(ここではpivot)をCROSS JOINを使って組み合わせを作成し、その組み合わせで名称の先頭に「★」ついているレコードのみを取得しています。


-- テンポラリテーブルの作成
CREATE TEMPORARY TABLE pivot (
 val integer PRIMARY KEY
);

-- テンポラリテーブルに1~5の値が入った状態にする
INSERT INTO pivot with recursive rec(val) as (
 values(1)
 union all
 select 
    Val+1
 from 
    rec
 where 
    Val+1 <= 5
) select Val from rec;

-- 名称の先頭に「★」がついたレコードのみを抽出
SELECT * FROM (
    SELECT
        case P.val
            when 1 then A.name1
            when 2 then A.name2
            when 3 then A.name3
            when 4 then A.name4
            when 5 then A.name5
        end as cross
    FROM
        col_test as A CROSS JOIN pivot P) AS X
WHERE
    X.cross like '★%';
"cross"
-------------------
★名称1_B
★名称2_C
★名称3_C
★名称4_D


PythonをApache上でCGIとして動作させる方法

apache

Pythonを使ってWEBアプリケーションを構築する場合、Apacheを使ってCGIとしてPythonを動作させるのが一般的です。
PythonをApache上で動作させるには、PythonとApacheをインストールして、Apacheの設定ファイルを環境にあわせて修正するだけでよいです。


今回は、PythonをApache上で動作させる方法について紹介していきます。


環境情報


  • OS:Windows10
  • Apache:Apache 2.4.41
  • Python:Phton3.9

Pythonスクリプトの格納


まず、使用するPythonスクリプトファイルをApacheの所定の場所に格納します。
ApacheのCGIとしてPythonを動かすので、Apacheのルートディレクトリに存在する「cgi-bin」に格納します。


PythonをCGIとして動作させる場所

今回は、以下の”Hello,World!”を表示するだけのスクリプトで動作確認をおこないます。


#! C:\Python34\python

print('Content-Type: text/html')
print('')
print('Hello,World!')

1行目は、Phthonのインストールパスを記載する必要があります。
筆者のマシンでは、「C:\Python34\python」にPythonをインストールしています。


Apacheのインストールについては省略してしまいましたが、以下の記事を参照してください。


Apache設定ファイルの修正


次に、PythonをApacheのCGIとして動かすためにApacheの設定ファイルを修正します。
修正するのは、Apacheの設定ファイルでは「httpd.conf」になります。


筆者のマシンのApacheは「C:\Apache24」にインストールしているのですが、「httpd.conf」の格納先は以下になります。


・C:\Apache24\conf\httpd.conf

「httpd.conf」の修正箇所は大きくわけて2カ所になります。 まず、Scriptエイリアスの設定で、「cg-bin」フォルダをcgi実行スクリプトフォルダとして有効にします。


<IfModule alias_module>
    #
    # Redirect: Allows you to tell clients about documents 
    # exist in your server's namespace, but do not anymore.  
    # will make a new request for the document at its location.
    # Example:
    # Redirect permanent /foo http://www.example.com/bar

    #
    # Alias: Maps web paths into filesystem paths and is used to
    # access content that does not live under the DocumentRoot.
    # Example:
    # Alias /webpath /full/filesystem/path/
    #
    # If you include a trailing / on /webpath then the server will
    # require it to be present in the URL.  You will also likely
    # need to provide a <Directory> section to allow access to
    # the filesystem path.

    #
    # ScriptAlias: This controls which directoriesscripts. 
    # ScriptAliases are essentially the same as
    # documents in the target directory are treated 
    # run by the server when requested rather than as 
    # client.  The same rules about trailing "/" apply
    # directives as to Alias.
    #
    ScriptAlias /cgi-bin/ "${SRVROOT}/cgi-bin/"
</IfModule>

通常、上記の設定はデフォルトで有効になっているので、特に設定変更する必要はないかもしれません。


もう1か所の変更箇所が、「cgi-bin」フォルダのディレクトリ設定です。
拡張子「.py」をスクリプトファイルとして認識します。

以下の例では、「.py」ファイルだけではなく「.cgi」「.pl」もスクリプトファイルとして設定しています。


<Directory "${SRVROOT}/cgi-bin">
   AllowOverride none
   AddHandler cgi-script .cgi .pl .py
   Options +ExecCGI
   Require all granted
</Directory>

上記の設定をおこなった後にApacheを再起動し、ブラウザでアクセスして格納したPythonスクリプトが動作するかを確認します。


以下の画面が表示されれば設定成功です。



PythonスクリプトとHTMLを共存させる場合


「cgi-bin」下にはスクリプトファイル(上記例では「.cgi」「.pl」「.py」)だけではなく、通常のHTMLファイルも格納したいことはあると思います。
その場合は、「cgi-bin」のディレクトリ設定で「AddHandler text/html」の設定をおこないます。


以下の例では、「cgi-bin」下に「.html」「.htm」「.txt」「.css」「.js」の配置を許可しています。


<Directory "${SRVROOT}/cgi-bin">
   AllowOverride none
   AddHandler cgi-script .cgi .pl .py
   AddHandler text/html .html .htm .txt .css .js
   Options +ExecCGI
   Require all granted
</Directory>

この設定をいれることで、「cgi-bin」下にスクリプトファイル以外の静的HTMLについても格納可能になります。



画像からExif情報を抜き出すJavaプログラム

Java

スマートフォンやデジタルカメルで撮影した画像のメタ情報として、Exifという情報があります。
このExif情報には、画像を取った位置情報や画像の確度といった、様々なメタ情報が格納されています。


今回は、このExif情報をJavaで取得する方法を紹介していきます。


環境情報


Java:Java1.8.0_281


Exif情報の取得プログラム


Exif情報の取得サンプルプログラムは以下になります。
出力結果が解り易いように、出力結果を「Exif IFD0」「Exif SubIFD」「GPS」の3つのカテゴリ毎に出力をおこなっています。


サンプルプログラムではExif情報を「Metadata」に格納し、「getDirectories()」でメタ情報を取得していきます。
Exif情報の代表的な以下3つの情報を標準出力しています。


項目

Tagクラスのメソッド

出力内容

タグタイプ

getTagType()

Exif情報のID

タグ名

getTagName()

タグのタイプ(名称)

説明

getDescription()

設定内容


import java.io.File;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;

public class Exif {

    /** Exif IFD0 */
    private static final String STR_IFD = "Exif IFD0";

    /** Exif SubIFD */
    private static final String STR_SUBIFD = "Exif SubIFD";

    /** GPS */
    private static final String STR_GPS = "GPS";

    // メイン処理
    public static void main(String[] args){

        System.out.print("start: Exif\r\n");

        try {

            /** 対象ファイルを読み込み */
            File file = new File("./sample.jpg");
            Metadata metadata = 
                ImageMetadataReader.readMetadata(file);

            /** ディレクトリ情報でループ */
            for (Directory directory : metadata.getDirectories()) {
                String dirName = directory.getName();

                /** Exif IFD0 */
                if (STR_IFD.equals(dirName)) {
                    System.out.print(
                        "------directoryName=" + dirName + 
                        "------\r\n");
                    for (Tag tag : directory.getTags()) {
                        String tagName = tag.getTagName();
                        int tagType = tag.getTagType();
                        String desc = tag.getDescription();
                        System.out.print(
                            Integer.toString(tagType) + ":" + 
                            tagName + "=" + desc + "\r\n");
                    }
                }

                /** Exif SubIFD */
                if (STR_SUBIFD.equals(dirName)) {
                    System.out.print(
                        "------directoryName=" + dirName + 
                        "------\r\n");
                    for (Tag tag : directory.getTags()) {
                        String tagName = tag.getTagName();
                        int tagType = tag.getTagType();
                        String desc = tag.getDescription();
                        System.out.print(
                            Integer.toString(tagType) + ":" + 
                            tagName + "=" + desc + "\r\n");
                    }
                }

                /** GPS */
                if (STR_GPS.equals(dirName)) {
                    System.out.print(
                        "------directoryName=" + dirName + 
                        "------\r\n");
                    for (Tag tag : directory.getTags()) {
                        String tagName = tag.getTagName();
                        int tagType = tag.getTagType();
                        String desc = tag.getDescription();
                        System.out.print(
                            Integer.toString(tagType) + ":" + 
                            tagName + "=" + desc + "\r\n");
                    }
                }
            }
        } catch (Exception e) {
            System.out.print("occured exception \r\n");
        }

        System.out.print("end: Exif\r\n");
    }
}

Exif情報の取得例


Exif情報の取得例を以下に説明します。
今回は、ネットで公開しているExif情報が格納されている画像に対してサンプルプログラムを実行しています。


画像を撮った位置情報についても確認できます。
一番下に出力されている「0:GPS」~「27:GPS」の情報になります。



start: Exif
------directoryName=Exif IFD0------
270:Image Description=SA390025
271:Make=KDDI-SA
272:Model=W51SA
274:Orientation=Top, left side (Horizontal / normal)
282:X Resolution=72 dots per inch
283:Y Resolution=72 dots per inch
296:Resolution Unit=Inch
306:Date/Time=2008:07:08 15:32:08
531:YCbCr Positioning=Center of pixel array
------directoryName=Exif SubIFD------
33437:F-Number=f/3.0
36864:Exif Version=2.20
36867:Date/Time Original=2008:07:08 15:32:08
36868:Date/Time Digitized=2008:07:08 15:32:08
37121:Components Configuration=YCbCr
37378:Aperture Value=f/2.8
37379:Brightness Value=1
37381:Max Aperture Value=f/2.8
37382:Subject Distance=0.0 metres
37383:Metering Mode=Average
37384:White Balance=Unknown
37385:Flash=Flash did not fire, auto
37386:Focal Length=4.8 mm
40960:FlashPix Version=1.00
40961:Color Space=sRGB
40962:Exif Image Width=1600 pixels
40963:Exif Image Height=1200 pixels
41729:Scene Type=Directly photographed image
41985:Custom Rendered=Normal process
41986:Exposure Mode=Auto exposure
41987:White Balance Mode=Auto white balance
41988:Digital Zoom Ratio=1
41989:Focal Length 35=28 mm
41990:Scene Capture Type=Standard
41992:Contrast=None
41993:Saturation=None
41994:Sharpness=None
41996:Subject Distance Range=Unknown
------directoryName=GPS------
0:GPS Version ID=2.200
1:GPS Latitude Ref=N
2:GPS Latitude=35° 42' 2.91"
3:GPS Longitude Ref=E
4:GPS Longitude=139° 45' 57.52"
18:GPS Map Datum=WGS-84
27:GPS Processing Method=65 83 67 73 73 0 0 0 71 80 83 45 70 73 88
end: Exif


バッチ処理の二重起動チェックをシェルだけで実現する方法

Linux

Linux系のОSでバッチを起動する場合は、シェルを作成して、そのシェルからJavaなどのプログラムを呼び出すケースがほとんどかと思います。


バッチで問題になるのが「バッチの重複起動」になります。
“バッチ突き抜け”とも言いますが、1つのバッチが終了する前に同じバッチが起動してしまうと二重起動の状態になってしまい、思わぬ不具合が発生してしまいます。
この場合、プログラムで重複起動をおこなわないような制御をおこなう必要があります。


今回は、このバッチ重複起動チェックをシェルだけで実現する方法を紹介します。


環境情報


  • OS:CentOS 7.6

pgrepコマンドとは


重複起動チェックで使用するコマンドは「pgrep」コマンドです。
「pgrep」コマンドは、プロセスが保持しているプロセス名属性からプロセスIDを取得するコマンド
になります。


「ps」コマンドと「grep」を足したコマンドですね。
psコマンドの出力結果をパイプ(|)で繋いでgrepで抽出することが多いかと思いますが、この操作を同時に実行することが「pgrep」コマンドになります。


基本的な文法は以下になります。

pgrep [オプション] [プロセスの特定パターン]

二重起動チェックのシェルプログラム


二重起動チェックのシェルプログラムは以下になります。
「pgrep」コマンドで自身のプロセス名でプロセス番号を取得し、そのプロセス番号と自身のプロセス番号と比較しています。
プロセス番号の比較結果が異なる場合は、自身のプロセスと同名の他プロセスが動作していると判断する、ということですね。


#!/bin/sh

echo "start"
PGREP=`pgrep -f $0 -o`
PGREP_RSLT=$?

if [ $$ != $PGREP  ];
then
  echo "$0は起動中です。二重起動チェックです。"
  exit 1
fi

sleep 180
echo "end"

上記サンプルプログラムを同時に実行すると、一方のシェルはチェックに掛かって起動できません。
二重起動チェックにかかりやすいように、180秒のスリープをおこないバッチ実行中の時間を長くとっています。


pgrepコマンドのオプションと特定パターン


二重起動チェックで使用している「pgrep」コマンドのオプションや、使用している特殊変数について説明していきます。


まずは「pgrep」コマンドのオプションです。
二重起動チェックで使用しているのは「-f」オプションと「-o」オプションになります。


「-f」「-o」オプション含めて、よく使用するオプションを紹介しておきます。


オプション フルオプション 説明

-f

–full

コマンドライン全体をパターンマッチの対象とする。

-o

–oldest

対象のプロセスから最も昔に起動されたものだけを対象とする。

-n

–newest

対象のプロセスから最も最近に起動されたものだけを対象とする。

-U ユーザ

–uid ユーザ

指定したユーザのプロセスを対象とする。「,」区切りで複数指定可能。

-v

–inverse

検索パターンの否定。


次に特殊変数についてです。


  • $0:実行中プロセスのシェルファイル名。
  • $?:直前に実行したコマンドの実行結果。成功時は「0」。失敗時は「0」以外。
  • $$:実行中プロセスのプロセスID。

この特殊変数は非常に便利です。
実際の動きを紹介していきます。


$0特殊変数。実行中プロセスのファイル名。


実行中プロセスのシェルファイル名は「test.sh」となります。
そのファイル名がそのまま特殊変数として格納されています。


#!/bin/sh

echo "start"
echo $0
echo "end"

test.sh

$?特殊変数。コマンド実行結果。


コマンドの実行結果が格納されています。
厳密にはコマンドの実行結果も参照、二重起動チェックに成功したか?失敗したか?についても処理分岐が必要となります。


#!/bin/sh

echo "start"
PGREP=`pgrep -f $0 -o`
echo $?
echo "end"

0

$$特殊変数。実行中プロセスのプロセスID。


実行中プロセスのプロセスIDが格納されています。


#!/bin/sh

echo "start"
echo $$
echo "end"

4269


JavaでNULL判定・空文字判定をおこなう場合に注意するポイント

Java

Javaで文字列のNULL判定や空文字判定をおこなう場合、判定ロジックに気を付けないと思いもよらない例外が発生する場合があります。

今回は、NULL判定や空文字判定をおこなう場合の注意すべきポイントをサンプルプログラム付きで説明していきます。


環境情報


  • Java:Java1.8.0_281

NULL判定


文字列の比較はStringクラスのequalsを使用しますが、equalsメソッドを使用する変数を比較元にするか?もしくは、比較先にする?によって、例外の発生パターンが異なってきます。

public class NullSample {

    // NULLの定数
    private static String DEFINE_NULL = null;

    // 空文字の定数
    private static String DEFINE_KARA = "";

    public static void main(String[] args){
        System.out.print("start: main\r\n");


        /*------------------------------------------*/
        /* NULL判定                             */
        /*------------------------------------------*/

        // 1.NULL判定/定数が左側
        try {
            String val = "abc";
            if (DEFINE_NULL.equals(val)) {
                System.out.print("val is null \r\n");
            } else {
                System.out.print("val is not null \r\n");
            }
        } catch (Exception e) {
            System.out.print("NULL判定で例外!定数が左側 \r\n");
        }

        // 2.NULL判定/定数が右側
        try {
            String val = "abc";
            if (val.equals(DEFINE_NULL)) {
                System.out.print("val is null \r\n");
            } else {
                System.out.print("val is not null \r\n");
            }
        } catch (Exception e) {
            System.out.print("NULL判定で例外!定数が右側 \r\n");
        }
        System.out.print("end: main\r\n");
    }
}

プログラムの実行結果は以下になります。


start: main
NULL判定で例外が発生!定数が左側  ← 1の判定結果
val is not null                    ← 2の判定結果
end: main

1の判定について、例外が発生しています。
equalsメソッドを使用するオブジェクト自体がNULLの場合は例外が発生するということがわかります。


そのため、実際のプログラムでは、NULLが格納される可能性のある変数ではequalsメソッドは使用しないことが推奨されます。


空文字判定


次は空文字の判定についてです。
サンプルプログラムで動きを確認してみます。


public class KaraSample {

    // NULLの定数
    private static String DEFINE_NULL = null;

    // 空文字の定数
    private static String DEFINE_KARA = "";

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        /*------------------------------------------*/
        /* 空判定                             */
        /*------------------------------------------*/

        // 1.空判定/定数が左側
        try {
            String val = "abc";
            if (DEFINE_KARA.equals(val)) {
                System.out.print("val is kara \r\n");
            } else {
                System.out.print("val is not kara \r\n");
            }
        } catch (Exception e) {
            System.out.print("空判定で例外が発生!定数が左側 \r\n");
        }

        // 2.空判定/定数が右側
        try {
            String val = "abc";
            if (val.equals(DEFINE_KARA)) {
                System.out.print("val is kara \r\n");
            } else {
                System.out.print("val is not kara \r\n");
            }
        } catch (Exception e) {
            System.out.print("空判定で例外が発生!定数が右側 \r\n");
        }
        System.out.print("end: main\r\n");
    }
}

val is not kara    ← 1の判定結果
val is not kara    ← 2の判定結果

1と2のどちらの方法であっても、例外が発生せずに空文字判定をおこなうことができています。


上記のサンプルプログラムは、判定結果が空文字ではないという結果になっていますが、試しに空文字での判定をおこなってみます。


public class KaraSample {

    // NULLの定数
    private static String DEFINE_NULL = null;

    // 空文字の定数
    private static String DEFINE_KARA = "";

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        /*------------------------------------------*/
        /* 空判定                             */
        /*------------------------------------------*/

        // 1.空判定/定数が左側
        try {
            String val = "";
            if (DEFINE_KARA.equals(val)) {
                System.out.print("val is kara \r\n");
            } else {
                System.out.print("val is not kara \r\n");
            }
        } catch (Exception e) {
            System.out.print("空判定で例外が発生!定数が左側 \r\n");
        }

        // 2.空判定/定数が右側
        try {
            String val = "";
            if (val.equals(DEFINE_KARA)) {
                System.out.print("val is kara \r\n");
            } else {
                System.out.print("val is not kara \r\n");
            }
        } catch (Exception e) {
            System.out.print("空判定で例外が発生!定数が右側 \r\n");
        }
        System.out.print("end: main\r\n");
    }
}

val is kara    ← 1の判定結果
val is kara    ← 1の判定結果

正常に、空文字の判定がおこなわれていることがわかります。



ClientHelloとServerHelloをJavaのDebugログで確認する

Java

SSLハンドシェイクにおいて基本となるのは、通信処理の最初に実施するClientHelloとServerHelloです。


今回は、このSSLハンドシェイクの挙動をJavaのDebugログで出力して、ClientHelloとServerHelloの動きを確認していこうと思います。


環境情報


  • OS:Windows10
  • Java:Java1.8.0_281

SSLハンドシェイクのプログラム


SSLハンドシェイクを確認するために、Javaのサンプルプログラムを作成して、実行結果をログ出力してみます。
サンプルプログラムは、ポータルサイトである「Excite」にアクセスしているのみです。


import java.io.FileOutputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class HTTPReq {

    public static void main(String[] args){

        try {
            FileOutputStream fos = new FileOutputStream("out.txt");
            PrintStream ps = new PrintStream(fos);
            System.setOut(ps); 

            System.out.print("start: main\r\n");

            // 1回目コネクション
            URL url = new URL("https://www.excite.co.jp/");
            HttpURLConnection con = null;

            con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod("POST");
            con.connect();

            int statusCode = con.getResponseCode();
            System.out.print("statusCode=" + Integer.toString(statusCode) + "\r\n");

        } catch (Exception e) {
            System.out.print("例外が発生!");
            e.printStackTrace();
        } finally {
        }
        System.out.print("end: main\r\n");
    }
}

Javaの実行バッチは以下になります。


@echo on
set CLASSPATH=.
java -Djavax.net.debug=all HTTPReq >> result.txt 2>&1

Javaのデバッグログは、debug=all」オプションを付けることで出力されます。

-Djavax.net.debug=all

Javaの標準出力は「out.txt」に出力し、デバッグログは「result.txt」に出力しています。


ClientHello


ClientHelloからServerHelloの流れは、SSLハンドシェイクの開始部分となります。
開始部分であっても、Javaのデバッグログには大量の情報が出力されます。


ClientHelloで、クライアントからサーバに対して送信するデータに以下があります。

  • SSL/TLSのバージョン
  • CipherSuiteのリスト
  • セッションID
  • ランダム値
  • 圧縮アルゴリズム
  • TLS拡張情報

実際に出力されるのは以下のような情報となります。

"ClientHello": {
  "client version"      : "TLSv1.2",
  "random"              : "XXX",
  "session id"          : "",
  "cipher suites"       : "[TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xC02C), ・・・]",
  "compression methods" : "00",
  "extensions"          : [
    "server_name (0)": {
      type=host_name (0), value=www.excite.co.jp
    },
    "supported_groups (10)": {
      "versions": [secp256r1, ・・・]
    },
    "ec_point_formats (11)": {
      "formats": [uncompressed]
    },
    "signature_algorithms (13)": {
      "signature schemes": [ecdsa_secp256r1_sha256, ・・・]
    },
    "signature_algorithms_cert (50)": {
      "signature schemes": [ecdsa_secp256r1_sha256, ・・・]
    },
    "extended_master_secret (23)": {
      
    },
    "supported_versions (43)": {
      "versions": [TLSv1.2, TLSv1.1, TLSv1]
    }
  ]
}

SSL/TLSのバージョンのバージョンは、クライアントで使用できるバージョンをサーバに通知しています。
TLS1、1.1、1.2などがあります。


CipherSuiteのリストは、クライアントで使用できるCipherSuiteをリストとしてサーバに通知しています。
このログ以前にも様々な情報が出力されるのですが、クライアントで使用できるCipherSuiteのリストを算出し、サーバに通知します。


セッションIDはクライアント側で保持しているセッションのIDです。
複数の通信をおこなう場合、クライアントはセッションで保持している情報を使用してSSLハンドシェイクをおこないます。
セッションIDを使用した場合、SSLハンドシェイクの手順をスキップすることができます。


ServerHello


ClientHelloに対するサーバ側のレスポンスがServerHelloになります。


ServerHelloで、サーバからクライアントに対して送信するデータには以下の種類があります。


  • 合意したSSL/TLSのバージョン
  • ランダム値
  • セッションID
  • 合意したCipherSuite
  • 圧縮アルゴリズム
  • TLS拡張情報

"ServerHello": {
  "server version"      : "TLSv1.2",
  "random"              : "XXX",
  "session id"          : "YYY",
  "cipher suite"        : "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC02F)",
  "compression methods" : "00",
  "extensions"          : [
    "renegotiation_info (65,281)": {
      "renegotiated connection": []
    },
    "server_name (0)": {
      
    },
    "ec_point_formats (11)": {
      "formats": [uncompressed, ansiX962_compressed_prime, ansiX962_compressed_char2]
    },
    "extended_master_secret (23)": {
      
    }
  ]
}

SSL/TLSのバージョンのバージョンは、クライアント側から提案したTLSのバージョンに対するサーバ側の返却になります。
このバージョンが一致しなければ、SSLハンドシェイクは成立しません。
サーバ側はTLS1.2で要求しているのにサーバ側がTLS1.0のみを許可している、といった場合がそのパターンになります。


合意したCipherSuiteは、クライアント側から提案したCipherSuiteのリストを受信したサーバ側が、そのリストから使用するCipherSuiteを決定します。


最終的に、ServerHelloDoneが出力されれば、ClientHelloからServerHelloの一連の動きは成功です。


javax.net.ssl|FINE|01|main|2021-04-24 23:11:34.992 JST|ServerHelloDone.java:151|Consuming ServerHelloDone handshake message (

)


Pythonの配列・リストの指定要素を削除する方法。OKパターンとNGパターンを紹介。

phtyonロゴ

Pythonでも、一般の他プログラミング言語と同様に配列やリストを扱うことができます。

今回は、配列やリストに対して指定要素を削除する際の方法を紹介していきます。


環境情報


  • Python 3.4.1

配列


配列に対しての要素削除方法について説明していきます。

削除でも様々な方法がありますが、それぞれサンプルプログラム付きで説明していきます。


全削除


配列の全削除は、clearを使用します。


プログラムとしては以下になります。

array = ["A", "B", "C", "D", "E"]
print(array)
array.clear()
print(array)

以下のように、配列の要素は全削除されます。

['A', 'B', 'C', 'D', 'E']
[]

指定位置要素を削除


削除する要素の位置(インデックス)が分かっている場合は、popを使います。
popを使うと、指定位置の要素が削除され、削除した要素の取得もおこなうことができます。


array = ["A", "B", "C", "D", "E"]
print(array)
val = array.pop(2)
print(val)
print(array)

以下のように、指定位置の要素を取得した上で、削除をおこなっています。

['A', 'B', 'C', 'D', 'E']
C
['A', 'B', 'D', 'E']

指定要素と一致する要素を削除


削除する要素の中身が分かっている場合は、removeを使います。


array = ["A", "B", "C", "D", "E"]
print(array)
array.remove("C")
print(array)

以下のように、指定した要素のみが削除されます。

['A', 'B', 'C', 'D', 'E']
['A', 'B', 'D', 'E']

 


繰り返し中に削除/NGパターン


配列の要素を繰り返しで検索していき、削除する要素であった場合は削除する、というパターンはよくあるかと思います。
この場合、配列を先頭から検索して削除する方法はNGになります。


array = ["A", "B", "C", "D", "E"]
lc = 0
for val in array:
    print(val)
    if val == "C":
        array.remove(val)
    print(array)
    lc = lc + 1
print(array)

NGなのは、要素の「C」を削除するときで要素がずれてしまい、次の検索要素が「E」になってしまっているという点です。
本来であれば、「C」の次は「D」が正解になります。


A
['A', 'B', 'C', 'D', 'E']
B
['A', 'B', 'C', 'D', 'E']
C
['A', 'B', 'D', 'E']
E
['A', 'B', 'D', 'E']
['A', 'B', 'D', 'E']

繰り返し中に削除/OKパターン


繰り返し中に削除する方式のOKパターンとしては、末尾から先頭へ繰り返しをおこなう方法です。
末尾から繰り返しをおこなうためには、「reversed」を使います。


array = ["A", "B", "C", "D", "E"]
lc = 0
for val in reversed(array):
    print(val)
    if val == "C":
        array.remove(val)
    print(array)
    lc = lc + 1
print(array)

末尾からループして指定要素を削除しているので、削除した「C」の後に「B」を検索しています。
この方法が正解であることがわかります。


E
['A', 'B', 'C', 'D', 'E']
D
['A', 'B', 'C', 'D', 'E']
C
['A', 'B', 'D', 'E']
B
['A', 'B', 'D', 'E']
A
['A', 'B', 'D', 'E']
['A', 'B', 'D', 'E']

リスト


リストに対しても配列と同じ方法で削除操作をおこなうことができます。


全削除


リストの全削除も、配列と同様にclearを使用します。


array = [["A", "B", "C"], ["D", "E", "F"], ["G", "H", "I"]]
print(array)
array.clear()
print(array)

[['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']]
[]

指定位置要素を削除


削除する要素の位置(インデックス)が分かっている場合も、配列と同様にpopを使います。


array = [["A", "B", "C"], ["D", "E", "F"], ["G", "H", "I"]]
print(array)
val = array.pop(1)
print(val)
print(array)

[['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']]
['D', 'E', 'F']
[['A', 'B', 'C'], ['G', 'H', 'I']]

指定要素と一致する要素を削除


削除する要素の中身が分かっている場合も、配列と同様にremoveを使います。


array = [["A", "B", "C"], ["D", "E", "F"], ["G", "H", "I"]]
print(array)
array.remove(["D", "E", "F"])
print(array)

[['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']]
[['A', 'B', 'C'], ['G', 'H', 'I']]

繰り返し中に削除


配列と同様に、リストの要素を繰り返しして削除をおこなう方法は、先頭から繰り返しをおこなうのではなく、末尾から繰り返しをおこなって削除をおこなう方法が正解となります。 配列と同様にreversedを使います。


array = [["A", "B", "C"], ["D", "E", "F"], ["G", "H", "I"]]
lc = 0
for val in reversed(array):
    print(val)
    if val == ["D", "E", "F"]:
        array.remove(val)
    print(array)
    lc = lc + 1
print(array)

['G', 'H', 'I']
[['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']]
['D', 'E', 'F']
[['A', 'B', 'C'], ['G', 'H', 'I']]
['A', 'B', 'C']
[['A', 'B', 'C'], ['G', 'H', 'I']]
[['A', 'B', 'C'], ['G', 'H', 'I']]


Javaの起動オプションにGCログ設定とヒープダンプを設定する

Java

Javaを用いたプログラムで、運用開始後に原因不明の障害が発生する場合は多々あります。


原因不明のメモリリーク、CPUの一時的な極端な上昇といった現象は、アプリケーションのログからはなかなか原因が突き止められない場合があります。
こういった事象に対応するために、GCログの出力と、OutOfMemory発生時のヒープダンプ出力をあらかじめ仕込んでおく必要があります。


今回は、Javaアプリケーションの起動設定について、GCログとヒープダンプの出力設定をおこなう方法について紹介します。


環境情報


  • Java 1.8.0_281

GCログの出力設定


GCログの出力設定は、Javaの起動オプションとして設定します。
設定オプションは以下になります。


 

-verbose:gc

GCログを出力する。

-Xloggc

GCログの出力先を設定する。

例)-Xloggc:./gc.log-%t

 

ファイル名に設定できる変数要素としては以下の2つがあります。

%t=ログファイルを作成した日時

%p=JavaプロセスのID

-XX:+UseGCLogFileRotation

GCログのローテーションをおこないます。

-XX:+PrintGCDateStamps

GCログに時間のタイムスタンプを表示します。

-XX:+PrintGCDetails

GCログの詳細を出力します。

-XX:NumberOfGCLogFiles

GCログのローテーションをおこなう際、ローテーションするファイルの数を定義します。

-XX:GCLogFileSize

1つのログファイルのファイルサイズを定義します。

定義したファイルサイズに到達したタイミングで、ローテーションがおこなわれます。

定義したファイルサイズに到達したタイミングで、ローテーションがおこなわれます。

 

M=メガ

k=キロ(最小は7K

 


設定例としては以下になります。
実行するプログラムは「GcSample」となり、10キロのGCログファイルを最大5ファイルでローテーションをおこなっています。
GCログファイルにはログファイルを作成した日時を設定するために「%t」オプションを定義していますが、以下はWindowsのバッチファイル例なので、”%%”と記載してエスケープしています。


java -Xms256m -Xmx256m -verbose:gc -Xloggc:./gc.log-%%t -XX:+UseGCLogFileRotation -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10k GcSample 

この定義でログファイルを出力した場合は、以下のような定義となります。


gc.log-2021-02-11_15-52-26.0
gc.log-2021-02-11_15-52-26.1
gc.log-2021-02-11_15-52-26.2
gc.log-2021-02-11_15-52-26.3
gc.log-2021-02-11_15-52-26.4.current ← 出力中ファイル

5ファイルでローテーションされています。


出力されたファイルはGCVierなどのビューアで参照するのが一般的になります。


ヒープダンプの出力設定


GCログを設定しておくことも大事なのですが、GCログだけではOutOfMemoryが発生した場合の、リーク箇所などについては詳細を判断することはできません。

ヒープダンプを設定しておくと、OutOfMemory発生時にヒープダンプが出力されるので、詳細の原因調査が可能となります。


-XX:+HeapDumpOnOutOfMemoryError

OutOfMemoryが発生した場合にヒープダンプを出力します。

-XX:HeapDumpPath

ヒープダンプの出力先フォルダ、および、ファイル名を指定します。

ファイル名を指定しない場合は、「java_pidxxx.hprof」といったファイル名になります。

xxxはプロセスIDです。

-XX:OnOutOfMemoryError

OutOfMemoryが発生した場合に実行するコマンドを定義することができます。

実行するコマンドは、シングルクォーテーションで囲んで定義します。

 

例)-XX:OnOutOfMemoryError=”cmd string.”


OutOfMemoryが発生してヒープダンプを出力した際、指定したフォルダにダンプファイルを出力するのですが、同名ファイルが存在する場合は上書きしてくれません。
つまり、同じプロセスIDのJavaプロセスで複数回のOutOfMemoryが発生した場合、一番最初のヒープダンプだけが残ります。


一番最初のヒープダンプを残すのではなく、一番最後のヒープダンプを残したい場合は、OnOutOfMemoryErrorオプションを使います。
以下の例では、ヒープダンプが発生したタイミングで出力したダンプファイルをバックアップフォルダに移動をおこないます。
この定義をおこなうことで、ヒープダンプ出力先フォルダにはダンプファイルは残らず、バックアップフォルダに最後のヒープダンプが残ることになります。


java -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ -XX:OnOutOfMemoryError="D:/gc/gc-move.bat" GcSample 

OnOutOfMemoryErrorオプションに定義したgc-move.batの中身は以下になります。
backというフォルダに、出力したヒープダンプファイルを強制上書きコピーしています。

move /Y java*.hprof ./back/

OnOutOfMemoryErrorオプションに定義したコマンドは、ヒープダンプが出力された後に実行されるようです。


①:OutOfMemoryが発生

②:ヒープダンプを出力

③:OnOutOfMemoryErrorオプションに定義したコマンドを実行


このような動きであることを考慮して、OnOutOfMemoryErrorオプションに定義したコマンドを検討する必要があります。


仮に、最初のヒープダンプファイル・最後のヒープダンプファイル、という事ではなく、ヒープダンプの履歴を取りたい(全てのヒープダンプファイルを残しておきたい)場合は、上記例の「gc-move.bat」を修正すれば、様々な定義をおこなうことができます。
しかし、ヒープダンプファイルはプロセスのヒープ情報をそのままダンプしているので、かなりサイズが大きいファイルとなります。
そのため、ダンプファイルの履歴をとることはおすすめはしません。



ロリポップレンタルサーバで、クーロンを使ってバッチを実行し、必ず結果をメールで受信

JavaScript

今回は、ロリポップレンタルサーバでクーロン設定をおこなう方法、および、シェル実行結果をメールで受け取る方法について紹介します。


ロリポップは、crontabを直接編集してクーロンの設定をおこなうことはできないですが、クーロン設定画面で細かい起動設定をおこなう事ができます。
このクーロン設定画面では実行結果を受け取るメールアドレスの設定も可能であり、実行結果のメール受信が可能です。


メールでの受信は、バッチの実行結果によってメール本文の内容を変更する方法についても紹介していきます。


環境情報


バッチから実行するバッチは、Pythonで実装されていることを前提とします。


  • レンタルサーバ:ロリポップ
  • シェルから呼び出すバッチ:Python

WEB画面でクーロン設定をおこなう


ロリポップでのクーロン設定は管理コンソールのWEB画面でおこないます。


ロリポップのユーザー専用ページにログインし、左メニューで「サーバーの管理」から「cron設定」を選択します。


ロリポップでのクーロン設定

cron設定画面がひらきます。
クーロンの設定、および、設定済クーロンの確認は、本画面でおこないます。



画面上にあるように、日付(月)・日付(日)・曜日・時間(時)・時間(分)をプルダウンで形式で選択する形で設定をおこないます。
筆者の契約しているプランはエコノミープランですが、以下の設定となります。
プランによって異なるのは最小実行間隔のみ(1分毎、5分毎)となります。


日付(月)

毎月、1月~12月

日付(日)

毎日、1日~31日

曜日

毎日、日曜日~土曜日

時間(時)

毎時、0時~23時

時間(分)

1分毎~10分毎、0分~59分


このように設定項目は豊富なので、ほとんどの設定のニーズに対応することができます。
しかし、crontabを直接編集すれば可能な設定はおこなうことはできません。
例えば、毎週、月曜日と金曜日だけ実行するという設定は、1つの設定ではおこなうことができません。
毎週、月曜日と金曜日に実行するといった設定をおこなう場合は、月曜日に実行するという設定と、金曜日に実行するという設定の2つの設定が必要となります。


複数の設定をおこなえばcrontabを直接編集することと同じ事ができるのであれば問題ないと思われますが、クーロンの設定には上限があるので注意が必要です。
クーロンの設定上限は、契約しているプランによって異なります。


 

cron登録数

最小実行間隔

エコノミー

1

5分毎

ライト

5

5分毎

スタンダード

10

1分毎

ハイスピード

10

1分毎

エンタープライズ

10

1分毎


メールでシェル実行結果を受け取る


クーロンの実行結果をメールで受け取ることができます。
設定は簡単で、メールアドレスをWEB画面で設定するだけです。



この設定をおこなうだけで、クーロンの実行結果が登録したアドレス宛にメールが送信されます。


しかし、メールが送信される条件は、クーロンの実行結果に出力がある場合、のみとなります。
つまり、標準出力やエラー出力といった出力がおこなわれない場合は、メールは送信されません。


メールでバッチが動作した結果を受信するためには、バッチを実行するシェルファイルに標準出力をおこなうように設定をおこなえばよいです。


#!/bin/bash
python cronSample.py
echo "バッチ処理が終了しました。" >&1

WEB画面で設定したメールアドレスには、標準出力した”クーロン設定での処理完了”とだけ記載されたメールが送信されます。


上記の設定では、クーロンが実行されたタイミングで必ずメールが送信されます。
本来であれば、クーロンの実行が失敗した時、といった一定の条件と一致した場合についてのみメールを送信したい場合がほとんどです。


その場合は、プログラムでの実行結果をシェルで受け取り、受け取った実行結果によって標準出力有無を判断する必要があります。


#!/bin/bash
ret=$(python cronSample.py)
if [ $ret = 0 ]; then
  echo "バッチ処理で異常が発生しました。" >&1
fi
#!/usr/local/bin/python3.4
# coding: utf-8
import sys

sys.stdout.write("0")

シェルの中身はシンプルで、Pythonで実行した結果が失敗(0)であった場合に標準出力をおこなっています。
結果、失敗したらメールが送信される、ということになります。


Pythonのプログラムでは、0を標準出力しているのみです。
これはサンプルプログラムでなので0を標準出力しているのみなのですが、実際のプラグラムでは、この部分を修正すればよいです。



Linuxでサーバ容量・フォルダサイズ・ファイルサイズを調べるコマンド

Linux

Linuxサーバ上で、サーバ全体のディスク使用率や、フォルダサイズ・ファイルサイズを調査する時に便利なコマンドについて紹介していきます。


サーバ容量を確認する(dfコマンド)


サーバ全体の容量について確認する際に使用するのはdfコマンドになります。


[@users]$ df
ファイルシス   1K-ブロック     使用    使用可 使用% マウント位置
devtmpfs           3981472        0   3981472    0% /dev
tmpfs              3999296      260   3999036    1% /dev/shm
/dev/sda1           613184     6908    606276    2% /boot/efi

以下のオプションがあります。


-a

すべてのディスクを表示。

隠しファイルシステムなど、全てのディスクを表示します。

-H

単位を付けて結果を表示。

容量の表示は数字が大きくなってしまうので、k(キロ)、M(メガ)、G(ギガ)の単位を付けて表示するのが一般的です。


フォルダサイズを確認する(duコマンド)


フォルダのサイズを確認する時はduコマンドを使用します。


[@users]$ du
28      ./numberAnalyze/module
1088    ./numberAnalyze/logs
1124    ./numberAnalyze
16      ./winningAnalyze/module
916     ./winningAnalyze/logs
940     ./winningAnalyze
16      ./winningExpected/module
26044   ./winningExpected/logs
26068   ./winningExpected
12      ./winningCollection/module/__pycache__
24      ./winningCollection/module
12      ./winningCollection/logs
44      ./winningCollection
28184   .

duコマンドではフォルダサイズだけではなく、ファイルサイズも確認することができます。
以下のコマンドでは、コマンドを発行しているフォルダ直下ファイルのファイルサイズも表示します。


[users]$ du ./*
4       ./loto.sh                       ★
28      ./numberAnalyze/module
1088    ./numberAnalyze/logs
1124    ./numberAnalyze
16      ./winningAnalyze/module
916     ./winningAnalyze/logs
940     ./winningAnalyze
12      ./winningCollection/module/__pycache__
24      ./winningCollection/module
12      ./winningCollection/logs
44      ./winningCollection
16      ./winningExpected/module
26044   ./winningExpected/logs
26068   ./winningExpected

★=直下のファイル


以下のコマンドでは、もう1階層下フォルダのファイルサイズを表示します。


[users]$ du ./*/*
1088    ./numberAnalyze/logs
28      ./numberAnalyze/module
4       ./numberAnalyze/numberAnalyze.sh         ★
916     ./winningAnalyze/logs
16      ./winningAnalyze/module
4       ./winningAnalyze/winningAnalyze.sh       ★
12      ./winningCollection/logs
12      ./winningCollection/module/__pycache__
24      ./winningCollection/module
4       ./winningCollection/winningCollection.sh ★
26044   ./winningExpected/logs
16      ./winningExpected/module
4       ./winningExpected/winningExpected.sh     ★

★=1階層下のファイル


指定した階層のフォルダサイズを確認したい場合は「–max-depth」オプションを使用します。
以下のコマンドでは、コマンドを実行したフォルダ直下について、フォルダ毎のサイズを表示しています。


[users]$ du --max-depth=1
1124    ./numberAnalyze
940     ./winningAnalyze
26068   ./winningExpected
44      ./winningCollection
28184   .

サイズを調べるフォルダを深くしたい場合は、「–max-depth」オプションの数字を大きくしていけばよいです。


[users]$ du --max-depth=2
28      ./numberAnalyze/module
1088    ./numberAnalyze/logs
1124    ./numberAnalyze
16      ./winningAnalyze/module
916     ./winningAnalyze/logs
940     ./winningAnalyze
16      ./winningExpected/module
26044   ./winningExpected/logs
26068   ./winningExpected
24      ./winningCollection/module
12      ./winningCollection/logs
44      ./winningCollection
28184   .

上記以外のよく使うオプションも紹介しておきます。


-h

読みやすい単位で表示する。

-H

読みやすい単位で表示する。

ただし、1024単位ではなく1000単位の値を使用。

-c

合計のサイズも表示

-k

キロ単位で表示

-m

メガ単位で表示