テスト工程の管理をするツール、TestLinkについて

よくあるテスト工程

よくあるテスト工程を思い出してください。

・テスト設計者は要件定義に合わせて、テスト仕様書を記載する。
・リーダはテスト仕様書をレビューしながら今回のリリースではどこをテストするかを決定し、誰にテストをさせるかを決定する。
・テスターはテスト仕様書に応じてテストを実施する。この際、様々なプラットフォーム(例:Chrome,IE,FireFox等)で同じテスト仕様書を元にテストする場合もある。
・テスト中は常に実行できるわけでなく、テストが失敗することもあるし、別のテストがNGのせいでテストが行えない場合もある。
・リーダーはその間、テストの進捗状況を確認する。
・おっと不具合修正により新しいリリースがされた。どれをテストし直すか判断する必要がある。もしかするとテスト仕様書自体を直す必要があるかもしれない。

我々はこれらの複雑な工程をどう対応していたか思い出してみましょう。

・テスト仕様書の版の管理を適切におこなえていたか。
・今回の仕事において、どのテストを行うべきか明確に定義されていたか
・テスト実施状況をモニタリングできて、進捗が簡単に把握できたか。
・複数のプラットフォームにまたがる場合、どのテストにおいてどのプラットフォームを行うことは明確になっているか
・テスト期間中に複数のリリースが発生した場合(そしてそれは必ず発生する)、どのテストがどのリリース物との関連付けは行われていたか。

おそらくExcelでテスト仕様書を記載していた場合、秩序だった管理は困難であったと思います。(あるいはそもそも管理していない)
この記事ではTestLinkというテスト管理ツールを用いることで、これらの問題に対応する方法について記載します。

TestLinkができること

TestLinkではテスト工程の管理をWebで一括で行う以下の機能を提供します。

  • テスト仕様書を記載しデータベースに記録できます
    • 記載したテスト仕様書は版管理され変更点を追うことが可能です。
  • テスト計画は作成したテスト仕様書をどのように実行するかの情報を入力します
    • リリースされたテスト対象を「ビルド」として管理します
    • テスト対象のOSやブラウザなどを「プラットフォーム」として管理します
    • 誰が、どのテスト仕様書をテストするか管理します
  • テスト実行の結果を記録できます
    • 成功、失敗を記録します。
    • バグトラッキングシステムと連携することも可能です
    • 管理者は現在、どの程度テストが完了しているかの集計を容易におこなえます。
  • 外部APIを使用することができます
  • オープンソースで開発されており、導入コストは学習コストだけとなります

TestLinkの環境作成

VMWareやVirtualBoxで仮想環境を作成するのが一番はやいと思います。
ここではVirtualBoxを使用した環境構築の例を紹介します。

1. TestLinkのイメージをbitnamiからダウンロードする。
下記のURLからTestlinkが動作する環境を一式をダウンロードすることが可能です。

https://bitnami.com/stack/testlink/virtual-machine

testlink001.png

コミュニティーに勧誘されますが、さっさと進めたい人は感謝をしつつ「No thanks, just take me to the download」をクリックします。

testlink002.png
※600MBくらいあるので、コーヒーでも淹れながら待ちましょう。

  1. VirtualBoxにインポートする
    まずVirtualBoxを起動して「ファイル」→「仮想アプライアンスのインポート」を選択します。

testlink003.png

ダウンロードしたファイルパスを入力後、「次へ」を押下します。
testlink004.png

必要に応じて仮想環境の設定を変更して「インポート」を押下します。今回は何も変更してません。
testlink005.png
※2、3分かかるので、緑茶でも淹れながら待ちましょう。

bitnami-testlinkが作成されたら仮想環境の構築は終了です。
image.png

3.VirtualBoxの初回起動
VirtualBoxで作成した仮想環境を起動すると以下のような画面が表示されます。
testlink007.png
①TestLinkにアクセスする際に使用するIPアドレスです。控えておきましょう。
②TestLinkにログインする際にするユーザアカウントとパスワードです。控えておきましょう。
③SSH、SFTP、SCPで使用するユーザアカウントとパスワードです。

なお「debian login:」に③のユーザ名とパスワードを入力してログインが可能です。この際に初期パスワードの変更を求められますが、Enterキーを押下することでスキップできます。

4.TestLinkのアクセスとパスワード変更
ホストコンピュータのブラウザで「http://①のIP」を指定することで下記の画面が表示されます
ここでユーザ名とパスワードに②に記載されているものを入力して「Log in」を実行してください。
testlink008.png

プロジェクト作成画面が表示されますが、アメリカ語は辛いのでまず、日本語化します。画面上部の「My Settings」アイコンを押下してください。

testlink009.png

Account Setting画面が表示されるのでLocaleで「Japanese」を選択後、「Save」ボタンを押下します。

testlink010.png

すると日本語が表示されるようになります。
testlink011.png

仮想環境の説明と設定

上記の手順で最低限、TestLinkを試す手順はできました。
しかしながら、現状のままだと色々使いづらいので仮想環境の設定を行います。TestLinkをお試しで動かすのが目的であれば、この項目は読み飛ばしてください。

再起動とシャットダウンの方法

以下のコマンドで再起動とシャットダウンが行えます。

再起動

sudo reboot

シャットダウン

sudo poweroff

SSHを有効にする

現状でもコマンドライン操作が行えますが、スクロールができなかったりキーボードが英語配列だったりして辛いのでTeraTermでつなげるようにします。
初期状態ではSSHサーバーは使用できなくなっており、TeraTermでつなげようとすると「Connection refused」となります。

そこで、下記のページを参考にSSHを有効にします。
https://docs.bitnami.com/virtual-machine/faq/get-started/enable-ssh/

testlink012.png

sudo rm -f /etc/ssh/sshd_not_to_be_run
sudo systemctl enable ssh
sudo systemctl start ssh

なお、英語配列のキーボードなので「_」は「-」+「SHIFT」キーを入力してください。ホストコンピュータに制御を戻す場合は右の「CTRL」キーまたはCTRL+ALT+DELを押します。

あとはTeraTermからSSHでログインできるようになります。この際のユーザーは「bitnami」です。

IPアドレスの固定化

現状のままだと仮想環境を起動するたびにIPが変わるのでサーバーとして使うのは辛いです。
そのため、以下を参考にIPアドレスを固定化します。

https://docs.bitnami.com/virtual-machine/faq/configuration/configure-static-address/

・まず以下のコマンドを実行してインターフェイス名「enXXXX」を取得します。

sudo ifconfig

testlink013.png

・/etc/systemd/networkにネットワークの設定ファイル25-wired.networkを作成します。

cd /etc/systemd/network
sudo cp 99-dhcp.network 25-wired.network
sudo vi 25-wired.network

・25-wired.networkを以下のように設定します。
testlink014.png

・ファイルを保存後、再起動を行います。再起動後のIPが変更されていることが確認できます。
testlink015.png

以降、ブラウザからも変更したIPで接続できます。

データベースの接続

TestLinkを使用していると、直接DBを操作したくなる場合が稀によくあります。
データベースはmysqlを使用しており以下のコマンドで接続できます。

mysql -u root -p②のパスワード

以下のコマンドでパスワードを変更しておいた方がいいでしょう。

set PASSWORD='パスワード';

なお実際にTestLinkが使用しているデータベース名やユーザ名、パスワードは以下のコマンドで確認できます。

bitnami@debian:~$ more /home/bitnami/apps/testlink/htdocs/config_db.inc.php
<?php
define('DB_TYPE', 'mysql');
define('DB_USER', 'bn_testlink');
define('DB_PASS', '0016b0b6c7');
define('DB_HOST', 'localhost:3306');
define('DB_NAME', 'bitnami_testlink');
bitnami@debian:~$

ケーススタディ

簡単なケーススタディを以下に紹介します。
image.png

・管理者はテストプロジェクトを作成します。テストプロジェクトにはテストケース、要件、および、キーワード済みのテスト仕様が含まれます。

・管理者はユーザの追加を行います。ユーザには様々な役割が存在しており、追加時にデフォルトの役割を指定できます。

役割 説明
admin すべての操作が行える管理者権限です
leader テストリーダに与える権限です。テスト計画の作成/編集、ビルドの作成/編集、役割の割り当て、テストケースのバージョンの凍結、キーワードの管理、テスト消化の確認等が行えます。
test designer テスト設計者に与える権限です。テストケースの参照、作成、編集、キーワード管理が行えます
tester テスターに与える権限です。テストケースの参照と実行並びにテスト消化の確認が行えます
senior tester 上級のテスターに与える権限です。テスターの権限に加えてテストケースの作成と編集を行えます
guest ゲスト用の役割で、テストケースの参照とテスト消化の確認が行えます

・リーダはそれぞれのテストプロジェクトで、各ユーザにどのような権限を与えるかを設定できます。

・テスト設計者はテストケースを作成します。テストケースはステップを介してテスト作業と予想される結果について記載できます。また、テストケースをまとめたテストスイートの作成をおこないテスト仕様を論理的に細分化します。

・リーダはテスト計画を作成します。テスト計画は現在のテストプロジェクトのテストケースで構成されます。またテスト計画にはビルド/リリース、ユーザ割り当て、およびテスト結果が含まれます。

・テスターはリーダの作成したテスト計画にそって、自分に割り当てられたテストを実施し、その結果を記録します。

・テスト中にテストケースが誤っていることに気づいた場合、テスト設計者はテストケースを修正することができます。

・リーダはテストの消化率の確認が行えます。

テストプロジェクトの追加

「admin」権限を持つ管理者ユーザはテストプロジェクトの追加を行えます。

1.「admin」権限を持つユーザがログイン後に表示されるメインページの「テストプロジェクトの管理」を押下します。

image.png

2.テストプロジェクトの管理画面で「作成」ボタンを押下します。
image.png

3.テストプロジェクト新規作成画面で項目を入力して作成を押下します。
image.png

項目 説明
既存のテストプロジェクトをコピー 既存のテストプロジェクトをコピーして新規プロジェクトを作成するか否かを設定できる
名前 テストプロジェクトの名称を入力する。必須項目
テストケースIDのプレフィックス テストケースに付与されるテストプロジェクト間で一意の接頭語を付与する。必須項目
備考 テストプロジェクトについての説明を記載する
その他の機能 要件管理機能、テスト優先度機能、自動テスト機能、機器機能の使用の有無を設定する
課題追跡システムとの連携 Redmineなどの課題管理システムと連携するかを指定する
Code Tracker Integration BitBucketのようなコード管理機能と連携するかを指定できるが、実験的機能
有用性 テストプロジェクトの有効と公開を指定できる。基本的にテストプロジェクトは削除しないで有効のチェックを外すことで使用できないようにしたほうが望ましい

なお、「admin」権限がログインした際にテストプロジェクトが1つも存在しない場合は、テストプロジェクト新規作成画面に遷移します。

ユーザの追加

管理者権限でログイン後、ホームでの「ユーザの管理」アイコンをクリックします。
image.png

ユーザの管理画面が開いたあとに「作成」を行います。
image.png

image.png
項目を入力して保存をすることでユーザを追加できます。
役割はデフォルトの役割になるので、とりあえずゲストとして追加しておき、後述のユーザの役割の割り当てで変更するといいでしょう。

ユーザの画面ではユーザの削除や編集も行えますが、基本的に削除は使用せずにユーザの編集から有効を無効にすることで対応しましょう。

テストプロジェクトにユーザの役割の割り当て

各テストプロジェクトにユーザの役割の割り当てを行います。
管理者またはリーダでログインした場合に表示される「役割の割り当て」をクリックします。
image.png

役割管理画面で「テストプロジェクト」を選択後、それぞれのユーザにたいする役割を指定して「実行」ボタンを押下します。
image.png

テストケースの作成

テスト設計者でログインしたあとに、テストケースを作成したいテストプロジェクトを選択し、「テストケースの編集」をクリックします。
image.png

テスト仕様の画面が開くので「データ操作」アイコンをクリックします。
image.png

表示された「テストスィートの作成」アイコンをクリックします。
image.png

テストスィート名と詳細を入力して「保存」をクリックします。
image.png

テスト仕様書のナビゲータのツリーにテストスィートが追加されます。
image.png

テストケースを追加したいテストスィートをツリー上から選択後、「データ操作」アイコンをクリックします。
image.png

表示された「テストケースを作成」アイコンをクリックします。
image.png

テストケースの情報を記載し、「作成」ボタンを押下します。
image.png
ステータスはテストケースの状態を表し、下記のいずれかになります。
・ドラフト
・レビュー待ち
・レビュー中
・やり直し
・廃止
・先送り
・完了
テストケースのレビューを行う場合は、このステータスを用いるといいでしょう。
なお、テスト実行計画でテストケースを追加する場合「廃止」と「先送り」のケースについては追加が行えません。

ツリーにテストケースが追加されます。
image.png

テストのステップを追加する必要がある場合は、テストケースを選択後、「ステップ追加」ボタンをクリックします。
image.png

ステップ追加画面が開くので「ステップ」と「期待値」を入力後、「保存」を押下します。
image.png

同様の作業を繰り返していきます。「保存して終了」を押下するとステップの追加が終了します。
image.png

テストケースにステップが追加されたことを確認します。
image.png

テスト計画

テストを実行するにはテスト計画を作成する必要があります。
リーダでログイン後、ホーム画面でテストプロジェクトを選択して「テスト計画の管理」をクリックします。
image.png

テスト計画の管理画面が表示されたら「作成」ボタンをクリックします。
image.png

テスト計画の概要画面で必要な項目を入力し、作成ボタンを押下します。
image.png
テスト計画はタスクであるべきで、たとえば「2019年夏コミリリース用」のテスト作業だったり「2019年冬コミリリース用」のテスト作業だったりします。

テスト計画画面を再度遷移するとテスト計画が追加されています。
image.png

つづいて、テスト計画に含める「ビルド/リリース」を作成します。テスト計画は1つ以上の「ビルド/リリース」を含める必要があります。1つ「以上」というのはテスト計画中にバグフィックス等で新しい「ビルド/リリース」が増える場合があるからです。
ホーム画面に戻って「ビルド/リリース」をクリックします。複数テスト計画が存在する場合は、テスト計画の選択をおこなってください。
image.png

ビルド管理画面が表示されたら「新規作成」ボタンを押下します。
image.png

必要な項目を入力して「作成」ボタンを押下します。
image.png

ビルド管理画面にもどると、作成したビルドが追加されています。
image.png

つづいてテスト計画にテストケースを追加します。
ホーム画面にもどって「テストケース」の追加をクリックします。
image.png

「テストケースの追加と削除」画面で、ツリーよりテストスィートを選択するとテストケースの一覧が表示されます。
image.png

テスト計画に追加したいテストケースを選択して「選択したものを追加」をクリックしましょう。
image.png

以下のように画面が更新されます。
image.png

追加されているテストケースをテスト計画から削除したい場合は、削除したい項目にチェックを付けて「選択したものを追加/削除」ボタンを押下します。
image.png

テスト計画に関連づいているテストケースと関連づいていないテストケースが混在している場合の画面は以下のようになります。
image.png
このように、テスト計画においてテスト仕様にあるテストケースから一部のテストケースを実施するということが可能です。

次にどのテストケースを誰がやるかを決めるため、テスト担当者を割り当てます。
ホーム画面の「テスト担当者の割り当て」をクリックします。
image.png

割り当てをしたいテストスィートまたはテストケースをツリーから選択します。
image.png

「テストケースに実行タスクを割り当てるテスト計画」画面が開きます。
image.png
もし、表示されない場合は、テスト担当者の割り当てができない場合を参照してください。
テストケースごとにユーザを割り当てられるので設定していき、最後に「Save Assignments」をクリックします。

以下のようにテストケースごとに実行を割り当てたユーザが表示されます。
image.png

テスト担当者の割り当てができない場合

以下のような問題が報告されています。
http://mantis.testlink.org/view.php?id=8467
権限からみが適切に設定できていない場合があるので見直します。

まずユーザ管理画面で「役割の一覧」を押下します。
image.png

修正する役割をクリックします。
image.png

「Assign TestCase Execution」にチェックをつけて保存します。
image.png

テストの実行

テストを実行するには権限のあるユーザでログインをして、テストプロジェクトとテスト計画を選択後、「テストの実施」をクリックします。
image.png

ツリーから実行したいテストケースを選択します。このツリーで表示されるテストケースは自分が割り当てられたものになります。
image.png

ビルドのテスト結果が表示されるので結果を入力します。
image.png

結果アイコンの説明:

アイコン 説明
image.png テストを成功とします
image.png テストを失敗とします
image.png テストをブロックとします。これは前提条件が満たせずにテストができなかった場合に選択します
image.png テストを成功として次のテストケースを表示します
image.png テストを失敗として次のテストケースを表示します
image.png テストをブロックとして次のテストケースを表示します

いずれかのテスト結果をクリックすると以下のように実行履歴が作成されます。
image.png
これは、テスト計画中に複数回テストを実施する可能性があることを表します。

画面キャプチャやログなどの添付ファイルを追加したい場合は「添付ファイル」アイコンをクリックします。
image.png

ファイルをアップロードすると添付ファイルの一覧が表示されるようになり、そのファイルは以下のように表示することもできます。
image.png

もしRedmineと連携してバグを登録したい場合は下記を参照してください。
https://needtec.exblog.jp/22518358/

テストケースの修正

テスト計画を実行中にテスト仕様書の修正が発生するケースがあります。
たとえば、仕様変更でテスト仕様書が変わることもありますし、テスト仕様書が誤っていた場合にそれを修正する場合があるからです。

テスト設計者でログインしてテストケースを編集しようとすると以下のように「このバージョンは実行中なので編集できません」と表示されます。
image.png
これはどのバージョンのテスト仕様書で実行されたかが明確になっているからです。テストケースを修正したい場合は新しいバージョンを作成する必要があります。この場合は「新バージョン作成」を押します。

これでバージョン2が追加されました。
image.png

歯車のアイコンをクリックしたのち、「編集」ボタンが表示されるので押下します。
image.png

テストケースの編集で任意の変更をして保存します。
image.png

つづいて、テスト計画に紐づいているテストケースのバージョンをアップする必要があります。
これを行うには役割の権限で「リンク済みテストケースバージョンの更新」にチェックがある必要があります。管理者権限でおこなうか、必要なユーザの役割に該当の権限を割り当ててください。
image.png

権限のあるユーザでログインすると「テストケースの更新」というリンクがあるのでクリックをします。
image.png

「リンクされているテストケースのバージョンを更新」画面のツリーでテストスィートをクリックすると以下のようにバージョンを選択できるようになります。対象のテストケースとそのバージョンを選択後、「テスト計画を更新」を押下してください。
image.png

指標の確認

テストの消化率などの指標の確認が行えます。
「テスト報告書と指標」または「指標メーター」のいずれかで確認することができます。

指標メーターの例:
image.png

テスト報告書と指標の例:
image.png

image.png

このように、現在のテストの状況をわかりやすく表示するだけでなく、テスト仕様書を作成して納品物に利用することが可能です。

グラフレポートが文字化けする場合

グラフレポートが文字化けする場合があります。
image.png

これはTestLinkが使用しているpchartというライブラリが使用しているフォントが日本語対応していないためです。
これを修正するには以下のようにします。

1 下記からIPAのFontを取得する
https://ja.osdn.net/projects/ipafonts/releases/46148

2 下記のパスに「ipagp.tff」をコピーする。

/opt/bitnami/apps/testlink/htdocs/third_party/pchart/Fonts

3 config.inc.phpのcharts_font_path を変更してフォントを切り替える

/opt/bitnami/apps/testlink/htdocs/config.inc.php

/**
 * fonts set used to draw charts
 **/
//$tlCfg->charts_font_path = TL_ABS_PATH . "third_party/pchart/Fonts/tahoma.ttf";
$tlCfg->charts_font_path = TL_ABS_PATH . "third_party/pchart/Fonts/ipagp.ttf";

/**
 * font size used to draw charts
 **/
$tlCfg->charts_font_size = 8;

4 ページを再読み込みして直っていることを確認
image.png

まとめ

今回は簡単にですが、テスト管理ツールであるtestlinkの紹介をしました。
古いバージョンであれば各種記事が日本語訳されているので参考にしてください。

testlinkは混乱したテスト作業を適切に管理する可能性を秘めているツールではありますが、正直、実際使うかどうかは、人員のスキルにあわせた方がいいと思います。

ぶっちゃけ、本来強力な効果を発生するであろうSIer的な環境ほど採用はきびしいです。いいたかないですが、テスト仕様の版管理とか、テスト仕様と実行結果は分離すべきいう概念自体が理解できる人間が管理者レベルにおいても少ない傾向があり、教育コストが高くなりすぎます。仮にそこを突破しても、テスターはぶっちゃけ、Excelしか使いたがらないですし。第一、まともにテスト技術を知っている人間が上にいるなら有償のテスト管理ソフト導入する

あと、問題が出た時にPHPとJavaScriptをみてバグか設定ミスか切り分けできる人間がいないときも導入やめといた方がいいでしょう。

SquashTMのビルドの方法と日本語対応の仕方

SquashTMとは

SquashTMはWebベースのテスト管理ツールです。
多言語化対応はしてますが、残念ながら日本語対応はしてません。
また、当然の権利のように、文字化けしたりします。

今回はSquashTMをビルドして文字化けを修正したり日本語対応の方法を調べたりします。

ビルドの方法

公式にいくつか文章はありますが、古いです。

すごく古い
https://sites.google.com/a/henix.fr/wiki-squash-tm/developer/how-to-install-squashtm-project-into-eclipse
※1.13以降はOSGi Frameworkはいらない。代りにSpringIDEがいる

ちょっと古い
https://bitbucket.org/nx/squashtest-tm/wiki/devguide/HowToInstallInIDE.md#!install-in-eclipse
※1.19になるとJDK7はサポートしていないが、JDK7で動くような記載になっている。

事前準備

下記を用意します。
・Java8以上
・eclipse
・mvn3.3以上
ToroiseHg (Gitのような分散構成管理ツール)

eclipceに入れるプラグインは以下の通り
・springIDE
image.png

・Groovy Development Tools
image.png

ビルドと実行方法

ソースコードの取得からmvn installまで

cd myeclipseworkspace
hg clone https://bitbucket.org/nx/squashtest-tm
cd squashtest-tm
mvn clean install -DskipTests -DskipITs
# 以下は不要の可能性がある
cd provision
mvn clean install -DskipTests

com.mycila:license-maven-pluginでエラーが出る場合

以下のようなエラーが発生する場合がある。

Failed to execute goal com.mycila:license-maven-plugin:2.11:check"

この場合はルートフォルダで以下のコマンドを実行する。

mvn license:format

provisionでエラーがでる場合

provisionのmvn installで以下のエラーが発生する場合がある。

Non-resolvable parent POM for org.squashtest.tm:squash-tm-provision:[unknown-version]: Could not find artifact org.squashtest.tm:squash-tm:pom:1.19.0.RC3-SNAPSHOT and 'parent.relativePath' points at no local POM

これはprovisionフォルダのpom.xml中のparentと親フォルダのpom.xmlの整合性が取れていない場合に発生する。
2019/7/6時点では以下のような修正が必要だった

parent/pom.xml

  <parent>
    <groupId>org.squashtest.tm</groupId>
    <artifactId>squash-tm</artifactId>
    <version>1.19.0.RELEASE</version> <<<<<< ここのVersionが親と食い違っていた
    <relativePath>../pom.xml</relativePath>
  </parent>

eclipseでの操作

eclipseで下記の操作を行う

プロジェクトのインポート

1 メニューから[ファイル]>[インポート]を選択
2 [Maven]>[既存のMavenプロジェクト]を背くん炊く
image.png
3 「hg clone」で作成したフォルダを選択

provisionモジュールをEclipseに導入

この作業は最新では不要の可能性がある。

1 メニューから[ウィンドウ]>[設定]を選択する。
2 [プラグイン開発]>[ターゲット・プラットフォーム]を選択して「追加」ボタンを押下
image.png
3 「空のターゲット定義で開始」を選択
image.png
4 ターゲットコンテンツにて「追加」ボタンを押下
image.png
5 ディレクトリーを選択する。
image.png
6 ロケーションに「squashtest-tm/provision/target/eclipse-provision/bundles」を入力する。
7 ターゲットコンテンツが追加されるので、引数タブでそれぞれ下記の値を入力する。
プログラム引数 :

-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -consoleLog -console

VM 引数 :

-Declipse.ignoreApp=true
-Dosgi.noShutdown=true
-Dorg.osgi.framework.system.packages.extra=com.sun.org.apache.xalan.internal.res,com.sun.org.apache.xml.internal.utils,
com.sun.org.apache.xpath.internal,com.sun.org.apache.xpath.internal.jaxp,com.sun.org.apache.xpath.internal.objects,com.sun.javadoc,
com.sun.tools.javadoc,javax.xml.namespace 
-Dbundles.configuration.location="${workspace_loc}/squashtest-tm/provision/target/config"
-Dorg.osgi.service.http.port=9090
-Dorg.osgi.service.http.port.secure=9443
  1. 完了後、今追加したターゲット定義にチェックを付ける

Spring IDEプラグインの設定

1 メニューから[実行]>[実行構成]を選択する
2 実行構成画面にて[Spring Bootアプリケーション]を右クリックして「新規」ボタンを押下
3 それぞれのタブで値を入力する
SpringBoot タブ
image.png

・プロジェクト:tm.web
・メイン型:org.squashtest.tm.SquashTm
・プロファイル:h2,dev

引数 タブ
image.png

プログラム引数:-XX:MaxPermSize=256m -Xmx1024m

クラスパス タブ
image.png

①「ユーザ・エントリー」を選択後、「拡張ボタン」を押下
②「フォルダの追加」を選択
③「tm.web/target/wro4j-spring-boot」を入力する

実行方法

Spring IDEの設定で実行構成を設定しているので、そこで実行ボタンを押下する。
その後、ブラウザから以下にアクセスする。
(http://localhost:8080/squash

起動中にエラーになる場合の対応

起動が失敗することがあった。この場合は以下を試してみた。
なお、ログで例外が出ていても動作はしている模様

  1. データベースを一回消してみる
    ・tm\data\squash-tm.mv.db
    ・tm\data\squash-tm.trace.db

  2. 「mvn clean install」を行ってみる。

色々修正してみる

「Attach Test Cases」画面での文字化けの修正をしてみる

テストスィートでテストケースを関連付ける際、日本語文字が文字化けする。
image.png

原因はjspにcontentTypeが設定していないため。
以下のように修正する。

tm\tm.web\src\main\webapp\WEB-INF\jsp\page\campaign-workspace\show-test-suite-test-plan-manager.jsp

<%@ taglib prefix="authz" tagdir="/WEB-INF/tags/authz"%>

↓これを追加
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>

<c:url var="testSuiteUrl" value="/test-suites/${ testSuite.id }" />

結構化けているページがあるのでjspについてencodeが明示されているか見直した方がいいが、error.jspだけは、文字コードが明示されていたので、そのままのエンコードにしておいた。

なお、全体的に日本語文字が化ける場合は、データベースを作成時にミスをしていてutf8に対応していないと考えられる。

日本語メッセージを追加してみる。

SquashTMはメッセージリソースをコピーすることで多言語対応が可能である。
1.tm\tm.web\src\main\webapp\WEB-INF\messages\tm\messages.properties をコピーして、messages_ja.propertiesを作成する。
2.Limyプロパティエディターで開く
3.日本語に置き換える
修正したら「tm-web」を「mvn install」したのち再起動。

なお、言語の切り替えはブラウザの規定の言語によって行われる。

CentOS7の場合の置き換えと再起動方法

作成したwarをサーバーに配置する方法は以下の通り

1 tm/tm.web/target/tm.webXXXXXXX.warをsquash-tm.warに改名
2 「sudo service squash-tm stop」でサービスをとめる
3 CentOS7の場合は「 /usr/lib/squash-tm/bundles/」にコピー
4 「sudo service squash-tm stop」でサービスを再開

ちなみにログは以下にあるのでtail -f あたりで監視しとくといい。
/var/log/squash-tm/squash-tm.log

さいごに

これでSquashTMでバグがでても自力で直せるようになりました。
バグがでても安心だな!(慢心)

オープンソースのテスト管理ツールを調べようとして何の成果も得られませんでしたぁぁ!!

はじめに

TestLinkは10年の歴史を誇るテスト管理ツールですが、さすがに10年前のツールなだけあって、GUI的に昨今厳しい評価を受けがちです。

そこでオープンソースでかつ、TestLinkに代わってテスト管理が行えるツールについて調査しようと思いました。
もしTestLinkの弱点であるGUIが改善されたものがあれば、Excelとの置き換えが捗ると考えたからであります。

駆逐してやる…!テスト管理から…Excelを残らず!

予選敗退組

実はオープンソースのテスト管理ツールは結構存在しています。
ここでは、実際にインストールする前に予選敗退となったいくつかを紹介します。

tarantula
https://github.com/prove/tarantula
http://www.testiatarantula.com/

最終コミットが2014年。
運営元から将来の計画もありませんとかいわれているので見送り。

Testopia
https://github.com/bugzilla/extensions-Testopia

最終コミットは2018年2月
Bugzillaを使ったのがかなり昔であり覚えていない、かつBugzilla5.xには対応していないようなので見送り。

qaManager
http://qamanager.sourceforge.net/site/en/
https://sourceforge.net/projects/qamanager/

テストケースの管理やリソースの管理ができそうだが、さすがに最終リリースが2008年は辛い

Radi
https://sourceforge.net/projects/radi-testdir/

軽量テスト管理ツール。 Radiはテスト計画の設定、テストイメージ/ビルドのテスト結果の更新(作成/編集)、バックアップ、ユーザー管理などの機能をサポートする。
2013年が最終更新。多分、死んだプロジェクト。

RTH
https://sourceforge.net/projects/rth/
https://requirementsandtestinghub.wordpress.com/

アプリケーションのライフサイクルを通して、要件、テスト、テスト結果、および欠陥を管理するように設計されたWebベースのツール
2013年時点でプログラマいなくなって、募集したのが最後の公式のメッセージ。

TCW
https://sourceforge.net/projects/tcw/
テストケースとその追跡ができるようだが、最終更新が2006年はさすがに厳しい。

moztrap
https://github.com/mozilla/moztrap
2019年にコミットしてたから生きていたプロジェクトかと思ったが死んでいる。
コミットしたのはREADMEのみ。
ログイン機構につかっている「login.persona.org」が2016年に停止したのでログイン画面からすすまない。
https://developer.mozilla.org/en-US/docs/Archive/Mozilla/Persona

予選通過組

過酷な予選を通過して実際インストールして使おうとしたツールについて紹介します。

Redmine impasse

http://kawasima.github.io/redmine_impasse/
Redmineのプラグインとしてテストケースの管理ができます。
このため、Redmineを使用している組織では導入コストを抑えられることが期待されます。
ただし、オリジナルのコードが古いので、最新のRedmineでは動かないと思われ予選落ちでした。

しかし、最近のプルリクエストを見ると4.x対応をしているような履歴があるのでこれを試してみます。
https://github.com/Neticoa/redmine_impasse

導入失敗の軌跡

実行環境

  • Redmine version 4.0.3.stable
  • Ruby version 2.5.5-p157 (2019-03-15) [x86_64-linux]
  • Rails version 5.2.2.1

※bitnamiの仮想環境
https://bitnami.com/stack/redmine/virtual-machine

インストール時のトラブル

プラグインを入れる際に、pluginフォルダで以下のコマンドを実行します。

rake redmine:plugins:migrate RAILS_ENV=production

この際、以下のエラーが発生しました。

NameError: uninitialized constant ImpasseProjectsHelperPatch::ImpasseSettingsHelper

これに関しては以下の修正で無理やり通すことができます。

impasse_projects_helper_patch.rb

require_dependency 'projects_helper'
# rake redmine:plugins:migrate RAILS_ENV=production エラー対応↓
require_dependency '../app/helpers/impasse_settings_helper'

module ImpasseProjectsHelperPatch

Impasseを有効にするとプロジェクトが開かなくなる

プロジェクトの設定でImpasseにチェックを付けます。
image.png

するとプロジェクトを開くと以下のようなエラーとなります。
image.png

この時のエラーログは以下のようになります。

Completed 200 OK in 47ms (Views: 32.2ms | ActiveRecord: 3.5ms)
Started GET "/projects/test1/settings" for 192.168.0.14 at 2019-07-08 15:13:21 +0000
Processing by ProjectsController#settings as HTML
  Parameters: {"id"=>"test1"}
  Current user: user (id=1)
  Rendering projects/settings.html.erb within layouts/base
  Rendered projects/_form.html.erb (15.8ms)
  Rendered projects/_edit.html.erb (17.2ms)
  Rendered projects/settings/_members.html.erb (5.2ms)
  Rendered projects/settings/_issues.html.erb (11.0ms)
  Rendered projects/settings/_versions.html.erb (5.1ms)
  Rendered projects/settings/_issue_categories.html.erb (4.2ms)
  Rendered projects/settings/_repositories.html.erb (3.2ms)
  Rendered projects/settings/_boards.html.erb (2.4ms)
  Rendered projects/settings/_activities.html.erb (13.7ms)
  Rendered common/_tabs.html.erb (70.9ms)
  Rendered projects/settings.html.erb within layouts/base (75.2ms)
Completed 500 Internal Server Error in 145ms (ActiveRecord: 10.5ms)

ActionView::Template::Error (No route matches {:action=>"index", :controller=>"impasse_test_case", :id=>"test1", :project_id=>#<Project id: 1, name: "Test1", description: "", homepage: "", is_public: true, parent_id: nil, created_on: "2019-05-16 08:22:02", updated_on: "2019-05-16 08:22:02", identifier: "test1", status: 1, lft: 1, rgt: 2, inherit_members: false, default_version_id: nil, default_assigned_to_id: nil>}):
    87:     <% end %>
    88: 
    89:     <h1><%= page_header_title %></h1>
    90: 
    91:     <% if display_main_menu?(@project) %>
    92:     <div id="main-menu" class="tabs">
    93:         <%= render_main_menu(@project) %>

ここで撤退しました。
いままで当該ツールを使っていた場合は、redmineのバージョンアップに合わせて対応する選択肢はあると思いますが、正直、初めて使う場合、メリットが薄いのが撤退の理由となります。

SquashTM

テスト管理ツールとして、JaSST Tokyo2017で導入事例が紹介されたツールです。
https://qiita.com/miwakai/items/843c76e50eef7719ae6d

日本語自体の対応はされていないものの、直観的なGUIは使いやすく、また機能的にも十分そろっているように見えます。

公式:
https://www.squashtest.org/en

Wiki:
https://sites.google.com/a/henix.fr/wiki-squash-tm/

デモサイト:
https://demo.squashtest.org/squash/login

インストール方法

公式ドキュメントに記載があるので参考にするとよいでしょう。
https://sites.google.com/a/henix.fr/wiki-squash-tm/installation-and-exploitation-guide

日本語のページだと以下があります。
https://qiita.com/soon/items/bc46138fb6fce97fed43
https://jupiterr.blog.so-net.ne.jp/2017-02-08

基本的な手順は以下になります。
(1)JDK8以上を入れる
(2)データベース(mysql/mariadb/postgresqlのいずれかを入れる。
 データベースは以下参考に作成する。
 https://sites.google.com/a/henix.fr/wiki-squash-tm/installation-and-exploitation-guide/2---installation-of-squash-tm/5---change-database
(3)httpdを入れる
(4)squash-tmを入れる
(5)(2)で作成したデータベースにSQLを流す

centosの例

mysql -u root -p squashtm < /usr/share/doc/squash-tm/database-scripts/mysql-full-install-version-XX.XX.XX.RELEASE.sql

(6)DBの接続情報の修正をする。
CentOS7の場合は「/etc/sysconfig/squash-tm」を以下参照して修正
https://sites.google.com/a/henix.fr/wiki-squash-tm/installation-and-exploitation-guide/2---installation-of-squash-tm/3---redhat-4-5-installation

(7)ファイアウォールの設定でポート80と8080を開ける。

centosの例

firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --reload
firewall-cmd --list-all

(8)squash-tmを起動する

centosの例

squash-tm 起動
/etc/init.d/squash-tm start

自動起動
chkconfig squash-tm on

使用した感想

機能としてはTestLinkと同等のものがそろっております。
その上で特別すぐれていると感じられるところは以下の通りです。

・GUIは直観的で使いやすい
・自動テストとの連携がしやすそう。
 Squash TAという自動テスト管理ツールとの連携
 gherkinという記法でテストケースでスクリプトを記載できる。
 https://sites.google.com/a/henix.fr/wiki-squash-tm/User/04---test-case-administration/script-management-for-gherkin-test-cases
・データセット機能が便利そう

データセットの使用例

データセットを使用することで、同じテストケースを別のパラメータで実施することが容易にできます。

(1)テストケースのparametersタブでデータセットを指定する。
 image.png

(2)テストケースに{$~}という記述を行いパラメータを埋め込む。
image.png

(3)テスト計画時に実行するテストケースで、どのデータセットを使うかを指定する。
image.png

(4)テスト実行中にデータセットで指定した値が表示される。
image.png

おしいところ

たいへん素晴らしいツールですが、utf8で入力した値が文字化けするページがいくつかあるという弱点があります。
image.png

この事象はロシアの同志がフォーラムで報告しており、バグとしてあがっています。(なお優先度は低)
https://forum.squashtest.com/viewtopic.php?f=37&t=3633

いずれ直るとは思いますが、現時点でこの不具合を自力で直したい場合は以下を参考にしてください。
SquashTMのビルドの方法と日本語対応の仕方 「Attach Test Cases」画面での文字化けの修正をしてみる

また、レポート画面が表示されなくなったり、テキストエリアの入力ができなくなることがたまにありました。
 →再現性が不明だが、ブラウザのキャッシュを削除したらとりあえず動作した。

私の感想としては英語ベースでやりとりする環境ならともかく日本語という環境では正直導入が厳しいと思います。
「eclipse用意して、次に独自の修正後、デプロイしてから使います」とかいうツールの提案は通す自信がないですし、日本語の資料を用意して説得するというのも提案するコストが高すぎます。

さいごに

最初は威勢のいいことは考えましたが数日の調査の結果、残念なことに、目ぼしい成果を上げることはできませんでした。

-でも役に立ったのですよね?休日や睡眠時間の犠牲は…Excelへの反撃の糧になったのですよね!!!?

もちろん・・いや・・今回の調査で我々は、いや、今回も・・くっ・・何の成果も得られませんでしたぁぁ!!
私が無能なばかりにただ、いたずらに休日と睡眠時間をなくし、Excelによるテスト管理を止めることが、できませんでしたぁぁ!!

謙虚な気持ちでJSONの仕様と各言語での実装例を調べなおす

JSONの仕様

JavaScript Object NotationはJavaScriptから派生したものですが、2019年現在、多くのプログラミング言語にてJSON形式のデータを生成および解析するためのコードが含まれています。

仕様改定の歴史としては以下のようになっています。

時系列 仕様 備考
2006年7月 RFC4627
2013年3月 RFC7158 RFC4627ではJSONの文章はobjectまたはarrayを必ず含む必要がありましたが、その制限が解除されました。
2013年10月 ECMA-404 1st edition
2014年3月 RFC7159
2017年12月14日 RFC8259およびIETF STD 90およびECMA-404 2nd edition RFC8259にて文字コードはBOMなしのUTF-8でエンコードすることが必須となりました。

JSON values

JSONの値はobject, array, number, string, true, false, null となります。

Object

objectは0個以上の名前/値のペアを囲む、一対のトークンとして表せられます。名前はstringになります。名前の後に「:」とトークンが続き、名前と値を分けます。単一の「,」は値を次の名前から分離します。
名前と値のペアの順序に重要性はありませんし、名前文字列が一意であることを要求するものではありませんし、複数あった場合の挙動は定義されていません。

Array

arrayは0個以上の値を囲む角括弧トークンです。値は「,」で区切ります。

Number

numberは余分の先頭0が存在しない10進数です。
マイナス記号、小数点、eまたはEが付与される場合があります。
数字として表すことのできないNaNやInfinityは許可されません。

10 → Numberとみなす
-10 → Numberとみなす
10.5 → Numberとみなす
1e+3 → Numberとみなす
1E-3 → Numberとみなす
-0 → Numberとみなす
0010 → Numberとみなさない
0x10 → Numberとみなさない
NaN → Numberとみなさない

String

stringは「\」によるエスケープシーケンス記法を含む、「"」でくくった文字列です。

\u4 hexadecimal digitsの例は以下のようになります。
・"\u3041"→"ぁ"となります。
・"\u002F"と"\u002f"は両方とも有効

実装例

ここでは以下のプログラミング言語で実際にJSONを操作してみます。
・JavaScript
・Python
・C#
・PowerShell
・Excel VBA

操作対象のJSONの例

いろいろなValueを読み込むJSON

test001.json

{
  "null" : null,
  "num1" : 12345,
  "num1" : 99999,
  "num2" : 123.45,
  "num3" : -123.45,
  "num4" : 1e+3,
  "num5" : 1E-3,
  "bool1" : true,
  "bool2" : false,
  "str1" : "abあいうえおcdef",
  "str2" : "str2:\n\r\\\t\/abc\b\u0030\u3041",
  "obj1" : {
    "a" : 125,
    "b" : {
       "c" : {
          "d1": {
             "v" : 1234
          },
          "d2" : 12345
       }
    }
  },
  "ary1" : [
    1,
    2,
    "test",
    {
      "a": 5.3
    }
  ],
  "ary2" : [
    1,
    2,
    3
  ]
}

大量データのJSON

test003.json

[
  { "num1" : 1, "num2" : 2 },
  { "num1" : 2, "num2" : 2 },
  { "num1" : 3, "num2" : 2 },
  { "num1" : 4, "num2" : 2 },
  { "num1" : 5, "num2" : 2 },
  // 数百メガバイトになるまで繰り返し
]

JavaScript

環境:
Windows10
node.js v10.16.0

標準のJSONオブジェクト

標準のJSONオブジェクトのparseとstringifyでJSONの操作が行えます。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON

var fs = require("fs");
var contents = fs.readFileSync("C:\\share\\jsontest\\test001.json");
var jsonData = JSON.parse(contents);
console.log(jsonData['null']);
console.log(jsonData['num1']);
console.log(jsonData['num2']);
console.log(jsonData['num3']);
console.log(jsonData['num4']);
console.log(jsonData['num5']);
console.log(jsonData['str1']);
console.log(jsonData['str2']);
console.log(jsonData['obj1']);
console.log(jsonData['obj1']['b']['c']);
console.log(jsonData['ary1']);
console.log(jsonData['ary1'][0]);

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
console.log(JSON.stringify(jsonData));

出力結果:

null
99999
123.45
-123.45
1000
0.001
abあいうえおcdef
str2:
\       /ab0ぁ
{ a: 125, b: { c: { d1: [Object], d2: 12345 } } }
{ d1: { v: 1234 }, d2: 12345 }
[ 1, 2, 'test', { a: 5.3 } ]
1
{"null":null,"num1":99999,"num2":123.45,"num3":-123.45,"num4":1000,"num5":0.001,"bool1":true,"bool2":false,"str1":"ab あいうえおcdef","str2":"str2:\n\r\\\t/abc\b0ぁ","obj1":{"a":125,"b":{"c":{"d1":{"v":1234},"d2":12345}}},"ary1":[1,2,"test",{"a":5.3}],"ary2":[1,2,3]}

JSON.parseでJSONからJavaScriptのオブジェクトに、JSON.stringify()でJavaScriptのオブジェクトからJSONに変換できていることが確認できます。
また、num1は重複したデータですが,エラーにはならず、後優先で値が出力されています。

JSONの各値の変換処理

JSON.parseのドキュメントによると引数にreviver が設定できることがわかります。
ここには関数を指定でき,JSONから取得した値を変換することが可能です。
下記の例は数値型をDecimal型に変換するサンプルとなります。

事前準備decimalのインストール


npm install decimal
var fs = require("fs");
// https://www.npmjs.com/package/decimal
var Decimal = require('decimal');
var contents = fs.readFileSync("C:\\share\\jsontest\\test001.json");
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
var jsonData = JSON.parse(contents, function (key, value) {
    //console.log(key, JSON.stringify(value));
    if (typeof value === 'number') {
      return Decimal(value);
    }
    return value;
  }
);

console.log('-----------------------------------------');

console.log(jsonData['num1'].constructor.name);
console.log(jsonData['num1'].toString());

console.log(jsonData['num2'].constructor.name);
console.log(jsonData['num2'].toString());

console.log(jsonData['obj1']['a'].constructor.name);
console.log(jsonData['obj1']['a'].toString());

出力結果

-----------------------------------------
Decimal
99999
Decimal
123.45
Decimal
125

出力結果より、JSON中のNumber型が全てDecimal型にJavaScriptのオブジェクトとして作成されたことが確認できます。

JSONStream

大きなサイズのJSONファイルを操作する場合、jsonライブラリではメモリを大量に消費し解析に失敗する可能性があります。
この場合は、JSONStreamを使用する必要があります。JSONStreamを使用することで、JSONのデータを一度に全てメモリに展開することなく解析することが可能になります。

インストール


npm install JSONStream
// https://stackoverflow.com/questions/15121584/how-to-parse-a-large-newline-delimited-json-file-by-jsonstream-module-in-node-j
var fs = require('fs'),
    JSONStream = require('JSONStream');

var stream = fs.createReadStream("C:\\share\\jsontest\\test003.json", {encoding: 'utf8'}),
    parser = JSONStream.parse('.');

stream.pipe(parser);

var cnt = 0
parser.on('data', function (obj) {
  // console.log(obj); 
  if (cnt === 0) {
    console.log(obj);
  }
  cnt = cnt + 1;
});

parser.on('end', function () {
  console.log(cnt);
  console.log('end');
});

ファイルストリームを作成してそれをJSONStreamのparserに渡します。
この際、オブジェクトを取得するたびに「data」イベントが発火し、すべてのストリームが完了したら「end」イベントが発火します。
このサンプルでは「data」イベント中で以下の処理をしています。
 ・最初に取得したオブジェクトの内容を表示
 ・dataイベントが発火した数を数える

また「end」イベントでは「data」イベントが発火した数を表示しています。

以下は出力結果になります

C:\dev\node>node jsontest3.js
{ num1: 1, num2: 2 }
6636801
end

詳しい使い方については下記を参照してください。
https://github.com/dominictarr/JSONStream

Python

環境:
Windows10
Python 3.7.4

jsonライブラリ

Pythonの標準ライブラリのjsonライブラリを使用することでJSONの操作を行うことが可能です。

以下のサンプルは「操作対象のJSONの例」を解析するためのプログラムです。

# coding:utf-8
import json

f = open('C:\\share\\jsontest\\test001.json', 'r', encoding="utf-8")

jsonData = json.load(f)
print ('----------------------------------------------------')
print(json.dumps(jsonData, sort_keys = True, indent = 4))
print ('----------------------------------------------------')
print (jsonData['null'])
print (jsonData['num1'])
print (type(jsonData['num1']))
print (jsonData['num2'])
print (type(jsonData['num2']))
print (jsonData['str1'])
print (jsonData['str2'])
print (type(jsonData['obj1']))
print (jsonData['obj1'])
print (jsonData['obj1']['a'])
print (type(jsonData['ary1']))
print (jsonData['ary1'])
print (jsonData['ary1'][0])

f.close()

出力結果は以下のようになります。

C:\dev\python3>python jsontest.py
----------------------------------------------------
{
    "ary1": [
        1,
        2,
        "test",
        {
            "a": 5.3
        }
    ],
    "ary2": [
        1,
        2,
        3
    ],
    "bool1": true,
    "bool2": false,
    "null": null,
    "num1": 99999,
    "num2": 123.45,
    "num3": -123.45,
    "num4": 1000.0,
    "num5": 0.001,
    "obj1": {
        "a": 125,
        "b": {
            "c": {
                "d1": {
                    "v": 1234
                },
                "d2": 12345
            }
        }
    },
    "str1": "ab\u3042\u3044\u3046\u3048\u304acdef",
    "str2": "str2:\n\r\\\t/abc\b0\u3041"
}
----------------------------------------------------
None
99999
<class 'int'>
123.45
<class 'float'>
abあいうえおcdef
str2:
\       /ab0ぁ
<class 'dict'>
{'a': 125, 'b': {'c': {'d1': {'v': 1234}, 'd2': 12345}}}
125
<class 'list'>
[1, 2, 'test', {'a': 5.3}]
1

json.load()でJSONファイルを展開してJSON中のvalueをPythonのデータに下記の表に従って変換します。

JSON Python
object dict
array list
string str
number int または 浮動小数点
true True
false False
null None

出力結果を確認すると上記の表にしたがって型が変換されていることが確認できると思います。
また、元データのJSONには「num1」という名称は2つ存在していますが、出力結果を見ると、後の方のデータが使用されていることが確認できます。

また、json.dumps()はPythonのデータをJSONに変換しています。

jsonライブラリを確認するとjson.loadにはいくつか興味深いパラメータがあります。次節ではそれらを確認してみましょう。

数値の型に対するコールバック関数

json.loadにはparse_floatとparse_intの二つの引数があります。これらの引数にはコールバック関数が設定できます。

以下は、コールバック関数を用いてJSONのnumber型をdecimalに変換した例になります。

# coding:utf-8
import json
import decimal

def callbackInt(d):
  return decimal.Decimal(d)

f = open('C:\\share\\jsontest\\test001.json', 'r', encoding="utf-8")

jsonData = json.load(f, parse_float = decimal.Decimal, parse_int = callbackInt)
print ('--------------------------------------------')
print (jsonData['num1'])
print (type(jsonData['num1']))
print (jsonData['num2'])
print (type(jsonData['num2']))
print (jsonData['num3'])
print (type(jsonData['num3']))

f.close()

出力結果

--------------------------------------------
99999
<class 'decimal.Decimal'>
123.45
<class 'decimal.Decimal'>
-123.45
<class 'decimal.Decimal'>

オブジェクトの型に対するコールバック関数

json.loadにはobject_hookというコールバック関数があります。これはJSON中にobjectを検知するたびに実行される関数です。
数値型に対するコールバック関数と同様に、これを用いてオブジェクトを任意の型に変換することも可能です。

以下の例ではオブジェクト型を検知するたびに、検知した値を出力しています。

# coding:utf-8
import json

def hook_func(dct):
  print (dct)
  return dct

f = open('C:\\share\\jsontest\\test001.json', 'r', encoding="utf-8")

jsonData = json.load(f, object_hook=hook_func)
print ('----------------------------------------------------')

f.close()

出力結果

C:\dev\python3>python jsontest_1.py
{'v': 1234}
{'d1': {'v': 1234}, 'd2': 12345}
{'c': {'d1': {'v': 1234}, 'd2': 12345}}
{'a': 125, 'b': {'c': {'d1': {'v': 1234}, 'd2': 12345}}}
{'a': 5.3}
{'null': None, 'num1': 99999, 'num2': 123.45, 'num3': -123.45, 'num4': 1000.0, 'num5': 0.001, 'bool1': True, 'bool2': False, 'str1': 'abあいうえおcdef', 'str2': 'str2:\n\r\\\t/abc\x080ぁ', 'obj1': {'a': 125, 'b': {'c': {'d1': {'v': 1234}, 'd2': 12345}}}, 'ary1': [1, 2, 'test', {'a': 5.3}], 'ary2': [1, 2, 3]}
----------------------------------------------------

C:\dev\python3>

出力結果を見てわかる通り、基本的に上から見ていきますが、オブジェクトの中にオブジェクトがある場合は、もっとも深い箇所から検知されていきます。
任意のオブジェクトに変換する場合は、コールバック関数の引数であるdictの全てのキーをみて判断するか、任意の項目に変換すべき型の情報を記載しておく必要があるでしょう。

オブジェクトや配列を含まないデータの場合

最近のJSONの仕様ではオブジェクトや配列を含まない場合もJSONをパースできます。
これは以下のようなコードで検証できます。

import json
json.loads('12345')
# 12345

jsonライブラリにおいては、この仕様を満たしています。

NaNやInfinityなどの数値じゃない値を含む場合

import json
n = json.loads('NaN')
print(n)
# nan
json.dumps(n)
# 'NaN'

NaNやInfinityを含む場合、デフォルトload/loadsの挙動ではnaninfに変換されます。
また、nanやinfを含むPythonの値をdump/dumpsでJSON化した場合、NaNやInfinityに変換されます。

もし、仕様と合わせてJSON中のNaNやInfinityの値をエラーとする場合は以下のようにします。

def errorfunc(d):
   print(d)
   raise

n = json.loads('NaN', parse_constant=errorfunc)

これを実行すると、NaNなどを検知した場合にerrorfuncが実行されて例外が発生します。
また、逆にPythonのデータをJSON化する場合にNaNやInfinityを認めない場合は以下のような実装になります。

n = json.loads('NaN')
json.dumps(n, allow_nan=False)

この場合、dumps実行時にnanやinfを検出すると「ValueError: Out of range float values are not JSON compliant」が発生します。

ijsonライブラリ

大きなサイズのJSONファイルを操作する場合、jsonライブラリではメモリを大量に消費し解析に失敗する可能性があります。
この場合は、ijsonを使用します。

ijsonはイテレータのインターフェイスを経由してJSONを解析することにより、すべてのJSONデータをメモリに展開する必要がなくなります。
以下のサンプルコードは大量データのJSONを解析している例です。

# coding:utf-8
import ijson

def test1():
  # 数百メガのファイルを開く
  f = open('C:\\share\\jsontest\\test003.json', 'r', encoding="utf-8")
  items = ijson.items(f, 'item')
  l = 0
  for i in items:
    if l == 0:
      print (i)  # 最初のオブジェクトのみ表示
    l = l + 1
  print (l)
  f.close()

if __name__ == "__main__":
  test1()

ijson.itemsにfile object と prefixを与えて抽出を行います。prefixを設定することで、特定の要素のみを抽出対象にすることができます。今回はarrayの各要素を取得したいので「item」を指定してます。

このファイルの実行結果としては以下のように、最初のオブジェクトと、オブジェクトの総数を出力します。

C:\dev\python3>python jsontest3_3.py
{'num1': 1, 'num2': 2}
6636801

ijson
https://pypi.org/project/ijson/

ijson - Github
https://github.com/isagalaev/ijson

C#

環境:
Windows10
VisualStudio2019
.NET 4.7.2

Json.NET

NewtonSoftが提供するJson.NETを持ちいることでJSONに対する操作を行います。
https://www.newtonsoft.com/json

ASP.NETで開発した人は、おそらく良く見るライブラリかと思います。

NuGetで以下のパッケージをインストールしてください。
image.png

型を指定しない使用方法

JsonConvert.DeserializeObjectに型を指定しないでJSONデータを読み込むことができます。これにより、あらかじめJSONの形に合わせたクラスを用意する必要がなくなります。

        static void Test1()
        {
            var contents = File.ReadAllText(@"c:\share\jsontest\test001.json");
            dynamic json = JsonConvert.DeserializeObject(contents);
            Console.WriteLine(json.num1.Type);
            Console.WriteLine(json.num1.Value);

            Console.WriteLine(json.num2.Type);
            Console.WriteLine(json.num2.Value);

            Console.WriteLine(json.num3.Type);
            Console.WriteLine(json.num3.Value);

            Console.WriteLine(json.num4.Type);
            Console.WriteLine(json.num4.Value);

            Console.WriteLine(json.num5.Type);
            Console.WriteLine(json.num5.Value);

            Console.WriteLine(json["null"].Type);
            Console.WriteLine(json["null"].Value == null);

            Console.WriteLine(json.bool1.Type);
            Console.WriteLine(json.bool1.Value);

            Console.WriteLine(json.bool2.Type);
            Console.WriteLine(json.bool2.Value);

            Console.WriteLine(json.str1.Type);
            Console.WriteLine(json.str1.Value);

            Console.WriteLine(json.str2.Type);
            Console.WriteLine(json.str2.Value);

            Console.WriteLine(json.obj1.a.Type);
            Console.WriteLine(json.obj1.a.Value);

            Console.WriteLine(json.ary1.Count);
            Console.WriteLine(json.ary1[0].Type);
            Console.WriteLine(json.ary1[0].Value);

        }

出力結果は以下のようになります。

Integer
99999
Float
123.45
Float
-123.45
Float
1000
Float
0.001
Null
True
Boolean
True
Boolean
False
String
abあいうえおcdef
String
str2:
\       /ab0ぁ
Integer
125
4
Integer
1

重複している値num1が、後優先で取得されていることが確認できます。

型を指定する方法

JsonConvert.DeserializeObjectに型を指定して呼び出すことで、JSONの解析結果を事前に用意したクラスに割り当てることも可能です。

使用するJSONデータ

[
  {"name":"Joe", "lv" : 10},
  {"name":"Jack", "lv" : 15},
  {"name":"Sara", "lv" : 13},
  {"name":"Ben", "lv" : 12}
]
       class Member {
            public string name { get; set; }
            public decimal lv { get; set; }

        }

        static void Test2()
        {
            var contents = File.ReadAllText(@"c:\share\jsontest\test004.json");
            List<Member> json = JsonConvert.DeserializeObject<List<Member>>(contents);
            foreach (var m in json)
            {
                Console.WriteLine(m.name );
                Console.WriteLine(m.lv);
            }

            Console.WriteLine("------------------------------------------");
            Console.WriteLine(JsonConvert.SerializeObject(json, Formatting.Indented));

        }

このサンプルではJSONをList\<Member>に明示的に変換することを行っています。
また、 List\<Member>をJsonConvert.SerializeObjectを使用してJSONに戻しています。

出力結果:

Joe
10
Jack
15
Sara
13
Ben
12
------------------------------------------
[
  {
    "name": "Joe",
    "lv": 10.0
  },
  {
    "name": "Jack",
    "lv": 15.0
  },
  {
    "name": "Sara",
    "lv": 13.0
  },
  {
    "name": "Ben",
    "lv": 12.0
  }
]

出力結果が10.0,15.0などの小数点表記になっているのはMemberクラスのlvがdecimalのためです。

大量のデータを解析する方法

大量のデータを少ないメモリで処理するにはJsonTextReaderを使用します。

        static void Test3()
        {
            using (FileStream fs = new FileStream(@"c:\share\jsontest\test003.json", FileMode.Open, FileAccess.Read))
            using (StreamReader sr = new StreamReader(fs))
            using (JsonTextReader reader = new JsonTextReader(sr))
            {
                int cnt = 0;
                while (reader.Read())
                {
                    if (reader.TokenType == JsonToken.StartObject)
                    {
                        // Load each object from the stream and do something with it
                        JObject obj = JObject.Load(reader);
                        //Console.WriteLine(obj["num1"]);
                        ++cnt;
                    }
                }
                Console.WriteLine(cnt);
            }
        }

ファイルストリームをJsonTextReaderに渡し、StartObjectを検知する度に処理を行うことで、すべてのデータをメモリに展開せずに解析が可能になります。

RFCより緩い挙動

RFC7159で認められていないJSONをJSONとしてみなす挙動が何点かあります。
https://github.com/JamesNK/Newtonsoft.Json/issues/646

たとえば以下のコードはjavascriptのJSON.parseでは例外となりますが、Json.NETでは通ってしまいます。

           // 二重引用符ではなく一重引用符を含むプロパティ
            dynamic json =  JsonConvert.DeserializeObject("{'a':1}");
            Console.WriteLine(json);
            // 引用符なしのプロパティ
            json = JsonConvert.DeserializeObject("{a:1}");
            Console.WriteLine(json);
            // NaN,Infinity, -Infinity
            json = JsonConvert.DeserializeObject("{\"a\":NaN}");
            Console.WriteLine(json.a);
            // 末尾のコンマ
            json = JsonConvert.DeserializeObject("{\"a\": 123,}");
            Console.WriteLine(json);
            // 空のコンマ
            json = JsonConvert.DeserializeObject("[1,,2]");
            Console.WriteLine(json);
            // 8進数
            json = JsonConvert.DeserializeObject("{\"a\": 010}");
            Console.WriteLine(json);
            // 16進数
            json = JsonConvert.DeserializeObject("{\"a\": 0x010}");
            Console.WriteLine(json);
{
  "a": 1
}
{
  "a": 1
}
NaN
{
  "a": 123
}
[
  1,
  null,
  2
]
{
  "a": 8
}
{
  "a": 16
}

2018年時点の仕様より多くのものを許可してしまっているということに注意しなければいけません。特に複数のプログラミング言語を使用している場合、注意が必要です。

例:C#で通ったJSONがnode.jsとかでは通らないとかがあり得る。

PowerShell

環境:
Windows10
PSVersion 5.1.17134.765
PSEdition Desktop

ConvertFrom-Json と ConvertTo-Json

ConvertFrom-JsonはJSONをPowerShellのオブジェクトに変換し、ConvertTo-JsonはPowerShellのオブジェクトをJSONに変換します。

ConvertFrom-Json
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertfrom-json?view=powershell-5.1

ConvertTo-Json
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-5.1

JavaScriptSerializer classを用いて実装しているとのことですが、これはC#で出てきたJson.NET を使用しているようです。

$json = Get-Content  -Encoding UTF8 c:\share\jsontest\test001.json  | ConvertFrom-Json
Write-Host $json.GetType()
Write-Host $json
Write-Host $json.num1.GetType()
Write-Host $json.num1
Write-Host $json.num2.GetType()
Write-Host $json.num2
Write-Host $json.num3.GetType()
Write-Host $json.num3
Write-Host $json.num4.GetType()
Write-Host $json.num4
Write-Host $json.num5.GetType()
Write-Host $json.num5
Write-Host $json.obj1.GetType()
Write-Host $json.obj1
Write-Host $json.obj1.a
Write-Host $json.obj1.b.GetType()
Write-Host $json.obj1.b
Write-Host $json.obj1.b.c.GetType()
Write-Host $json.obj1.b.c
Write-Host $json.obj1.b.c.d1.GetType()
Write-Host $json.obj1.b.c.d1
Write-Host $json.ary1.GetType()
Write-Host $json.ary1
Write-Host $json.ary1[0]

# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-5.1
$str = ConvertTo-Json -depth 100 -InputObject $json
Write-Host $str

実行結果は以下のようになります。

System.Management.Automation.PSCustomObject
@{null=; num1=99999; num2=123.45; num3=-123.45; num4=1000; num5=0.001; bool1=True; bool2=False; str1=abあいうえおcdef; str2=str2:
\       /ab0ぁ; obj1=; ary1=System.Object[]; ary2=System.Object[]}
System.Int32
99999
System.Decimal
123.45
System.Decimal
-123.45
System.Double
1000
System.Double
0.001
System.Management.Automation.PSCustomObject
@{a=125; b=}
125
System.Management.Automation.PSCustomObject
@{c=}
System.Management.Automation.PSCustomObject
@{d1=; d2=12345}
System.Management.Automation.PSCustomObject
@{v=1234}
System.Object[]
1 2 test @{a=5.3}
1
{
    "null":  null,
    "num1":  99999,
    "num2":  123.45,
    "num3":  -123.45,
    "num4":  1000,
    "num5":  0.001,
    "bool1":  true,
    "bool2":  false,
    "str1":  "abあいうえおcdef",
    "str2":  "str2:\n\r\\\t/abc\b0ぁ",
    "obj1":  {
                 "a":  125,
                 "b":  {
                           "c":  {
                                     "d1":  {
                                                "v":  1234
                                            },
                                     "d2":  12345
                                 }
                       }
             },
    "ary1":  [
                 1,
                 2,
                 "test",
                 {
                     "a":  5.3
                 }
             ],
    "ary2":  [
                 1,
                 2,
                 3
             ]
}

重複しているnum1は後方が優先されており、JSONのObject型はSystem.Management.Automation.PSCustomObject、JSONのArray型はSystem.Object[]に変換されています。
JSONのNumber型はInt32,Double,Decimalのいずれかの型に変換されていることが確認でき、型の取り扱いに注意が必要と考えられます。

Excel VBA

環境:
Windows10
Office16(32bit)

JScriptを用いた変換

UTF8のJSONファイルをADODB.Streamで読み込み、それをJScriptのオブジェクトに変換する方法が、下記のページに紹介されていました。

How to parse JSON with VBA without external libraries?
https://stackoverflow.com/questions/19360440/how-to-parse-json-with-vba-without-external-libraries

この方法は正常に動作しないケースがあります。

Private Function ReadUtf8File(ByVal path As String) As String
    Dim objStream, strData

    Set objStream = CreateObject("ADODB.Stream")

    objStream.Charset = "utf-8"
    objStream.Open
    Call objStream.LoadFromFile(path)

    ReadUtf8File = objStream.ReadText()

    objStream.Close
    Set objStream = Nothing
End Function

Sub Test1_1()
    ' https://stackoverflow.com/questions/19360440/how-to-parse-json-with-vba-without-external-libraries
    Dim json As Object
    Dim scriptControl As Object

    Set scriptControl = CreateObject("MSScriptControl.ScriptControl")
    scriptControl.Language = "JScript"

    Dim contents As String
    contents = ReadUtf8File("C:\\share\\jsontest\\test001.json")

    Set json = scriptControl.Eval("(" + contents + ")")
    Debug.Print json

    ' 配列が操作できない

End Sub

配列の操作ができない
image.png

ウォッチ式を見る限り配列として取得されているようだが、配列の操作が一切できない。

array, objectを含まないJSONが解析できない

Sub Test1_2()
    Dim json As Object
    Dim scriptControl As Object

    Set scriptControl = CreateObject("MSScriptControl.ScriptControl")
    scriptControl.Language = "JScript"

    Dim contents As String
    contents = "12345"

    ' エラー
    Set json = scriptControl.Eval("(" + contents + ")")
End Sub

VBA-JSON

VBA-JSONはVBAでJSONの変換を可能としたライブラリです。
https://github.com/VBA-tools/VBA-JSON

使用方法としては以下の通りです。
1. 「Microsoft Scripting Runtime」を参照設定すること
2. JsonConverter.basを取り込む
3. 下記のような実装を行う

Sub test2_1()
    Dim contents As String
    contents = ReadUtf8File("C:\\share\\jsontest\\test001.json")
    Dim Parsed As Dictionary
    Set Parsed = JsonConverter.ParseJson(contents)
    Debug.Print Parsed.Item("num1")
    Debug.Print Parsed.Item("num2")
    Debug.Print Parsed.Item("null")
    Debug.Print Parsed.Item("obj1").Item("a")
    Debug.Print Parsed.Item("ary1").Count
    Debug.Print Parsed.Item("ary1").Item(1)
    Debug.Print Parsed.Item("ary1").Item(2)
    Debug.Print Parsed.Item("ary1").Item(3)
End Sub

先ほどと違い配列データも正常に取得が可能になっています。

Test2_1
 99999 
 123.45 
Null
 125 
 4 
 1 
 2 
test

ただし、最新の仕様に対応していないため、以下のようにobjectまたはarrayを含まないJSONについて仕様通り動作しません。

Sub test2_2()
    Dim contents As String
    contents = "12345"
    Dim Parsed As Dictionary
    ' エラー
    Set Parsed = JsonConverter.ParseJson(contents)
End Sub

さいごに

今回は謙虚な気持ちでJSONについて調べなおしてみました。

わかってたけど なにもわかっちゃいなかったことも まれによくあるので、わかったつもりになっているモノこそ基礎から見直してみることも必要かもしれません。

さもないと慢心の精霊に取りつかれて「JSONはobjectまたはarrayを必ず含む必要ある!どや!( ・`ー・´)」とか言って老害っぷりをさらすことになります。

その他・参考

Wikipedia
https://ja.wikipedia.org/wiki/JavaScript_Object_Notation
https://en.wikipedia.org/wiki/JSON

how to parse a large, Newline-delimited JSON file by JSONStream module in node.js?
https://stackoverflow.com/questions/15121584/how-to-parse-a-large-newline-delimited-json-file-by-jsonstream-module-in-node-j

Json.NETドキュメント
https://www.newtonsoft.com/json/help/html/Introduction.htm

公式ドキュメントが読まれない こんな世の中じゃ ポイズン

まえがき

むかーし、グーグルで適当な個人サイト探すより、なるべく公式ドキュメントを参照してねという動画を作ったことがあります。

残念ながら、「動けばいいんだよ」勢には効果がなかったようなので、最近、見つけた一見動くが、公式ドキュメントで認められていないことやってバグが出る例を出してリベンジしようと思います。

問題

Excel VBAで動的配列の作成されているか否かを判断するにはどうしたらいいでしょうか?
たとえば以下のコードでエラーになる場合と、ならない場合を判別する方法を考えてみてください。

Public Sub test()
     Dim v() As Variant
     ' Debug.Print UBound(v) ' エラーになる

     ReDim v(1) As Variant
     Debug.Print UBound(v)

     Erase v
     ' Debug.Print UBound(v) ' エラーになる
End Sub

想定される回答

おそらく、googleとかで検索した場合に、この問題で想定される回答は次の4つになります。

OnErrorを使用する方法

動的配列が作成されていなかったり、削除されている場合でUBoundやLBoundでエラーが発生するなら、素直にエラーハンドリングをして動的配列の作成の有無をチェックします。

Public Sub testArraySample()
    Dim v() As Variant
    Debug.Assert isEmptyArray(v) = True

    ReDim v(1) As Variant
    Debug.Assert isEmptyArray(v) = False

    Erase v
    Debug.Assert isEmptyArray(v) = True
End Sub

Private Function isEmptyArray(v() As Variant) As Boolean
On Error GoTo ErrUbound:
    Dim tmp As Long
    tmp = UBound(v)
    isEmptyArray = False
    Exit Function
ErrUbound:
    If Err.Number <> 9 Then
        Err.Raise Err.Number, Err.Source, Err.Description, Err.HelpFile, Err.HelpContext
    End If
    isEmptyArray = True
End Function

SafeArrayGetDimを利用する方法

VBAの配列は動的であれ、固定であれ、SafeArrayになっています。
http://msdn.microsoft.com/en-us/library/windows/desktop/ms221482%28v=vs.85%29.aspx

このSafeArrayの次元数はSafeArrayGetDimで取得できます。次元数が0ならまだ配列が作られていないと判断できます。
http://msdn.microsoft.com/en-us/library/windows/desktop/ms221539%28v=vs.85%29.aspx

Private Declare PtrSafe Function SafeArrayGetDim Lib "oleaut32" (ByRef psa() As Any) As Long

Private Function isEmptyArray(v() As Variant) As Boolean
    If SafeArrayGetDim(v) <> 0 Then
        isEmptyArray = False
    Else
        isEmptyArray = True
    End If
End Function

Sgnを使う方法

Sgnを利用することで判定できます。
なんで動くかはよくわかりません(ドヤァ!)

Private Function isEmptyArray(v() As Variant) As Boolean
    If Sgn(v) Then
        isEmptyArray = False
    Else
        isEmptyArray = True
    End If
End Function

Not Notを利用する場合

Not Notのテクニックを使うことで判定できます。
なんで動くかはよくわかりません(ドヤァ!)

Private Function isEmptyArray(v() As Variant) As Boolean
    If Not Not v Then
        isEmptyArray = False
    Else
        isEmptyArray = True
    End If
End Function

各実装の評価

まず、簡単な動作確認だと、この4つのソースコードは、それっぽく動作します。
その上で、レビューで、このコードに遭遇した場合どう判断するか考えてみてください。

仮に私がレビュワーだった場合は以下のような判定をします。

OnErrorを使用する方法

VBAの言語仕様通りの実装なのでレビューを通します。

SafeArrayGetDimを利用する方法

WinAPIを使っているので32bitと64bitの両方のExcelで動くか微妙な宣言ですが、少なくともMSDNの根拠があるので32bit/64bit両方のExcelで動作確認しておくことという条件でレビューを通します。

また、レビュー時に、SafeArrayのあたりの根拠の説明ができない場合は、仮に動くコードでも却下します。

Sgnを使う方法

これは却下します。
この実装の動かないケースについては以下のページに詳しく書いてあります。

VBAで配列のNull判定にSgn関数を使ってはいけない
https://qiita.com/satoko138/items/7e06dda56683065968f7

簡単にいうと、配列の割り当て前はアドレスが0になる。
IF分岐でそのままだと判定できないので、Sgn関数を通してからIF分岐に渡している。

しかし、Sgn関数の仕様として引数には「Double containing any valid numeric expression.」とあるので、配列などを渡すと、一見動く場合もあるが、予期せぬ動作をしてしまう。

Not Notを使う方法

これも却下です。
Sgn関数の代わりにNot論理演算子をつかって回避していますが、VBAの論理演算子の説明に「§ A logical operator is invalid if the declared type of any operand is an array or a UDT.」、つまり論理演算子に配列渡しても無効と明記してあります。

「Sgnを使う方法」とことなり、予期せぬ結果になるパターンを見つけられていませんが、「見つからない=不具合がない」ということではないので、先の公式ページより信憑性の高い資料を提出しない限り却下です。

いいたいこと

・とりあえず公式のページを参考に論理をたてられるようにしましょう
 →Qiita含めて個人サイトより優先的に参考にしましょう。

・もし、公式のページが読みづらくて、個人サイトとかを参考にするとしても、その個人サイトが公式を参考にしているかで信憑性を判断しましょう。

・動けばいいってのはだめです。
 →テストの原則で「欠陥がない」ことは示せないから、よくわからんけど動いたなんてのはレビューで通せるわけないです。

・とりあえず公式ページ参照していれば、結果が間違っても責任回避できます。

(補足)
・Googleで調べて何故か公式より信憑性のないサイトが上にくる場合はGoogleの設定を英語設定にするとマシになるかもしれません
 https://needtec.sakura.ne.jp/wod07672/?p=9205

 

検索結果のトップから公式のページを取り戻す

問題

グーグルで検索していると、公式ページより、個人が適当にまとめたページが上に来てしまう場合が稀によくあります。
たとえば「VisualStudio Test」を調べてみると、本来であればMicrosoftのページがトップに来てほしいですが、実際は以下のように個人のまとめた記事がトップに来てしまいます。

qiita1.png

さすがにQiitaがMicrosoftのページより上に来ちゃあかんだろ。

サイトごとブロックはしたくないが、なるべく公式ページが上に来るように考えてみました。

対策

アメリカの検索エンジンを使う。

Chromeの設定方法

(1)設定画面」から「検索エンジンの管理」をクリック
qiita2.png

(2)追加ボタンをクリック
qiita3.png

(3)検索エンジンの追加にて必要項目を入力して追加を押下
qiita4.png

検索エンジン:任意の文字
キーワード:任意の文字
URL:{google:baseURL}search?gl=us&hl=en&gws_rd=cr&pws=0&q=%s

(4)追加した検索エンジンをデフォルトに設定する。
qiita5.png

やったぜ

qiita7.png

なお、キーワードに日本語を混ぜるとダメみたいです。
「Visual Studio テスト」の例

qiita8.png

今回は英語を優先したかったので「gl=us&hl=en」となりましたが、「gl=ru&hl=ru」に変更してロシア優先とかもできます。
qiita9.png

 参考

日本からアメリカのGoogle (google.com) で英語で検索する方法
https://www.suzukikenichi.com/blog/how-to-search-on-google-com-in-english-from-japan/

RPA九人衆による「アカネチャンカワイイヤッタ」の自動化

レギュレーション

各RPAツールでVOICEROID2の茜ちゃんに「アカネチャンカワイイヤッタ」と言わせた後にファイルを保存します。
画像認識できる場合は葵ちゃんにもしゃべってもらいます。

環境:
 Windows10 64bit
 VoiceRoide2

画面構成
スライド1.PNG

タブの中の子要素が取れない問題について:
https://teratail.com/questions/53276

保存時の画面遷移
スライド2.PNG

参加ツール

ツール名 簡単な説明
VBA + UIAutomation UIAutomationをCOM経由でVBAで実行して画面操作します。ツールに頼らない裸の強さを見せてくれます。
PowerShell + UIAutomation UIAutomationを.NET経由でPowerShellを使って実行します。Windows7以降ならOfficeすら不要という強さがあります。
WinAppDriver Microsoftが開発したSeleniumライクの自動操作を実現するツール。Seleniumを使うなら俺も使えという熱い気持ちが伝わってきます。
Friendly 本来はテストツール。操作対象のアプリにテスト用のDLLをインジェクションするという荒業をみせて、参加選手のなか唯一タブの中の要素を画像認識を使わずに操作した豪の物です。
PyAutoGUI Pythonでの自動操作を実現します。基本的に画像認識で操作を行いますが、旨く作ればMacでもLinuxでも動作します。最近のPythonブームによって躍進が期待されます。
UWSC 古のツールの中で唯一UIAutomationが認識できる要素を解析できたつわものです。バランスの取れたいい選手ですが、このたび公式サイトが閉鎖されるというアクシデントがあり引退がささやかれています。
sikulix 画像認識に特化したツール。IDEが使いやすくデザインされています。またJavaで動作してどこでも動くうえ、スクリプト自体はPythonやRuby,JavaScriptで記載できる欲張りセットになっております。
RocketMouse 昔からある自動操作ツール。画像認識はできるが、オブジェクトの認識はWin32で作ったものしかできません。今回は試用版による参加
UIPath 2018年のforresterの調査でRPAのリーダーと言わしめた製品。高価格帯からは唯一の参戦だが、Community版なら個人や小規模事業では使用できるというサプライズ。

なお、AutoIt選手とAutoHotKey選手につきましてはUIAutomationのCOMを触るためのIFを用意する必要があり、それ以外だと、画像認識しかできないので今回は欠場となっております。

アカネチャンカワイイヤッターの実行

VBA + UIAutomationでアカネチャンカワイイヤッター

Officeさえ入っていればWindowsの自動操作が行えます。
RPAツールなんていらんかったんや

画面上の要素を正確に捕捉できるため、違うPCでも動かしやすいという利点があります。
操作対象のオブジェクトの調査はInspectを用いて行うとよいでしょう。

inspect.png

VBAによる実装

https://github.com/mima3/rpa_akanechan/tree/master/vba(UIAutomationCom)

参照設定
image.png

Module1

Option Explicit

Public Sub Kawaii()
    Dim vr As New VoiceRoid
    Dim mainForm As IUIAutomationElement
    Set mainForm = vr.GetMainWindowByTitle(vr.GetRoot(), "VOICEROID2")
    If (mainForm Is Nothing) Then
        Set mainForm = vr.GetMainWindowByTitle(vr.GetRoot(), "VOICEROID2*")
        If (mainForm Is Nothing) Then
            MsgBox "VOICEROIDE2が起動していない"
            Exit Sub
        End If
    End If

    ' 茜ちゃんしゃべる
    Call vr.SetText(mainForm, 0, "アカネチャンカワイイヤッタ")
    Call vr.pushButton(mainForm, 0)

    ' しゃべり終わるまで待機
    Dim sts As String
    Do While sts <> "テキストの読み上げは完了しました。"
        sts = vr.GetStatusBarItemText(mainForm, 0)
        Call vr.SleepMilli(500)
    Loop

    ' 音声保存
    Call vr.pushButton(mainForm, 4)

    ' 5秒以内に音声保存画面が表示されたら保存ボタンを押す
    Dim saveWvForm As IUIAutomationElement
    Set saveWvForm = vr.WaitMainWindowByTitle(mainForm, "音声保存", 5)
    Call vr.pushButton(saveWvForm, 0)

    ' 名前を付けて保存に日付のファイル名を作る
    Dim saveFileForm As IUIAutomationElement
    Set saveFileForm = vr.WaitMainWindowByTitle(saveWvForm, "名前を付けて保存", 5)
    Call vr.SetTextById(saveFileForm, "1001", Format(Now(), "yyyymmddhhnnss.wav"))
    SendKeys "{ENTER}"

    ' 情報ポップアップのOKを押下
    Dim infoForm As IUIAutomationElement
    Set infoForm = vr.WaitMainWindowByTitle(saveWvForm, "情報", 60)
    Call vr.pushButton(infoForm, 0)
End Sub

VoiceRoid.cls

Option Explicit
Private uia As UIAutomationClient.CUIAutomation
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Private Sub Class_Initialize()
    Set uia = New UIAutomationClient.CUIAutomation
End Sub
Private Sub Class_Terminate()
    Set uia = Nothing
End Sub
Public Sub SleepMilli(ByVal millisec As Long)
    Call Sleep(millisec)
End Sub
' ルートのディスクトップ要素を取得
Public Function GetRoot() As IUIAutomationElement
    Dim ret As IUIAutomationElement
    Set ret = uia.GetRootElement
    Set GetRoot = ret
End Function

' 指定の子ウィンドウをタイトルから取得する
Public Function GetMainWindowByTitle(ByRef form As IUIAutomationElement, ByVal name As String) As IUIAutomationElement
    Dim cnd As IUIAutomationCondition
    Dim ret As IUIAutomationElement
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_NamePropertyId, name)

    Set ret = form.FindFirst(TreeScope_Element Or TreeScope_Children, cnd)

    Set GetMainWindowByTitle = ret
End Function

' 指定の子ウィンドウをタイトルから取得できまで待機
Public Function WaitMainWindowByTitle(ByRef form As IUIAutomationElement, ByVal name As String, ByVal timeOutSec As Double) As IUIAutomationElement
    Dim start As Variant
    start = timer()
    Dim ret As IUIAutomationElement

    Set ret = GetMainWindowByTitle(form, name)
    Do While ret Is Nothing
        If timer() - start > timeOutSec Then
            Exit Function
        End If
        Set ret = GetMainWindowByTitle(form, name)
        Call SleepMilli(100)
    Loop
    Set WaitMainWindowByTitle = ret
End Function

' ボタンを指定Indexを押下する
Public Sub pushButton(ByRef form As IUIAutomationElement, ByVal ix As Long)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "Button")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)

    Dim ptn As IUIAutomationInvokePattern
    Set ptn = list.GetElement(ix).GetCurrentPattern(UIA_PatternIds.UIA_InvokePatternId)
    Call ptn.Invoke

End Sub

' 指定のClassNameがTextBoxに値を設定する
Public Sub SetText(ByRef form As IUIAutomationElement, ByVal ix As Long, ByVal text As String)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "TextBox")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)

    Dim editValue As IUIAutomationValuePattern
    Set editValue = list.GetElement(ix).GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId)
    Call editValue.SetValue(text)

End Sub

' 指定のAutomationIdでTextBoxに値を設定する
Public Sub SetTextById(ByRef form As IUIAutomationElement, ByVal id As String, ByVal text As String)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_AutomationIdPropertyId, id)

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)

    Dim editValue As IUIAutomationValuePattern
    Set editValue = list.GetElement(0).GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId)
    Call editValue.SetValue(text)

End Sub

' 指定のClassNameがTextBoxの値を取得する
Public Function GetStatusBarItemText(ByRef form As IUIAutomationElement, ByVal ix As Long) As String
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "StatusBarItem")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)

    GetStatusBarItemText = list.GetElement(ix).CurrentName

End Function

備考

・画像認識はUIAutomationの範囲からはずれるために実施していません。

・タブの子要素が取得できません。茜ちゃんから葵ちゃんに切り替えたり、感情を変更することができないことになります。

・UIAutomationで要素を検索して値の取得や操作をしているだけです。
 ただし、ディスクトップを検索する場合、直下の子供だけを検索するようにしないと時間がかかるので注意してください。

・名前を付けて保存時にファイル名入力後にENTERを押下しています。これはロストフォーカス時に入力前の文字にもどってしまう事象の対策です。他のツールにおいても同様の実装をおこなっています。

PowerShell+UIAutomationでアカネチャンカワイイヤッター

PowerShellさえ入っているWin7以降ならOfficeすら不要で自動操作ができます。
また、VBAに対するアドバンテージとしては、.NETの機能が簡単に利用できるようになったことです。
管理者権限がなければps1ファイルが実行できないという勘違いをしていましたが、実際はそんなことはないので、学習コストさえ払えるならPowerShellに移行したほうがよいでしょう。

PowerShellでの実装

https://github.com/mima3/rpa_akanechan/tree/master/powershell(UIAutomation.NET)

kawaii.ps1

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-type -AssemblyName System.Windows.Forms

$source = @"
using System;
using System.Windows.Automation;
public class AutomationHelper
{
    public static AutomationElement RootElement
    {
        get
        {
            return AutomationElement.RootElement;
        }
    }
    public static AutomationElement GetMainWindowByTitle(string title) {
        PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
        return RootElement.FindFirst(TreeScope.Children, cond);
    }

    public static AutomationElement ChildWindowByTitle(AutomationElement parent , string title) {
        try {
            PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
            return parent.FindFirst(TreeScope.Children, cond);
        } catch {
            return null;
        }
    }
    public static AutomationElement WaitChildWindowByTitle(AutomationElement parent, string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = ChildWindowByTitle(parent, title);
            if (ret != null) {
                return ret;
            }
            TimeSpan ts = DateTime.Now - start;
            if (ts.TotalSeconds > timeout) {
               return null;
            }
            System.Threading.Thread.Sleep(100);
        }
    }
}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies("UIAutomationClient", "UIAutomationTypes")

# 5.0以降ならusingで記載した方が楽。
$autoElem = [System.Windows.Automation.AutomationElement]

# ウィンドウ以下で指定の条件に当てはまるコントロールを全て列挙
function findAllElements($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# ウィンドウ以下で指定の条件に当てはまるコントロールを1つ取得
function findFirstElement($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# 要素をValuePatternに変換
function convertValuePattern($elem) {
    return $elem.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
}

# 指定の要素をボタンとみなして押下する
function pushButton($form, $index) {
    $buttonElemes = findAllElements $form $autoElem::ClassNameProperty "Button"
    $invElm = $buttonElemes[$index].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
    $invElm.Invoke()
}

# 指定のAutomationIDのボタンを押下
function pushButtonById($form, $id) {
    $buttonElem = findFirstElement $form $autoElem::AutomationIdProperty $id
    $invElm = $buttonElem.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
    $invElm.Invoke()
}

# 指定の内容をしゃべらせる
function speakText($mainForm, $message) {
    try {
        # テキストの検索
        $textboxElems = findAllElements $mainForm $autoElem::ClassNameProperty "TextBox"
        $messageValuePtn = convertValuePattern $textboxElems[0]
        $messageValuePtn.SetValue($message);

        # 音声保存ボタン押下
        pushButton $mainForm 0

        # 読み上げ中は待機
        $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, "テキストの読み上げは完了しました。")
        do
        {
          Start-Sleep -m 500 
          $elems = $mainForm.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
        }
        while ($elems.Count -eq 0)

        return $True
    } catch {
        Write-Error "ファイルの保存に失敗しました"
        $_
        return $False
    }
}

# しゃべる内容を設定後指定のファイルに保存
function saveText($mainForm , $message, $outPath) {
    try {
        # テキストの検索
        $textboxElems = findAllElements $mainForm $autoElem::ClassNameProperty "TextBox"
        $messageValuePtn = convertValuePattern $textboxElems[0]
        $messageValuePtn.SetValue($message);

        # 音声保存ボタン押下
        pushButton $mainForm 4

        #音声保存ウィンドウが表示される可能性
        $saveWvForm = [AutomationHelper]::WaitChildWindowByTitle($mainForm, "音声保存", 2)
        pushButton $saveWvForm 0

        #名前を付けて保存
        $saveFileForm = [AutomationHelper]::WaitChildWindowByTitle($saveWvForm, "名前を付けて保存", 5)
        if ($saveFileForm -eq $null) {
            return $False;
        }
        $txtFilePathElem = findFirstElement $saveFileForm $autoElem::AutomationIdProperty "1001"
        $txtFilePathValuePtn = convertValuePattern $txtFilePathElem
        $txtFilePathValuePtn.SetValue($outPath);
        [System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
        #エンターでないとコンボボックスが効いて、元に戻る。
        #pushButtonById $saveFileForm "1"

        # ここでファイルの上書きがtxtとwav分でる可能性があるが、ファイル名を一意にすることで回避すること

        # 情報ポップアップがでるまで待機
        $infoWin = [AutomationHelper]::WaitChildWindowByTitle($saveWvForm, "情報", 60)
        if ($infoWin -eq $null) {
            return $False;
        }
        pushButton $infoWin 0
        return $True
    }
    catch {
        Write-Error "ファイルの保存に失敗しました"
        $_
        return $False
    }

}

# メイン処理
$mainForm = [AutomationHelper]::GetMainWindowByTitle("VOICEROID2")
if ($mainForm -eq $null) {
    $mainForm = [AutomationHelper]::GetMainWindowByTitle("VOICEROID2*")
}
if ($mainForm -eq $null) {
    Write-Error "VOICEROID2を起動してください"
    exit 1
}

# しゃべる
$ret = speakText $mainForm 'アカネチャンカワイイヤッタ'
if ($ret -eq $False ) {
    exit
}

# 保存する
$fileName =  Get-Date -Format "yyyyMMddHHmmss.wav"
saveText $mainForm 'アカネチャンカワイイヤッタ' $fileName

備考

・タブにたいする制限はVBAのUIAutomationと同じです。

・PowerShell中にC#のコードを埋め込んでいる理由は「名前を付けて保存」ダイアログを操作するためです。
 下記を参照してください。
 >PowerShellのUIAutomationは複雑怪奇なり

・using等の新しい機能は使わないようにしているのでPowershell2.0あたりでも動くと思います(未検証)

・PowerShellで画像認識をやりたい場合は以下を参照してください。
 C#やPowerShellで画面上の特定の画像の位置をクリックする方法
 https://needtec.sakura.ne.jp/wod07672/?p=9168

WinAppDriverでアカネチャンカワイイヤッター

Seleniumライクな操作でWindowsアプリを操作するためにマイクロソフトが開発したツールです。Seleniumの操作とほぼ同じなので、学習コストは低くなることが期待できます。

その構成は以下のようになります。

RPA画面構成.png

操作プログラムは操作対象のプログラムを直接操作するのでなくWebAppDriver経由で操作をおこないます。
操作プログラムとWebAppDriverの間は下記のようなJSONデータでやりとりが行われています。

image.png

WebAppDriverはダウンロードページ から入手してください。

WinAppDriverUiRecorderについて

XPathを用いてWindowの要素を操作するのですが、そのXPathの検査にはWinAppDriverUiRecorderを使用します。

RPA画面構成.png

C# Codeのタブを選択すると行った操作の内容の実装例が表示されます。
image.png

ただし、基本的にあてにはならないのでXPathの参考程度にするといいでしょう。
またマルチディスプレイで作業している場合、1つめのディスプレイしか認識しないので注意してください。

WinAppDriverを使用した実装

https://github.com/mima3/rpa_akanechan/tree/master/visualstudio/WinAppDriverSemple

NuGetで取得した資材。
 ・Appium.WebDriver v3.0.0.2


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium;
using System.Globalization;

namespace WinAppDriverSemple
{

    class Program
    {
        static WindowsDriver<WindowsElement> desktopSession;
        private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723/";

        // 指定の要素が検索できるまで待機する
        public static WindowsElement WaitElementByAbsoluteXPath(WindowsDriver<WindowsElement> root, string xPath, int nTryCount = 15)
        {
            WindowsElement uiTarget = null;

            while (nTryCount-- > 0)
            {
                try
                {
                    uiTarget = root.FindElementByXPath(xPath);
                }
                catch
                {
                }

                if (uiTarget != null)
                {
                    break;
                }
                else
                {
                    System.Threading.Thread.Sleep(500);
                }
            }

            return uiTarget;
        }

        static void Main(string[] args)
        {
            // DesktopからVOCAROID2を検索
            DesiredCapabilities desktopCapabilities = new DesiredCapabilities();
            desktopCapabilities.SetCapability("app", "Root");
            desktopCapabilities.SetCapability("deviceName", "WindowsPC");
            desktopSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), desktopCapabilities);
            String hwnd;
            WindowsElement appElem;
            appElem = desktopSession.FindElementByName("VOICEROID2");
            hwnd = appElem.GetAttribute("NativeWindowHandle");
            if (hwnd.Equals("0"))
            {
                appElem = desktopSession.FindElementByName("VOICEROID2*");
                hwnd = appElem.GetAttribute("NativeWindowHandle");
            }
            DesiredCapabilities appCapabilities = new DesiredCapabilities();
            hwnd = int.Parse(hwnd).ToString("x");
            appCapabilities.SetCapability("appTopLevelWindow", hwnd);
            WindowsDriver<WindowsElement> appSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), appCapabilities);

            // しゃべらせる
            var txtMsg = appSession.FindElementByXPath("//Edit[@AutomationId=\"TextBox\"]");
            txtMsg.Click();
            // 英語キーボードじゃないと記号が旨く送信できない(Seleniumの仕様っぽい)
            txtMsg.SendKeys(Keys.LeftControl + "a");
            txtMsg.SendKeys(Keys.Delete);
            txtMsg.SendKeys("アカネチャンカワイイヤッタ");

            var btnPlay = appSession.FindElementByXPath("//Button[@ClassName=\"Button\"]/Text[@ClassName=\"TextBlock\"][@Name=\"再生\"]");
            btnPlay.Click();

            // 保存開始
            var statusBar = WaitElementByAbsoluteXPath(appSession, "//StatusBar[@ClassName =\"StatusBar\"]/Text[@ClassName=\"StatusBarItem\"][@Name=\"テキストの読み上げは完了しました。\"]/Text[@ClassName=\"TextBlock\"][@Name=\"テキストの読み上げは完了しました。\"]");
            if (statusBar == null)
            {
                Console.Error.WriteLine("読み上げ失敗");
                return;
            }

            var btnSave = appSession.FindElementByXPath("//Button[@ClassName=\"Button\"]/Text[@ClassName=\"TextBlock\"][@Name=\"音声保存\"]");
            btnSave.Click();

            // 音声保存画面でOK押下
            var btnSaveOk = appSession.FindElementByXPath("//Window[@ClassName =\"Window\"][@Name=\"音声保存\"]/Button[@ClassName=\"Button\"][@Name=\"OK\"]");
            btnSaveOk.Click();

            // 名前を付けて保存
            var txtFileName = appSession.FindElementByXPath("//Window[@ClassName=\"#32770\"][@Name=\"名前を付けて保存\"]/Pane[@ClassName=\"DUIViewWndClassName\"]/ComboBox[@Name=\"ファイル名:\"][@AutomationId=\"FileNameControlHost\"]/Edit[@ClassName=\"Edit\"][@Name=\"ファイル名:\"]");
            String hankakuKey = Convert.ToString(Convert.ToChar(0xE0 + 244, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
            // 英字キーボードだと以下のキーで半角全角切り替えになる
            txtFileName.SendKeys("`"); // 0xFF40

            txtFileName.SendKeys(Keys.LeftControl + "a");
            txtFileName.SendKeys(Keys.Delete);
            //
            txtFileName.SendKeys(System.DateTime.Now.ToString("yyyymMMddhhmmss") + ".wav");
            txtFileName.SendKeys(Keys.Enter);

            //
            var infoOk = WaitElementByAbsoluteXPath(appSession, "//Window[@ClassName=\"#32770\"][@Name=\"情報\"]/Button[@ClassName=\"Button\"][@Name=\"OK\"]");
            infoOk.Click();

        }
    }
}

備考

・Windows10でしか動作しません。

・WinAppDriverで公開されているものはテストコードとUIRecorderのみです。WinAppDriver自体のコードは公開されていません。

・UIAutomation同様、タブの子要素が取得できません。

日本語キーボードの場合、記号が正常に表示されません。
 例:editBox.SendKeys("a/b\c"); // →a/b]c
 https://github.com/Microsoft/WinAppDriver/issues/194

・XPathで要素の指定は容易に行えます。しかしながらパフォーマンスがUIAutomationに比べてかなり落ちます。
 今回はすこしでも早くなることを期待して、ディスクトップのルートからでなく、アプリケーションから検索するようにしています。

・名前を付けて保存をする際のファイル名がどうしても全角になってしまい、そこを「`」を送信することでごまかしています。

Friendlyでアカネチャンカワイイヤッター

操作プログラムが使用しているFrendlyが操作対象の茜ちゃんにDLLインジェクションをします。
それにより、そこでプロセス間通信を行い画面の要素の情報を取得しています。

image.png

この仕組みのため、マイクロソフト製のUIAutomationとWinAppDriverでも、やれないことを平然とやってのけます。そこにしびれるあこがれる~!!!
ただし、操作対象のアプリケーションにテスト用のDLLを差し込んだものをテストや運用で使っていいのかという問題がありますので導入時にはよく検討すべきです。一方、単体テストや、再起動可能な画面の自動操作では非常に強力なライブラリです。

日本の会社が作った仕組みなので、公式サイトのドキュメントをみるのが一番いいでしょう。
また、GitHubにコードが公開されています。

Friendlyでの実装例を教えてくれるTestAssistantツール

TestAssistantというツールが提供されており、画面の要素の調査がおこなえます。
要素を選択してコードのサンプルを作成したり、実際作成したサンプルをツール上で実行できたりと、かなり強力なツールになっています。

RPA画面構成.png

同一アプリに対する操作について

同一アプリに複数のプロセスがFriendlyを使用して操作すると以下のエラーを出力してエラーになります。

エラー内容

型 'Codeer.Friendly.FriendlyOperationException' のハンドルされていない例外が Codeer.Friendly.Windows.dll で発生しました

追加情報:アプリケーションとの通信に失敗しました。

対象アプリケーションが通信不能な状態になったか、

シリアライズ不可能な型のデータを転送しようとした可能性があります。

たとえば、TestAssistantで要素を調べならがら、コードを書いている場合によく遭遇します。
この場合は、操作対象のアプリケーションを起動しなおす必要があります。

ウィルスバスターの検知

設定によってはウィルスバスターによって誤検知されるので注意してください。

image.png

Friendlyによる実装

https://github.com/mima3/rpa_akanechan/tree/master/visualstudio/FriendlySample

Nugetで取得したもの
・Codeer.Friendly
・Codeer.Friendly.Windows
・Codeer.Friendly.Windows.Grasp
・Codeer.Friendly.Windows.NativeStandardControls
・Codeer.TestAssistant.GeneratorToolKit
・RM.Friendly.WPFStandardControls

Program.cs

using Codeer.Friendly;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using RM.Friendly.WPFStandardControls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Codeer.Friendly.Windows.NativeStandardControls;
using Codeer.Friendly.Dynamic;

namespace AkanechanKawaii
{
    class Program
    {
        static void Main(string[] args)
        {
            // プロセスの取得
            Process[] ps = Process.GetProcessesByName("VoiceroidEditor");
            if (ps.Length == 0)
            {
                Console.Error.WriteLine("VOICEROID2を起動してください");
                return;
            }

            // WindowsAppFriendをプロセスから作成する
            // 接続できない旨のエラーの場合、別のプロセスでテスト対象のプロセスを操作している場合がある。
            // TestAssistant使いながら動作できないようなので、注意。
            var app = new WindowsAppFriend(ps[0]);

            var mainWindow = WindowControl.FromZTop(app);

            // 茜ちゃんしゃべる
            WPFTextBox txtMessage = new WPFTextBox(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 2));
            txtMessage.EmulateChangeText("アカネチャンカワイイヤッタ");

            WPFButtonBase btnPlay = new WPFButtonBase(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 3, 0));
            btnPlay.EmulateClick();

            // ステータスバーを監視してしゃべり終わるまでまつ
            String sts;
            do
            {
                System.Threading.Thread.Sleep(500);
                var txtStatusItem = mainWindow.IdentifyFromVisualTreeIndex(0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0).Dynamic(); ;
                sts = txtStatusItem.Text.ToString();
            } while (!sts.Equals("テキストの読み上げは完了しました。"));

            // 保存ボタン押下
            // ダイアログが表示されると引数なしのEmulateClickだと止まるのでAsyncオブジェクトを渡しておく
            var async = new Async();
            WPFButtonBase btnSave = new WPFButtonBase(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 3, 5));
            btnSave.EmulateClick(async);

            // 音声保存ダイアログ操作
            var dlgSaveWav = mainWindow.WaitForNextModal();
            var asyncSaveWin = new Async();
            WPFButtonBase buttonOK = new WPFButtonBase(dlgSaveWav.IdentifyFromLogicalTreeIndex(0, 1, 0));
            buttonOK.EmulateClick(asyncSaveWin);

            // ファイル名指定後の保存
            var asyncSaveFile = new Async();
            var dlgFileSave = dlgSaveWav.WaitForNextModal();
            NativeEdit editFileName = new NativeEdit(dlgFileSave.IdentifyFromZIndex(11, 0, 4, 0, 0));
            editFileName.EmulateChangeText(System.DateTime.Now.ToString("yyyymMMddhhmmss") + ".wav");

            NativeButton btnSaveOk = new NativeButton(dlgFileSave.IdentifyFromDialogId(1));
            btnSaveOk.EmulateClick(asyncSaveFile);

            // 情報ダイアログが表示されるまで待機してOKを押下
            var dlgInfo = WindowControl.WaitForIdentifyFromWindowText(app, "情報");
            NativeButton btn = new NativeButton(dlgInfo.IdentifyFromWindowText("OK"));
            btn.EmulateClick();

            //非同期で実行した保存ボタン押下の処理が完全に終了するのを待つ
            asyncSaveFile.WaitForCompletion();
            asyncSaveWin.WaitForCompletion();
            async.WaitForCompletion();

            // 葵ちゃんに切り替えてしゃべる
            // UIAutomationだと葵ちゃん切り替えが行えない。
            WPFListView ListView = new WPFListView(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 3, 0, 1, 0, 2));
            ListView.EmulateChangeSelectedIndex(1);
            txtMessage.EmulateChangeText("オネエチャンカワイイヤッタ");
            btnPlay.EmulateClick();
            ListView.EmulateChangeSelectedIndex(0);

        }
    }
}

備考

・基本的にIdentifyFromLogicalTreeIdxで取得している要素はTestAssistantで取得しています。

・引数なしのEmulateClickで制御がおわるまでかえってこないボタンについてはAsyncをわたして、最後でWaitForCompletion()を実行して終了を待っています。

・UIAutomationでもWinAppDriverでも取れないタブの中身を取得できるため、葵ちゃんに切り替えてしゃべってもらっています。

PyAutoGUIでアカネチャンカワイイヤッター

Pythonで自動操作を行えます。
いままでのツールやライブラリと違い、PyAutoGuiはMacやUnixでも動作するので複数のOSで同じ操作をする場合に有効になると想定されます。

PyAutoGUIによる実装

https://github.com/mima3/rpa_akanechan/tree/master/PyAutoGui

import time
import pyautogui
import pyperclip
import datetime

# クリップボードを経由する場合
# http://sagantaf.hatenablog.com/entry/2017/10/18/231750
def copipe(string):
    pyperclip.copy(string)
    pyautogui.hotkey('ctrl', 'v')

# 指定の画像が表示されるまで待つ
def waitPicture(f):
    print(f)
    ret = None
    while ret is None:
        ret = pyautogui.locateOnScreen(f, grayscale=False, confidence=.8)
        print (ret)
        if ret is not None:
            return ret
        time.sleep(1)

mainButtons = pyautogui.locateOnScreen('mainbutton.bmp', grayscale=False, confidence=.8)
if mainButtons is None:
    print (u'VOICEROID2の再生ボタンが見つかりません')
    exit()

# テキスト選択
pyautogui.click(mainButtons[0] + 30, mainButtons[1] )

# テキストのクリア
pyautogui.hotkey('ctrl', 'a')
pyautogui.press('del')

# テキストの設定
copipe(u'アカネチャンカワイイヤッタ')

# 再生
pyautogui.click(mainButtons[0], mainButtons[1] + mainButtons[3] / 2 )

# 読み上げまで待機
time.sleep(0.5)
waitPicture('status.bmp')

# 音声の保存
pyautogui.click(mainButtons[0] +  mainButtons[2], mainButtons[1] + mainButtons[3] / 2 )

wavSave = waitPicture('wavSave.bmp')
pyautogui.click(wavSave[0] + 5, wavSave[1] + wavSave[3] / 2 )

# ファイルの保存
fileSave = waitPicture('fileSave.bmp')
pyautogui.click(fileSave[0], fileSave[1])
copipe(datetime.datetime.now().strftime("%Y%m%d%H%M%S.wav"))
pyautogui.press('enter')

# 情報ダイアログ
info = waitPicture('info.bmp')
pyautogui.click(info[0] + info[2], info[1] + info[3])

# 葵ちゃんしゃべる
time.sleep(0.5)
aoi = pyautogui.locateOnScreen('aoi.bmp', grayscale=False, confidence=.8)
pyautogui.click(aoi[0], aoi[1])

# テキスト設定
pyautogui.click(mainButtons[0] + 30, mainButtons[1] )
pyautogui.hotkey('ctrl', 'a')
pyautogui.press('del')
copipe(u'オネエチャンカワイイヤッタ')
pyautogui.click(mainButtons[0], mainButtons[1] + mainButtons[3] / 2 )

# 茜ちゃんに戻す
akane = pyautogui.locateOnScreen('akane.bmp', grayscale=False, confidence=.8)
pyautogui.click(akane[0], akane[1])

解説

・キーボード操作処理で日本語入力に対応していないため、pyperclipを使用してクリップボード経由で文字を設定しています。クリップボードの内容が重大な場合のシナリオについて留意してください。

・locateOnScreenで画像認識をしており、その精度はconfidenceパラメータにより制御しています。画像が認識しずらい場合、この値を下げてみてください。

・あくまで画像認識なので、実行前に対象のコントロールが隠れていたりしないことを確認してから実行してください。他にも解像度の変更やウィンドウサイズや位置の違いで簡単に動かなくなります。

・マルチディスプレイの場合、1つめのディスプレイに操作対象のウィンドウがないと動作しないです。

UWSCでアカネチャンカワイイヤッター

image.png

10年以上前から存在するツールです。
レコード機能が強力でAutoHotKeyやAutoItでは認識しないような画面の要素を検出できます。
また、画像認識などの機能そろっており、おそらく、もっとも使いやすいツールの一つでした。

残念なことに、2018年ころよりサイトが閉鎖されてしまい、今後使用することはできないでしょう。

UWSCによる実装

https://github.com/mima3/rpa_akanechan/tree/master/UWSC

id = GETID("VOICEROID2", "HwndWrapper[VoiceroidEdito", -1)
If id=NULL Then
    id = GETID("VOICEROID2*", "HwndWrapper[VoiceroidEdito", -1)
EndIf

// 再生を行う
SLEEP(1)
SENDSTR(id, "アカネチャンカワイイヤッタ", 1, True, True)

CLKITEM(id, "", CLK_BTN , True, 0)

// ステータスバーをみて再生完了を待つ
sts = ""
While sts <> "テキストの読み上げは完了しました。"
    Sleep(0.1)
    GETITEM(id, ITM_STATUSBAR)
    sts = ALL_ITEM_LIST[6]
Wend

// 保存ボタン
CLKITEM(id, "", CLK_BTN , True, 5)

// 音声保存画面
idSaveWv = GETID("音声保存", "HwndWrapper[VoiceroidEdito", -1)
CLKITEM(idSaveWv, "OK", CLK_BTN , True, 0)

// 名前を付けて保存画面
idFileSave = GETID("名前を付けて保存", "#32770", -1)
SENDSTR(idFileSave, PARAM_STR[0], 0, True, True)
KBD(VK_ENTER)
//CLKITEM(idFileSave, "保存", CLK_ACC)

// OK押下
idInfo = GETID("情報", "#32770", -1)
CLKITEM(idInfo, "OK", CLK_BTN)

// 葵ちゃんに切り替え
SLEEP(1)
ret = CHKIMG("aoi.bmp")
BTN(LEFT, CLICK, G_IMG_X, G_IMG_Y)
SLEEP(0.5)
SENDSTR(id, "オネエチャンカワイイヤッタ", 1, True, True)
CLKITEM(id, "", CLK_BTN , True, 0)

SLEEP(0.5)
CHKIMG("akane.bmp")
BTN(LEFT, CLICK, G_IMG_X, G_IMG_Y)

備考

・Tabの要素は取得できませんが、画像認識により代替できます。

・レコーダ―では記録されない要素がありますが、スクリプトを書くと要素を取得できます。

・CHKIMGはBMP形式のみが対象です。

・サイト閉鎖の問題もあり今後利用するのは厳しいでしょう。

sikulixでアカネチャンカワイイヤッター

画像認識に特化したツールです。
Ruby,Python,JavaScriptで記載されたスクリプトをJavaで解析して動作します。
基本がJavaなのでMacやLinuxでも動作します。ただし1.1.4よりJavaの64ビットが要求されています。

下記がIDEになります。
image.png

UWSC,pyAutoGuiともに画像認識は行えますが、使用する画像はあらかじめ用意する必要がありました。しかしsikulixでは、必要な際にディスクトップ全体から切り取って使用できます。

また画像のどこをクリックするかという指定もGUI上で行えます。
image.png

操作記録の機能こそないものの、直観的に作成できる貴重なツールです。

sikulixの実装

https://github.com/mima3/rpa_akanechan/tree/master/sikulix/sikulix.sikuli

import datetime
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
# マルチディスプレイの場合は、1のディスプレイじゃないと動かない模様

# 茜ちゃんしゃべる
click(Pattern("1558790034069.png").targetOffset(-174,-16))
type('a', Key.CTRL)
type(Key.DELETE)
paste(u"アカネチャンカワイイヤッタ")

click(Pattern("1558790034069.png").targetOffset(-162,14))
wait(2)

wait("1558790542060.png")
click(Pattern("1558790034069.png").targetOffset(121,16))

# 音声保存画面
click(Pattern("1558790796902.png").targetOffset(-39,2))

# 名前を付けて保存
click(Pattern("1558790905402.png").targetOffset(-45,-35))
type('a', Key.CTRL)
type(Key.DELETE)
paste(datetime.datetime.now().strftime("%Y%m%d%H%M%S.wav"))
type(Key.ENTER)
click(Pattern("1558790905402.png").targetOffset(-41,38))

# 情報のOKボタンクリック
click(Pattern("1558791076872.png").targetOffset(87,50))

# 消えるまでまつ
# 時間が読めない場合はregionとってexistsで消えるまで見る
wait(1)

# 葵ちゃん
click("1558791449664.png")
click(Pattern("1558790034069.png").targetOffset(-174,-16))
type('a', Key.CTRL)
type(Key.DELETE)
paste(u"オネエチャンカワイイヤッタ")

click(Pattern("1558790034069.png").targetOffset(-162,14))
wait(2)

click("1558791495218.png")

備考

・画像ファイル名になっている箇所はIDE上は画像が表示されます。またtargetOffsetについては赤い十字で表現されます。

・今回ためしたsikuliのバージョンは1.1.4で、使用しているPythonはJythonで2.7になります。また、Javaに組み込んでいるため、通常のPythonより使い勝手がわるい可能性があります。
 先に紹介したpyAutoGuiとの使い分けとしてはPythonでどの程度やらせるかが、一つの基準になるでしょう。

・マルチディスプレイの場合、1つめのディスプレイにないと動作しません。
補足:回避できそうですが、少なくとも↑のコードと当方の環境では動かなかったです。
https://sikulix-2014.readthedocs.io/en/latest/screen.html#multi-monitor-environments

・画像認識を使用しているのでウィンドウが隠れたりすると正常に動作しません。

・下記のコードは日本語を表示するためのものです。

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

・その他詳細はココにまとめました

Rocket Mouose Proでアカネチャンカワイイヤッター

古くからあるツールで、1万円前後で入手できます。今回は14日使える試用版で作成しました。
いままで紹介したツールと違い、スクリプトなどは記載しません。

以下のように処理をGUIで列挙していく形になります。
image.png

このため、単純な処理は容易に作成できますが、複雑な分岐がある場合は対応できません。

Rocket Mouose Proによる実装と解説

https://github.com/mima3/rpa_akanechan/tree/master/rmpro

Rocket Mouseでは「最初の処理」、「繰り返しの処理」、「最後の処理」の3つありますが、今回は「最初の処理」と「最後の処理」のみ使用します。
また、最後の処理につては完了メッセージを表示するだけになります。

最初の処理1行目
image.png

image.png

テキストと再生ボタンの画像認識を行い、認識できた場合はテキストをクリックします。
認識できなければ「最期の処理の1行目」にジャンプし処理を終了します。

最初の処理2~4行目
image.png

この処理はキーボード操作でテキストをクリアしたのち、入力したい文字を入れています。

最初の処理5行目
image.png
image.png
これは最初の処理1行目とほぼ同じで押下している箇所が違うだけです。
今回は再生ボタンをおしています。

最初の処理6行目
image.png
image.png
この処理は「テキストを読み上げました」と表示されるまで無限ループをしています。

最初の処理7行目
ショートカットキーで音声保存をしています。

最初の処理8行目
image.png
「音声保存」というタイトルのウィンドウが表示されるまで待機します。

最初の処理9行目
image.png
image.png

OKボタンが表示されたらクリックする、されなければ終了としています。

最初の処理10行目
最初の処理8行目と同様に「名前を付けて保存」画面がでるまで待ちます。

最初の処理11行目
image.png

時刻を取得して書式を整えたあと、変数$now$に格納します。

最初の処理12~13
ファイル名に格納した変数$now$を設定後Enterを押します。
これにより名前を付けて保存ダイアログが終了します。

最初の処理14~15
「情報」というウィンドウが表示されるまでまち、表示されたらEnterを押して終了します。

最初の処理16~
あとは今まで出た内容と同じように、葵ちゃんを画像認識で選択後、しゃべらせています。

備考

・マルチディスプレイの場合、1つめのディスプレイにないと動作しません。

・また、条件分岐の制約上エラー処理に弱いです。たとえば、画像が見つからない場合に無限ループになったりします。

UiPathでアカネチャンカワイイヤッター

2018年のforresterの調査でRPAのリーダーと言わしめた製品になっています。
https://samfundsdesign.dk/siteassets/media/downloads/pdf/the_forrester_wave_rpa_2018_uipath_rpa_leader.pdf

今回の走者のなかで、唯一、数十、数百万のツールですが、実はいくつかの条件をみたすことで UiPath Community Editionを使用することができます。
https://www.uipath.com/ja/freetrial-or-community

今まで見てきたオブジェクト識別機能、画像による識別機能、操作記録は当然そろっており、フローチャートによるわかりやすいインターフェイスを提供しています。これは.NETのWFを使用しており、流れ図の部品にあたるアクティビティをカスタムアクティビティとして作成できます。
https://qiita.com/UmegayaRollcake/items/c9ff9a01b101ba9193fc

また、さらには国内製品唯一のアドバンテージだった日本語のローカライズも対応されています。
さすが、リーダーを自称し、他称されるだけの機能です。

UiStudioの起動

ライセンス認証をしたあとのUiPath.Studioの場所は以下になりました。
 C:\Users\名前\AppData\Local\UiPath\

プロジェクト

今回作成したプロジェクトのファイルは下記の通りです。
https://github.com/mima3/rpa_akanechan/tree/master/UiPathSample

image.png

シーケンスの中に「しゃべる+保存」アクティビティと「葵ちゃんに切り替え」アクティビティがあります。

しゃべる~保存アクティビティ

各UIの操作をひとつづ追加していくこともできますし、画面の操作をレコードしてあとで細かいところを修正することもできます。

変数の設定

シーケンス内で有効、アクティビティ内で有効といったスコープを極めて変数を定義できます。
image.png

設定値ではVB.NETの式が使用でき、今回は現在時刻のファイル名を構築してます。

処理の流れ

・テキストを入力して再生~完了まで
image.png

・音声保存
image.png

あかねちゃんに切り替えアクティビティ

UIPathでもタブの子要素になっている要素を検知することはできないので画像識別を利用します。
image.png

完走した感想

完走した感想ですが、色々と昔のツールが脱落して新規ツールが増えていきました。
まず、昔ながらのツールであるAutoHotKey,AutoIt,RocketMoude、UWSCのうち、UIAutomationでとれる内容を解析することができたのはUWSCだけでした。そしてそのUWSCもすでに命数が尽きています。
(もちろんCOMをサポートしているツールは頑張れば対応できますが、それをやるなら別の手段をとると思います。)
もはやWin32の時代でないと思うと諸行無常を感じます。

また、テスト工程で使うなら、Friendlyが魅力的です。テスト対象をかえずに、テスト用に魔改造が色々できそうです。

複数OS対応ならば、画像の範囲を工夫してSikulixか、pyAutoGuiを検討することになると思います。ただし、画像認識なのでいずれにしても、確実に動作させるのは難しいでしょう。

IT活用しない縛りのレギュレーションの会社ではVBAかPowerShellでUIAutomationをたたくしかないです。

UIPathについては他の高価格帯と比較しないと意味がなさそうなので、ここでは言及しないでおきますが、たぶん触っておいて損はないと思います。

…とここまでやっておいて書くのもアレですが、VBAやPowerShellで動かすのをRPAといっていいのか、そもそもRPAって一体全体なんですか、どなたに伺えばいいんですかという話になりそうなので、そろそろ終わりとうございます。

(補足)
RTAごっこするんじゃなくて、茜ちゃんを、ちゃんと動かすなら先駆者兄貴のライブラリを使うといいっすよ。
https://github.com/mikoto2000/TTSController

先生助けてっ!、10年前までStackOverflowで質問されていたVbScriptが息していないの!!

〇〇言語なんて今は使われていないよ!
今は〇〇言語が流行っているよ!

…とかいうブラジリアン柔術的なマウンティング・コミュニケーションがよく行われますが、実際のところ、その数値的な背景を聞くことは少ないです。

今回は人気サイトのStackOverflowで、特定の言語が、特定の期間で、どのくらい質問が発生したかをしらべてみて、いくつかのプログラミング言語のトレンドを見てみようと思います。

StackOverflowのAPIを使う。

StackOverflowにはAPIが公開されています。

https://api.stackexchange.com/docs

登録方法やアクセストークンの取りかたは、以下のページを参考にしました。

StackOverflowのAPIを試してみる
https://qiita.com/yoichiro6642/items/a951178fbd2d82256d68

アクセストークンがなくても300回はリクエストできますが、すぐ切れるので登録しておいたほうがいいと思います。

特定のタグの質問をDBに取り込む

StackOverflowのAPIは1日1万回程度の上限があるので、一旦、DBに格納します。
以下のプログラムは特定のタグを作成日順にソートして取得するものになります。

stackoverflow_create.py

import http.client
import json
import sys
import gzip
import time

import stackoverflow_setting

args = sys.argv
if len(args) != 5:
    print(u'python stackoverflow.py start_page tag access_token application_key')
    exit(1)
start_page = args[1]
tag = args[2]
access_token = args[3]
appkey = args[4]

stackoverflow_setting.connectDb(tag)
from stackoverflow_db import *
stackoverflow_setting.db.create_tables([Questions, TagRelations])

item_ids = []
error_cnt = 0

def query(conn, tag, page):
    api = "/2.2/questions?access_token=" + access_token + "&tagged=" + tag + "&sort=creation&order=asc&page=" + str(page) + "&pagesize=100&site=stackoverflow&key=" + appkey
    print(api)
    global error_cnt
    try:
        conn.request("GET", api)
        res = conn.getresponse()
        if (res.status != 200):
            raise Exception(res.status, res.reason)
    except Exception:
        if error_cnt == 2:
            raise Exception
        error_cnt = error_cnt + 1
        time.sleep(5)
        conn.close()
        conn = http.client.HTTPSConnection("api.stackexchange.com", 443)
        return query(conn, tag, page)
    data = res.read()
    data = gzip.decompress(data)
    jsondata = json.loads(data)
    error_cnt = 0
    return jsondata['items']

def convertToDb(items):
    result_items = []
    tags = []
    for i in items:
        if i['question_id'] in item_ids:
            continue
        item_ids.append(i['question_id'])
        r = {
            'question_id': i['question_id'],
            'view_count': i['view_count'],
            'answer_count': i['answer_count'],
            'score': i['score'],
            'title': i['title'],
            'link': i['link'],
            'last_activity_date': i['last_activity_date'],
            'creation_date': i['creation_date']
        }
        result_items.append(r)
        for t in i['tags']:
            tags.append({'name': t, 'question_id': i['question_id']})
    return result_items, tags

conn = http.client.HTTPSConnection("api.stackexchange.com", 443)
page = int(start_page)
while True:
    questions = query(conn, tag, page)
    questions, tags = convertToDb(questions)
    if (len(questions) == 0):
        break
    with stackoverflow_setting.db.transaction():
        Questions.insert_many(questions).execute()
        TagRelations.insert_many(tags).execute()
    page = page + 1

conn.close()
stackoverflow_setting.db.close()

stackoverflow_setting.py

from peewee import *

db = None

def connectDb(tag):
    global db
    db = SqliteDatabase('stackoverflow_' + tag + '.sqlite3')

stackoverflow_db.py

from peewee import *
import dateutil.parser
from dateutil.relativedelta import relativedelta
from datetime import datetime, date, timedelta
import time
import stackoverflow_setting

class Questions(Model):
    question_id = CharField(primary_key=True)
    view_count = IntegerField(null=True)
    answer_count = IntegerField(null=True)
    score = IntegerField(null=True)
    link = CharField(null=True)
    title = CharField(null=True)
    last_activity_date = DateTimeField(null=True)
    creation_date = DateTimeField(null=True, index=True)

    class Meta:
        database = stackoverflow_setting.db

class TagRelations(Model):
    name = CharField()
    question_id = CharField()

    class Meta:
        database = stackoverflow_setting.db
        primary_key = CompositeKey('name', 'question_id')

def getMonthHistogram(start_date, end_date, interval):
    start_date = dateutil.parser.parse(start_date)
    end_date = dateutil.parser.parse(end_date)
    ret = []
    tmp_from_date = start_date
    tmp_to_date = tmp_from_date + relativedelta(months=interval)
    while tmp_from_date <= end_date:
        data = (
            tmp_from_date.strftime('%Y-%m'),
            Questions.select().where(
                (Questions.creation_date >= tmp_from_date.timestamp()) &
                (Questions.creation_date < tmp_to_date.timestamp())
            ).count()
        )
        ret.append(data)
        tmp_from_date = tmp_from_date + relativedelta(months=interval)
        tmp_to_date = tmp_to_date + relativedelta(months=interval)
    return ret

使い方は以下のようになります。

# 例 1ページ目からvbaのタグが付いている質問をとる
python stackoverflow_create.py 1 vba アクセストークン クライアントアプリのキー

# 例 2088ページ目からC#のタグが付いている質問をとる
python stackoverflow_create.py 2088 c# アクセストークン クライアントアプリのキー

pythonとかjavascriptとかを取得すると当然の権利のように1万リクエストを超えるので、2日以上かけて取得する必要があります。

特定期間の質問数を検索する。

作成したDBから特定期間の質問数がいくつあったかを調べるスクリプトは以下のようになります。

stackoverflow_hist.py

import sys
import codecs
import stackoverflow_setting
import matplotlib.pyplot as plt

args = sys.argv
if len(args) != 5:
    print(u'python stackoverflowhist.py tag 2008-1-1 2019-6-1 6')
    exit(1)
tag = args[1]

stackoverflow_setting.connectDb(tag)

from stackoverflow_db import *
results = getMonthHistogram(args[2], args[3], int(args[4]))
x = []
y = []
label = []
i = 1
for ret in results:
    print("%s\t%s" % (ret[0], ret[1]))
    if i == 1 or i == len(results):
        label.append(ret[0])
    else:
        label.append('')
    y.append(ret[1])
    x.append(i)
    i = i + 1
plt.bar(x, y)
plt.xticks(x, label)
plt.show()
stackoverflow_setting.db.close()

使い方の例は以下のようになります。

# 2008/6/1~2019-5-31まで12か月単位でjavascriptの検索数を調べる
python stackoverflow_hist.py javascript 2008-6-1 2019-5-31 12

色々集計してみる

2008年6月~2019年5月に作成されたデータを基に色々集計してみます。
対象はjavascript,python,vba,vbscript,wsh,powershellでやってみます。
image.png

javascript python vba vbscript wsh powershell c# java
Jun-08 7951 5859 631 355 51 389 22474 11453
Jun-09 27921 18803 1689 633 68 876 58992 36601
Jun-10 60941 33862 2575 948 78 1540 93063 73095
Jun-11 110921 49546 4334 1348 116 2752 123462 118936
Jun-12 157764 77398 8218 2104 136 4444 150648 162733
Jun-13 230173 111358 14654 2600 129 6463 175102 215847
Jun-14 231230 119682 17026 2408 113 7774 150981 210787
Jun-15 271217 149880 22415 2372 120 9647 157980 215667
Jun-16 259393 170602 24120 1873 99 10930 142999 185500
Jun-17 233279 200035 24897 1520 57 11417 120097 161678
Jun-18 226486 242475 23581 1105 37 12488 120278 160181

総数だけでみるとjavascript,python,java,C#とvba,wsh,vbscrip,powershellの差が大きいです。
数値の遷移としてはjavascript,python,javaは同様の動きをしています。

次に「(前年-当年)/前年 × 100」で成長率を表示してみます。

image.png

javascript python vba vbscript wsh powershell c# java
Jun-09 251.16 220.93 167.67 78.31 33.33 125.19 162.49 219.58
Jun-10 118.26 80.09 52.46 49.76 14.71 75.80 57.76 99.71
Jun-11 82.01 46.32 68.31 42.19 48.72 78.70 32.66 62.71
Jun-12 42.23 56.21 89.62 56.08 17.24 61.48 22.02 36.82
Jun-13 45.90 43.88 78.32 23.57 -5.15 45.43 16.23 32.64
Jun-14 0.46 7.47 16.19 -7.38 -12.40 20.28 -13.78 -2.34
Jun-15 17.29 25.23 31.65 -1.50 6.19 24.09 4.64 2.32
Jun-16 -4.36 13.83 7.61 -21.04 -17.50 13.30 -9.48 -13.99
Jun-17 -10.07 17.25 3.22 -18.85 -42.42 4.46 -16.02 -12.84
Jun-18 -2.91 21.22 -5.29 -27.30 -35.09 9.38 0.15 -0.93

成長率がプラスをキープしているのはpythonとpowershellです。

意外なことにjavascriptは3年連続マイナス成長。とはいえ元の数が大きいのでまだまだ健在でしょう。

vbaは直近1年がマイナスなだけで、総数は少ないものの意外と元気です。

wshとvbscriptは10%以上のマイナス成長を続けています。
image.png
10年前から人気がないとか言ってはいけない
今やるならPowerShellをやったほうがいいでしょう。

javascript詳細

期間 質問数
2008-06 7951
2009-06 27921
2010-06 60941
2011-06 110921
2012-06 157764
2013-06 230173
2014-06 231230
2015-06 271217
2016-06 259393
2017-06 233279
2018-06 226486

image.png

2015年~2016年をピークに頭打ちですが、いまでも一年で20万件は検索されています。
前半部はjavascriptの人気があがったというより、StackOverflowの人気が上がったということだと思います。

なお、同時に着けられたタグのトップ30は以下のようになります。

使用SQL

select name, count(name) as cnt from tagrelations group by name order by cnt desc
タグ
jquery 530487
html 334424
css 153873
angularjs 117362
php 115413
node.js 101428
ajax 91324
reactjs 63514
json 61099
arrays 54661
html5 53401
asp.net 32122
regex 29810
angular 28283
twitter-bootstrap 24911
c# 24187
forms 22946
d3.js 22164
google-chrome 22144
typescript 22071
dom 20409
google-maps 18990
java 18280
express 17844
canvas 17640
ecmascript-6 16592
function 15535
object 15535
vue.js 15321
android 14983

python詳細

期間 質問数
2008-06 5859
2009-06 18803
2010-06 33862
2011-06 49546
2012-06 77398
2013-06 111358
2014-06 119682
2015-06 149880
2016-06 170602
2017-06 200035
2018-06 242475

image.png

質問数が右肩上がりです。
2018~2019でついにJavaScript越えを果たしました。

同時に着けられたタグのトップ30は以下のようになります。

タグ
django 101189
python-3.x 100255
pandas 92483
python-2.7 58871
numpy 54335
list 37161
matplotlib 33047
dictionary 26463
dataframe 25322
regex 24182
flask 22056
tensorflow 21907
tkinter 21646
string 19524
csv 19327
arrays 18897
json 17186
selenium 14538
html 13503
beautifulsoup 13329
opencv 12718
mysql 12353
scipy 12109
machine-learning 11968
google-app-engine 11727
scikit-learn 11333
linux 11237
sqlalchemy 11041
web-scraping 10943
multithreading 10762

vba詳細

期間 質問数
2008-06 631
2009-06 1689
2010-06 2575
2011-06 4334
2012-06 8218
2013-06 14654
2014-06 17026
2015-06 22415
2016-06 24120
2017-06 24897
2018-06 23581

image.png

javascriptの10分の1程度の質問数であるのは想定通りとして、質問数の年毎の遷移が余り変わっていないということです。
つまり、まだだ、まだVBAはたおれんよ!

同時に着けられたタグのトップ30は以下のようになります。

タグ
excel 109286
excel-vba 80136
ms-access 9588
access-vba 5445
ms-word 4343
outlook 3674
excel-formula 3626
sql 3424
arrays 2869
word-vba 2845
excel-2010 2595
outlook-vba 2316
loops 2282
userform 1797
powerpoint 1620
excel-2007 1482
html 1449
range 1307
macros 1256
ms-access-2010 1233
web-scraping 1229
powerpoint-vba 1164
vbscript 1115
email 1104
c# 1092
if-statement 1072
pivot-table 1043
sql-server 1022
date 1017
combobox 984

vbscript詳細

期間
2008-06 355
2009-06 633
2010-06 948
2011-06 1348
2012-06 2104
2013-06 2600
2014-06 2408
2015-06 2372
2016-06 1873
2017-06 1520
2018-06 1105

image.png

いやいや・・・WSHでタグ付けされている可能性もあるですし・・・

タグ
asp-classic 2161
excel 1368
vba 1134
batch-file 1120
windows 910
javascript 656
excel-vba 608
qtp 534
html 532
c# 434
xml 421
wsh 419
hta 417
sql 403
powershell 391
regex 379
hp-uft 346
cmd 344
wmi 290
scripting 289
vb.net 282
sql-server 280
arrays 267
internet-explorer 229
outlook 229
asp.net 224
csv 211
com 203
ms-access 196
automation 179

wsh詳細

期間
2008-06 51
2009-06 68
2010-06 78
2011-06 116
2012-06 136
2013-06 129
2014-06 113
2015-06 120
2016-06 99
2017-06 57
2018-06 37

image.png

2008年より下回っているじゃないかたまげたなぁ。

タグ
vbscript 419
javascript 165
jscript 161
windows 137
batch-file 64
php 45
vba 45
powershell 44
cmd 43
excel 40
scripting 38
c# 36
windows-scripting 35
com 34
shell 31
hta 28
wso2 27
activex 24
excel-vba 21
internet-explorer 20
command-line 18
registry 18
html 16
python 16
c++ 14
sendkeys 14
scheduled-tasks 13
wmi 12
activexobject 11
asp-classic 11

powershell詳細

期間
2008-06 389
2009-06 876
2010-06 1540
2011-06 2752
2012-06 4444
2013-06 6463
2014-06 7774
2015-06 9647
2016-06 10930
2017-06 11417
2018-06 12488

image.png

総数的にはVBAには届かないものの、右肩上がりなので成長性はあると思います。vbscriptなんていらんかったんや。。。

タグ
windows 4389
c# 3142
powershell-v2.0 3142
azure 2857
active-directory 2219
csv 2212
powershell-v3.0 2115
batch-file 1767
regex 1523
.net 1494
xml 1357
arrays 1296
scripting 1198
sql-server 1196
cmd 1150
powershell-v4.0 1085
excel 967
sharepoint 831
exchange-server 769
python 751
powershell-remoting 741
string 693
wmi 667
json 664
sql 653
iis 595
powershell-v5.0 534
tfs 530
variables 516
azure-powershell 515

c#詳細

期間 質問数
Jun-08 22474
Jun-09 58992
Jun-10 93063
Jun-11 123462
Jun-12 150648
Jun-13 175102
Jun-14 150981
Jun-15 157980
Jun-16 142999
Jun-17 120097
Jun-18 120278

image.png

傾向としてはjavascriptと同じような動きをしています。
あと、他のタグとの関連性にasp.net-mvcが上位に食い込んできたのは意外でした。

タグ
.net 175846
asp.net 159596
wpf 90093
asp.net-mvc 70512
winforms 68023
linq 55997
entity-framework 46230
xaml 32100
sql-server 30717
sql 28984
visual-studio 28121
xml 25926
javascript 24200
wcf 22112
unity3d 21815
multithreading 21341
json 19774
jquery 16357
regex 15650
asp.net-mvc-4 15609
asp.net-web-api 14805
asp.net-core 13992
windows 13887
generics 13546
visual-studio-2010 13492
string 13428
arrays 13423
xamarin 13396
mvvm 13338
datagridview 12924

java詳細

期間 質問数
Jun-08 11453
Jun-09 36601
Jun-10 73095
Jun-11 118936
Jun-12 162733
Jun-13 215847
Jun-14 210787
Jun-15 215667
Jun-16 185500
Jun-17 161678
Jun-18 160181

image.png

このグラフの形はコピペミスかとビビる。C#と同じ傾向っすね。
ただ、他のタグとの関連としてandroidが圧倒的な一位ということは他の言語でandroid開発が可能になってきた後どうなるかは気になります。

タグ
android 232060
spring 91000
swing 71641
eclipse 54432
hibernate 47824
arrays 41285
maven 35411
multithreading 33980
xml 31989
json 31063
spring-boot 27971
spring-mvc 27854
string 27653
jsp 26005
mysql 25968
jpa 23074
servlets 21815
arraylist 21692
tomcat 20904
regex 20575
jdbc 19291
javafx 18894
javascript 18303
java-ee 16452
selenium 16107
rest 15977
sql 15005
generics 14791
web-services 13930
junit 13920

まとめ

StackOverflowの質問数の遷移で各言語のトレンドを見てみました。
感覚的にはPythonやVbScriptなどは数値に違和感はありませんでしたが、Vbaの質問頻度が下がっていないのは意外でした。

なお、JavaとかRubyとか人気のありそうな言語をいくつも、同じように調べるとAPIの制限がキツイので複数の人間で協力するか、長い日数かけてやる必要があると思います。

Sikulix1.1.4を使って画面の自動操作をする

概要

Sikuli(シークリー)は画像認識を用いて、Windows/Mac/Linuxの画面を操作することが可能です。
http://sikulix.com/
なお、語源としてはインディアンの文化の奉納物で未知のものを見て理解する力という意味とのことです。

今回は1.1.4を使って検証しますが、最新バージョンは現時点で色々とドキュメントと実装の食い違いがあったり、最新のコードをビルドしないと動かなかったりするので、画像認識で自動操作したいだけの人は1.1.3を使用した方がいいと思います。
また、今回は「アカネチャンカワイイヤッタ-」よりちょっと詳しく記載してます。

現在のリリース状況

2019/10時点のリリースの状況は以下の通りになっています。
image.png

https://launchpad.net/sikuli/+series

本記事は1.1.4時点の記事なので注意してください。
※以下を読んで軽く使った感じだと、1.1.4と2.0で大きな違いは感じませんでしたが・・・
https://sikulix-2014.readthedocs.io/en/latest/news.html

環境構築

2019/5時点の最新リリースの導入方法

現時点で最新の1.1.4のWindows10への導入方法について記載します。

(1)Java8以上のJDK(64bit) を入手します
以下のいずれかから入手します。今回の実験では「Pleiades All in One」に入っていたJDK8を使用しました。

Oracle/Sun
https://www.oracle.com/technetwork/java/javase/downloads/index.html

OpenJDK
https://openjdk.java.net/

Pleiades All in One
http://mergedoc.osdn.jp/

(2)下記のページより必要なJARを取得し同一フォルダに配置します
https://raiman.github.io/SikuliX1/downloads.html

文中の以下のリンクからJARを取得します。
・Download the ready to use sikulixapi.jar
・The Jython interpreter 2.7.1 for python scripting (the default)
・The JRuby interpreter 9.x for ruby scripting
・Download the ready to use sikulix.jar (SikuliX IDE)

※sikulix.jarを起動後、jruby,jythonのjarは以下のフォルダに移動します。
・C:\Users\ユーザ\AppData\Roaming\Sikulix\Extensions

(3)IDEの起動

1.1.4時点

SET JAVA_HOME=C:\pleiades201904\java\8
"%JAVA_HOME%\bin\java.exe"  -jar sikulix.jar

2.0時点

SET JAVA_HOME=C:\pleiades201904\java\8
"%JAVA_HOME%\bin\java.exe"  -jar sikulix-2.0.0.jar

最新ソースの使用方法

(1)下記のGitHubから最新をダウンロードします。
https://github.com/RaiMan/SikuliX1

(2)「Pleiades All in One」にてMavenのプロジェクトとしてインポートします。

(3)JDK12でビルド(実行→Maven Install)をおこなうことでtargetフォルダにjarが作成されるので、それを利用する。

簡単なチュートリアル

Hello World

伝統的なHello WorldをSikulixでやってみます。
メモ帳を立ち上げて、「はろーわーるど!」と打ち込みます。

手で起動する場合を考えてみましょう。
たとえば、以下のような手順になると思います。

(1)メモ帳を起動する
(2)メモ帳が起動するまでまつ
(3)メモ帳のテキストエリアをクリックする
(4)「はろーわるど!」と入力する。

(1)メモ帳を起動する。
sikulixではアプリケーションを起動する場合は、以下のようなスクリプトを記述します。

app = App('アプリケーションのパス')
app.open()

メモ帳の場合は以下のようになります。

image.png

(2)メモ帳が起動するまでまつ
以下のようなメモ帳が起動するまで待機するにはどうしたらいいでしょうか?
image.png

まず、「スクリーンショットを撮る」をクリックします。
image.png

ディスクトップ上で矩形を選択できるようになりますのでメモ帳タイトルバーが入るように矩形を選択すると、IDEに以下のような画像が表示されます。

image.png

選択した画像が表示されるまで待つために「wait()」コマンドを以下のように使用します。

image.png

なお、昔のバージョンではコマンドリストなるものが存在したそうですが、1.1.4では消えたので、おとなしく手でコマンドを入力してください。

(3)メモ帳のテキストエリアをクリックする
画像の位置をクリックするには「click()」コマンドを使用します。
waitと同じように以下のように記載します。

image.png

これで画像をクリックするようになりましたが、クリックする位置はデフォルトで中央になっています。これを変更するには、click()中の画像をクリックしてください。

パターン設定というポップアップが表示されると思います。
image.png

ここで、「ターゲットオフセット」タブを選択してください。
image.png

この画面でクリック位置を設定できます。
マウスホイールで拡大、縮小ができ、画像上を押下して、自動操作中にクリックしたい位置を指定できます。
「ターゲットオフセット」画面では、クリック位置を十字カーソルで表現しています。現在は「書式」メニューあたりをクリックするようになっているので、以下のようにテキストエリアをクリックしてみましょう。

image.png

クリック位置を変更すると、画面下部のターゲットオフセットの値が0から変化したと思います。
クリック位置を確定するために「OK」ボタンを押下してください。

image.png
IDE上の画像にクリック位置が赤いカーソルで表現されるようになりました。

(4)「はろーわるど!」と入力する。
キーボード入力には「type」コマンドと「paste」コマンドの2種類が存在します。
日本語入力を行いたい場合は「paste」コマンドを利用します。「paste(u'はろーわーるど!')」とIDEに入力します。
image.png

では、記載したスクリプトを起動して確認してみましょう。
スクリプトを起動するにはimage.pngをクリックします。

image.png

「Run Immediately」または「Save all and Run」が表示されます。保存しないですぐ実行するか、保存してから実行するかを選択できますが、今回は保存しないで実行するために「Run Immediately」を選択します。

これでメモ帳が起動して「はろーわーるど!」と表示されると思います。

その他、公式のチュートリアルは下記を参照してください。
https://sikulix-2014.readthedocs.io/en/latest/index.html#tutorials

IDEの使い方

ファイル

image.png

新規作成

新規にファイルを作成します。
image.png

この時点では、「C:\Users\ユーザ名\AppData\Local\Temp\」以下に一時フォルダが作成されており、保存時にその内容を移動することになります。

開く

image.png

以下のファイルを開きます。

  • sikuli拡張子のフォルダ
  • py拡張子のファイル
  • その他テキストファイル

・sikuli拡張子のフォルダはスクリプトファイルとスクリプトで用いる画像ファイルが格納されているフォルダです。
以下はさきほどのHelloWorldで作成した内容を保存したsikuli拡張子フォルダの中身になります。
image.png

・py拡張子ファイルはpythonのファイルを選択します。このファイルも実行が可能です。

・その他テキストファイルはただのテキストファイルとしてひらくため、実行することはできません。

保存、名前を付けて保存、save all

・「保存」は現在編集中のスクリプトまたはテキストを上書き保存します。ただし、新規作成直後については名前を付けて保存と同じ処理になります。

・「名前を付けて保存」は現在編集中のスクリプトを保存場所を選択して保存します。

・「save all」は現在開いている全てのタブの内容を上書き保存します。

なお、保存時に使われていない画像ファイルは削除されます。
この挙動は環境設定により変更可能です。

実行ファイルとして保存

skl拡張子としてエクスポートをします。
エクスポートしたファイルは別のスクリプトから利用できます。

たとえば以下のスクリプトを含むsklを作成します。

myModule.skl

from sikuli import *
def myFunction(s):
    Debug.log(1, 'myFunction%s'  ,s)

print ('test')

このファイルで定義した関数myFunctionは別のスクリプトから以下のように利用できます。

addImportPath(makePath(getParentFolder() , 'myModule.skl'))
# myModule読み込み
import myModule
# 別モジュール
myModule.myFunction('xxx')

これによりスクリプトファイルの部品化が行えるようになります。
なお、sklファイルはsikuli拡張子のフォルダをzipで圧縮したものです。

※以下のヘルプによるとコマンドラインよりsklファイルの実行もできたようですが、1.1.4の段階ではコマンドラインからは実行できないようです。
https://sikulix-2014.readthedocs.io/en/latest/faq/010-command-line.html

Export as jar

・Jarファイルとして出力します。

・ドキュメント的には単独で動作可能に見えますが、実際は単独で動作しません。
https://github.com/RaiMan/SikuliX1/issues/33

これは1.1.4で取り消された機能のようです。
https://sikulix-2014.readthedocs.io/en/latest/newsbugs.html

・JrubyではJarのExport自体実装されていないようです。
https://answers.dogfood.paddev.net/sikuli/+question/678016

Open Specials Files

特殊な設定ファイルを修正します。
なお、2019/4リリース時点で動作しません。

GitHub版のコードをコンパイルして実行すると以下のような画面が表示されます。
image.png

No Name Path
1 Sikulix Global Options C:\Users(User)\AppData\Roaming\Sikulix\SikulixStore\SikulixOptions.txt
2 SikuliX Extensions Options C:\Users(User)\AppData\Roaming\Sikulix\Extensions\extensions.txt
3 SikuliX Additional Sites C:\Users(User)\AppData\Roaming\Sikulix\Lib\site-packages\sites.txt

Sikulix Global Options

Settingsの初期値を設定できます。

SikulixOptions.txt

# key = value
Settings.LogTime = True

上記を設定した場合の再起動直後のSettings.LogTimeがTrueになります。
その他、Settingsの内容は下記を参照してください。

https://sikulix-2014.readthedocs.io/en/latest/scripting.html#Settings

SikuliX Extensions Options

pythonやjythonのパスを指定できます。

# add absolute paths one per line, that point to other jars,
# that need to be available on Java's classpath at runtime
# They will be added automatically at startup in the given sequence

# empty lines and lines beginning with # or // are ignored
# delete the leading # to activate a prepared keyword line

# pointer to a Jython install outside SikuliX
# jython = c:/jython2.7.1/jython.jar

# the Python executable as used on a commandline
# activating will enable the support for real Python
# python = C:\Users\XXXXXX\AppData\Local\Programs\Python\Python36-32\python.exe

pythonの指定の目的がいまいちわかりませんでした。
※runScriptでpyファイルを実行しても同じ拡張子のjythonのスクリプトランナーが優先されて実行されてしまうので、動かないようにみえる。

SikuliX Additional Sites

起動時に自動的に「sys.path」の後ろに追加されるJytho,Python,SikuliXのパスを指定します。

addImportPathを使わなくても以下のように設定することで使用することができるようになります。

sites.txt

# add absolute paths one per line, that point to other directories/jars,
# where importable modules (Jython, plain Python, SikuliX scripts, ...) can be found.
# They will be added automatically at startup to the end of sys.path in the given sequence

# lines beginning with # and blank lines are ignored and can be used as comments
C:\tool\sikulix\myModule.sikuli
# myModule読み込み
import myModule
# 別モジュール
myModule.myFunction('xxx')
exit(1)

Reset IDE

IDEを再起動します。
環境設定を変更した場合に必要です。

環境設定

「画面キャプチャ」タブ

image.png

画面キャプチャの設定を変更できます。
「スクリーンショットを撮る」を実行した場合、デフォルトではタイムスタンプのファイルが作成されますが、ここを「オフ(手動入力)」とすることで「スクリーンショットを撮る」を実行する度に以下のような画面が表示されて任意のファイル名を設定できるようになります。

image.png

「テキスト編集」タブ

image.png

テキストのタブ数やフォントを変更できます。

「全般」タブ

image.png

自動更新は未実装。
Languageは言語を変更できます。

more options

image.png

メッセージの表示レベルや、実行時に自動保存を行うか否かとかの設定ができます。

IDE上でスクリプトを実行する

IDE上でスクリプトを実行するためには以下の3つの方法があります。

(1)ツールバーのボタンから実行する
image.png

(2)実行メニューから実行する
image.png

(3)スクリプトの行番号を右クリックして、コンテキストメニューから実行する
image.png

メニュー 説明
実行 スクリプトを先頭から実行する。スクリプトに変更がある場合は保存を要求される。
スローモーションで実行 クリック時にSettings.SlowMotionDelayで指定した秒(デフォルト2秒)の時間をかけてゆっくり移動する。移動先にはマーカimage.pngが表示される。ただし1.1.4(2019/05)時点ではバグのため動作しない模様。GitHubの最新では期待通り動作する。
run selection 選択しているコードのみを実行する。スクリプトに変更があっても保存を要求されない。
run line カーソルのある行のみ実行する。スクリプトに変更があっても保存を要求されない。
run from line カーソルのある行から実行する。スクリプトに変更があっても保存を要求されない。
run to line カーソルのあるところまで実行する。スクリプトに変更があっても保存を要求されない。

ドキュメントタブのコンテキストメニュー

image.png

About

image.png

ドキュメントの保存場所が表示されます。

Set Type

使用する言語を選択できます。
選択後は、ドキュメントの内容はクリアされるので、新規作成の直後に選ぶとよいでしょう。
image.png

以下の内容が選択できます。
・jython
・ruby
・javascript
・text(ただのプレーンテキストで実行はできません)

Insert Path

以下のダイアログを表示してファイルのパスを選択できます。
image.png

選択後はIDEに以下のようにパスが表示されます。
image.png

ただし、Windowsの場合、「\」はエスケープ文字なので「\\」としてあげないと、正常なパスになりません。

Move Tab

タブの位置を移動します。

(1)移動するタブを選択して「Move Tab」を選択
image.png

(2)タブが消えます。
image.png

(3)移動させたい場所のタブを選択して、コンテキストメニューを開き「Insert Right」または「Insert Left」を入力する。
image.png

Insert Leftを選択した例:選択したタブの左側に移動する
image.png

Reset

メッセージや実行中の変数をリセットします。
たとえば以下のコードが存在します。

print(x)
x = 1

・1行目を「run line」を実行するとNameErrorが発生します。
・2行目を実行した後に1行目を実行すると「1」と表示されます。
 これは、内部的には変数の内容を覚え続けているためです。
・この後は、「Reset」を実行するまでは、1行目を「run line」してもNameErrorになりません。
・「Reset」を実行することで初期状態にもどり1行目を「run line」するとNameErrorになります。

ツールバー

image.png

スクリーンショットを撮る(CTRL+SHIFT+2)

全てのスクリーンが以下のようなマスクがかかり範囲を選択できるようになります。
image.png

範囲を選択した箇所は画像として保存されて、IDEに表示されます。
ファイル名は環境設定により、手動で入力するか、自動で設定するかを選択可能です。

Pattern

「スクリーンショットを撮る」で作成した画像はPatternオブジェクトに変換可能です。
IDE上の画像をクリックすると下記の画面が表示されます。

image.png

・ファイルタブはファイルのパス情報が記載されています。
・マッチングプレビューは類似度を設定できます。
・ターゲットオフセットは画像のどこをターゲットにするかを指定できます。

以下の画像をマッチングプレビューを使用して類似度を変更してみます。
image.png

0.7の場合は類似の画像が2つ検知されます。
image.png

0.98にあげると1つのみとなります。
image.png

このように類似度を実際の画面を確認しながら選択することができます。
パターン設定画面で設定したファイルは以下のようにPatternオブジェクトとして扱われるようになります。

pattern = Pattern("image.png").similar(0.98).targetOffset(-153,-87)

Patternの公式ドキュメント
https://sikulix-2014.readthedocs.io/en/latest/pattern.html?highlight=Pattern

コード
https://github.com/RaiMan/SikuliX1/blob/08eb817c70afafdabff87d46cadb372255a66ad7/API/src/main/java/org/sikuli/script/Pattern.java

画像を挿入する

ディスク上にある画像を選択してIDEに表示します。
選択した画像はsikuliフォルダにコピーされて使用されます。
作成後の画像については「スクリーンショットを撮る」と同じくPatternとして処理できます。

Region

スクリーン上の範囲を選択できます。
IDE上は以下のように表示されます。
image.png
※左下の赤い領域が選択した箇所

IDE上は先ほどの「スクリーンショットを撮る」と「画像を挿入する」と同じような見え方をしますが、実際のコード上は異なります。
メニューの「表示」→「Toggle Thumnails」のチェックをOFFにすると実際のコードが表示されます。

image.png

RegionのfindメソッドやfindTextメソッドを用いて領域範囲内の画像やテキストの検索を行います。

Regionについての公式ドキュメントは下記の通りです。
https://sikulix-2014.readthedocs.io/en/latest/region.html

コードは以下になります。
https://github.com/RaiMan/SikuliX1/blob/08eb817c70afafdabff87d46cadb372255a66ad7/API/src/main/java/org/sikuli/script/Region.java

Location

Locationはスクリーン上の一点の位置(x、y)を取り扱います。
LocationをクリックするとRegionと同様にスクリーン上の範囲を選択できるようになります。

Locationの座標として採用されるのは赤い十字の中心となります。
image.png

作成したLocationについては以下のように使用します。

# 674,217 に移動してクリックする
Location(674, 217).click()

Locationについての公式ドキュメントは下記の通りです。
https://sikulix-2014.readthedocs.io/en/latest/location.html

またソースコードは以下になります。
https://github.com/RaiMan/SikuliX1/blob/08eb817c70afafdabff87d46cadb372255a66ad7/API/src/main/java/org/sikuli/script/Location.java

Offset

Locationと同様に選択を行います。
このときに作成されるコードは以下のようになります。

offset = Offset(58, 28)

Offsetの目的については以下に説明が記載されています。
https://answers.launchpad.net/sikuli/+question/446476

たとえば以下のような画面があり、「含まれるテキスト」項目に入力をする必要がある場合を考えましょう。
image.png

この場合、image.pngという画像から下に一定量はなれた箇所をクリックする必要があります。
この際にOffsetを使用します。

実際のコードにすると以下のようになります。
image.png

Offsetのコードは以下になります。
https://github.com/RaiMan/SikuliX1/blob/08eb817c70afafdabff87d46cadb372255a66ad7/API/src/main/java/org/sikuli/script/Offset.java

Show

カーソルのある行のRegion,ファイル名,Patternの画像がスクリーン全体のどこにあるかを検索して強調表示します。

image.png

ShowIn

基本的にShowと同じ使い方です。
Showは検索範囲がスクリーン全体ですが、ShowInの場合、検索前に検索範囲を矩形で設定できます。

代表的な処理

画像検索

openCvを利用して画像の検索が行えます。

find()

指定の範囲で指定の画像が存在するか検索します。存在する場合は最初に見つけた画像の位置を返却します。
見つからない場合はFindFailed例外が発生します。

rc = Region(8,-836,876,597)
try:
    ret = rc.find(Pattern("masktest1.png"))
except FindFailed:
    print ('not found')
    exit()
ret.highlight(2)
exit()

wait()

指定の範囲で指定の画像が出現するまで待機します。出現した場合は最初に見つけた画像の位置を返却します。
見つからない場合はFindFailed例外が発生します。

rc = Region(8,-836,876,597)
try:
    ret = rc.wait(Pattern("masktest1.png"), 10)
except FindFailed:
    print ('not found')
    exit()
ret.highlight(2)
exit()

exists()

指定の画像が出現するまで待機します。存在した場合は画像領域を返します。
もし見つからない場合、Noneを返します。

rc = Region(8,-836,876,597)

ret = rc.exists(Pattern("masktest1.png"), 10)
print(ret)
if ret is None:
    print ('Not found')
    exit()
ret.highlight(2)

has()

指定の画像が存在するか確認します。存在した場合はTrueを返します。
もし見つからない場合、Falseを返します。

rc = Region(8,-836,876,597)

ret = rc.has(Pattern("masktest1.png"), 10)
print(ret)

waitVanish

指定の範囲で指定の画像が消えるまで待ちます。指定秒の間に消えなかったらFalse、消えたらTrueを返します

rc = Region(8,-836,876,597)
ret = rc.waitVanish(Pattern("masktest1.png"), 10)
print(ret)

変更点の監視

onChangeに変更が発生した場合に処理するイベントハンドラを登録します。

# https://sikulix-2014.readthedocs.io/en/latest/region.html?highlight=observe%20
def changed(event):
        print "something changed in ", event.region
        for ch in event.getChanges():
                ch.highlight() # highlight all changes
        wait(1)
        for ch in event.getChanges():
                ch.highlight() # turn off the highlights

# 範囲選択が表示されてRegionを選択する
r = selectRegion("select a region to observe")

# 50ピクセル変更したらchangedを呼び出す

r.onChange(50, changed)

# 30秒間監視
r.observeInBackground(); wait(30)
r.stopObserver()

この例では、チェックボックスを付けたりして画面が変更した箇所が強調表示されます。
image.png

findAll()

指定範囲内で指定の画像が存在する箇所を全て列挙します。存在しない場合は空配列となります。

rc = Region(8,-836,876,597)

list = rc.findAll(Pattern("masktest1.png"))
for i in list:
    i.highlight(2)
    print(i.getScore())

exit()

マスクと透過画像の使用

画像検索では透過画像またはマスクを利用して画像の一部を無視して検索が行えます。

検索範囲:
image.png

透過画像を利用する例

検索画像:
image.png

コード:
image.png

findAllでマッチした画像を検索して、そのスコアを表示します。
この例だと以下のようになります。

image.png

黒をマスクとして利用する

検索画像
image.png

コード:
image.png

今回は「Pattern("masktest_black.png").exact().mask()」と「exact()」を利用して類似度を0.99まで上げています。
この例での結果は透過の場合と同じで2件、類似度が1.0で取得できました。

マスク画像の合成

検索画像1 masktest1.png
image.png

マスク画像 masktest1mask.png
image.png

コード

rc = Region(8,-836,876,597)

list = rc.findAll(Pattern("masktest1.png").exact().mask("masktest1mask.png"))
for i in list:
    i.highlight(2)
    print(i.getScore())

exit()

結果については「黒をマスクとして利用する」場合と同じになります。

テキスト検索

sikulix1.1.4ではTess4Jを使用してテキストの認識をします。

ただし、OCRなので100%の精度を期待してはいけないです。

使用例

(1)言語情報を取得します。
https://github.com/tesseract-ocr/tessdata/tree/3.04.00
日本語の場合は「jpn.traineddata」になります。

(2)下記のフォルダに配置します。
C:\Users\ユーザ名\AppData\Roaming\Sikulix\SikulixTesseract\tessdata

(3)特定の範囲内のテキストを読み込んでみます

tr = TextOCR.start()
tr.setLanguage("jpn")
r = Region(0,63,159,74)
print r.text()

findText()

指定の文字が存在するかを検査します。存在した場合は文字の領域のRegionを返します。
もし見つからない場合、FindFailed例外が発生します。

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

Settings.OcrLanguage = 'jpn'
#Settings.OcrTextSearch = False   # バージョンによってTrueにするとImage.isValidでNullPointerError
Settings.OcrTextRead  = True
Settings.SwitchToText = False
TextOCR.start()
r = Region(0,0,100,150)

try:
    ret = r.findText(u'あい')
except FindFailed as ex:
    print ('error' + str(ex))
    exit(1)
ret.highlight(2)

waitText()

指定の文字が出現するまで待機します。存在した場合は文字の領域のRegionを返します。
もし指定した秒数まで見つからない場合はFindFailed例外が発生します。

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

Settings.OcrLanguage = 'jpn'
#Settings.OcrTextSearch = False   # バージョンによってTrueにするとImage.isValidでNullPointerError
Settings.OcrTextRead  = True
Settings.SwitchToText = False
TextOCR.start()
# tr = TextOCR.start()
# tr.setLanguage('jpn')
r = Region(0,0,100,150)

try:
    ret = r.waitText(u'あい', 10) # 10秒待機
except FindFailed as ex:
    print ('error' + str(ex))
    exit(1)
ret.highlight(2)

hasText()

指定の文字が存在するかを検査します。存在した場合は文字の領域のRegionを返します。
もし見つからない場合、Noneを返します。

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

Settings.OcrLanguage = 'jpn'
#Settings.OcrTextSearch = False   # バージョンによってTrueにするとImage.isValidでNullPointerError
Settings.OcrTextRead  = True
Settings.SwitchToText = False
TextOCR.start()
r = Region(0,0,100,150)
ret = r.hasText(u'あい')
ret.highlight(2)
exit()

existsText()

指定の文字が出現するまで待機します。存在した場合は文字の領域のRegionを返します。
もし見つからない場合、Noneを返します。

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

Settings.OcrLanguage = 'jpn'
#Settings.OcrTextSearch = False   # バージョンによってTrueにするとImage.isValidでNullPointerError
Settings.OcrTextRead  = True
Settings.SwitchToText = False
TextOCR.start()
r = Region(0,0,100,150)
ret = r.existsText(u'あい',10)
if ret is None:
    print ('Not found')
    exit()
ret.highlight(2)
exit()

findAllText()

指定範囲内で指定の文字が存在する箇所を全て列挙します。存在しない場合は空配列となります。

Settings.OcrLanguage = 'jpn'
#Settings.OcrTextSearch = False   # バージョンによってTrueにするとImage.isValidでNullPointerError
Settings.OcrTextRead  = True
Settings.SwitchToText = False
TextOCR.start()
r = Region(0,0,100,150)
result = r.findAllText(u'あい')
for item in result:
    item.highlight(2)

findWord/findLine

単語単位または行単位で検索します。単語間に空白を開ける英語だと予期したとおりに動かしやすいですが、日本語のような言語だと厳しいかもしれません。
見つからない場合は、「java.lang.ClassCastException」が発生してますが、おそらく、意図していない例外だと思います。

import sys
# 対策:public org.python.core.PyObject o object has no attribute setdefaultencoding 
reload(sys) 
sys.setdefaultencoding('utf-8')

Settings.OcrLanguage = 'jpn'
#Settings.OcrTextSearch = False   # TrueにするとImage.isValidでNullPointerError
# Settings.OcrTextRead  = True
# Settings.SwitchToText = False
TextOCR.start()
r = Region(0,2,243,203)

ret = r.findWord(u'He')
ret.highlight(2)
ret = r.findLine(u'He')
ret.highlight(2)

# 「彼」の前後にスペースがないと認識しないかも
ret = r.findWord(u'彼')
ret.highlight(2)

# 「彼」の前後にスペースがなくてもOK
ret = r.findLine(u'彼')
ret.highlight(2)
exit(1)

findWord(u'He')
image.png

findLine(u'He')
image.png

findWord(u'彼')
image.png

findLine(u'彼')
image.png

画面操作

click(PSMRL[, modifiers])

指定の座標でクリックを行います。
PSMRLにはPattern、string,match, region, locationが指定できます。
modifiersは省略可能な引数で1つ以上のキーボード操作を指定できます。

# find()やwait()で最期に見つけた座標をクリックする
click() 

# 座標を指定してクリックする
click(Location(103, 635))

# 指定の画像をクリックする
click("test.png")

# 指定の画像をSHIFTを押しながらクリックする
click("test.png", KeyModifier.SHIFT)

mouseMove(PSRML)

指定の座標にマウスポインタを移動させます。
PSMRLにはPattern、string,match, region, locationが指定できます。

# 指定の画像へ移動
mouseMove("test.png")

# 指定の座標に移動
mouseMove(Location(278, 182))

doubleClick(PSMRL[, modifiers])

指定の座標でダブルクリックをします。
PSMRLにはPattern、string,match, region, locationが指定できます。
modifiersは省略可能な引数で1つ以上のキーボード操作を指定できます。


# find()やwait()で最期に見つけた座標をダブルクリックする
doubleClick()

# 座標を指定してダブルクリックする
doubleClick(Location(103, 635))

# 指定の画像をダブルクリックする
doubleClick("test.png")

rightClick(PSMRL[, modifiers])

指定の座標で右クリックをします。
PSMRLにはPattern、string,match, region, locationが指定できます。
modifiersは省略可能な引数で1つ以上のキーボード操作を指定できます。

rightClick("test.png")
sleep(5) # コンテキストメニューが表示される。実運用ではwaitを使う

dragDrop(PSMRL, PSMRL[, modifiers])

ドラッグアンドドロップを実行します。
PSMRLにはPattern、string,match, region, locationが指定できます。
modifiersは省略可能な引数で1つ以上のキーボード操作を指定できます。

操作対象の領域
image.png

コード
image.png

実行結果
image.png

※あくまでテストです。実際にエクスプローラーの操作を画像認識でやると苦労します。
たとえば、フォルダは以下のように中にファイルがあるかどうかでアイコンが変わったりします。

image.png

type([PSMRL, ]text[, modifiers])

PSMRLにはPattern、string,match, region, locationが指定できます。
textには入力するASCIIの文字を指定します。
modifiersには同時に押下するSHIFTなどのキーを指定します。

# CTRL+Aを押して全選択する
type(Location(351, 122), 'a' , KeyModifier.CTRL)
# 選択している文字を削除
type(Key.DELETE)
# abcをタイプ
type(Location(351, 122), 'abc')

paste([PSMRL, ]text)

クリップボード経由で任意のテキストを貼り付けます。日本語文字をテキストに入力する場合に使用します。
PSMRLにはPattern、string,match, region, locationが指定できます。
textには貼り付けたい文字を指定します。

コード:
image.png

実行前:
image.png

実行後:
image.png

その他操作、低レベルの画面操作

以下を参照してください。
https://sikulix-2014.readthedocs.io/en/latest/region.html#low-level-mouse-and-keyboard-actions

プライマリー以外のスクリーンの操作

マルチディスプレイでの操作

以下のようなマルチディスプレイ環境だとします。
image.png

・getNumberScreens()コマンドでディスプレイが幾つあるか取得します。
・3のディスプレイを操作する場合は「Screen(3).click(...)」と記述します。
・座標の関係は以下のようになります。(参照)
image.png

マルチディスプレイ環境での実装例:
image.png
この例では、全てのスクリーンに対して指定の画像が存在するかを調べて、存在すれば操作をするような動きになります。

VNC経由の操作

TigerVNC Viewerをベースに実装したVNCの処理を経由して別PCの画面が操作可能です。
以下の例ではVNC経由で画面をキャプチャしたものになります。

vnc = vncStart('192.168.0.18', port=5900, password=u"パスワード")
print(vnc)
tmp = vnc.capture(Region(0,0,1000,1000))
path = tmp.save()
print(path)
vnc.stop()
exit(1)

Androidに対する操作

Sikulixを用いてAndroidを操作するにはAndroidStudioをインストールした際に追加されるADBが必要です。
ADBを経由してAndroidのタップやスクリーンキャプチャを実現してます。

実験段階のためか、不安定な動きです。

IDEの起動方法

ADBのパスをSikulixに教える必要があるので、環境変数sikulixadbにADBのパスを指定して、SikulixIDEを起動します。

@echo off
SET JAVA_HOME=C:\pleiades201904\java\12
set sikulixadb=C:\Program Files (x86)\Android\android-sdk\platform-tools\adb.exe
"%JAVA_HOME%\bin\java.exe"  -jar C:\tool\sikulix\sikulixide-1.1.4-SNAPSHOT-complete.jar

Androidの画像キャプチャの方法

(1)AndroidをUSBでつなげて「menuToolAndroid」を選択します

(2)ポップアップが表示されるので「Default Android」を選択します。
image.png

(3)「スクリーンショットを撮る」を実行するとAndroidからキャプチャを撮るか聞かれるので「はい」を選択します
image.png

(4)PC上にAndroidのキャプチャが表示されるので範囲を選択します。
image.png

(5)IDEに選択範囲が表示されます。あとはPCと同じです。
image.png

(4)でフリーズする場合

Xperia SO-02Kの実機だと固まりました。
この場合、ADBDevice.javaのcaptureDeviceScreenMatを以下のように修正してむりやり動かします。

ADBDevice.java

  public Mat captureDeviceScreenMat(int x, int y, int actW, int actH) {
// 略
            if (!endOfStream) {
              if (pixel[3] == -1) {
                if (npr >= y && npc >= x && npc < maxC) {
                  image[atImage++] = pixel[0];
                  image[atImage++] = pixel[1];
                  image[atImage++] = pixel[2];
                  image[atImage++] = pixel[3];
                }
              } else {
                log(-1, "buffer problem: %d", nPixels);
                // TODO Xperia SO-02K pixel[3] is 0
                //return null;
              }
            } else {
              break;
            }

無理やり動かしたせいか、PC上の範囲選択の座標がずれて上手くいかなくなっていました。

実行方法

ADBScreen.start()で作成したオブジェクトに対して操作を行うことで、Androidの操作が行えます。

image.png

サーバー経由の実行

HTTPサーバーを起動して、外部からの自動操作を受け付けます。

(1)sikulixをサーバーとして起動します。

%JAVA_HOME%\bin\java.exe  -jar sikulix.jar -s

(2)ブラウザで以下を入力してスクリプトが格納されているフォルダを設定します。

http://127.0.0.1:50001/scripts/c:/tool/sikulix

操作対象の「.sikuli」を格納しているフォルダをscriptsの後に入力してください。
上記の例の場合だと「c:\tool\sikulix」が選択されます。

(3)「hello.sikuli」スクリプトを実行します。

http://127.0.0.1:50001/run/hello

その他は以下のページ参照してください
https://sikulix-2014.readthedocs.io/en/latest/scenarios.html#experimental-runserver-run-scripts-from-anywhere-with-zero-delay

なお、1.1.4の2019年4月リリース版だと動作しなかったのでGithubからビルドした資材を使用して確認しました。

PythonServer経由の実行

py4Jを使用してJythonではなく、本物のpythonで実装ができます。この機能は現在開発中です。

(1)PythonServerを起動

%JAVA_HOME%\bin\java.exe  -jar sikulix.jar -p

(2)py4Jのインストール

pip install py4j

(3)sikulix4pythonの入手
https://github.com/RaiMan/sikulix4python

(4)Pythonのコードを記述して実行

from sikulix4python import *
switchApp("notepad") # メモ帳にフォーカスが遷る

その他ドキュメントがおいついてなさそうなところ

HTTP上の画像の使用

以下によると「addHTTPImagePath」でサイトを追加して使用するような記述になっています。
https://sikulix-2014.readthedocs.io/en/latest/scripting.html

1.1.4ではaddHTTPImagePathは廃止されており、以下のようにして実現できます。

# ImagePath.addHTTP('https://pbs.twimg.com/profile_images/1111729635610382336/')
# addHTTPImagePathは以下に1.1.4統合されたっぽい。
addImagePath('https://pbs.twimg.com/profile_images/1111729635610382336/')
click('_65QFl7B_200x200.png')

PowerShellの実行

下記のドキュメントで「returnCode = runScript('powershell get-process')」のような記載ができるようにかかれていますが、1.1.4ではps1という拡張子を持つ外部ファイルを実行することでのみPowerShellを使用できます。
https://sikulix-2014.readthedocs.io/en/latest/scripting.html

以下の例ではPowerShellの標準出力をUTF-8にして無理やり日本語を扱えるようにしています。

test.ps1

chcp 65001
ls
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
returnCode = runScript('C:\\tool\\sikulix\\test.ps1')
print returnCode
print RunTime.get().getLastCommandResult().encode('utf-8')
print RunTime.get().getLastCommandResult().encode('utf-8').replace('ディレクトリ', 'フォルダ')

このコードはlsの結果で表示される「ディレクトリ」という文字列をpythonの処理で「フォルダ」に変換してコンソールに出力しています。

出力

ディレクトリ: C:\tool\sikulix 
Mode LastWriteTime Length Name 
---- ------------- ------ ---- 
d----- 2019/06/03 16:41 hello2.sikuli 
// 略
Active code page: 65001 
フォルダ: C:\tool\sikulix 
Mode LastWriteTime Length Name 
---- ------------- ------ ---- 

初心にもどってJava ~キサマはHello Worldを嘗めたッッッ~

キサマはHello Worldを嘗めたッッッ

プログラミング言語を始める場合に「Hello World」と出力することはよくあります。
特に業務で使わないといけなくなった場合だとゴキブリダッシュばりの速度でHelloWorldを書いて次の段階に進もうとします。

だが、私はこのHello Worldが、どう動いていたのか理解していたのでしょうか。
今回は、烈先生に「キサマはHello Worldを嘗めたッッッ」とボコられない程度に初心に戻ってHello Worldを見なおしてみようと思います。

ゴキブリダッシュ的 Hello World

環境

Windows 10
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)

Helloworldの作成~実行

いつものようにHello Worldを作成して実行します。
まずテキストエディタでjavaのコードを作成します。

HelloWorld.java

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

作成したjavaファイルを入力としてjavacコマンドを実行してclassファイルを作成します。
今回は「-g:none」オプションを付けてデバッグ情報は生成しないようにします。

javac -g:none HelloWorld.java

作成した「HelloWorld.class」をjavaコマンドで実行します。

>java HelloWorld
Hello World

「Hello World」と出力されました。
cやc++で作成した実行ファイルと異なり、作成したHelloWorld.classはJavaが入っている環境であれば、WindowsでもMacでもLinuxでも同様に動作します。
いやー、Javaって便利ですね。

終わり!!閉廷!!以上!!皆解散!!

そんなふうに考えていた時期が俺にもありました

いままではそれで終わらせてました。
そう、このHelloWorld.classがどういうファイルなのかを考える機会がなかったのです。
今回はせっかくなので、作成されたバイナリファイルの内容を確認してみます。

HelloWorld.classの中身は以下のようなバイナリになっています。

image.png

このバイナリファイルを読み解くには、「The Java® Virtual Machine Specification」を読む必要があります。
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

今回使用するJVM仕様

今回使用するJVMの仕様を簡単に記載しますが、さっさとクラスファイルのバイナリを解析をしたい人はスキップしてください。

classファイルの構造は以下の通りです。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

u1は1バイト、u2は2バイト、u4は4バイトのデータを表します。
その他については構造体なので都度説明します。

magic はクラスファイルフォーマットを識別するマジックナンバーで、「0xCAFEBABE」です。
なお、このマジックナンバーの由来はCAFEBABE: Java's Magic Wordに記載されているので、一読しておくとデートの時に使ってドン引かれます。

minor_version、major_versionはこのクラスファイルのマイナーバージョン番号とメジャーバージョン番号です。メジャーバージョン番号とマイナーバージョン番号によって、クラスファイル形式のバージョンが決まります。

constant_pool[]とconstant_pool_countは文字列定数、クラス名およびインタフェース名、フィールド名、その他の定数を表す構造体のテーブルとその数です。
constant_poolへのインデックスは1~constant_pool_count-1が有効範囲になります。

access_flagsは以下のフラグの組み合わせになります。

フラグ名 解釈
ACC_PUBLIC 0x0001 publicが宣言されています。そのパッケージの外部からアクセスすることができます。
ACC_FINAL 0x0010 finalが宣言されています。サブクラスは許可されていません。
ACC_SUPER 0x0020 invokespecial命令によって呼び出されたときに、スーパークラスメソッドを特別に扱います。
ACC_INTERFACE 0x0200 クラスではなくインタフェースです。
ACC_ABSTRACT 0x0400 abstractを宣言されています。インスタンス化してはいけません。
ACC_SYNTHETIC 0x1000 コンパイラによって生成されたものであることをしめします。※例えばクラス内にクラスを作成したときに作成されるクラスファイル(ex. Hello$Test.class)に付与されていました
ACC_ANNOTATION 0x2000 アノテーションとして宣言されています。
ACC_ENUM 0x4000 enum型として宣言されています。

this_class はconstant_pool[]中の有効なインデックス値である必要があります。インデックスで参照されるデータは当該ファイルで指定されたクラスの情報を保持するCONSTANT_Class_info構造体である必要があります。

super_class は0またはconstant_pool[]中の有効なインデックス値である必要があります。インデックスで参照されるデータはこのクラスファイルで定義されたクラスの直接のスーパークラスを表すCONSTANT_Class_info構造体である必要があります。

interfaces_count、interfaces[]はこのクラスファイルで定義されているクラスのインターフェイスを表すCONSTANT_Class_info構造体へのインデックスの配列になっています。インターフェイスがない場合はinterfaces_countは0となり、interfaces[]は存在しません。

fields_count、fields[]はこのクラスファイルで定義されているクラスのフィールドを表すfield構造体の配列になっています。フィールドがない場合はfields_countは0となり、fields[]は存在しません。

methods_count、methods[]はこのクラスファイルで定義されているクラスのメソッドを表すmethod構造体の配列になっています。フィールドがない場合はmethods_countは0となり、methods[]は存在しません。

attributes_count、attributes[]はこのクラスファイルで定義されているクラスにつての属性情報を表すattribute構造体の配列になっています。

constant_pool

この構造体は1バイトのタグでどのような構造体になるかが決定されます。

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

以下で今回使用する分は説明するので、その他の構造体については以下を参照してください。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140

CONSTANT_Class

クラスまたはインタフェースを表すために使用されます。

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

tagにはCONSTANT_Classをあらわす7が格納されます。
name_index項目の値は constant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスになります。

CONSTANT_Fieldref

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

tagにはCONSTANT_Fieldref_info値をあらわす9が格納されます。
class_indexの値はconstant_poolテーブル中のCONSTANT_Class_info構造体へのインデックスです。
name_and_type_index項目 の値はconstant_poolテーブル中のCONSTANT_NameAndType_info構造体へのインデックスです。

CONSTANT_Methodref

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

tagにはCONSTANT_Methodref値をあらわす10が格納されます。
class_indexの値はconstant_poolテーブル中のCONSTANT_Class_info構造体へのインデックスです。
name_and_type_index項目 の値はconstant_poolテーブル中のCONSTANT_NameAndType_info構造体へのインデックスです。

CONSTANT_String

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

tagにはCONSTANT_String値をあらわす8が格納されます。
string_indexの値はconstant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスです。

CONSTANT_NameAndType

フィールドまたはメソッドを表すために使用されます。ただし、それが属するクラスまたはインタフェースの型は示されません。

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

tagにはCONSTANT_NameAndType値をあらわす12が格納されます。
name_index項目の値はconstant_pool中のCONSTANT_Utf8_info構造体への有効なインデックスでなければなりません。

CONSTANT_Utf8

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

tagにはCONSTANT_Utf8値をあらわす1が格納されます。
lengthにはbytes配列内のバイト数を表します(文字列の長さではありません)
byte配列には文字列のバイトが含まれます。また終端文字は含まれません。この文字列の詳細については下記を参照してください。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.7

method構造体

method_info { 
    u2 access_flags; 
    u2 name_index; 
    u2 descriptor_index; 
    u2 attributes_count; 
    attribute_info attributes [attributes_count]; 
}

access_flags項目の値はこのメソッドへのアクセス許可とこのメソッドのプロパティを示すために使用されるフラグの組み合わせです。

フラグ名 説明
ACC_PUBLIC 0x0001 publicを宣言されました。そのパッケージの外部からアクセスすることができます。
ACC_PRIVATE 0x0002 privateを宣言しました。定義クラス内でのみアクセス可能です。
ACC_PROTECTED 0x0004 protectedを宣言しました。サブクラス内でアクセスできます。
ACC_STATIC 0x0008 staticを宣言しました。
ACC_FINAL 0x0010 finalを宣言しました。上書きされてはならない。
ACC_SYNCHRONIZED 0x0020 synchronizedを宣言しました。
ACC_BRIDGE 0x0040 Javaプログラミング言語用のコンパイラによって生成されたブリッジメソッドを示すために使用されます。Java Generics - Bridge method?を参照してください。
ACC_VARARGS 0x0080 可変数の引数で宣言されています。
ACC_NATIVE 0x0100 nativeを宣言しました。Java以外の言語で実装されている。
ACC_ABSTRACT 0x0400 abstractを宣言しました。実装は提供されていません。
ACC_STRICT 0x0800 strictfpを宣言しました。
ACC_SYNTHETIC 0x1000 コンパイラによって生成されたソースコードに表示されないことを示しています。

name_indexの値はconstant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスです。メソッド名またはまたはが格納されています。

descriptor_indexの値はconstant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスです。メソッド記述子が格納されています。

attributes_count、attributes[]はこのクラスファイルで定義されているクラスにつての属性情報を表すattribute構造体の配列になっています。

attribute構造体

この構造体は属性によって構造体の形が変わります。共通的な形式は以下のようになります。

attribute_info { 
    u2 attribute_name_index; 
    u4 attribute_length; 
    u1 info [attribute_length]; 
}

attribute_name_indexの値はconstant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスです。
attribute_lengthは後続の情報の長さをバイト数であらわします。
infoについては属性ごとにことなります。

属性 Location
SourceFile ClassFile
InnerClasses ClassFile
EnclosingMethod ClassFile
SourceDebugExtension ClassFile
BootstrapMethods ClassFile
ConstantValue field_info
Code method_info
Exceptions method_info
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations method_info 49.0
AnnotationDefault method_info
MethodParameters method_info
Synthetic ClassFile, field_info, method_info
Deprecated ClassFile, field_info, method_info
Signature ClassFile, field_info, method_info
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations ClassFile, field_info, method_info
LineNumberTable Code
LocalVariableTable Code
LocalVariableTypeTable Code
StackMapTable Code
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations ClassFile, field_info, method_info, Code

ここで説明しない項目については下記を参照してください。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7

Code属性

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

attribute_name_indexとattribute_lengthは共通の形式で説明したものです。
attribute_name_indexで指定された文字は「Code」である必要があります。

max_stack項目の値は、 このメソッドのオペランドスタックの最大の深さになります。

max_locals項目の値は、このメソッドの呼出し時に割り当てられたローカル変数の数です。

code_length項目の値は、code[]の数となります。

code配列は、メソッドを実装するJava仮想マシン・コードの実際のバイトを示します。
このコードについては以下に説明があります。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

exception_table_lengthはexception_tableのエントリ数を格納します。

exception_tableは例外情報を表します。
exception_tableの各項目の内容は以下のようになります。
・start_pc、end_pc:例外ハンドラが有効になっているcode配列のインデックス値を表します。javaのコードでいうとtry区で囲まれている範囲になります。
・handler_pcの項目は例外ハンドラの開始するcode配列のインデックス値です。javaのコードでいうとcatch区で囲まれている範囲になります。
・catch_typeは0または、constant_poolテーブルへの有効なインデックスであり、そのインデックスは例外クラスをあらわすCONSTANT_Class_info構造体です。

attributes_count、attributes[]はこのクラスファイルで定義されているクラスにつての属性情報を表すattribute構造体の配列になっています。

HelloWorld.classのバイナリ解析

バイナリエディタの選定と設定

マシン語兄貴は、どんなバイナリエディタを使用しても16進数を読み解くことができるでしょうが、正直、令和の時代になって16進数を読むには辛いので、なるべく楽に読めそうなバイナリエディタを検討してみました。

今回は、BZEditorを使用します。
BZEditorの採用理由は以下の通りです。
・Windowsで使える(最近はビルドすればMacOSでも使える模様)
・構造体表示ができる。
・日ノ本言葉が使える
・ソースが公開されているので、その気になれば拡張できる。
 https://github.com/devil-tamachan/binaryeditorbz

その他のバイナリエディタを検討したい方はWikipediaにバイナリエディタの比較表を確認してみるといいでしょう。
https://en.wikipedia.org/wiki/Comparison_of_hex_editors
この中ではHxDが使いやすそうでした。

BZEditorの設定

構造体の定義

構造体の定義をBZEditorの実行ファイルと同じフォルダにあるBz.defで定義できます。
なお、固定サイズの構造体しか指定できませんので完璧に解析できません。

Bz.def

struct ClassFile_1 {
       BYTE magic[4];
       short minor_version;
       short majoir_version;
       short constant_pool_count;
} class;
struct ClassFile_2 {
       BYTE access_flags[2];
       short this_class;
       short super_class;
       short interfaces_count;
} class;

struct CONSTANT_Class {
       BYTE tag;
       short index;
} class;

struct CONSTANT_Methodref_info {
       BYTE tag;
       short class_index;
       short name_and_type_index;
} class;

struct CONSTANT_Fieldref {
       BYTE tag;
       short class_index;
       short name_and_type_index;
} class;

struct CONSTANT_NameAndType_info {
       BYTE tag;
       short name_index;
       short descriptor_index;
} class;

struct CONSTANT_String_info {
       BYTE tag;
       short string_index;
} class;

struct CONSTANT_Utf8 {
       BYTE tag;
       short length;
} class;

struct Code_attribute {
       short attribute_name_index;
       int attribute_length;
       short max_stack;
       short max_locals;
       int code_length;
} class;

このBZ.defはC言語ライクに記載できます。
使用できる型としては以下のコードのTYPESTR[NUM_MEMBERS] を参照してください。
https://github.com/devil-tamachan/binaryeditorbz/blob/master/Bz/BZFormVw.cpp

BZEditor起動後、「表示」>「構造体表示」をチェックすることで構造体表示用の子ウィンドウが表示されます。
image.png

アドレスをダブルクリックすることで、そのアドレスを始点とした構造体情報を表示します。
image.png

バイトの並びの変更

classファイルを解析するときはMotorolaを選択します。
image.png

BZEditorによるclassファイルの解析

では先頭からClassFileの解析をしていきます。

先頭~constant_pool_countまで

image.png

magicは「0xCAFEBABE」が表示されています。
minor versionは0
major_versionは52です。
constant_pool_countは26になり、次のバイトからconstant_poolのエントリになります。

constant_pool[]の解析

constant_pool[1]

image.png
1バイト目が0x0A=10なので、このconstant_poolのエントリはCONSTANT_Methodrefになります。
class_indexは6、name_and_type_indexは12です。
これらのインデックスが実際になにを指示しているかはconstant_poolを全て見終わった後に確認します。

constant_pool[2]

image.png
1バイト目が0x09なので、このconstant_poolのエントリはCONSTANT_Fieldrefになります。
class_indexは13、name_and_type_indexは14です。

constant_pool[3]

image.png

1バイト目が0x08なので、のconstant_poolのエントリはCONSTANT_Stringになります。
indexは15となります。

constant_pool[4]

image.png

1バイト目が0x0A=10なので、このconstant_poolのエントリはCONSTANT_Methodrefになります。
class_indexは16、name_and_type_indexは17です。

constant_pool[5]

image.png

1バイト目が0x07なので、このconstant_poolのエントリはCONSTANT_Classになります。
indexは18になります。

constant_pool[6]

image.png

1バイト目が0x07なので、このconstant_poolのエントリはCONSTANT_Classになります。
indexは19になります。

constant_pool[7]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは6となり、そのあとの6バイトで「\<init>」という文字を格納しています。

constant_pool[8]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは3となり、そのあとの3バイトで「()V」という文字を格納しています。

constant_pool[9]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは4となり、そのあとの4バイトで「Code」という文字を格納しています。

constant_pool[10]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは4となり、そのあとの4バイトで「main」という文字を格納しています。

constant_pool[11]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは22となり、そのあとの22バイトで「([Ljava/lang/String;)V」という文字を格納しています。

constant_pool[12]

image.png

1バイトめが0x0C=12なので、このconstant_poolのエントリはCONSTANT_NameAndType_info構造体になります。
name_indexは7,descriptor_indexは8になります。

constant_pool[13]

image.png

1バイト目が0x07なので、このconstant_poolのエントリはCONSTANT_Classになります。
indexは20になります。

constant_pool[14]

image.png

1バイトめが0x0C=12なので、このconstant_poolのエントリはCONSTANT_NameAndType_info構造体になります。
name_indexは21,descriptor_indexは22になります。

constant_pool[15]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは11となり、そのあとの11バイトで「Hello World」という文字を格納しています。

constant_pool[16]

image.png

1バイト目が0x07なので、このconstant_poolのエントリはCONSTANT_Classになります。
indexは23になります。

constant_pool[17]

image.png

1バイトめが0x0C=12なので、このconstant_poolのエントリはCONSTANT_NameAndType_info構造体になります。
name_indexは24,descriptor_indexは25になります。

constant_pool[18]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは10となり、そのあとの10バイトで「HelloWorld」という文字を格納しています。

constant_pool[19]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは16となり、そのあとの16バイトで「java/lang/Object」という文字を格納しています。

constant_pool[20]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは16となり、そのあとの16バイトで「java/lang/System」という文字を格納しています。

constant_pool[21]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは3となり、そのあとの3バイトで「out」という文字を格納しています。

constant_pool[22]

image.png
1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは21となり、そのあとの21バイトで「Ljava/io/PrintStream;」という文字を格納しています。

constant_pool[23]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは19となり、そのあとの19バイトで「java/io/PrintStream」という文字を格納しています。

constant_pool[24]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは7となり、そのあとの7バイトで「println」という文字を格納しています。

constant_pool[25]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは21となり、そのあとの21バイトで「(Ljava/lang/String;)V」という文字を格納しています。

HelloWorld.classのconstant_poolのまとめ

constant_poolをまとめると以下のようになります。

No 構造体 内容
1 CONSTANT_Methodref class_indexは6:HelloWorld、name_and_type_indexは12: :V()
2 CONSTANT_Fieldref class_indexは13:java/lang/System、name_and_type_indexは14: out:Ljava/io/PrintStream;
3 CONSTANT_String indexは15:「Hello World」
4 CONSTANT_Methodref class_indexは16:java/io/PrintStream、name_and_type_indexは17:println:(Ljava/lang/String;)V
5 CONSTANT_Class indexは18:「HelloWorld」
6 CONSTANT_Class indexは19:「java/lang/Object」
7 CONSTANT_Utf8 「\<init>」という文字列です
8 CONSTANT_Utf8 「()V」という文字列です
9 CONSTANT_Utf8 「Code」という文字列です
10 CONSTANT_Utf8 「main」という文字列です
11 CONSTANT_Utf8 「([Ljava/lang/String;)V」という文字列です
12 CONSTANT_NameAndType_info name_indexは7:「\<init>」,descriptor_indexは8:「()V」。Method Descriptorsを参照
13 CONSTANT_Class indexは20;「java/lang/System」
14 CONSTANT_NameAndType_info name_indexは21:「out」,descriptor_indexは22:「Ljava/io/PrintStream;」Field Descriptorsを参照
15 CONSTANT_Utf8 「Hello World」という文字列です
16 CONSTANT_Class indexは23:「java/io/PrintStream」
17 CONSTANT_NameAndType_info name_indexは24:「println」,descriptor_indexは25:「(Ljava/lang/String;)V」Method Descriptorsを参照
18 CONSTANT_Utf8 「HelloWorld」という文字列です
19 CONSTANT_Utf8 「java/lang/Object」という文字列です
20 CONSTANT_Utf8 「java/lang/System」という文字列です
21 CONSTANT_Utf8 「out」という文字列です
22 CONSTANT_Utf8 「Ljava/io/PrintStream;」という文字列です
23 CONSTANT_Utf8 「java/io/PrintStream」という文字列です
24 CONSTANT_Utf8 「println」という文字列です
25 CONSTANT_Utf8 「(Ljava/lang/String;)V」という文字列です

access_flags~interfaces[]まで

image.png

・access_flagsは0x0021。つまりACC_SUPER(0x20) と ACC_PUBLIC(0x01)となります。

・this_classはconstant_pool[5]なのでHelloWorldクラスです。

・super_classはconstant_pool[6]なのでjava/lang/Objectクラスです。

・interfaces_countは0で、次に続くinterfaces[]は存在しません。

fields_count~fileds[]まで

image.png

fields_countが0のため、次に続くfiledsは存在しません。

methods_count~methods[]

image.png

methods_countは0x0002のため2件、method_info構造体が続きます。

method_info[0]

image.png

・access_flagsは0x0001。つまりACC_PUBLIC(0x01)となります。

・name_indexはconstant_pool[7]の「\<init>」となります。
 これはJavaコンパイル時に作成された暗黙のコンストラクタになります。

・description_indexはconstant_pool[8]の「()V」となります。

・attributes_countは1となり、attributes構造体が1つ存在します。

method_info[0].attributes[0]

image.png

attribute_name_indexはconstant_pool[9]の「Code」になるため、この構造体はCode_attribute構造体になります。

attribute_lengthは17バイトとなり、この構造体のサイズを決定します。

max_stackは1, max_localsは1となります。

code_lengthは5となり次の「0x 2A B7 00 01 B1」がバイトコードであることを表します。
0x2aはaload_0になります。この命令はthisをオペランドスタックにつみます。

0xb7はinvokesplecialになります。
この命令は後続の2バイトをconstant_poolのインデックスとしてメソッドを呼び出します。
今回の場合「0x00 01」なのでconstant_pool[1]である、「java/lang/Object."":()V」を呼び出すことになります。

0xb1はreturnになります。

exception_table_length, attributes_countはともに0になります。

method_info[1]

image.png

・access_flagsは0x0009。つまりACC_PUBLIC(0x01)とACC_STATIC(0x08)となります。

・name_indexはconstant_pool[10]の「main」となります。

・description_indexはconstant_pool[11]の「([Ljava/lang/String;)V」」となります。

・attributes_countは1となり、attributes構造体が1つ存在します。

method_info[1].attributes[0]

image.png

attribute_name_indexはconstant_pool[9]の「Code」になるため、この構造体はCode_attribute構造体になります。

attribute_lengthは21バイトとなり、この構造体のサイズを決定します。

max_stackは2, max_localsは1となります。

code_lengthは9となり次の「0x B2 00 02 12 03 B6 00 04 B1」がバイトコードであることを表します。

0xb2はgetstaticです。
この命令は後続の2バイトをconstant_poolのインデックスとしてstaticクラスからフィールドを取得します。
今回の場合「0x00 02」なのでconstant_pool[2]である、「java/lang/System」クラスの「 out:Ljava/io/PrintStream」を取得します。
取得した結果はオペランドスタックに積みます。

0x12はldcです。
この命令は後続の1バイトをconstant_poolのインデックスとして使用して、その内容をオペランドスタックに積みます。
今回の場合は「0x03」なのでconstant_pool[3]の「Hello World」という文字列をオペランドスタックに積みます。

0xb6はinvokevirtualです。
この命令は後続の2バイトをconstant_poolのインデックスとして使用して、そのメソッドを実行します。
今回の場合は「0x00 04」なのでconstant_pool[4]である、java/io/PrintStreamクラスのprintln:(Ljava/lang/String;)Vを実行します。

0xb1はreturnになります。

exception_table_length, attributes_countはともに0になります。

ClassFile のattributes_count、attributes[]

image.png

attributes_countが0なのでattributesのデータは存在しません。

クラスファイルの解析のまとめ

このようにJVMの仕様書とバイナリエディタでクラスファイルの解析が行えます。
ただし、こんな面倒なバイナリエディタを使用しなくてもjavapコマンドで解析できます。

>javap -v HelloWorld
Classfile /C:/XXXXXXX/HelloWorld.class
  Last modified 2019/06/09; size 340 bytes
  MD5 checksum 3ee6d0a4b44197baaeb0cec79a0b73d3
public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#12         // java/lang/Object."<init>":()V
   #2 = Fieldref           #13.#14        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #15            // Hello World
   #4 = Methodref          #16.#17        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #18            // HelloWorld
   #6 = Class              #19            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = NameAndType        #7:#8          // "<init>":()V
  #13 = Class              #20            // java/lang/System
  #14 = NameAndType        #21:#22        // out:Ljava/io/PrintStream;
  #15 = Utf8               Hello World
  #16 = Class              #23            // java/io/PrintStream
  #17 = NameAndType        #24:#25        // println:(Ljava/lang/String;)V
  #18 = Utf8               HelloWorld
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/System
  #21 = Utf8               out
  #22 = Utf8               Ljava/io/PrintStream;
  #23 = Utf8               java/io/PrintStream
  #24 = Utf8               println
  #25 = Utf8               (Ljava/lang/String;)V
{
  public HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

バイナリを読むより全然らくですね。
いやぁ、これでHelloWorldの理解が深まりました。めでたしめでたし。

(調査を)まだやるかい

もうちょっとこのHelloWorld.classがどう動くか掘り下げてみようと思います。

インタラプタとコンパイラ

よくJavaは複数回コンパイルされる場合があるといわれますが、どういうことでしょうか。
これについてはTobias Hartmann氏が記述したThe Java HotSpot VMに記載があります。

image.png

上記の図のように作成されたバイトコードはC1またはC2でコンパイルされたマシンコードで実行されるか、インタプリタで実行されるかのいずれかになります。
また、Java8ではインタプリタで動作していたものが途中からC1でコンパイルしたものに変わったり、インタプリタ→C1→C2と段階的にコンパイルしていく場合があります。

つまり、私はHelloWorldがどう動いたなんて全然わかっちゃいなかったんだ・・・

コンパイルされたのかどうか

実行されたバイトコードがインタプリタで実行されたのか、それともコンパイルされたマシンコードで実行されたかを確認する方法はあるのでしょうか?
javaを実行時に「-XX:+PrintCompilation」を使用することで、それは判明します。

>java  -XX:+PrintCompilation HelloWorld
     73    1       3       java.lang.String::hashCode (55 bytes)
     74    2       3       java.lang.String::equals (81 bytes)
     75    4     n 0       java.lang.System::arraycopy (native)   (static)
     76    3       4       java.lang.String::charAt (29 bytes)
     76    5       3       java.lang.Object::<init> (1 bytes)
     78    6       4       sun.misc.ASCIICaseInsensitiveComparator::toLower (16 bytes)
     78    7       4       sun.misc.ASCIICaseInsensitiveComparator::isUpper (18 bytes)
     79    8       4       java.lang.String::length (6 bytes)
     79    9       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
     80   10       3       java.lang.Character::toLowerCase (9 bytes)
     80   11       3       java.lang.CharacterData::of (120 bytes)
     81   15       1       java.lang.Object::<init> (1 bytes)
     81    5       3       java.lang.Object::<init> (1 bytes)   made not entrant
     81   12       3       java.lang.CharacterDataLatin1::toLowerCase (39 bytes)
     82   13       3       java.lang.CharacterDataLatin1::getProperties (11 bytes)
     82   17       3       java.io.WinNTFileSystem::isSlash (18 bytes)
     84   16       3       java.lang.AbstractStringBuilder::append (29 bytes)
     84   18  s    3       java.lang.StringBuffer::append (13 bytes)
     85   14       3       java.lang.Math::min (11 bytes)
     86   19       3       java.lang.StringBuilder::append (8 bytes)
     88   20       3       java.lang.String::getChars (62 bytes)
     90   22       3       java.lang.String::indexOf (70 bytes)
     91   21       3       java.util.Arrays::copyOfRange (63 bytes)
     92   23       3       java.lang.System::getSecurityManager (4 bytes)
Hello World

PrintCompilation を使用することでコンパイルされて実行されたメソッドがわかります。
この出力の詳細についてはStackOverflowとかにありますが、せっかくなのでOpenSDKのソースコードを見てみることにします。

Java8のソースコードは以下から取得可能です。
https://download.java.net/openjdk/jdk8u40/ri/openjdk-8u40-src-b25-10_feb_2015.zip

このコード中でPrintCompilationを付与した際に出力される内容を作成していると考えられる下記のコードをみてみます。

openjdk\hotspot\src\share\vm\compiler\compileBroker.cpp

// ------------------------------------------------------------------
// CompileTask::print_compilation_impl
void CompileTask::print_compilation_impl(outputStream* st, Method* method, int compile_id, int comp_level,
                                         bool is_osr_method, int osr_bci, bool is_blocking,
                                         const char* msg, bool short_form) {
  if (!short_form) {
    st->print("%7d ", (int) st->time_stamp().milliseconds());  // print timestamp
  }
  st->print("%4d ", compile_id);    // print compilation number

  // For unloaded methods the transition to zombie occurs after the
  // method is cleared so it's impossible to report accurate
  // information for that case.
  bool is_synchronized = false;
  bool has_exception_handler = false;
  bool is_native = false;
  if (method != NULL) {
    is_synchronized       = method->is_synchronized();
    has_exception_handler = method->has_exception_handler();
    is_native             = method->is_native();
  }
  // method attributes
  const char compile_type   = is_osr_method                   ? '%' : ' ';
  const char sync_char      = is_synchronized                 ? 's' : ' ';
  const char exception_char = has_exception_handler           ? '!' : ' ';
  const char blocking_char  = is_blocking                     ? 'b' : ' ';
  const char native_char    = is_native                       ? 'n' : ' ';

  // print method attributes
  st->print("%c%c%c%c%c ", compile_type, sync_char, exception_char, blocking_char, native_char);

  if (TieredCompilation) {
    if (comp_level != -1)  st->print("%d ", comp_level);
    else                   st->print("- ");
  }
  st->print("     ");  // more indent

  if (method == NULL) {
    st->print("(method)");
  } else {
    method->print_short_name(st);
    if (is_osr_method) {
      st->print(" @ %d", osr_bci);
    }
    if (method->is_native())
      st->print(" (native)");
    else
      st->print(" (%d bytes)", method->code_size());
  }

  if (msg != NULL) {
    st->print("   %s", msg);
  }
  if (!short_form) {
    st->cr();
  }
}

1列目はタイプスタンプが出力されます。

2列目はcompilation_idとmethod_attributesです。cocmpilation_idは4桁の数字です。
method_attributesはフラグの組み合わせで以下のように表示されます。

文字 条件
% OCRメソッドの場合.enum型のMethodCompilationが定義されていてInvocationEntryBciとInvalidOSREntryBciがあって、このInvalidOSREntryBciの場合
s synchronizedの場合
! exception_handlerを持つ場合
b blockingの場合
n nativeコードの場合

3列目はTieredCompilationがONの場合、コンパイルレベルが表示されます。
このTieredCompilationについては-XX:-TieredCompilationまたは+XX:-TieredCompilationオプションで制御できますが、Java8の場合デフォルトはONです。
コンパイルレベルは以下のようになります。

level 内容
0 interpreter
1 C1 with full optimization (no profiling)
2 C1 with limited profiling
3 C1 with full profiling
4 C2

つまり、同じC1といっても3段階に分かれています。

4列目はメソッド名が出力されます。

さて最初の-XX:+PrintCompilationの出力結果を見てみましょう。
そこにはHelloWorldクラスのmainは含まれいないので、そこのコードはインタプリタで実行されていることがわかります。

マシンコードにコンパイルされているとかいっても、その内容はみえないの?

C1,C2でコンパイルして作成されたマシンコードはファイルに出力されるわけでもなくメモリ上に存在するだけです。
この内容を確認するにはいくつかの手順が必要です。

まず、逆アセンブラができるhsdis-amd64.dllを入手します。
Windowsの場合は以下からダウンロードできるでしょう。
https://sourceforge.net/projects/fcml/files/

DLLをダウンロードしたら、そのDLLにパスを通してください。

その後、以下のコマンドを実行します。

java -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+PrintAssembly -XX:+LogCompilation  HelloWorld

カレントディレクトリにlogファイルが作成されて、どんなマシンコードを作ったか閲覧できます。

出力例

Decoding compiled method 0x0000000002d00750:
Code:
RIP: 0x2d008a0 Code size: 0x000001f0
[Entry Point]
[Constants]
  # {method} {0x00000000192f4fc0} &apos;hashCode&apos; &apos;()I&apos; in &apos;java/lang/String&apos;
  #           [sp+0x40]  (sp of caller)
  0x0000000002d008a0: mov     r10d,dword ptr [rdx+8h]
  0x0000000002d008a4: shl     r10,3h
  0x0000000002d008a8: cmp     r10,rax
  0x0000000002d008ab: jne     2c35f60h          ;   {runtime_call}
  0x0000000002d008b1: nop     word ptr [rax+rax+0h]
  0x0000000002d008bc: nop
[Verified Entry Point]
  0x0000000002d008c0: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x0000000002d008c7: push    rbp
  0x0000000002d008c8: sub     rsp,30h
  0x0000000002d008cc: mov     rax,193e7ac8h
  0x0000000002d008d6: mov     esi,dword ptr [rax+8h]
  0x0000000002d008d9: add     esi,8h
  0x0000000002d008dc: mov     dword ptr [rax+8h],esi
// 略

ログファイルが大量でみずらい・・・

マシンコードを出力したログファイルは大量に情報が出力されていて、目的の情報をみつけるのにも苦労するでしょう。
この場合、JitWatchで閲覧するとよいでしょう。
https://github.com/AdoptOpenJDK/jitwatch/

image.png

image.png

詳しい使い方は以下を参照してください。

JITWatchでJITコンパイルを見よう!
https://www.sakatakoichi.com/entry/2014/12/04/202747

インタプリタは、どうやってバイトコードを解釈しているのか?

いままでで、HelloWorld::mainはインタプリタで動作していることがわかりました。
では以下のgetstatic~returnといった命令は、具体的にどこで、どうやって処理されているのでしょうか?

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

この謎を解明しようとOpenSDKのソースコードを漁っていた調査班はついに、その該当箇所を発見しました。
getstaticや、ldcなどの命令は解釈と実行はbytecodeInterpreter.cppのrunWithChecks()/run()で行っているのを発見したのです。

/*
 * BytecodeInterpreter::run(interpreterState istate)
 * BytecodeInterpreter::runWithChecks(interpreterState istate)
 *
 * The real deal. This is where byte codes actually get interpreted.
 * Basically it's a big while loop that iterates until we return from
 * the method passed in.
 *
 * The runWithChecks is used if JVMTI is enabled.
 *
 */
# if defined(VM_JVMTI)
void
BytecodeInterpreter::runWithChecks(interpreterState istate) {
# else
void
BytecodeInterpreter::run(interpreterState istate) {
# endif
  // 略
# ifndef USELABELS
  while (1)
# endif
  {
# ifndef PREFETCH_OPCCODE
      opcode = *pc;
# endif
      // Seems like this happens twice per opcode. At worst this is only
      // need at entry to the loop.
      // DEBUGGER_SINGLE_STEP_NOTIFY();
      /* Using this labels avoids double breakpoints when quickening and
       * when returing from transition frames.
       */
  opcode_switch:
      assert(istate == orig, "Corrupted istate");
      /* QQQ Hmm this has knowledge of direction, ought to be a stack method */
      assert(topOfStack >= istate->stack_limit(), "Stack overrun");
      assert(topOfStack < istate->stack_base(), "Stack underrun");

# ifdef USELABELS
      DISPATCH(opcode);
# else
      switch (opcode)
# endif
      {
      CASE(_nop):
          UPDATE_PC_AND_CONTINUE(1);
      // 略
      }
    }
  }
}

これはメソッド中の全てのバイトコードを実行するまでwhileでループし、命令コードに合わせてCASE区で分岐されて実行されています。
たとえばgetstaticは以下のような実装になっています。

getstatic

      CASE(_getfield):
      CASE(_getstatic):
        {
          u2 index;
          ConstantPoolCacheEntry* cache;
          // 注釈:現在のバイトコードの位置pc+1から2バイトデータを取得してindexに格納する
          index = Bytes::get_native_u2(pc+1);

          // QQQ Need to make this as inlined as possible. Probably need to
          // split all the bytecode cases out so c++ compiler has a chance
          // for constant prop to fold everything possible away.
          // 注釈:constatnt_tableからindexを指定して値をとる。
          cache = cp->entry_at(index);
          if (!cache->is_resolved((Bytecodes::Code)opcode)) {
            CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode),
                    handle_exception);
            cache = cp->entry_at(index);
          }

# ifdef VM_JVMTI
          if (_jvmti_interp_events) {
            int *count_addr;
            oop obj;
            // Check to see if a field modification watch has been set
            // before we take the time to call into the VM.
            count_addr = (int *)JvmtiExport::get_field_access_count_addr();
            if ( *count_addr > 0 ) {
              if ((Bytecodes::Code)opcode == Bytecodes::_getstatic) {
                obj = (oop)NULL;
              } else {
                obj = (oop) STACK_OBJECT(-1);
                VERIFY_OOP(obj);
              }
              CALL_VM(InterpreterRuntime::post_field_access(THREAD,
                                          obj,
                                          cache),
                                          handle_exception);
            }
          }
# endif /* VM_JVMTI */

          oop obj;
          if ((Bytecodes::Code)opcode == Bytecodes::_getstatic) {
            // 注釈:constant_table[2]のクラス情報を取得してobjに入れる
            Klass* k = cache->f1_as_klass();
            obj = k->java_mirror();
            MORE_STACK(1);  // Assume single slot push
          } else {
            obj = (oop) STACK_OBJECT(-1);
            CHECK_NULL(obj);
          }

          //
          // Now store the result on the stack
          //
          TosState tos_type = cache->flag_state();
          // 注釈:constant_table[2]のフィールド情報を取得してfield_offsetに入れる
          int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {
            if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
              OrderAccess::fence();
            }
            if (tos_type == atos) {
              VERIFY_OOP(obj->obj_field_acquire(field_offset));
              SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), -1);
            } else if (tos_type == itos) {
              SET_STACK_INT(obj->int_field_acquire(field_offset), -1);
            } else if (tos_type == ltos) {
              SET_STACK_LONG(obj->long_field_acquire(field_offset), 0);
              MORE_STACK(1);
            } else if (tos_type == btos) {
              SET_STACK_INT(obj->byte_field_acquire(field_offset), -1);
            } else if (tos_type == ctos) {
              SET_STACK_INT(obj->char_field_acquire(field_offset), -1);
            } else if (tos_type == stos) {
              SET_STACK_INT(obj->short_field_acquire(field_offset), -1);
            } else if (tos_type == ftos) {
              SET_STACK_FLOAT(obj->float_field_acquire(field_offset), -1);
            } else {
              SET_STACK_DOUBLE(obj->double_field_acquire(field_offset), 0);
              MORE_STACK(1);
            }
          } else {
            if (tos_type == atos) {
              // 注釈:constant_table[2]のクラスのフィールドを取得してその結果をオブジェクトとしてスタックに格納する。
              VERIFY_OOP(obj->obj_field(field_offset));
              SET_STACK_OBJECT(obj->obj_field(field_offset), -1);
            } else if (tos_type == itos) {
              SET_STACK_INT(obj->int_field(field_offset), -1);
            } else if (tos_type == ltos) {
              SET_STACK_LONG(obj->long_field(field_offset), 0);
              MORE_STACK(1);
            } else if (tos_type == btos) {
              SET_STACK_INT(obj->byte_field(field_offset), -1);
            } else if (tos_type == ctos) {
              SET_STACK_INT(obj->char_field(field_offset), -1);
            } else if (tos_type == stos) {
              SET_STACK_INT(obj->short_field(field_offset), -1);
            } else if (tos_type == ftos) {
              SET_STACK_FLOAT(obj->float_field(field_offset), -1);
            } else {
              SET_STACK_DOUBLE(obj->double_field(field_offset), 0);
              MORE_STACK(1);
            }
          }
          // getstaticの3バイト先の命令を実行する。
          UPDATE_PC_AND_CONTINUE(3);
         }
// Have to do this dispatch this way in C++ because otherwise gcc complains about crossing an
// initialization (which is is the initialization of the table pointer...)
# define DISPATCH(opcode) goto *(void*)dispatch_table[opcode]
// 略
# define UPDATE_PC_AND_CONTINUE(opsize) {                        \
        pc += opsize; opcode = *pc;                             \
        DO_UPDATE_INSTRUCTION_COUNT(opcode);                    \
        DEBUGGER_SINGLE_STEP_NOTIFY();                          \
        DISPATCH(opcode);                                       \
    }

次の命令を実行する場合はDISPACHが実行されます。
これはgoto文になっており、命令用のラベルにジャンプすることで次の命令を実行しています。

このようにインタラプタに関しては、このコードを起点に見ていけばなんとなくどんな処理をしているかの雰囲気はつかめそうです。
この時点で3万文字こえているのでこれ以上解析した情報をまとめて乗せるのはつらいお

まとめ

今回は初心に戻ってHelloWorldがどのように動作するか見てみました。

「百聞は一見にしかず 百見は一触にしかず」とは言いますが、すみません、HelloWorldなめてました。
すごくめんどくさかったです。

参考:

Demystifying the JVM: Interpretation, JIT and AOT Compilation
https://metebalci.com/blog/demystifying-the-jvm-interpretation-jit-and-aot-compilation/#disqus_thread

DEMYSTIFYING THE JVM: JVM VARIANTS, CPPINTERPRETER AND TEMPLATEINTERPRETER
https://metebalci.com/blog/demystifying-the-jvm-jvm-variants-cppinterpreter-and-templateinterpreter/#disqus_thread

JITWatchによるJava JITコンパイルの調査
https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-MA15-Architect-newland.pdf

JITWatchでJITコンパイルを見よう!
https://www.sakatakoichi.com/entry/2014/12/04/202747

[Java]〈Hello World〉をバイナリエディタだけで使って出力させてみた
https://tech.recruit-mp.co.jp/etc/java_class_hello_world/