20191011のJavaScriptに関する記事は30件です。

初心者によるプログラミング学習ログ 123日目

100日チャレンジの123日目

Twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

123日目は、

本業が忙しいのでよくよく学習できませんでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

const hoge = { ...hoge }はシャローコピーだと思ってたけどシャローコピーじゃなかった

バカみたいな話ですが、今まで、

const hoge = { ...hoge }

みたいなコードを書いてシャローコピーしたつもりになってたんですが、全然そんなことなかったって話です。黒歴史だ・・・

例えば、以下のようなクラスAを作ったとして、

class A {
    constructor() {
        this.aaa = 'aaa';
        this.bbb = {
            ccc: 'ccc'
        };
    }
    afunc() {
        console.log('クラスAの関数だよ');
    }
    get aGetter() {
        return this.aaa;
    }
}

そのインスタンスを以下のように作り、スプレッド演算子でシャローコピー(のつもり)を取ると、

const a = new A()
const _a = { ...a }

_a.aaa_b.bbb.ccc にはアクセスできるんですが、_a.afunc()_a.aGetter にはアクセスできません。

また、instanceof の結果も変わります。

// Aクラスのインスタンスかどうか
console.log(a instanceof A) // => true
console.log(_a instanceof A) // => false

まあ空オブジェクトにプロパティを詰め直しているイメージだと思うので、 instanceof が変わるのはまあそうだよねって感じですが、メソッドやGetterも取れなくなるのはちょっと予想外でした。

と思ったら以下のコードは動くんですよね

const c = {
  d() {
    console.log('d')
  },
}

const _c = { ...c }

_c.d() // => メソッドだけどアクセスできる!!

オブジェクトリテラルだといけるのかよどういうことなんだ・・・

ReactのpropやStore周りを書いていると、スプレッド演算子を使ったこんな感じのコードをよく書いているので、気をつけたいです。

シャローコピーを取る方法

以下のように書くとちゃんとシャローコピーを取れるっぽいです。 instanceof の結果もtrueになります

const _a = Object.assign(Object.create(Object.getPrototypeOf(a)),a)

以上。

以下は検証コードです。暇な人はコピペして実行してみてください

"use strict";
class A {
    constructor() {
        this.aaa = 'aaa';
        this.bbb = {
            ccc: 'ccc'
        };
    }
    afunc() {
        console.log('クラスAの関数だよ');
    }
    get aGetter() {
        return this.aaa;
    }
}

class B extends A {
    constructor() {
        super(...arguments);
        this.ddd = 'ddd';
        this.eee = {
            fff: 'fff'
        };
    }
    bfunc() {
        console.log('クラスBの関数だよ');
    }
    get bGetter() {
        return this.ddd;
    }
}

const b = new B();
const _b = { ...b };
const __b = { ...Object.create(Object.getPrototypeOf(b)), ...b };
const ___b = Object.assign(Object.create(Object.getPrototypeOf(b)), b);

console.log(b);
console.log(_b);
console.log(__b);
console.log(___b);

// Bクラスのインスタンスかどうか
console.log(b instanceof B); // => true
console.log(_b instanceof B); // => false
console.log(__b instanceof B); // => false
console.log(___b instanceof B); // => true

// Aクラスのインスタンスかどうか
console.log(b instanceof A); // => true
console.log(_b instanceof A); // => false
console.log(__b instanceof A); // => false
console.log(___b instanceof A); // => true

// Bクラスメソッドへのアクセス
console.log(b.bfunc); // => 返ってくる
console.log(_b.bfunc); // => undefined
console.log(__b.bfunc); // => undefined
console.log(___b.bfunc); // => 返ってくる

// Aクラスメソッドへのアクセス
console.log(b.afunc); // => 返ってくる
console.log(_b.afunc); // => undefined
console.log(__b.afunc); // => undefined
console.log(___b.afunc); // => 返ってくる

// Bクラスのgetプロパティへのアクセス
console.log(b.bGetter); // => ddd
console.log(_b.bGetter); // => undefined
console.log(__b.bGetter); // => undefined
console.log(___b.bGetter); // => ddd

// Aクラスのgetプロパティへのアクセス
console.log(b.aGetter); // => aaa
console.log(_b.aGetter); // => undefined
console.log(__b.aGetter); // => undefined
console.log(___b.aGetter); // => aaa

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GASによるスクレイピングのために作った機能

Google Apps Scriptでスクレイピングをし易くするために、フロントエンドの方にお馴染みのDOM機能の一部のようなプログラムを作成。

// XmlServiceを前提としたScrapingお助けProgram

/*
* Method: getElementById
* @param {XmlService.Element} element
* @param {string} idToFind
* @return {XmlService.Element} 
* Description: if id does not exist, null will return.
*/
function getElementById(element, idToFind) { 
  var descendants = element.getDescendants();
  for (i in descendants) {
    var elmt = descendants[i].asElement();
    if (elmt != null){
      var id = elmt.getAttribute('id')
      if (id != null) {
        if (idToFind === id.getValue()){
          return elmt;
        }
      }
    }
  }
}

/*
* Method: getElementsByClassName
* @param {XmlService.Element} element
* @param {string} classToFind
* @return {Array XmlService.Element[]}
* Description: if id does not exist, empty Array will return.
*/
function getElementsByClass(element, classToFind){
  var classes = [];
  var descendants = element.getDescendants();
  for (i in descendants) {
    var elmt = descendants[i].asElement();
    if (elmt != null){
      var class = elmt.getAttribute('class');
      if (class != null) {
        if (classToFind === class.getValue()){
          classes.push(elmt.asElement());
        }
      }
    }
  }
  return classes;
}

/*
* Method: getElementsByTagName
* @param {XmlService.Element} element
* @param {string} tagToFind
* @return {Array XmlService.Element[]}
* Description: if id does not exist, empty Array will return.
*/
function getElementsByTagName(element, tagToFind){
  var tags = []
  var descendants = element.getDescendants();
  for (i in descendants) {
    var elmt = descendants[i].asElement();
    if (elmt != null) {
      var tag = elmt.getName();
      if (tag != null) {
        if (tagToFind === tag) {
          tags.push(elmt);
        }
      }
    }
  }
  return tags;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

23行のhtmlでマインスイーパーを作った(Qiitaで遊べるよ!!)

はじめに

以前、html+css+jsで作ったマインスイーパーの記事を書いたのだが200行のVue.jsでスネークゲームを作った100行のHaskellでスネークゲームを作ったの記事がトレンドに上がっているのを見つけ、自分もより短いプログラムでマインスイーパーを実装したくなった。
まずは100行を目指してプログラムを書いていたのだが、どうせならもっと短くしようと思いたった7行でテトリスを実装「七行プログラミング」とはJavaScript ショートコードテクニック集(ES6含む)を参考にしてコードを圧縮した。
その結果、当初の目標を大幅に超える23行でマインスイーパーを実装できた。

さあ、マインスイーパーで遊ぶのだ!

See the Pen HTML_Minesweeper_Min by T.D (@td12734) on CodePen.

大きい画面で遊びたい人はこちら
Qiitaだと横が30行ぐらいでゲームの盤面が潰れ始めるので大画面推奨。

操作説明

  • 左クリック:マスを開ける
  • 右クリック:空いていないマスの上に旗を立てる
  • キーボード:盤面の縦横サイズ、地雷の数を入力する

遊び方

マスをクリックして、地雷が無いマスを全て開けるゲームです。地雷のマスをクリックしたら負け。
開けたマスに書いてある数字はそのマス周囲8マスのうち、地雷があるマスの個数です。
やり直しは「Start」ボタンを押してください。
上の入力フィールドに数値を入力し、ゲームの難易度を変更することも可能です。

参考:Windows版における難易度

  • 難易度:横×縦(地雷数)
  • 初級:9×9(10)
  • 中級:16×16(40)
  • 上級:30×16(99)

プログラム

コード

ゲームは以下のコードで実装した。七行プログラミングのルールに従い、1行は79文字以下としている。

<body>w<input id=W type=number required value=9 min=8 max=70>h<input id=H type=
number required value=9 min=8 max=25>*<input id=M type=number required value=9
min=9><input type=button value=Start onclick=S()><p id=p><table id=t style=
"border:solid;border-collapse:collapse"><script>window.onload=S=function(){if(R
(W))W.value=9;if(R(H))H.value=9;M.max=((w=W.value)-1)*((h=H.value)-1);if(R(M))M
.value=10;m=f=z=M.value;s=performance.now();T();for(i=t.rows.length;i-->0;t.
deleteRow(0));a=new Array(h);for(i=h;i-->0;){a[i]=new Array(w);r=t.insertRow(0)
;for(j=w;j-->0;){d=r.insertCell(o=a[i][j]=0);[d.w,d.h,d.style]=[j,i,"width:24"+
"px;height:24px;border:solid;text-align:center;cursor:default"];d.onclick=
function(){if(!V()||this[v].match("[#-8]"))return;if(!o)do if(a[y=(Math.random(
)*h)|0][x=(Math.random()*w)|0]<1&&(Math.abs(this.w-x)>1||Math.abs(this.h-y)>1)
&&z--)a[y][x]=1;while(z);if(!a[this.h][this.w])N(this);else if(f="Lose")for(i=h
;i-->0;)for(j=w;j-->0;)if(a[i][j])t.rows[i].cells[j][v]=""};d.oncontextmenu=
function(){if(V()&&!this[v].match("[*-8]"))if(!this[v]&&f-->0)this[v]="#";else
if(++f)this[v]="";return !1}}}};T=()=>((p[v="innerHTML"]=((k=((e=((performance.
now()-s)/1e3)|0)/60)|0)<10?"0":"")+k+((l=e%60)<10?":0":":")+l+(+f>=f?" #":" ")+
f)&&V())?setTimeout(T):0;V=()=>+f>=f;R=r=>!r.reportValidity();function N(c){if(
!c||c[v].match("[*-8]"))return;c.style.background="gray";if(~c[v].indexOf("#")
&&f++)T();if(!(c[v]=(a[y=c.h][(x=c.w)-1]>0)+(a[y][x+1]>0)+(y>0&&a[y-1][x-1]>0)+
(y>0&&a[y-1][x]>0)+(y>0&&a[y-1][x+1]>0)+(y<h-1&&a[y+1][x-1]>0)+(y<h-1&&a[y+1][x
]>0)+(y<h-1&&a[y+1][x+1]>0))&&(c[v]="-"))for(c.i=9;c.i-->0;)if(c.i!=4)N(((X=c.w
+1-(c.i/3|0))>=0)*((Y=c.h+1-c.i%3)>=0)*(X<w)*(Y<h)>0?t.rows[Y].cells[X]:0);if(
++o>=w*h-m)f="Win"}</script>

何書いてあるか分からないと思うので書き下したコードを以下に貼る。

<body>
  w<input id=W type=number required value=9 min=8 max=70>
  h<input id=H type=number required value=9 min=8 max=25>
  *<input id=M type=number required value=9 min=9>
  <input type=button value=Start onclick=S()>
  <p id=p>
  <table id=t style="border:solid;border-collapse:collapse">
  <script>
    window.onload = S = function () {
      if (R(W)) W.value = 9;
      if (R(H)) H.value = 9;
      M.max = ((w = W.value) - 1) * ((h = H.value) - 1);
      if (R(M)) M.value = 10;
      m = f = z = M.value;
      s = performance.now();
      T();
      for (i = t.rows.length; i-- > 0; t.deleteRow(0));
      a = new Array(h);
      for (i = h; i-- > 0;) {
        a[i] = new Array(w);
        r = t.insertRow(0);
        for (j = w; j-- > 0;) {
          d = r.insertCell(o = a[i][j] = 0);
          [d.w, d.h, d.style] = [j, i, "width:24px;height:24px;border:solid;text-align:center;cursor:default"];
          d.onclick = function () {
            if (!V() || this[v].match("[#-8]")) return;
            if (!o) do if (a[y = (Math.random() * h) | 0][x = (Math.random() * w) | 0] < 1 && (Math.abs(this.w - x) > 1 || Math.abs(this.h - y) > 1) && z--) a[y][x] = 1; while (z);
            if (!a[this.h][this.w]) N(this);
            else if (f = "Lose") for (i = h; i-- > 0;)for (j = w; j-- > 0;)if (a[i][j]) t.rows[i].cells[j][v] = ""
          };
          d.oncontextmenu = function () {
            if (V() && !this[v].match("[*-8]")) if (!this[v] && f-- > 0) this[v] = "#";
            else if (++f) this[v] = "";
            return !1
          }
        }
      }
    };
    T = () => ((p[v = "innerHTML"] = ((k = ((e = ((performance.now() - s) / 1e3) | 0) / 60) | 0) < 10 ? "0" : "") + k + ((l = e % 60) < 10 ? ":0" : ":") + l + (+f >= f ? " #" : " ") + f) && V()) ? setTimeout(T) : 0;
    V = () => +f >= f;
    R = r => !r.reportValidity();
    function N(c) {
      if (!c || c[v].match("[*-8]")) return;
      c.style.background = "gray";
      if (~c[v].indexOf("#") && f++) T();
      if (!(c[v] = (a[y = c.h][(x = c.w) - 1] > 0) + (a[y][x + 1] > 0) + (y > 0 && a[y - 1][x - 1] > 0) + (y > 0 && a[y - 1][x] > 0) + (y > 0 && a[y - 1][x + 1] > 0) + (y < h - 1 && a[y + 1][x - 1] > 0) + (y < h - 1 && a[y + 1][x] > 0) + (y < h - 1 && a[y + 1][x + 1] > 0)) && (c[v] = "-")) for (c.i = 9; c.i-- > 0;)if (c.i != 4) N(((X = c.w + 1 - (c.i / 3 | 0)) >= 0) * ((Y = c.h + 1 - c.i % 3) >= 0) * (X < w) * (Y < h) > 0 ? t.rows[Y].cells[X] : 0);
      if (++o >= w * h - m) f = "Win"
    }
  </script>

結構長くなったように見えるがこれでも49行しかない。
ただ、コードの解読はまだ困難だと思うので以下で説明を行う。

変数、関数の説明

コード圧縮の都合上、全ての変数名と関数名は1文字になっている。
その結果、可読性が大幅に失われたのでここでは変数名、関数名について説明する。

  • 変数、関数名
    • 1文字に圧縮する前に付けたであろう名前
    • この行以降は動作説明など

グローバル変数

  • a
    • mineArray
    • 地雷があるか否かを格納した配列
    • 0なら空きマス、1なら地雷
  • d
    • td
    • テーブルのセル
  • e
    • elapsedTime
    • ゲーム開始から経過秒
  • f
    • flags,finishString
    • 持っている旗(残りの地雷)の数
    • 地雷を踏んだ時、地雷以外のマスを全て開けた時はゲーム結果の文字列を格納する
  • h
    • height
    • 盤面の縦サイズ
  • i,j
    • お馴染みのループ調整変数
  • k
    • 特に無し
    • 経過分数
  • l
    • 特に無し
    • 経過秒数
  • m
    • mines
    • 地雷の数
  • o
    • opendCells
    • 既にクリックして開けたマスの数
  • p
    • pElement
    • 経過時間、残りの旗の数、勝敗を表示するpタグの要素
  • r
    • tr
    • reportValidity
    • 初期化時はRの引数の役割を果たす
    • その後、テーブルの行を格納する
  • s
    • startTime
    • ゲーム開始時間
  • t
    • tableElement
    • ゲームの盤面
  • v
    • innerHTMLValue
    • 文字列の"innerHTML"
  • w
    • width
    • 盤面の横サイズ
  • x
    • xRandom
    • 地雷敷設時に使用する、ランダムな横の座標
    • 周囲の地雷の数を数える時、横の座標を格納する
  • y
    • yRandom
    • 地雷敷設時に使用する、ランダムな縦の座標
    • 周囲の地雷の数を数える時、縦の座標を格納する
  • z
    • 特に無し
    • 地雷敷設時に使用する、敷設すべき残りの地雷数
  • H
    • HeightInput
    • 縦サイズの入力フィールド
  • M
    • MineInput
    • 地雷の入力フィールド
  • W
    • WidthInput
    • 横サイズの入力フィールド
  • X
    • 特に無し
    • 周囲のマスを開ける時、横の座標を格納する
  • Y
    • 特に無し
    • 周囲のマスを開ける時、縦の座標を格納する

ローカル変数

  • c
    • cell
    • セル
  • h
    • height
    • セルの縦座標
  • i
    • お馴染みのループ調整変数
  • w
    • width
    • セルの横座標

関数

  • N
    • NoMineCellClick
    • 地雷が無いマスをクリックしたときの処理を行う
  • R
    • ReportInValidity
    • 入力フィールドの数値が不正
    • 不正ならtrue
  • S
    • Start
    • Startボタンを押した時やページを開いた時に初期化処理を行う
  • T
    • TextChange
    • タイマー,旗の数のテキストを変更する
  • V
    • isValidGame
    • ゲームが有効か(終了していないか)
    • fが数値(旗の数)ならtrue

動作説明、工夫点

全部解説が必要そうだが、面倒なので一部だけ解説を行う。
気が向けば全部解説するかも。

html部分

<script>タグ以外は閉じタグが無くても動作するので、閉じタグを消して文字を削減している。
属性値の"もstyle以外は無くても動くので省略している。
また、idを設定しているが、idと同じ名前の変数を宣言していなければgetElementByIdをしていなくてもそのidを持つ要素にアクセス可能。

小数点以下を切り捨てる

切り捨ては通常Math.floor()を使うがこれは長すぎる。
なのでビット演算を利用して{数値}|0で小数点以下を切り捨てている。

ゲームが終了していないかどうか

V = () => +f >= f;

上のようにゲームが終了していないかどうかを判断しているが、分かりやすく書くと下と同じことをやっている。

function V () {
  return isNumber(f);
}

fには周囲の地雷の個数、つまり1から8までの数字と勝敗、つまりWinとLoseの文字列が入り得るがfが数字ならゲームが終わっていない(true)を返す。
ただ、isNumberは長いのでfの前に+を付けて数字に変換し、変換前後で値が変わっていないかで数字かどうかを判断している。
仮にfが1なら1 >= 1 → true、fがLoseならNaN >= Lose → falseのように判断される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue + SVG で作るグラフィックアプリの骨格

はじめに

この記事はもう一つの記事、
Vue + Canvas で作るグラフィックアプリの骨格
の SVG 版です。

Canvas や SVG のおかげでブラウザ上でもインタラクティブにグラフィックを簡単に扱えるようになりました。

おまけに Vue を使うとまるでパソコン上のアプリを作るかのように Web 上にグラフィックアプリを作ることができます。

この記事では Vue + SVG でマウスで四角を書いていくような 80 行程度の簡単なサンプルを作ることで、Web 上のグラフィックアプリの骨格を示したいと思います。

できあがったものは、以下のような感じです。
ss.gif

準備

  • node, npm をダウンロードします

https://nodejs.org
この記事では node v12.4.0 で検証しています

  • vue-cli をダウンロードして、とりあえず qvs という名前でアプリを作ります。
$ npm i @vue/cli -g
$ vue create qvs
$ cd qvs
$ yarn serve

この記事では vue 3.12.0 で検証しています。yarn を使ってない人は最後の行は

$ npm run serve

になると思います。

なお、以下のコードは ESLint の掟に従ってないので、vue create qvs の時に ESLint をはずさないと多分エラーになると思われます。
また、セパレータを前に書いたり、引数とか変数の名前が _ だったりするのは単なる趣味ですので、気に入らなくても怒らないでください。

コード

src/App.vue を以下のようにしてみます。

App.vue
<template>
    <div id=app style="overflow: scroll">
        <div id=menu style="position: fixed; top: 0; left: 0">
            <button @click="mode='select'"  >select </button>
            <button @click="mode='rect'"    >rect   </button>
        </div>
        <svg
            style       = "margin: 24px 4px 4px 4px; background: aliceblue"
            :class      = "{ drawRect: mode == 'rect' }"
            :width      = "extent[ 0 ]"
            :height     = "extent[ 1 ]"
            @mousedown  = "mouseDown"
            @mousemove  = "mouseMove"
            @mouseup    = "mouseUp"
            @keyup.esc  = "keyUpESC"
        >
            <template v-for="_ in elements">
                <rect v-bind="_" stroke=black fill=none />
            </template>

            <rect v-bind=dragRect stroke=blue fill=none />
        </svg>
    </div>
</template>

<script>
export default {
    data    : () => (
        {   b           : null
        ,   c           : null
        ,   mode        : 'select'
        ,   elements    : []
        }
    )
,   computed: {
        extent  () {
            return [ 700, 500 ]
        }
    ,   svg     () {
            return this.$el.getElementsByTagName( 'svg' )[ 0 ]
        }
    ,   dragRect() {
            return ( ! this.b || ! this.c )
            ?   null
            :   {   x       : this.b.offsetX
                ,   y       : this.b.offsetY
                ,   width   : this.c.offsetX - this.b.offsetX
                ,   height  : this.c.offsetY - this.b.offsetY
                }
        }
    }
,   methods : {
        mouseDown( _ ) {
            this.b = _
        }
    ,   mouseMove( _ ) {
            this.c = _
        }
    ,   mouseUp( _ ) {
            this.c = _
            switch ( this.mode ) {
            case 'rect':
                this.elements.push( this.dragRect )
                break
            }
            this.b = null
        }
    ,   keyUpESC( _ ) {
            this.mode = 'select'
        }
    }
,   mounted() {
        this.svg.setAttribute( 'tabindex', 0 )
    }
}
</script>

<style>
.drawRect {
    cursor: crosshair
}
</style>

勘所

キーイベントの取得

svg に tabindex 属性をつけると、キーイベントを取得できるようになります。

    this.svg.setAttribute( 'tabindex', 0 )

カーソルの切り替え

mode が 'select' と 'rect' の2つの状態が存在します。
'rect' の状態の時にクロスヘアカーソルを表示するために
クラスとスタイルのバインディングを動的に使っています。

    :class = "{ drawRect: mode == 'rect' }"


<style>
.drawRect {
    cursor: crosshair
}
</style>

v-bind でプロパティを一括設定

プロパティ名と同じキーを持つ辞書を用意して

dragRect = {
    x       : 100
,   y       : 200
,   width   : 300
,   height  : 400
}
<rect v-bind=dragRect />

とやると、

<rect x=100 y=200 width=300 height=400 />

と展開されます。

最後に

この記事が何かの役にたてたら幸いです。わからないことがあればコメントください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

YYTypeScript#5「axiosとfetchの違いが知りたい」「初心者だがCLIツールを作りたい。どう始めるべき?」「Vue 3.0で何が変わるのか?」「デメテルの法則を満たさないコードがだめな理由」「サーバサイドフレームワークを知りたい」「ライブラリの型定義が残念なときの対処法を教えて」「Null Object PatternはTypeScript的にどうなのか?」

これは2019年10月11日に開催したTypeScriptイベントYYTypeScript#4のイベントレポートです。

YYTypeScriptは一言で「TypeScripterの部室」です。発表者の話を聞く「一方向的な勉強会」とは真逆で、TypeScriptについて、雑に・ゆるく・ワイワイ話しながらTypeScripter同士の交流を深める「双方向的な座談会」の形式になります。集まった人たちで「今日話たいこと」「聞きたいこと」をいくつか挙げていき、それをテーマに雑談していきます。

今回の配信動画

過去回の配信動画YouTubeプレイリスト「YYTypeScript」
前回YYTypeScript#3「VueをTSで書いて良かったこと、大変だったことを教えて!」「React Hooksの最適な粒度って?」「クライアントサイドDDD」「皆さん、decorator自作してますか?」「Node.jsゼロインストールのメリットって何?」 - Qiita

雑談

axiosとfetchの違いが知りたい (じゅり)

esa.ioのお引越しツールを開発していて気になった。

・・・

  • superagentというのもありますね(火種)
  • axios → HTTPクライアントライブラリ
  • fetch → HTTPクライアントライブラリ (ブラウザのネイティブ)
  • サーバサイドは?
    • Node.jsにはもともとないので、ライブラリが必要
  • Fetch API - Web API | MDN
  • axiosだと、リクエスト送信時やレスポンス受信時全般のフックを設定できたりとかします
    • 「特定のURLへリクエストする時は、cookieからuser tokenを取り出してheaderにセットしてから送る」とか設定すると、利用側は意識しなくても勝手にやってくれる
  • axiosだと、JSONの取り回しが便利な感がある。
  • Vue使ってる人はaxios馴染みあるよね
    • Vue公式にもfetch APIに対応できますって書いてある
    • メリットとしては外部リソースを必要としないこと
  • polyfill
    • ブラウザによっては未サポートの機能が有るので、どのブラウザでも動かせるようにするために入れる
      • ブラウザがサポートしてる → ネイティブの機能を使う
      • ブラウザがサポートしてない → polyfillが動く
      • PromiseなんかはIEがサポートしてないので、IEで動かすならpolyfill必要だった記憶
  • nuxt.js は axios を捨てて ky になる様子 - Qiita

JSを学んでいるTS(JS)初心者がコマンドで実行するアプリケーションを作ってみようとした場合、どう始めるのが良いか (ぬーたけ)

これをTSで書き直したい。

・・・

  • PHPQueryを使っていたが、TSでは?
    • cheerio - npm
      • jQueryのインターフェイスとほぼ同じ。
      • $でDOM操作できる。
        • PHPでいうところのGoutte
  • ここでなぜNode.jsが出てくるのか?
    • JavaScript(ECMAScript)の実行環境
    • 現状、サーバー上でのjs実行環境はNode.jsしかなかったと思う
  • cheerioで取ってきて、パースすればできそう
  • 開発にはts-nodeを使うと便利
    • 最初はts-node使わないのも楽しい
      • ちゃんとJSに書き換わっているのが分かるので
      • ts-nodeを使わないってどうやるの?
        • ts-node src/index.ts = tsc && node dist/index.js
      • PlayGroundとかはリアルタイムにコンパイルされて結果が見えるので、勉強にはおすすめですよ〜

サーバサイドで重いバッチ書くとき気を付けてることとかあれば知りたいです! (matmau5)

巨大なテキストファイルを読み込んでDBに突っ込むとか

・・・

  • メモリがボトルネックになることがあるから
  • DBに突っ込むならtransaction begin しておいてコケてもやり直しがしやすいようにした方が良さそう

Vue 3.0で何が変わるのか? (すいん)

  • TypeScriptで書き直してると聞いたが。

・・・

  • Reactのhookみたいなのが追加されるっぽい
  • 結構がっつり書き方変わるイメージあります
  • Evan You氏がVue.js 3.0をプレビュー
    • 新しいコアは、圧縮時のサイズが20KBから10KBに削減される見込み
    • Vueは,バージョン3をTSXサポートを備えたTypeScriptで書き直すことで,開発者エクスペリエンスのさらなる向上を目指している。
  • tsx
    • TS XML
    • JSXのTypeScript版
    • JSの中にXMLが書けるJSのサブセット
      • XMLリテラルというXMLがそのまま書けるのが特徴の言語
  • TSXは型の恩恵を受けやすい
  • もともとVueのエンジニアってJSXが嫌でVueを選んだ人もいるはずなので、どうなんだろう?
    • TSX書くならReactでいいじゃんみたいなことは言われそうな気はちょっとしますね・・・
  • Vue2のテンプレートも今までどおり使えるのかな?
  • デザイナーってJSX扱えるの?
    • vueテンプレートの場合は多少勉強してもらう必要があるが、そこまで難しくはないかも。
    • Reactの場合は、デザイナーさんにもTSX(のロジック以外)とCSS(styled-components)も書いてもらっています
      • コンポーネント分け方論争になったりします
      • CSSの中の条件分岐ロジックとか、変数とかも適宜やってもらってたりしています

デメテルの法則 (かきうち)

これがだめな理由を知りたい。

getAnimal()->getCat()->getAmericanShortHair()

・・・

  • この例はあんまり良くなかったと思う (じゅり)
  • 実務でEC周りでgetを連鎖するコードを書いていて疑問だった。
  • デメテルに反するとどういう問題があるのですか?
    • テストが大変になる
    • getの連鎖を解決できる、巨大なオブジェクトを用意しないといけなくなるので・・・
describe('Hoge', () => {
    // Hoge#getXxx內部で return this.aaa.bbb.ccc.ddd.value とかやってると・・・
    const hoge = new Hoge({
        aaa: {
            bbb: {
                ccc: {
                    ddd: {
                        value: 1
                    }
                }
            }
        }
    })

    it('should return xxx', () => {
        expect(hoge.getXxx).toBe(1)
    })
})
  • Viewにコードが散らばるよりは、モデルにgetterがあった方がテストの面でも、仕様追加変更の面でも便利に感じました
  • そのクラスを見れば、何ができるのかがわかるようにするのが良さそう
// リファクタリング後
class Item {
    getItemPriceBySku(sku) {
        ...
    }
}

item.getItemPriceBySku('sku-XXXXX')
  • 呼び出し元が、隣の隣のオブジェクトを知らなくても良くなるようにする
  • Item *-> "0..n" SKU みたいな関係だと、#findXxx とか #filterXxx みたいなメソッド生やすことあります
    • 詳しい抽出条件は利用側で任意に設定できるので、これなら肥大化せずにすむんじゃないかなーと
    • 頻繁に使われる抽出条件が有るなら、それ自体独立した関数として定義するとか、固定のメソッドとして生やしちゃっても良いんじゃないかなと
class Item {
    findPriceBy(finder) {
        return this.skuList.find(finder)
    }
}

// 特定のSKUに紐づく価格がほしい
item.findPriceBy(sku => sku.id === targetSku.id)


// 特定の日に発売された商品の価格がほしい
item.findPriceBy(sku => sku.publishedAt === 'xxxx-xx-xxTxx:xx:xx')
  • 確かに、人が書いたコードで隣の隣の隣のオブジェクトとかを参照していると、テストがわかりずらかった経験があります

サーバーサイドTypeScriptやったことないので、どういうフレームワークとかライブラリがあるのか知りたいです! (KuuK)

何をやりたいかによる
ウェブアプリケーションを作りたい?

・・・

  • TS純正 → Nest.js (フルスタックフレームワーク)
  • express.js (マイクロフレームワーク)
  • koa.js (マイクロフレームワーク)
    • rubyのsinatraみたいなイメージ
    • 薄くて良かった
    • expressを良くしたいというモチベーションで作った
    • 数百行でできている
    • ミドルウェアというプラグインを追加して使う
  • 型定義がちゃんとしたものでないとTSの恩恵を受けづらいので、選ぶときはそこを注意したほうが良い
  • DBだとTypeORM
    • Nest.jsだとバンドルされてる
    • Sequelize というのもある
  • テンプレートエンジンは何がある?

ライブラリの型定義が残念なときのワークアラウンドを知りたい (すいん)

作者に直してもらうのではなくて、自分で使うときに工夫するためのワークアラウンドが知りたい

・・・

  • hoge.d.ts みたいの作ってinterface上書きしてる
  • tsignoreを書いて回避
  • tsconfigにtypeRootでも何でもいいので型定義を上書きをする
    • anyとかobjectを使っちゃってるようなところを、より厳密にする感じです?
      • methodが足りなかったりするのを生やしたり
      • 型定義だけ上書きしても実装は変わらないと思うのですが、型定義不備を補うって感じでしょうか
        • ですね
          • なるほど!
  • PR送り先が無かったりするやつがあったり。。。

Null Object PatternはTypeScript的にどうなのか? (すいん)

https://dev.to/jamesmh/unhealthy-code-null-checks-everywhere-2720 を読んで感じた疑問。

・・・

  • nullガードを書く
  • union型でもいいんじゃない?という感想もある
function doSomething(): null | Foo {}
  • TS3.7からのオプショナルチェイニングで代替できる?
    • ?だらけになるのではー?
    • かなしい・・・
  • 「nullかどうか」を考慮から追い出せるのがNullObjectPatternの旨味だと思ってます
    • オプショナルだとまさに?だらけになりそう、nullガードは分岐だらけになる
      • 結局「nullかどうか」を考慮することは変わらず要求されてる感
      • 安全にはなるけれど、考えるべきことは減ってない
    • 「存在しない」という状態が有るなら、それをNullObjectという形で一つの状態として明示的に定義した方が良さそうな気がします
  • ちゃんと型が定義されていれば、Null Objectパターンじゃなくて良さそう
  • コンパイル時に頑張るのが型で、実行時に落とさなようにするのがNull Object Patternって解釈であっていますかね
    • あってそう

参加してよかったこと(参加者の感想)

  • 初耳なワードを色々しれてよかった
  • 「デメテルの法則」という言葉を知れた。
  • 初心者向けの質問から中、上級者向けの質問まで幅広く楽しめました。
  • サーバーサイドTypeScriptの話や、デメテルの法則の話が聞けて非常に勉強になりました

YYTypeScriptは毎週やってます

YYTypeScriptについてワイワイ話したい方は、YYTypeScriptのイベント情報をチェックしてみて下さい。

以上、YYTypeScriptのレポートでした。次回もワイワイやっていきたいと思います! では、また来週!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

YYTypeScript#4「axiosとfetchの違いが知りたい」「初心者だがCLIツールを作りたい。どう始めるべき?」「Vue 3.0で何が変わるのか?」「デメテルの法則を満たさないコードがだめな理由」「サーバサイドフレームワークを知りたい」「ライブラリの型定義が残念なときの対処法を教えて」「Null Object PatternはTypeScript的にどうなのか?」

これは2019年10月11日に開催したTypeScriptイベントYYTypeScript#4のイベントレポートです。

YYTypeScriptは一言で「TypeScripterの部室」です。発表者の話を聞く「一方向的な勉強会」とは真逆で、TypeScriptについて、雑に・ゆるく・ワイワイ話しながらTypeScripter同士の交流を深める「双方向的な座談会」の形式になります。集まった人たちで「今日話たいこと」「聞きたいこと」をいくつか挙げていき、それをテーマに雑談していきます。

今回の配信動画

過去回の配信動画YouTubeプレイリスト「YYTypeScript」
前回YYTypeScript#3「VueをTSで書いて良かったこと、大変だったことを教えて!」「React Hooksの最適な粒度って?」「クライアントサイドDDD」「皆さん、decorator自作してますか?」「Node.jsゼロインストールのメリットって何?」 - Qiita

雑談

axiosとfetchの違いが知りたい (じゅり)

esa.ioのお引越しツールを開発していて気になった。

・・・

  • superagentというのもありますね(火種)
  • axios → HTTPクライアントライブラリ
  • fetch → HTTPクライアントライブラリ (ブラウザのネイティブ)
  • サーバサイドは?
    • Node.jsにはもともとないので、ライブラリが必要
  • Fetch API - Web API | MDN
  • axiosだと、リクエスト送信時やレスポンス受信時全般のフックを設定できたりとかします
    • 「特定のURLへリクエストする時は、cookieからuser tokenを取り出してheaderにセットしてから送る」とか設定すると、利用側は意識しなくても勝手にやってくれる
  • axiosだと、JSONの取り回しが便利な感がある。
  • Vue使ってる人はaxios馴染みあるよね
    • Vue公式にもfetch APIに対応できますって書いてある
    • メリットとしては外部リソースを必要としないこと
  • polyfill
    • ブラウザによっては未サポートの機能が有るので、どのブラウザでも動かせるようにするために入れる
      • ブラウザがサポートしてる → ネイティブの機能を使う
      • ブラウザがサポートしてない → polyfillが動く
      • PromiseなんかはIEがサポートしてないので、IEで動かすならpolyfill必要だった記憶
  • nuxt.js は axios を捨てて ky になる様子 - Qiita

JSを学んでいるTS(JS)初心者がコマンドで実行するアプリケーションを作ってみようとした場合、どう始めるのが良いか (ぬーたけ)

これをTSで書き直したい。

・・・

  • PHPQueryを使っていたが、TSでは?
    • cheerio - npm
      • jQueryのインターフェイスとほぼ同じ。
      • $でDOM操作できる。
        • PHPでいうところのGoutte
  • ここでなぜNode.jsが出てくるのか?
    • JavaScript(ECMAScript)の実行環境
    • 現状、サーバー上でのjs実行環境はNode.jsしかなかったと思う
  • cheerioで取ってきて、パースすればできそう
  • 開発にはts-nodeを使うと便利
    • 最初はts-node使わないのも楽しい
      • ちゃんとJSに書き換わっているのが分かるので
      • ts-nodeを使わないってどうやるの?
        • ts-node src/index.ts = tsc && node dist/index.js
      • PlayGroundとかはリアルタイムにコンパイルされて結果が見えるので、勉強にはおすすめですよ〜

サーバサイドで重いバッチ書くとき気を付けてることとかあれば知りたいです! (matmau5)

巨大なテキストファイルを読み込んでDBに突っ込むとか

・・・

  • メモリがボトルネックになることがあるから
  • DBに突っ込むならtransaction begin しておいてコケてもやり直しがしやすいようにした方が良さそう

Vue 3.0で何が変わるのか? (すいん)

  • TypeScriptで書き直してると聞いたが。

・・・

  • Reactのhookみたいなのが追加されるっぽい
  • 結構がっつり書き方変わるイメージあります
  • Evan You氏がVue.js 3.0をプレビュー
    • 新しいコアは、圧縮時のサイズが20KBから10KBに削減される見込み
    • Vueは,バージョン3をTSXサポートを備えたTypeScriptで書き直すことで,開発者エクスペリエンスのさらなる向上を目指している。
  • tsx
    • TS XML
    • JSXのTypeScript版
    • JSの中にXMLが書けるJSのサブセット
      • XMLリテラルというXMLがそのまま書けるのが特徴の言語
  • TSXは型の恩恵を受けやすい
  • もともとVueのエンジニアってJSXが嫌でVueを選んだ人もいるはずなので、どうなんだろう?
    • TSX書くならReactでいいじゃんみたいなことは言われそうな気はちょっとしますね・・・
  • Vue2のテンプレートも今までどおり使えるのかな?
  • デザイナーってJSX扱えるの?
    • vueテンプレートの場合は多少勉強してもらう必要があるが、そこまで難しくはないかも。
    • Reactの場合は、デザイナーさんにもTSX(のロジック以外)とCSS(styled-components)も書いてもらっています
      • コンポーネント分け方論争になったりします
      • CSSの中の条件分岐ロジックとか、変数とかも適宜やってもらってたりしています

デメテルの法則 (かきうち)

これがだめな理由を知りたい。

getAnimal()->getCat()->getAmericanShortHair()

・・・

  • この例はあんまり良くなかったと思う (じゅり)
  • 実務でEC周りでgetを連鎖するコードを書いていて疑問だった。
  • デメテルに反するとどういう問題があるのですか?
    • テストが大変になる
    • getの連鎖を解決できる、巨大なオブジェクトを用意しないといけなくなるので・・・
describe('Hoge', () => {
    // Hoge#getXxx內部で return this.aaa.bbb.ccc.ddd.value とかやってると・・・
    const hoge = new Hoge({
        aaa: {
            bbb: {
                ccc: {
                    ddd: {
                        value: 1
                    }
                }
            }
        }
    })

    it('should return xxx', () => {
        expect(hoge.getXxx).toBe(1)
    })
})
  • Viewにコードが散らばるよりは、モデルにgetterがあった方がテストの面でも、仕様追加変更の面でも便利に感じました
  • そのクラスを見れば、何ができるのかがわかるようにするのが良さそう
// リファクタリング後
class Item {
    getItemPriceBySku(sku) {
        ...
    }
}

item.getItemPriceBySku('sku-XXXXX')
  • 呼び出し元が、隣の隣のオブジェクトを知らなくても良くなるようにする
  • Item *-> "0..n" SKU みたいな関係だと、#findXxx とか #filterXxx みたいなメソッド生やすことあります
    • 詳しい抽出条件は利用側で任意に設定できるので、これなら肥大化せずにすむんじゃないかなーと
    • 頻繁に使われる抽出条件が有るなら、それ自体独立した関数として定義するとか、固定のメソッドとして生やしちゃっても良いんじゃないかなと
class Item {
    findPriceBy(finder) {
        return this.skuList.find(finder)
    }
}

// 特定のSKUに紐づく価格がほしい
item.findPriceBy(sku => sku.id === targetSku.id)


// 特定の日に発売された商品の価格がほしい
item.findPriceBy(sku => sku.publishedAt === 'xxxx-xx-xxTxx:xx:xx')
  • 確かに、人が書いたコードで隣の隣の隣のオブジェクトとかを参照していると、テストがわかりずらかった経験があります

サーバーサイドTypeScriptやったことないので、どういうフレームワークとかライブラリがあるのか知りたいです! (KuuK)

何をやりたいかによる
ウェブアプリケーションを作りたい?

・・・

  • TS純正 → Nest.js (フルスタックフレームワーク)
  • express.js (マイクロフレームワーク)
  • koa.js (マイクロフレームワーク)
    • rubyのsinatraみたいなイメージ
    • 薄くて良かった
    • expressを良くしたいというモチベーションで作った
    • 数百行でできている
    • ミドルウェアというプラグインを追加して使う
  • 型定義がちゃんとしたものでないとTSの恩恵を受けづらいので、選ぶときはそこを注意したほうが良い
  • DBだとTypeORM
    • Nest.jsだとバンドルされてる
    • Sequelize というのもある
  • テンプレートエンジンは何がある?

ライブラリの型定義が残念なときのワークアラウンドを知りたい (すいん)

作者に直してもらうのではなくて、自分で使うときに工夫するためのワークアラウンドが知りたい

・・・

  • hoge.d.ts みたいの作ってinterface上書きしてる
  • tsignoreを書いて回避
  • tsconfigにtypeRootでも何でもいいので型定義を上書きをする
    • anyとかobjectを使っちゃってるようなところを、より厳密にする感じです?
      • methodが足りなかったりするのを生やしたり
      • 型定義だけ上書きしても実装は変わらないと思うのですが、型定義不備を補うって感じでしょうか
        • ですね
          • なるほど!
  • PR送り先が無かったりするやつがあったり。。。

Null Object PatternはTypeScript的にどうなのか? (すいん)

https://dev.to/jamesmh/unhealthy-code-null-checks-everywhere-2720 を読んで感じた疑問。

・・・

  • nullガードを書く
  • union型でもいいんじゃない?という感想もある
function doSomething(): null | Foo {}
  • TS3.7からのオプショナルチェイニングで代替できる?
    • ?だらけになるのではー?
    • かなしい・・・
  • 「nullかどうか」を考慮から追い出せるのがNullObjectPatternの旨味だと思ってます
    • オプショナルだとまさに?だらけになりそう、nullガードは分岐だらけになる
      • 結局「nullかどうか」を考慮することは変わらず要求されてる感
      • 安全にはなるけれど、考えるべきことは減ってない
    • 「存在しない」という状態が有るなら、それをNullObjectという形で一つの状態として明示的に定義した方が良さそうな気がします
  • ちゃんと型が定義されていれば、Null Objectパターンじゃなくて良さそう
  • コンパイル時に頑張るのが型で、実行時に落とさなようにするのがNull Object Patternって解釈であっていますかね
    • あってそう

参加してよかったこと(参加者の感想)

  • 初耳なワードを色々しれてよかった
  • 「デメテルの法則」という言葉を知れた。
  • 初心者向けの質問から中、上級者向けの質問まで幅広く楽しめました。
  • サーバーサイドTypeScriptの話や、デメテルの法則の話が聞けて非常に勉強になりました

YYTypeScriptは毎週やってます

YYTypeScriptについてワイワイ話したい方は、YYTypeScriptのイベント情報をチェックしてみて下さい。

以上、YYTypeScriptのレポートでした。次回もワイワイやっていきたいと思います! では、また来週!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ejsのテンプレートに文字列の配列(?)を渡すと「"」(ダブルクォーテーション)が削られてエラーになる対応方法

本内容を利用した場合の一切の責任を私は負いません。

バージョン

  • OS
    OS 名: Microsoft Windows 10 Home
    OS バージョン: 10.0.18362 N/A ビルド 18362
    システムの種類: x64-based PC
  • Chrome
    バージョン: 77.0.3865.90(Official Build) (64 ビット)
  • node.js
    node-v10.16.0-win-x64
    ejs@2.6.2
    express@4.17.1

本題

ejsが古いためかもしれないが、文字列の配列(?)をテンプレートに渡すと、文字列の「"」(ダブルクォーテーション)括りが削られてエラーになってしまう。
その回避方法。

server.js
const express = require('express');
const ejs = require('ejs');

const App = express();

App.engine('ejs', ejs.renderFile);
App.get('/',
    function(req, res)
    {
        res.render('client.ejs',
            {
                ToTemplate: ["ab", "cd", "ef"]
            });
    });
App.listen(8080);
views/client.ejs
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>double quotation into array by ejs</title>
<script id="script_1">
var no_good_1 = <%= ToTemplate %>;
var no_good_2 = [<%= ToTemplate %>];
<%
var no_good_3 = [];
var good = [];

for (var i = 0; i < ToTemplate.length; i++)
{
    no_good_3.push(ToTemplate[i].toString());
    good.push("\"" + ToTemplate[i].toString() + "\"");
}
%>
var no_good_3 = [<%- no_good_3 %>];
var good = [<%- good %>];
</script>
</head>
<body>
&lt;script id="script_1"&gt;」タグ内<br>
<br>
<script>
var content = document.getElementById("script_1");
document.write(content.text);
</script>
</body>
</html>

上記の結果。

out.htm
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>double quotation into array by ejs</title>
<script id="script_1">
var no_good_1 = ab,cd,ef;
var no_good_2 = [ab,cd,ef];

var no_good_3 = [ab,cd,ef];
var good = ["ab","cd","ef"];
</script>
</head>
<body>&lt;script id="script_1"&gt;」タグ内<br>
<br>
<script>
var content = document.getElementById("script_1");
document.write(content.text);
</script>
</body>
</html>
display.txt
「<script id="script_1">」タグ内

var no_good_1 = ab,cd,ef; var no_good_2 = [ab,cd,ef]; var no_good_3 = [ab,cd,ef]; var good = ["ab","cd","ef"];

上記は下記のgithubに。
https://github.com/github895439/other_qiita/tree/master/double_quotation_into_array_by_ejs

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

続 Blockly で中学校プログラミング教育のための教材を作る(プロトタイプ)

目指すこと

前回に引き続き、中学校における「ネットワークを活用した双方向性のあるコンテンツのプログラミング」の教材を目指します。今回のプロトタイプはここからアクセスできます(出席番号は今は使用しないため、送信ボタンを押すだけでエディタが起動します)。

前回からの変更点

前回、WebSocket を使ったデータの送受信機能を実装しましたが、今回は GUI アプリを制作するための環境を構築します。プロトタイプなので、いい加減なところもありますが、以下のような環境を実装しました。

  • 基本的なビジュアル要素(Button, Label, TextBox)が使える
  • イベントハンドラを定義できる
  • ブレークポイントを設定できる

上2つについては Blockly 上からコードを編集できるようにします。ブレークポイントについては generator によるコード生成を行ったあと、Barista言語の runtime が持つ setBreakpoint() 等のメソッドを利用します。

基本的なビジュアル要素を使う

まず GUI アプリの見た目を Blockly で編集します。
スクリーンショット 2019-10-11 19.19.53.png
Button クラスや、TextBox クラスをインスタンス化して、変数に格納するところから始まりますが、中学校ではこの表現は難しいかもしれません。また、ウィンドウに追加しないと何も表示されません。

イベントハンドラを定義する

スクリーンショット 2019-10-11 19.21.48.png
見た目を編集した時に使った変数に対して、イベントハンドラを定義します(画像の上部分「ボタンがクリックされたら〜」のブロック)。「サーバーへ送信する」ブロックの中身は本来 WebSocket を用いた通信ですが、今回はページ内で完結するようにして Barista の runtime を使いました。通信は行われず、別の仮想マシン内で実行されます。
次のデータを処理するブロックで、受け取ったデータを加工して返します。
スクリーンショット 2019-10-11 19.24.09.png
繰り返しますが、今回はまだ WebSocket を使っていません。ここはサーバ側で行う処理を記述しますが、「全員に送信する」とは、WebSocket で接続された全てのクライアントにメッセージを送信するブロックです。ここまで編集したら、プログラムを実行します。
スクリーンショット 2019-10-11 19.24.52.png
画像下部分がウィンドウに相当します。今はボタンが1つと、テキストボックスが1つあります。試しにテキストボックスに名前を入力して、送信するボタンを押してみます。
スクリーンショット 2019-10-11 19.25.18.png
テキストボックスの内容が更新されました。

ブレークポイントを設定する

中学校のプログラミング教育では、デバッグのできる学習環境が求められているようです。例えば Scratch にはそのような機能はありません。そこで試作段階ですが、ブレークポイントを設定して変数の中身を確認できるようにしました。
スクリーンショット 2019-10-11 19.26.20.png
コードをクリックすると、オレンジ色に反転します。送信ボタンを押すとブレークポイントで止まり、message 変数の中身を表示することができます。しかし、続けて送信ボタンが押された時など不完全な部分が残っています。

Blockly エディタにブロックを追加する

本環境に新しくブロックを定義するには、次のような手順が必要です。

  1. barista_blocks.js に、ブロックの構造を記述する
  2. generator を編集して、build.py generators を実行、生成した barista_compressed.js をプロジェクトにコピー
  3. barista_sns.html に必要ならば、built-in function を追加する(runtime.addBuiltinFunction())
  4. toolbox にブロックを追加する

今回のプロトタイプに興味を持たれた方は GitHub よりご連絡お願い致します。

今後の課題

次は編集したプログラムを Flutter サーバに置いて、全ての生徒からアクセスできるようにする必要があります。前回の WebSocket を使った環境と組み合わせれば可能です。
あとはコードの表現を英語ではなく日本語にしたいです。識別子には日本語が使えるので、メソッド名も日本語で記述できるはずです。
全体的には、今のままでは中学校よりも高校レベルになってしまったように思いますので、よりわかりやすく取り組める教材に仕上げていきたいと考えています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue + Canvas で作るグラフィックアプリの骨格

はじめに

この記事はもう一つの記事、
Vue + SVG で作るグラフィックアプリの骨格
の Canvas 版です。

Canvas や SVG のおかげでブラウザ上でもインタラクティブにグラフィックを簡単に扱えるようになりました。

おまけに Vue を使うとまるでパソコン上のアプリを作るかのように Web 上にグラフィックアプリを作ることができます。

この記事では Vue + Canvas でマウスで四角を書いていくような 100 行程度の簡単なサンプルを作ることで、Web 上のグラフィックアプリの骨格を示したいと思います。

できあがったものは、以下のような感じです。
ss.gif

準備

  • node, npm をダウンロードします

https://nodejs.org
この記事では node v12.4.0 で検証しています

  • vue-cli をダウンロードして、とりあえず qvc という名前でアプリを作ります。
$ npm i @vue/cli -g
$ vue create qvc
$ cd qvc
$ yarn serve

この記事では vue 3.12.0 で検証しています。yarn を使ってない人は最後の行は

$ npm run serve

になると思います。

なお、以下のコードは ESLint の掟に従ってないので、vue create qvc の時に ESLint をはずさないと多分エラーになると思われます。
また、セパレータを前に書いたり、引数とか変数の名前が _ だったりするのは単なる趣味ですので、気に入らなくても怒らないでください。

コード

src/App.vue を以下のようにしてみます。

App.vue
<template>
    <div id=app style="overflow: scroll">
        <div id=menu style="position: fixed; top: 0; left: 0">
            <button @click="mode='select'"  >select </button>
            <button @click="mode='rect'"    >rect   </button>
        </div>
        <canvas
            style       = "margin: 24px 4px 4px 4px; background: aliceblue"
            :class      = "{ drawRect: mode == 'rect' }"
            :width      = "extent[ 0 ]"
            :height     = "extent[ 1 ]"
            @mousedown  = "mouseDown"
            @mousemove  = "mouseMove"
            @mouseup    = "mouseUp"
            @keyup.esc  = "keyUpESC"
        />
    </div>
</template>

<script>
export default {
    data    : () => (
        {   b           : null
        ,   c           : null
        ,   mode        : 'select'
        ,   elements    : []
        }
    )
,   computed: {
        extent  () {
            return [ 700, 500 ]
        }
    ,   canvas  () {
            return this.$el.getElementsByTagName( 'canvas' )[ 0 ]
        }
    ,   ctx     () {
            return this.canvas.getContext( '2d' )
        }
    ,   dragRect() {
            return ( ! this.b || ! this.c )
            ?   null
            :   [   this.b.offsetX
                ,   this.b.offsetY
                ,   this.c.offsetX - this.b.offsetX
                ,   this.c.offsetY - this.b.offsetY
                ]
        }
    }
,   methods : {
        draw() {
            this.ctx.clearRect( 0, 0, ...this.extent )

            for ( let _ of this.elements ) this.ctx.strokeRect( ..._ )

            if ( ! this.dragRect ) return
            switch ( this.mode ) {
            case 'rect':
                this.ctx.setLineDash( [ 1 ] )
                this.ctx.strokeRect( ...this.dragRect )
                this.ctx.setLineDash( [] )
                break
            }
        }
    ,   mouseDown( _ ) {
            this.b = _
            this.draw()
        }
    ,   mouseMove( _ ) {
            this.c = _
            this.draw()
        }
    ,   mouseUp( _ ) {
            this.c = _
            switch ( this.mode ) {
            case 'rect':
                this.elements.push( this.dragRect )
                break
            }
            this.b = null
            this.draw()
        }
    ,   keyUpESC( _ ) {
            this.mode = 'select'
            this.draw()
        }
    }
,   mounted() {
        this.canvas.setAttribute( 'tabindex', 0 )
        this.draw()
    }
}
</script>

<style>
.drawRect {
    cursor: crosshair
}
</style>

勘所

キーイベントの取得

canvas に tabindex 属性をつけると、キーイベントを取得できるようになります。

    this.canvas.setAttribute( 'tabindex', 0 )

カーソルの切り替え

mode が 'select' と 'rect' の2つの状態が存在します。
'rect' の状態の時にクロスヘアカーソルを表示するために
クラスとスタイルのバインディングを動的に使っています。

        :class = "{ drawRect: mode == 'rect' }"
<style>
.drawRect {
    cursor: crosshair
}
</style>

最後に

この記事が何かの役にたてたら幸いです。わからないことがあればコメントください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue + SVG で作るグラフィックアプリの骨格

はじめに

Canvas や SVG のおかげでブラウザ上でもインタラクティブにグラフィックを簡単に扱えるようになりました。

おまけに Vue を使うとまるでパソコン上のアプリを作るかのように Web 上にグラフィックアプリを作ることができます。

この記事では Vue + SVG でマウスで四角を書いていくような 80 行程度の簡単なサンプルを作ることで、Web 上のグラフィックアプリの骨格を示したいと思います。

できあがったものは、以下のような感じです。

ss.gif

準備

  • node, npm をダウンロードします

https://nodejs.org
この記事では node v12.4.0 で検証しています

  • vue-cli をダウンロードして、とりあえず qvc という名前でアプリを作ります。
$ npm i @vue/cli -g
$ vue create qvc
$ cd qvc
$ yarn serve

この記事では vue 3.12.0 で検証しています。yarn を使ってない人は最後の行は

$ npm run serve

になると思います。

なお、以下のコードは ESLint の掟に従ってないので、vue create qvc の時に ESLint をはずさないと多分エラーになると思われます。
また、セパレータを前に書いたり、引数とか変数の名前が _ だったりするのは単なる趣味ですので、気に入らなくても怒らないでください。

コード

src/App.vue を以下のようにしてみます。

App.vue
<template>
    <div id=app style="overflow: scroll">
        <div id=menu style="position: fixed; top: 0; left: 0">
            <button @click="mode='select'"  >select </button>
            <button @click="mode='rect'"    >rect   </button>
        </div>
        <svg
            style       = "margin: 24px 4px 4px 4px; background: aliceblue"
            :class      = "{ drawRect: mode == 'rect' }"
            :width      = "extent[ 0 ]"
            :height     = "extent[ 1 ]"
            @mousedown  = "mouseDown"
            @mousemove  = "mouseMove"
            @mouseup    = "mouseUp"
            @keyup.esc  = "keyUpESC"
        >
            <template v-for="_ in elements">
                <rect v-bind="_" stroke=black fill=none />
            </template>

            <rect v-bind=dragRect stroke=blue fill=none />
        </svg>
    </div>
</template>

<script>
export default {
    data    : () => (
        {   b           : null
        ,   c           : null
        ,   mode        : 'select'
        ,   elements    : []
        }
    )
,   computed: {
        extent  () {
            return [ 700, 500 ]
        }
    ,   svg     () {
            return this.$el.getElementsByTagName( 'svg' )[ 0 ]
        }
    ,   dragRect() {
            return ( ! this.b || ! this.c )
            ?   null
            :   {   x       : this.b.offsetX
                ,   y       : this.b.offsetY
                ,   width   : this.c.offsetX - this.b.offsetX
                ,   height  : this.c.offsetY - this.b.offsetY
                }
        }
    }
,   methods : {
        mouseDown( _ ) {
            this.b = _
        }
    ,   mouseMove( _ ) {
            this.c = _
        }
    ,   mouseUp( _ ) {
            this.c = _
            switch ( this.mode ) {
            case 'rect':
                this.elements.push( this.dragRect )
                break
            }
            this.b = null
        }
    ,   keyUpESC( _ ) {
            this.mode = 'select'
        }
    }
,   mounted() {
        this.svg.setAttribute( 'tabindex', 0 )
    }
}
</script>

<style>
.drawRect {
    cursor: crosshair
}
</style>

勘所

キーイベントの取得

svg に tabindex 属性をつけると、キーイベントを取得できるようになります。

        this.svg.setAttribute( 'tabindex', 0 )

カーソルの切り替え

mode が 'select' と 'rect' の2つの状態が存在します。
'rect' の状態の時にクロスヘアカーソルを表示するために
クラスとスタイルのバインディングを動的に使っています。

            :class      = "{ drawRect: mode == 'rect' }"

<style>
.drawRect {
    cursor: crosshair
}
</style>

最後に

この記事が何かの役にたてたら幸いです。わからないことがあればコメントください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript(関数)

関数

「いくつかの処理をまとめたもの」

関数の定義

以下のように関数の用意をすることを「関数を定義する」と呼ぶ

//関数の書き方
const 定数名 = function(){
    まとめたい処理
} 

//例
const introduce = function(){
    console.log(“こんにちは”);
    console.log(“私は山田太郎です”);
} ;

関数の呼び出し

関数を定義しただけでは処理は実行されない

「定数名()」で関数の処理を実行できる

//例
const introduce = function(){
    console.log(“こんにちは”);
    console.log(“私は山田太郎です”);
} ;
introduce();

アロー関数

「function()」の部分を「() =>」と書く
この書き方のことをアロー関数と呼ぶ

//例
const introduce = function(){
    console.log(“こんにちは”);
    console.log(“私は山田太郎です”);
} ;

↓↓↓

const introduce = ()=>{
    console.log(“こんにちは”);
    console.log(“私は山田太郎です”);
} ;

引数

関数に与える追加情報のようなもの

const 定数名 = (引数名)=>{
    
}

//例
const introduce  = (name)=>{
    
}

引数(追加情報)を受け取る、関数(処理)の呼び出し

「定数名(値)」
と書いて呼び出す
関数は指定した値を受け取り、その値は引数に代入される

スクリーンショット 2019-10-11 16.57.10.png
スクリーンショット 2019-10-11 16.59.43.png
(後から情報を設定できるってこと!)

複数の引数を、受け取る

(追加情報が増える)

const 定数名 = (1引数 , 2引数 , ・・・) => {
    処理
}
定数名();

//例
const introduce = (name, age) => {
    console.log(`私は${name}です。`)
    console.log(`私は${age}歳です。`)
}
introduce(“山田太郎” , 22);

//コンソール
私は山田太郎です。
私は22際です。

戻り値

呼び出し元で受けとる処理結果を戻り値と呼ぶ
関数が戻り値を返すと言う

呼び出し元
「定数名(2 ,5);」
引数
「const add = (number1,number2) =>」
戻り値
「7」

戻り値のある関数

関数の中で return を使うと、呼び出し元で値を受け取れるようになる
「return 値」と書くことで、関数はその値を戻り値として返す
関数の呼び出し部分を定数に代入することができる

スクリーンショット 2019-10-11 17.23.03.png

:point_up:returnは関数の処理を終了させる性質を持っている
returnをした後に処理を入れても実行されないので注意!

//例
const add = (a,b) => {
    return a + b ;
    console.log(“計算しました”);
} 
add(1,3);
//コンソール
4

//下線部分は表示されない!

スコープ

関数の引数や変数は、その関数内でしか使うことができない
それぞれの定数や変数の使用できる範囲のことを スコープ と呼ぶ

//関数内で定数を定義した場合

const introduce = () => {
    const name = “山田太郎”;
    
    
    //定数name使える(スコープ)
}
//定数nameが使えない

//関数外で定数を定義した場合
const name = “山田太郎”;
const introduce = () => {
    
    
    //定数name使える(スコープ)
}
//定数name使える(スコープ)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptで既存のオブジェクトにプロパティを足したオブジェクトを作る

以前は

const newObject = Object.assign({newProp:'hoge'},oldObject);

とObject.assignを使って書いていたのですが、

const newObject2 = {newProp:'hoge',...oldObject};

とした方がスマートな気がしてこちらを最近は使っています。
スプレッド構文を使うと、

const newObject3 = {...oldObject,newProp:'hoge'};

とすることで、newPropがすでにoldObjectにあっても、oldObjectを汚さずに上書きできるのも良い感じです。
(Object.assignは第一引数のObjectを書き換えてしまう)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ESLint 7.0.0 の開発が始まる

現在、実施される Breaking Changes について議論中です。Node.js 8.x が EOL になる頃にアルファ版が出始めると予想しています。

実施される可能性が高い Breaking Changes をちょっとだけ覗いてみましょう。

  • Node.js 8.x のサポート終了。
  • RFC20『設定ファイルによる追加の対象ファイル』 ディレクトリを指定してリントした場合、現在は*.jsファイルだけが対象になります。7.0.0 からは、overridesセクションのファイル指定にマッチするファイルも自動的に対象になります。TypeScript や Vue.js など、.js 以外のファイルを利用している場合に便利になります。
  • RFC32『個人設定ファイルを非推奨にする』 ESLint は、CWD 配下に ESLint の設定ファイルが存在しなかった場合、自動的にホームディレクトリの設定ファイルを探して利用します。 7.0.0 ではこの動作が非推奨になります。8.0.0 で削除されると思います。
  • RFC33『ディレクティブ コメントのコメント』 /* eslint-disable no-undef -- typeofで調べたから */ のように、各ディレクティブ コメントに説明を付与できるようになります。反面、-- が設定の中にあるとうまく動かなくなるかもしれません。
  • RFC34『単一行ディレクティブ コメント』 /* eslint-disable */ のようなブロックコメントのみサポートしているディレクティブ コメントが、// eslint-disable のように行コメントでも利用可能になります。ディレクティブ コメントと同じ単語で始まる行コメントが誤動作するかもしれません。
  • RFC37overrides/ignorePatternsの基準パスを変更』 --config/--ignore-path オプションで指定した設定ファイルの overrides/ignorePatterns プロパティの基準パスが CWD に変更されます。検証スクリプト自体をパッケージ化して再利用している場合などは、新しい振る舞いで便利になります。
  • RFC39『プラグインの読込元を変更』 ESLint 5.x までは ESLint がインストールされている場所を基準にプラグインを読み込んでいました。6.x では CWD を基準にプラグインを読み込んでいます。7.0.0 からはリント対象ファイルに最も近い設定ファイルを基準にプラグインを読み込むようになります。モノリポのように、node_modules が複数存在するようなリポジトリを利用している場合に便利になります。
  • RFC40『非同期 API に移行する』 ESLint を Node.js から利用する場合は CLIEngine 等のクラスを利用します。これらがすべて同期 API を使っているため、できないことがたくさんありました。例えば進捗表示、並列化、そして ES Modules の利用等です。7.0.0 では新たに非同期 API を提供し、今の同期 API は非推奨になります。
  • RFC42『並列化』 リント対象のファイル数に応じて、ファイルが多ければスレッドを作って並列処理するようになります。スレッドを使えない古い Node.js で実行されている場合は、代わりに子プロセスで並列化します。PoC では、20 秒程度かかっていた ESLint 自身のソースコードのリントが、7.5 秒程度で終わるようになりました。

※ これらが本当に実施される保証はありません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

A-FrameでWebVRアプリケーションを作成する-クリックイベントを追加してみる

はじめに

前回の記事ではA-Frameを使って色々なオブジェクトを配置しました。今回はこれらに動きを加えていきたいと思います。

準備

準備としてオブジェクトを配置します。前回と同様にデモからソースを引っ張ってきました。

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello, WebVR! • A-Frame</title>
    <meta name="description" content="Hello, WebVR! • A-Frame">
    <script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script>
  </head>
  <body>
    <a-scene background="color: #FAFAFA">
      <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box>
      <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere>
      <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>
      <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane>
    </a-scene>
  </body>
</html>

02_demo01.png

オブジェクトの色を変える

それでは早速イベントを追加していきます。クリックをした時に、視点の中心にあるオブジェクトの色を変えてみたいです。

まずは今のままでは視点の中心が分かりにくいのでカーソルを表示します。<a-scene>タグ内に以下を挿入します。

<a-camera>
  <a-cursor></a-cursor>
</a-camera>

02_demo02.png
色を変更するイベントを追加します。クリックの度に変更する色を赤、緑、青と変化させるようにしました。
イベントの書き方は公式のドキュメントを参考にしています。
https://aframe.io/docs/0.9.0/introduction/javascript-events-dom-apis.html

<script>
  var i = -1;
  var COLORS = ['red', 'green', 'blue'];
  AFRAME.registerComponent('camera-listener', {
    init: function () {
      this.el.addEventListener('click', function (evt) {
        i = (i + 1) % COLORS.length;
        this.setAttribute('material', 'color', COLORS[i]);
      });
    }
  });
</script>

各オブジェクトにこのイベントを割り当てます。"camera-listener"を追記するだけです。

<a-box camera-listener position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box>
<a-sphere camera-listener position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere>
<a-cylinder camera-listener position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>

02_demo03.gif
簡単ですね。

オブジェクトを移動させる

次にオブジェクトを移動させてみましょう。動かしたいオブジェクトをクリック、持っていきたい位置にマウスを動かし再度クリックしたらそこに配置します。
動かしやすいように床を大きくし、オブジェクトを一つだけにしました。

<a-scene background="color: #FAFAFA">
  <a-box position="0 0.5 -3" height="1" color="#4CC3D9" shadow></a-box>
  <a-plane position="0 0 -4" rotation="-90 0 0" width="10" height="10" color="#7BC8A4" shadow></a-plane>
</a-scene>

02_demo04.png
こちらの箱を動かしていきましょう。

<script>
  AFRAME.registerComponent('move', {
    init: function () {
      var isMove = false;
      this.el.addEventListener('click', function (evt) {
        var mode = document.getElementById("mode");
        isMove = !isMove;
        mode.setAttribute('visible', isMove);
      });

      var plane = document.getElementById('plane1');
      plane.addEventListener('mouseenter', function (evt) {
        var box = document.getElementById("box1");
        if(isMove) {
          evt.detail.intersection.point.y += box.getAttribute('height') / 2;
          box.setAttribute('position', evt.detail.intersection.point);
        }
      });
    }
  });
</script>
<a-camera cursor="rayOrigin: mouse;"></a-camera>
<a-box move id="box1" position="0 0.5 -3" height="1" color="#4CC3D9" shadow></a-box>
<a-plane id="plane1" position="0 0 -4" rotation="-90 0 0" width="10" height="10" color="#7BC8A4" shadow></a-plane>
<a-text id="mode" value="Move" color="#000" position="-0.9 2 -3" scale="1.5 1.5 1.5" visible="false"></a-text>

02_demo05.gif
できました。

解説

var isMove = false;
this.el.addEventListener('click', function (evt) {
  var mode = document.getElementById("mode");
  isMove = !isMove;
  mode.setAttribute('visible', isMove);
});

これは箱をクリックした時のイベントです。クリックの度にisMoveを書き換えながら、"Move"の表示非表示を切り替えます。

var plane = document.getElementById('plane1');
  plane.addEventListener('mouseenter', function (evt) {
  var box = document.getElementById("box1");
  if(isMove) {
    evt.detail.intersection.point.y += box.getAttribute('height') / 2;
    box.setAttribute('position', evt.detail.intersection.point);
  }
});

これは地面にマウスカーソルが当たる度に呼ばれるイベントです。isMoveが真ならばマウスカーソルの位置に移動します。

<a-camera cursor="rayOrigin: mouse;"></a-camera>

これはマウスでオブジェクトを操作したかったので追加しています。詳細は公式のドキュメントをご確認ください。
https://aframe.io/docs/0.9.0/components/cursor.html

おわりに

今回はオブジェクトの色を変えたり、移動させたりしました。
次回はこれらを使った簡単なゲームを作ってみたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【adb logcat】windows+javascriptのandroid開発でconsole.logのログ出力をフィルタする方法

解決したいこと

windows 環境でjavascript を使ってandroid アプリを開発しているとき、console.log の出力を確認したかった。
具体的には、cordova を使って開発していた。
デバック方法としては、chrome を使うやり方もあるがadbを使ったほうがアプリを再起動しても常に監視できるので楽。

$ adb logcat | grep CONSOLE

調べるとLinux環境では上記のようにgrep hogeを使ってフィルタしてあげればhogeが含まれる文章だけが表示される
でもwindowsではgrepは使えない...

解決方法

$ adb logcat -s chromium:I

logcat に関する公式の説明を見ると、普通にコマンドからフィルタできた。
上記のコマンドは実際にcordova のconsole.logをフィルタしたい場合の指定の仕方。
フィルタに関する説明は、

「ログ出力をフィルタリングする」から見ることができる。

指定の仕方だが、まずフィルタをかけずにlogcatしてフィルタしたい目的のconsole.logを見つける。

$ adb logcat
>  I/ActivityManager(  585): Starting activity: Intent { action=android.intent.action...}

I,ActivityManagerは任意に変わっているはずなので、それを確認して

$ adb logcat -s ActivityManager:I

とすれば目的のconsole.logだけ出力される

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Promiseのメモ - その3: 小ネタ

Promiseのメモ - その1: resolveとthenPromiseのメモ - その2: チェーン、例外 の追記の小ネタです。

プロミスチェーンは、新しいPromiseオブジェクトを次々に作ってコールバック関数を登録していくものです。ところで、新しいPromiseではなく、同じPromiseに対して2回thenで関数を登録したらどうなるでしょう。

function makePromise(num) {
  return new Promise((resolve, reject) => {
    if(num % 2 == 0)
      resolve('OK');
    else
      reject('NG');
  });
}

let promise = makePromise(2);

promise.then((x) => {
  console.log(`then1 callback: ${x}`);
  return x + x;
});

setTimeout(() => {
  console.log(promise);
  promise.then((x) => {
    console.log(`then2 callback: ${x}`);
  });
}, 1000);
結果
then1 callback: OK
Promise {<resolved>: "OK"}
then2 callback: OK

履行済み、かつコールバック関数を1度実行したことのあるPromiseオブジェクトでも、あとからコールバック関数を追加すれば実行される、ということがわかります。結果の値("OK")はそのままです。

以上、特に実用性はない実験でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでマインスイーパをつくる

動機

暇なときになんとなくマインスイーパを作ってみたら楽しかったので、記録として。
(ゲーム終了判定などは割愛してます。。)

マインスイーパのルールと処理

遊び方はここに分かりやすく載っています。
http://dotpico.com/mine/ja/rule.php

左クリックの処理

  • クリックすると、そのセルを開くことができる
    • そのセルに爆弾が入っていたら、その時点でゲームオーバー
    • そのセルに爆弾が入っていなければ、周囲に存在する爆弾の数が表示される
    • そのセルに爆弾が入っておらず、周囲に1つも爆弾が存在しないとき、周囲のセルが自動的に開かれる

右クリックの処理

  • 1回右クリックすると、そのセルに「フラグ」をつけることができる
  • 2回右クリックすると、そのセルに「?」をつけることができる
  • 3回右クリックすると、元に戻る

ダブルクリックの処理

  • 開いたセルに書いてある数字と、その周囲の「フラグ」の数が一致しているとき、そのセルをダブルクリックすると、「周囲の開かれていないセル」を一度に開くことができる

実装プログラム

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <style>
        td {
            padding: 0px;
            width: 40px;
            height: 40px !important;
            text-align: center;
            border: 1px solid rgb(60, 60, 60);
        }

        /** 未開封のクラス */
        .closed {
            background-color: lightgray;
            color: black;
            cursor: pointer;

        }

        /** 開封して爆弾だったときのクラス */
        .bombed {
            background: red;
            color: white;
            cursor: auto;
        }

        /** 開封して爆弾ではなかったときのクラス */
        .opened {
            background: white;
            color: black;
            cursor: auto;
        }
    </style>
</head>

<body>
    <div id="msg"></div>
    <table>
        <tbody id="target">
            <!-- 中身はJavaScriptでつくる -->
        </tbody>
    </table>

    <script src="main.js" type="text/javascript"></script>
</body>

</html>
main.js
'use strict';

//===================================
// マインスイーパ用のセルクラス
// セルはこのクラスのインスタンスとする
//===================================
class MSCell extends HTMLTableCellElement {

    //-----------------------------------
    // コンストラクタ
    //-----------------------------------
    constructor() {
        super();

        // イベント登録
        this.addEventListener('click', this.clickFunc);
        this.addEventListener('contextmenu', this.clickRightFunc);
        this.addEventListener('dblclick', this.clickDblFunc);
    }

    //-----------------------------------
    // マインスイーパ初期設定
    // x座業、y座標、爆弾かどうかをパラメータにとる
    //-----------------------------------
    init(x, y, bombFlg) {

        // 開封フラグ(未開封のときfalse/開封済みのときtrue)
        this.openedFlg = false;
        // x座標
        this.x = x;
        // y座標
        this.y = y;
        // 爆弾フラグ(爆弾のときtrue/爆弾でなければfalse)
        this.bombFlg = bombFlg;
        // 見た目のクラス
        this.classList.add('closed')

    }

    //-----------------------------------
    // 周辺セルを設定する
    // 周辺セルと、周辺セルの合計爆弾数を設定する
    //-----------------------------------
    setArounds(arounds) {
        // 周辺セル
        this.arounds = arounds;
        // 周辺セルの爆弾数
        this.aroundBombCount = this.arounds.filter(around => around.bombFlg).length;
    }

    //-----------------------------------
    // そのセルの中身を表示する
    //-----------------------------------
    show() {
        if (this.bombFlg) {
            // 爆弾のときは「爆」
            this.textContent = '';

            // 見た目の変更
            this.classList.remove('closed');
            this.classList.add('bombed');
        } else {
            // 爆弾ではないとき
            if (this.aroundBombCount > 0) {
                // 周辺の爆弾数が1個以上のときは数を表示
                this.textContent = this.aroundBombCount;
            }

            // 見た目の変更
            this.classList.remove('closed');
            this.classList.add('opened');
        }
    }

    //-----------------------------------
    // セルを左クリックしたときの関数
    //-----------------------------------
    clickFunc() {

        if (this.openedFlg) {
            // 開封済みのときは何もしない
            return;
        }

        if (this.textContent === '' || this.textContent === '') {
            // 「旗」や「?」がついてるときも何もしない
            return;
        }

        // 開封済みにする
        this.openedFlg = true;

        // このセルを開く
        this.show();

        if (this.bombFlg) {
            // このセルが爆弾のときはゲームオーバーなので全セルを開く
            msCells.forEach(button => button.show());
        } else {
            // このセルが爆弾でないとき
            if (this.aroundBombCount === 0) {
                // 周囲に爆弾が無いときは周囲のセルを全部開く
                this.arounds.forEach(around => around.clickFunc());
            }
        }
    }

    //-----------------------------------
    // セルを右クリックしたときの関数
    //-----------------------------------
    clickRightFunc(e) {

        // 右クリックメニュー禁止
        e.preventDefault();

        if (this.openedFlg) {
            // 既に開かれている場合は何もしない
            return;
        }

        if (this.textContent === '') {
            // 旗を表示
            this.textContent = '';
        } else if (this.textContent === '') {
            // ?を表示
            this.textContent = '';
        } else if (this.textContent === '') {
            // 元に戻す
            this.textContent = '';
        }

    }

    //-----------------------------------
    // セルをダブルクリックしたときの関数
    //-----------------------------------
    clickDblFunc() {
        if (!this.openedFlg) {
            // 既に開かれている場合は何もしない
            return;
        }

        // 周囲の旗の数を取得
        let flgCount = this.arounds.filter(around => around.textContent === '').length;

        // 周囲の旗の数と、クリックしたセルに表示されている爆弾数が一致していれば
        // 周囲のセルをすべて開く
        if (this.aroundBombCount === flgCount) {
            this.arounds.forEach(around => around.clickFunc());
        }
    }

}

//===================================
// カスタム要素の定義
//===================================
customElements.define('ms-td', MSCell, { extends: 'td' });

//===================================
// 全セルを格納しておく変数
//===================================
let msCells = [];

//===================================
// ゲーム初期化用関数
//===================================
let initGame = function (xSize, ySize) {

    // ボタン配置
    for (let y = 0; y < ySize; y++) {
        let tr = document.createElement('tr');
        for (let x = 0; x < xSize; x++) {
            // セルを作る
            let msCell = document.createElement('td', { is: 'ms-td' });
            // セルの初期化
            msCell.init(x, y, Math.random() * 100 < 10);
            // セルをtrにいれておく
            tr.appendChild(msCell);
            // msCellsにも入れておく
            msCells.push(msCell);
        }
        document.getElementById('target').appendChild(tr);
    }

    // aroundsの設定
    msCells.forEach(msCell => {

        // 周囲8マスを取得
        let arounds = msCells.filter(otherCell => {

            if (msCell === otherCell) {
                return false;
            }

            let xArea = [msCell.x - 1, msCell.x, msCell.x + 1];
            let yArea = [msCell.y - 1, msCell.y, msCell.y + 1];

            if (xArea.indexOf(otherCell.x) >= 0) {
                if (yArea.indexOf(otherCell.y) >= 0) {
                    return true;
                }
            }
            return false;
        });

        // 周囲8マスをaroundsとして設定
        msCell.setArounds(arounds);
    });

}

//===================================
// ゲーム初期化
//===================================
initGame(15, 15);

実装のポイント

セルの作り方

JavaScriptのcreateElementでtableタグの中身をつくっていますが、
その中のtdタグは、独自に定義したカスタム要素「MsCell」です。(Msはマインスイーパの略)
MsCellはHTMLTableCellElementを継承しています。

そのため、見た目は普通のtdタグと同じですが、
マインスイーパに必要なopenedFlgなどのプロパティと、clickFuncなどの関数を持っています。

セルに対する動作

ポイントは二つです。

 開いたセルに爆弾が入っていなければ、そのセルの周囲に存在する爆弾の数が表示される

つまり、セルはその周囲のセルの中身を知っている必要があります。
そこで、セルにaroundsというプロパティを持たせ、周囲の8セルを参照させておくことにしました。

 開いたセルに爆弾が入っておらず、そのセルの周囲に1つも爆弾が存在しないとき、周囲のセルが自動的に開かれる

クリックされたセルの周囲の爆弾数で0であれば、そのセルのaroundsもすべて再帰的にクリックしていきます。
また、再帰処理を抜けるための条件として、openedFlg(未開封:false/開封済:true)を持たせています。

参考文献

Custom Elements v1で独自のHTML要素を定義する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デモンエクスマキナのコンソール風アニメーション

ezgif-3-a39df8b3db24.gif

こんな感じのアニメーションをJSで作ってみました。

デモページ: https://sujoyu.github.io/dxm-text-animation/demo

インストール

リポジトリ: https://github.com/sujoyu/dxm-text-animation

npm に上げたりモジュール化してないので愚直に。

<script src="DXMEffect.js"></script>
<script>
  const elements = document.querySelectorAll('.dxm-effect');
  [].forEach.call(elements, function (el) {
      const dxmEffect = new DXMEffect(el, { 
      speed: 30,
      delay: 100,
      })
      dxmEffect.init()
  })
</script>

サンプルコード

普通の使い方

<p class="dxm-effect">The quick brown fox
jumps over the lazy dog.</p>

遅いアニメーション

const elements = document.querySelectorAll('.dxm-effect');
[].forEach.call(elements, function (el) {
    const dxmEffect = new DXMEffect(el, { 
    speed: 300,
    delay: 1000,
    })
    dxmEffect.init()
})

開始にディレイを入れる

<p class="dxm-effect" data-dxmeffect-start-delay="1000">The quick brown fox jumps over the lazy dog.</p>

The quick brown fox jumps over the lazy dog.

複数要素を順次アニメーションする

<p id="effect1" class="dxm-effect">The quick brown fox jumps over the lazy dog.</p>
<p id="effect2" class="dxm-effect" data-dxmeffect-after="#effect1">The quick brown fox jumps over the lazy dog.</p>
<p id="effect3" class="dxm-effect" data-dxmeffect-after="#effect2">The quick brown fox jumps over the lazy dog.</p>

注意

  • 等幅フォント使うのがおぬぬめ。
  • 中に使えるのはプレーンテキストだけです。 white-space: preを使ってるので不要な改行や空白を入れないよう注意してください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript:非数(NaN)かそれ以外かを判断するには Number.isNaN() を使う

数と非数(NaN)のどちらかを返す処理があり、その後の処理のために、NaN を判断する必要があったので調べた結果 JavaScript にはそうしたチェックを行うための関数があるということで、NaN とはというところも含めてまとめておきます。

要約

  • NaN は非数を表す値である。
  • isNaN() 関数は誤った解釈をすることがある。
  • NaN(非数) かどうかをチェックしたい場合は Number.isNaN() を使った方がいい。

NaNとは

NaN とは、Not a Number の略で、つまり「数ではない」値のことです。
Wikipedia の NaN の項においては、以下の様に説明されています。

コンピュータにおいて、主に浮動小数点演算の結果として、不正なオペランドを与えられたために生じた結果を表す値またはシンボルである。 
Wikipedia:NaN より引用

つまり、計算の結果が浮動小数点数だけで表現できない場合に返す値という認識ですかね。

NaN を返すような処理

returnNaN.js
var num_1 = 0 / 0; // 0で0を割る
var num_2 = undefined / 10; // 未定義値を割ったりしようとする。
var num_3 = "string" * 10; // 数以外のものを掛けようとする。
var num_4 = Infinity / Infinity; // 無限大で無限大を割ろうとする。
var num_5 = Math.sqrt(-3); // 負の数の平方根を求めようとする。
  1. これはそのままですね。
  2. 未定義の値を使って四則演算しようとすると NaN を返します。
  3. 文字列と数を掛けようとしたり割ろうとしたりすると NaN を返します。引き算もですね。ただし、JavaScript において、足し算に関しては + 演算子が文字列結合演算子として解釈されるので、文字列と数を足し合わせた文字列が返ってきます。
  4. これもそのままです。
  5. これは結果に虚数が含まれており、浮動小数点数のみで表現することができないため、NaN を返すということらしいです。

Wikipedia:NaN より参考

NaN かどうかを確かめるには

JavaScript において、あるオブジェクトが、NaN を確かめるための組み込み関数やメソッドが用意されています。

isNaN(val) 関数

isNaN() 関数は値が NaN かどうかをチェックする関数です。
冒頭の要約でも述べたように、この関数は非常にややこしい関数で、 val を誤検知して間違った値を返す可能性があるため使わない方がいいです。しかし、後の Number.isNaN() メソッドとの比較のために一応記しておきます。

サンプル

numisNaN.js
var val_1 = 100 * 100;
var check_val_1 = isNaN(val_1); 
console.log(check_val_1);
// => false
var val_2 = 0 / 0;
var check_val_2 = isNaN(val_2);
console.log(check_val_2);
// => true

上記のサンプルは基本的なものですが、以下のようなパターンもあります。

numisNaN.js
var val_3 = "100";
var check_val_3 = isNaN(val_3);
console.log(check_val_3);
// => false

isNaN()関数は、引数が Number 型ではなかった場合、一度 Number 型への型変換を試みるため、false が返ってくるということです。
今回の場合は、確かにval_3は、NaN ではないため、これでよいのですが、以下の様な誤検出を招きます。

numisNaN.js
var val_4 = "string";
var check_val_4 = isNaN(val_4);
console.log(check_val_4);
// => true;
var val_5 = undefined;
var check_val_5 = isNaN(val_5);
console.log(check_val_5);
// => true;

以上のコードではどちらの値も NaN ではないにも関わらず、true を返しています。
isNaN() 関数にはこうした解釈をするため、使用する際にはこうした仕様を考慮する必要がありますが、こういった誤検出を防ぐためにより堅牢な Number.isNaN() メソッドがあるので、そちらを使った方がいいです。

MDN web docs mozilla : isNaN()より参考

Number.isNaN(val)

Number.isNaN() はある値に対して、NaN で、なおかつ Number 型かどうかをチェックします。どちらも満たすのなら、true を返しますが、いずれかを満たさない場合は false を返します。

サンプル

numisNaN.js
var val_6 = 100;
var check_val_6 = Number.isNaN(val_6);
console.log(check_val_6);
// => false;
var val_7 = "100";
var check_val_7 = Number.isNaN(val_7);
console.log(check_val_7);
// => false;
var val_8 = 0 / 0;
var check_val_8 = Number.isNaN(val_8);
console.log(check_val_8);
// => true;

isNaN()関数とほぼ同じように見えますが、Number.isNaN() は、型変換によって NaN となってしまうパターンに惑わされません。

numisNaN.js
var val_9 = "string";
var check_val_9 = Number.isNaN(val_9);
console.log(check_val_9);
// => false;
var val_10 = undefined;
var check_val_10 = Number.isNaN(val_10);
console.log(check_val_10);
// => false;

上記はいずれの値も NaN ではないのに isNaN()関数だと true を返しますが Number.isNaN() を使えば、 false を返してくれます。

MDN web docs mozilla : Number.isNaN()より参考

まとめ

以上のように、Number.isNaN()isNaN()関数よりも堅牢なメソッドであるため、NaN かどうかをチェックしたい場合は Number.isNaN() を使うべきだということがわかりました。

誤った解釈や誤字などありましたら、ご指摘くださりますと大変ありがたいです。

参考

Wikipedia : NaN
MDN web docs mozilla : NaN
MDN web docs mozilla : isNaN()
MDN web docs mozilla : Number.isNaN()

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js: http.IncommingMessageをHTTP形式の文字列に変換する関数

Node.jsのhttp.IncommingMessageをHTTP形式の文字列に変換するデバッグ用関数を作ったので紹介します。

どんな関数かというと、次のように第一引数にhttp.IncommingMessageオブジェクトを渡すと:

textifyRequest(req)

下記のような文字列をPromiseで返す関数です:

POST /foobar HTTP/1.1
Host: localhost:9000
Connection: close
Transfer-Encoding: chunked

hello

http.IncommingMessageをHTTP形式の文字列に変換する関数

const textifyRequest = (req: IncomingMessage): Promise<string> =>
  new Promise(resolve => {
    let text = `${req.method} ${req.url} HTTP/${req.httpVersion}\n`

    // serialize headers
    let nextKey = undefined as string | undefined
    for (let keyOrValue of req.rawHeaders) {
      if (nextKey === undefined) {
        nextKey = keyOrValue
      } else {
        text += `${nextKey}: ${keyOrValue}\n`
        nextKey = undefined
      }
    }
    text += '\n'

    // serialize body
    req
      .on('data', chunk => text += chunk)
      .on('readable', () => req.read())
      .on('end', () => resolve(text))
  })

使用例

// HTTPサーバ
http.createServer((req: IncomingMessage, res: ServerResponse): void => {
  textifyRequest(req).then(console.log) // リクエストを受け取ったところに仕込む
  res.end()
}).listen(9000, () => {
  // 上で立てたHTTPサーバにリクエストを送ってみる
  const req = http.request({ host: 'localhost', port: 9000, method: 'POST', path: '/foobar' })
  req.write('hello')
  req.end()
})

実行結果

POST /foobar HTTP/1.1
Host: localhost:9000
Connection: close
Transfer-Encoding: chunked

hello

注意点

HTTPボディを文字列バッファに積んで返すので、データサイズの大きいリクエストには不向きな実装になっています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】map関数を用いたおしゃれな配列処理

はじめに

決まった繰り返し処理といえばforですが、JavaScriptのES6以降ではmap関数を使ってもっと簡単でおしゃれな書き方で書けます。

map関数は各要素に対して決まった繰り返し処理をして、新しい配列を生成するのに使える便利な関数です

例として、[1,2,3,4,5,6]といったような各要素を二乗した配列を作ることを考えます。

通常のfor文から書き換え

まずは通常のfor文で書いてみます。

const list = [1,2,3,4,5,6];
const newList = [];

for(let i = 0; i< list.length ; i++){
    newList[i] = list[i]*list[i];
}

console.log(newList); //[1,4,9,16,25,36]

list[i]が煩わしい気がします。
これをforEach文で書き直すとこうなります。

const list = [1,2,3,4,5,6];
const newList = [];

list.forEach(function(item, index){
  newList[index] = item*item;
}

console.log(newList);

newListの宣言が煩わしい気がします。
これをmapで修正するとこうなります。

const list = [1,2,3,4,5,6];

const newList = list.map(function(item){
    return item*item;
});

console.log(newList) //[1,4,9,16,25,36]

returnされたものが新しい配列の要素一つになります。

新しい配列はlistから生成するんだ」ということが一瞬で伝わって、非常にすっきり書けていますね。
自分自身を更新(して複製)」する時にMapは便利です。

これを使うとReactのDOMを複数生成するのに便利だったりします。

参考

この記事は「CodeShip」内での実際の質疑応答や指導・アドバイスの一部を基に作成しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ドラクエウォークでも使われているらしい GeoHex を YOLP 地図に表示する

GeoHex とは

GeoHex は地図上を六角形の領域で区切った位置情報表現。
六角形のひとつひとつに場所を表すコードが付いている。

geohex.png

GeoHex

Hexagonal geo-coding system / Original mapping project
GeoHex/ GEOHEX devide whole target area into honeycomb geometry regions.

ドラゴンクエストウォークで使われているらしい

ドラゴンクエストウォークはスマートフォン向け位置情報ゲーム。
参考情報: ドラゴンクエストウォーク 公式プロモーションサイト | SQUARE ENIX

ドラゴンクエストウォークを起動して、各種情報の画面を見ると、 GeoHex のライセンスが表示されている。

dragon-quest-walk.png

GeoHex をどのように使っているのかわからないが、もしかして距離を算出するのに使っているのだろうか?

GeoHex を YOLP 地図上に表示する

YOLP:Yahoo! JavaScriptマップAPI の上に GeoHex を表示するサンプルを作成した。

動作確認環境: macOS Mojave + Google Chrome 77.0.3865.90

geohex-on-yolp-1.png

geohex-on-yolp-2.png

動作サンプルページ: GeoHex on YOLP (Yahoo! Open Local Platform JavaScript Map API)

ソースコード

地図上に GeoHex を表示するための GeoHexLayer クラスを作成し、Map オブジェクトにレイヤー追加することで GeoHex 表示を実現している。

GeoHexLayer クラス (geohexlayer.js)

// GeoHex を表示するレイヤーです
class GeoHexLayer extends Y.Layer {

  constructor() {
    super();
    this.initializedLayer = false; // このレイヤー独自の初期化が終わっているか
  }

  // レイヤーを描画します
  // Y.Layer.drawLayer をオーバーライドしています
  drawLayer(force) {

    // レイヤーの初期化
    if (!this.initializedLayer) {
      this.initializedLayer = true; // レイヤー初期化済みフラグをONに
      this.hexLevel = 7; // Hex の大きさを表すレベル
      this.canvas = null; // 描画用 Canvas
      this.drawnItems = new Array(); // 描画済み Hex のリスト
      this.getMap().bind("move", this.onMove, this); // 地図移動時のイベントを捕捉する
    }

    // GeoHex 描画処理
    this.createCanvas();
    this.drawGeoHexList();
  }

  // 地図移動時に実行します
  onMove() {
    this.drawLayer();
  }

  // Canvas 要素を生成します
  createCanvas () {

    // すでに Canvas 要素が存在していたらクリアする
    if (this.canvas) {
      this.canvas.remove();
      this.drawnItems = new Array();
    }

    // Canvas 要素を生成
    let canvas = document.createElement("canvas");
    canvas.style.position = "fixed";
    canvas.style.top = 0;
    canvas.style.left = 0;

    // 地図を表示している要素を取得
    const container = this.getMapContainer();
    if (container && container[0]) {
      // Canvas 要素のサイズを地図表示している要素に合わせる
      canvas.width = container[0].offsetWidth;
      canvas.height = container[0].offsetHeight;
      // Canvas 要素を追加
      container[0].appendChild(canvas);
    }

    this.canvas = canvas;
  }

  // GeoHex を描画します
  drawGeoHexList() {

    // 地図表示領域の矩形を緯度経度座標で取得
    const bounds = this.getMap().getBounds(); // LatLngBounds
    const sw = bounds.getSouthWest();
    const ne = bounds.getNorthEast();

    // getXYListByRect.js を使用して
    // 矩形領域内の Hex リストを取得 ([{"x":x, "y":y}] 形式)
    const hexBuffer = true;
    const xyList = getXYListByRect(sw.lat(), sw.lng(), ne.lat(), ne.lng(), this.hexLevel, hexBuffer);

    // Hex をひとつずつ描画
    for(let xy of xyList) {
      // hex_v3.2_core.js を使用して Zone オブジェクトを取得
      const zone = GEOHEX.getZoneByXY(xy.x, xy.y, this.hexLevel);
      // Hex を描画
      this.drawGeoHex(zone);
    }
  }

  // Hex を描画します
  // zone: Hex 領域1つを表すオブジェクト
  drawGeoHex (zone) {

    // 描画済みなら何もしない
    if (this.drawnItems.includes(zone)) {
      return;
    }

    // 六角形の緯度経度をピクセル座標に変換
    const pixels = this.coordsToPixels(zone.getHexCoords());

    // Canvas に描画
    const ctx = this.canvas.getContext("2d");
    ctx.strokeStyle = "black";

    // 六角形を描画
    this.storokeHexagon(ctx, pixels);

    // Code を描画 (本来なら配置位置や文字の大きさを動的に調整すべき)
    ctx.fillStyle = "white"; // 背景色
    ctx.fillRect(pixels[1].x + 5, pixels[1].y + 6, 100, 16);
    ctx.fillStyle = "black"; // 文字色
    ctx.font = "14px serif";
    ctx.fillText(zone.code, pixels[1].x + 10, pixels[1].y + 20);

    // 描画済みリストに追加
    this.drawnItems.push(zone);
  }

  // GeoHex 形式の緯度経度の配列をコンテナ座標へ変換します
  // coords: GeoHex 形式の緯度経度オブジェクトの配列
  coordsToPixels(coords) {
    let pixels = new Array();
    for(let c of coords) {
      pixels.push(this.fromLatLngToContainerPixel(new Y.LatLng(c.lat, c.lon)));
    }
    return pixels;
  }

  // 六角形を描画します
  // ctx: CanvasRenderingContext2D オブジェクト
  // pixels: コンテナ座標の配列
  storokeHexagon(ctx, pixels) {
    ctx.moveTo(pixels[5].x, pixels[5].y);
    for(let p of pixels) {
      ctx.lineTo(p.x, p.y);
    }
    ctx.stroke();
  }
};

HTML

<!DOCTYPE html>
<html>
<head>
<title>GeoHex on YOLP (Yahoo! Open Local Platform JavaScript Map API)</title>
<meta charset="UTF-8">
<style>
html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}
#map {
  width: 100%;
  height: 100%;
}
</style>
</head>
<body>

<div id="map"></div>

<!-- YOLP Yahoo! JavaScriptマップAPI -->
<script type="text/javascript" charset="utf-8" src="https://map.yahooapis.jp/js/V1/jsapi?appid=YOUR_APPLICATION_ID"></script>

<!-- GeoHex ライブラリ -->
<script src="http://geohex.net/src/script/hex_v3.2_core.js"></script>
<script src="http://geohex.net/src/script/getXYListByRect.js"></script>

<!-- 今回作成した GeoHexLayer -->
<script src="geohexlayer.js"></script>

<script>

// Map オブジェクトを生成
var ymap = new Y.Map("map", {
  "configure": {
    "dragging": true,
    "singleClickPan": false,
    "doubleClickZoom": true,
    "continuousZoom": true,
    "scrollWheelZoom": true
  }
});

// GeoHexLayer を追加
ymap.addLayer(new GeoHexLayer('map'));

// 名古屋駅付近の地図を表示
var p = {lat : 35.171962, lon : 136.8817322, zoom : 16};
ymap.drawMap(new Y.LatLng(p.lat, p.lon), p.zoom, Y.LayerSetId.NORMAL);

// リサイズ時に地図を再描画
window.addEventListener("resize", () => {
  ymap.updateSize();
});

</script>
</body>
</html>

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

三項演算子の省略形

こんな処理があって

const hoge = true
piyo = hoge === true ? 1 : 0

false時の処理がいらないとする時に

piyo = hoge === true ? 1 : ‘’
piyo = hoge === true ? 1 : null

とかしないで

trueの時
piyo = hoge === true && 1
falseの時
piyo = hoge === true || 0

と書くことができる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js: stream.Readableのデータの前後にデータを追加する方法

Node.jsのstream.Readableのデータの前後にデータを追加する方法を紹介する。

const { Readable } = require('stream')

// 元データのストリーム
const source = Readable.from(['source content\n'])

// 元データの前後にデータを追加する処理(ジェネレーターを使う)
const wrap = (stream) => (async function *() {
    yield 'prepended content\n'
    yield* stream[Symbol.asyncIterator]()
    yield 'appended content\n'
})()

// データが追加されたストリーム
const wrappedContent = Readable.from(wrap(source))
wrappedContent.pipe(process.stdout)

実行結果

prepended content
source content
appended content

stream.Readableには当然ながらwriteメソッドが生えていないので、Readableオブジェクトを初期化する段階で加工済みのデータを渡してやる必要がある。

しかし、ストリームの内容をすべてバッファに入れると、メモリーを消費したりと、ストリーム処理ではなくなっていまうので、ジェネレーターを使う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

30分で基礎がしっかりわかる【Vuex】-入門

はじめに

皆さん、こんにちは!Webシステム開発エンジニアの蘭です!
今日は【Vuex】について語りたいと思います。

Vuexって何?

Vuexはすべてのコンポーネントでデータを一元管理するための仕組みです。

何故Vuex?

Vueで足りるんじゃない?
アプリを構築している中、最初はコンポーネント内だけでdataを操作してたが、コンポネントが多くなってくると、コンポーネントで共有して使うデータが現れます。その時思いついたのが、Vueの$emitやpropsを使うことでコンポーネント間のデータ共有問題を解決してました。

しかしアプリが大きくなるに連れ、コンポーネントが更に多くなり、共有データが300以上に増加、もうこのコンポーネントのデータはどのコンポーネントから持ってきたのかが分からない!:scream:
まさに開発に連れ、地獄の道へと進んでしまってたのです。

その時に、現れたのがVuexでした。

Vuexっていつ導入すべきなのか?

ここで想像してみましょう。
例えばコンポネントは一つの店だとします。
Aコンポーネントはバナナしか在庫がなく、Bコンポーネントはりんご、Cコンポーネントはスイカ等、それぞれ一種類の果物しか預かってません。

それで各コンポーネントがフルーツパフェを作りたい時に、コンポーネントAはBにりんごを買ってきたり、BはAからバナナを入荷してましたが、なにかややこしいですよね。
それでVuexの考え方はコンポネントで皆使う共有の果物は全部Storeという多きな倉庫に入れといて、他の果物が欲しいコンポネントは倉庫から入荷しましょうという改善方法が生まれました。
その倉庫がVuexでの「Store(ストア)」です
めでたし、めでたし:smiley:

・Vuexでのデータ管理のイメージ

共有データを一つのStoreに管理する
alt

Vuexってどんな時に使うといいの?

・アプリケーション全体で使用されるデータ→Vuexで管理する
・コンポーネントの内部のみで使用されるデータ→dataオプションで管理する

実例:Vuexのシンプルなストア(倉庫)

・以下の例はVuexストア(倉庫)から変数countを取得します。

・コンポーネントからVuexストア共有変数countをstore.state.countで取得。
・VuexではStoreの共有変数countを直接変更できない為、
・対策:ストア共有変数を変更する関数increment()をストア内に準備し、コンポーネントではボタンを押した後、store.commit('increment')経由で共有変数countを間接的に更新する。

See the Pen Vuex_Simple_Store_Demo by Uramaya (@uramaya) on CodePen.

要するに:ストアと単純なグローバルオブジェクトの違い

・ストアの状態を直接変更することはできない。明示的にmutationsをコミットすることによってのみストアの状態を変更する。これによりすべての状態変更に追跡可能な記録を残すことが保証される。

Vuexをやってみよう!

Vuexのインストール

・通常開発ではnpmやyarnでインスタンスします。
  ※前提:Vueのインストール済み

vuex_npm_install.
npm install vuex --save
vuex_yarn_install.
yarn add vuex

・npmやyarnでvuexをインストールの方、
 以下のVuexを明示的に導入が必要。

import_Vuex.
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

・試しであれば、cdnでもOK

vuex_cdn.
https://cdnjs.cloudflare.com/ajax/libs/vuex/2.0.0/vuex.js

Vuexの概念

以下の内容はこちらを参照しています。

Vuexのストアは構成要素として5つの概念を持つ
【state】, 【mutations】, 【getters】, 【actions】, 【modules】
以下は上記の5つの概念について説明します。

alt

 1.【state】

  ・ストアで管理する状態(共有変数、データ)。コンポーネントのdataみたいにデータを保存する場所。
  ・gettersから参照され、更新はmutationsで行う

ストアから状態を取り出す`一番シンプルな方法は、算出プロパティ (computed)で取得しまた返すことです。

・こちらは直接'store.state.count'でストア状態を持ってきてますが、

・開発で以下の欠点があります。

モジュールを使うとき、ストアの状態を使っている全てのコンポーネントをインポートしなければなりません。(ストア状態を共有してるモジュールが100個あった場合、地獄に落ちます。)

See the Pen Vuex_Store_Example by Uramaya (@uramaya) on CodePen.

・上記解決方法:ルートコンポーネントに store オプションを指定

これですべての子コンポーネントにストアを共有する事ができます。

index.vue
new Vue({
  el: '#app',
   //ルートインスタンスに store オプションを渡す
   //★これをすることで、this.$store で各コンポーネントから参照することができます。
   store,
});

See the Pen gOOpGxN by Uramaya (@uramaya) on CodePen.

mapState ヘルパー:算出プロパティ(Computed)で宣伝方法の改善

・以下を見るとわかりますが、算出プロパティを全て宣伝するのは冗長です。

Before:元の算出プロパティ(Computed)宣伝方法
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  count2 () {
      return this.$store.state.count2
    }
  count3 () {
      return this.$store.state.count3
    }
   ...
  }
}
After:MapState使用後.
computed: mapState({
    count:  'count',  // count: state => state.countと同義
    count2: 'count2', // count2: state => state.count2と同義
    count3: 'count3'  // count3: state => state.count3と同義
  })

・npmやyarnでvuexをインストールの方、魔法の言葉を忘れずに

mapState.
import {mapState} from 'vuex'

 2.【getters】

  ・state内の状態をもとに算出した値を返す関数が書かれる場所
  ・stateのデータを加工して表示
  ・state加工するので、最初の引数はstate
  ・状態をフィルタリング、カウントした値を返す

See the Pen Vuex_Store_Example_getters by Uramaya (@uramaya) on CodePen.

 3.【mutations】

  ・stateを更新する関数が書かれる場所
  ・stateの更新はしない
  ・第一引数は必ずstate, それ以降の引数はpayload
  ・state状態を更新する際は必ずcommitを使用


See the Pen
Vuex_Store_Example_mutations
by Uramaya (@uramaya)
on CodePen.


 4.【actions】

  ・非同期処理や外部API通信する場所
  ・actionで非同期処理を開始→
   実際stateの更新はmutationsをcommitで実行する
    ⇛actionでstateの更新は行わない
    ⇛最終的にmutationsにデータをコミットする関数
    ⇛commitは同期でなければならない

Actions処理

以下の例でActions処理の過程を見てみよう。

全体の処理の流れとしては、コンポーネントからdispatchでアクションを呼び出し、アクション内で外部APIなどからの非同期処理を行った後、commitでミューテーションを使いステートを更新するという流れとなります。
上記はこちらから引用。

・コンポーネントからストアの処理を呼び出すメモ:
  MutationsはCommit / Actionsはdispatch

コンポーネント内のmethodsにActionsをdispatchで実行.
 methods: {
    increment(plus) {
   //plusは引数
      this.$store.dispatch('incrementAsync', plus);
      this.$store.dispatch('warningAsync');
    }
  },
その後、ストアのActionsでcommitを呼び出します.
  actions:{
    incrementAsync({ commit }, plus) {
      setTimeout(()=>{
        alert("これが非同期処理です。");
        commit('increment', plus)
      }, 5000) //非同期で五秒遅らせる事ができる。
    },
    warningAsync({ commit }) {    
        commit('warning')
    }
  },
そして、commitでMutationsからステートを変更します.
  mutations: {
    //★ここでstate状態を変更する関数を用意
    increment (state, plus) {
      state.total += plus
      state.warning_show = false
    },
    warning (state) {
      state.warning = "5秒お待ち下さい。"
      state.warning_show = true
    }
  }, 

それでは実装してみよう。

非同期処理

See the Pen Vuex_Store_Example_actions by Uramaya (@uramaya) on CodePen.

API通信

・今回非同期のAPI通信では、ライブラリ「axios」を使います。
・async関数を使ってみる(必須ではない):

async関数:
async関数・メソッドはメソッドの冒頭に async をつけることで、 await が使え、awaitは必ずaxios.get等通信処理が終了し、responseもしくはerrorが返ってきた後に、awaitの処理を実行します。

See the Pen ExxVjym by Uramaya (@uramaya) on CodePen.

 【state】、【getters】、【mutations】、【actions】を使ってTodoListを作ってみよう!

ここまで読んでいただき、ありがとうございます!
ではモジュールを理解する前に、以下のコードで遊んでみてください。

See the Pen Vue + Vuex Demo by Arpit Gupta (@arpitg) on CodePen.

 5.【modules】

  ・上記4つのストア構成要素を分割したもの。
  ・アプリケーションの肥大化に伴い大きくなるストアに対し、見通しをよく保つためにモジュールに分割する。

以下の記事を参考しました。
  Vuex公式-モジュール

開発に連れ、コードが膨大する中、全て何百個のstate, getters, mutations, actionsを一つのファイルにまとめるのは事実上管理不可能です。
ここで解決法が【モジュール】です。
モジュールとは元々凄く膨大なstateやgetters等を小さなモジュール単位に分割して、管理することです。
もちろん、これで保守、修正、管理が便利になります。

モジュールの使い方を見てみよう!

・1.同一ファイルでモジュール分け
同一ファイルでモジュール分け.
//モジュールA
const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

//モジュールB
const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    module_a: moduleA,
    module_b: moduleB
  }
})

store.state.module_a // -> `moduleA` のステート
store.state.module_b // -> `moduleB` のステート
・2.違うファイルでモジュール分け(現場)

・モジュールの分け方について以下を参考しました。
  Vuex の Modules 機能
  vuexでmoduleを分ける方法と注意点

 ・(1)モジュール:「superFunction」と「header」を storeに登録
/store/index.js
import Vue from "vue";
import Vuex from "vuex";

import superFunction from "./superFunction";
import header from "./header";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    superFunction,
    header
  }
});

export default store;
 ・(2)モジュールの指定を明確にする為、 namespaced: true にすることを忘れずに

 ・namespacedについて以下を引用しました。
   Vuexのストアをモジュールに分割する

・Vuex名前空間の概念:【namespaced: true】 とは

namespaced:オプションをtrueにすることで、それぞれのモジュールに名前空間を与えて呼び出し方を管理することができる。

ただstateについては、namespaced: trueの有無は関係なく一律で$store.state.(モジュール名).data.messageのように呼び出す。

その他のmutation, action, getterはnamespaced: trueを与えなかった場合はモジュールを使用せずグローバルに登録したときと同じように呼び出せる。

もしnamespaced: trueを与えていない複数のモジュール内で名前が被った場合、mutationとactionはそれぞれが同時に実行される。
getterは以下のようなエラーが発生する。

[vuex] duplicate getter key: greetingC.

namespaced: trueを与えたmutation, action, getterは、名前の前にモジュール名/を付与して呼び出すことで、上記のエラーを避けられます。

console.log(this.$store.getters['moduleA/greeting']
console.log(this.$store.commit('moduleA/greeting')
console.log(this.$store.dispatch('moduleA/greeting')
// getterは[], mutationとactionは()で囲むので注意

・モジュールを作ってみよう

/store/superFunction/index.js
const state = {
  appNumber: 0
};

const getters = {
  appNumber(state) {
    return state.appNumber;
  }
};

const actions = {
  changeNumber({ commit }, val) {
    commit("changeNumber", val);
  }
};

const mutations = {
  changeNumber(state, value) {
    state.appNumber = state.appNumber + value;
  }
};

const superFunction = {
  namespaced: true, // 忘れずに
  state,
  getters,
  actions,
  mutations
};

export default superFunction; //モジュールの名前
 ・(3)コンポネントでモジュール:superFunctionを使う

以下の注意点:
・getter と action の参照の仕方が変わる
・this.$store.getters["moduleName/getterName"]
・this.$store.dispatch("moduleName/actionName")

/components/AppMain.vue
<template>
  <main>
    {{appNumber}}
    <Controller :changeNumber="changeNumber"/>
  </main>
</template>

<script>
import Controller from "./Controller";
export default {
  components: {
    Controller
  },
  computed: {
    appNumber() {
      return this.$store.getters["superFunction/appNumber"];
    }
  },
  methods: {
    changeNumber(val) {
      this.$store.dispatch("superFunction/changeNumber", val);
    }
  }
};
</script>

・上記の実際のソースコードを見て試してみよう【Codesandbox】
https://codesandbox.io/embed/w67wklz0pk?fontsize=14

おまけに

・npmやyarnでVuexをインストールした方は、必ずuseでVuexの参照をしてください。

/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import App from './App.vue'

Vue.use(Vuex); //忘れずに

const store = new Vuex.Store({
......

・Vuexで開発する際に、以下の記事も参考にできればと思います。
  Vuexを用いた開発プロジェクト用にガイドラインを作成した話
  メンテナンスしやすいVueComponentを設計するために気をつけていること

・VueとVuexの練習
 30minくらいで学ぶVue.jsとVuex
 簡単なTODOアプリで Vue + Vuex を学んでみよう
 簡単なTODOアプリソースコード(Github)

・Vueの基本概念
 10分で基礎がわからるVue.js-入門

最後に

今回はVuexについて基本の使い方を紹介しました。
自分のニーズに沿って、是非Vuexを開発で使って見てください!:D

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js: stream.Readableを作るシンプルな方法

本稿では、どのようにしてNode.jsのstream.Readableを作ったらいいかを示す。

stream.Readableを作るサンプルコード

const { Readable } = require('stream')
const stream = Readable.from(['hello'])
stream.pipe(process.stdout)

実行結果

hello

説明

Readable.fromメソッドはReadableオブジェクトを生成するユーティリティメソッドで、第一引数にIterable<any>またはAsyncIterable<any>を取る。

上のサンプルコードではArray<string>を渡しているが、Iterable反復処理プロトコルを実装しているオブジェクトなら何でもいいので、配列の他にジェネレーターを渡すこともできる:

const gen = function *() {
    yield 'hello'
    yield 'world'
}
const stream = Readable.from(gen())
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】PHPとAjax(jquery)で進捗状況(プログレスバー)を表示する

概要

  • 社内ツールでビューからオブジェクトを呼び出してデータの挿入をするツールを作りました
  • 処理に時間がかかるためプログレスバーを表示する方向にしました
  • データを送るということが構造上?知識的に難しかったので一時ファイルに吐き出し、それをajaxで一定時間ごとに読み込むという形で実現できました

最終的にどうなったか

  • テスト検証のgifで実際にデータを入れているシーンなので進捗の推移が遅いです。

progress.gif

処理構成(簡潔に)

  1. index.php(ビューとビューからの処理が書かれている)の内容が画面に表示されています
  2. ユーザーは色々とチェックボックスやラジオボタンなどの条件を入れます
  3. 確認ボタンを押すと2.で入力された条件を元にデータを抽出しします(抽出したのは画面に表示)
  4. さらに登録ボタンを押すと、抽出された内容がDBにInsertされ処理が完了するというものです。

データの挿入条件

  • 約8万件の情報
  • DBの負荷を考えて5000件単位でInsert
  • 処理開始から終了まで約15分

挿入中の画面状態

  • 左上のくるくるがずっと出ている感じ

挿入後の画面状態

  • 挿入後は画面が切り替わり、例外等なければ完了メッセージが出る

環境

  • PHP5.2.8
  • RHEL4 、、、社内ツールということもありリプレースの予定もなく古いです。

ajaxで値のやり取りではなくローカルファイルから随時読み込む形に

進捗状況はアップロードばかり?調べてもあまり出てこない

  • 調べても出てくるのはファイルをローカルからサーバーにアップロードするときの進捗状況の表示ばかりでした
  • 大体がビューと同じファイルに書かれた処理の値をjs側で取得している内容でした
  • 別phpファイルにリクエストを投げて取得するのもありましたが今回はオブジェクトとして呼び出したファイルから3ファイル先のオブジェクトを呼び出しそこでInsertするようにしていたので実現したいことと違う処理に感じました

実装(実際よりも簡単にしたのを書きます)

進捗状況までの流れ

1.Insert開始
2.Insertはループになっておりそのなかで進捗状況を上書きで書き出すようにしました

insert.php
foreach ($idData as $id) {
    // 進捗率を求める(小数点以下はfloor()で切り捨て)
    $percent = floor($insertedIdCnt / $allIdCnt * 100);

    // ファイルに書込み
    $fp = fopen('percentNow.log', 'w');
    fwrite($fp, $percent);
    fclose($fp);

    // 挿入処理

    // 1ループ10秒止めておく
    sleep(10);
}
  • この段階で毎ループで進捗率($percent)が上書きされていきます
  1. ビュー側で上で吐き出したファイルを読み込んでいきます(以下ではjsファイルとして書いてますが実際にはphpファイルです
progress.js
var Progress = (function() {
    function Progress(p) {
        this.bar = document.querySelectorAll('#progressBar > .progressBarBody')[0];
        this.p = p;
        this.update();
    }
    Progress.prototype.update = function() {
        this.bar.style.width = this.p + '%';
    }
    Progress.prototype.countup = function(data) {
        if (this.p < 100) {
            this.p = Number(data);
        }
        this.update();
    }
    return Progress;
}());

// 進捗率と進捗バーを更新する部分です
var updateProgress = function(progress) {
    $.ajax('./hogehoge/percent.log', {
        dataType: 'text',
        success: function(data) {
            $('#progress').html('進捗状況: '+data+'%');
                progress.countup(data);
        }
    });
}

// 今回はsubmitが2つあるためclickとボタンのID(#register)で処理します
// 10秒ごとにInsert≒進捗率変更なので1秒ごとに取得すれば十分かなと思いそう設定しています
$('#register').on('click', function() {
    $('#progressArea').html('<div id="progressBar" class="progress"><div class="progressBarBody"></div></div>');
    var progress = new Progress(0);
    $('#progress').html('進捗状況: 0%');
    setInterval(function() {
        updateProgress(progress);
    }, 1000);
});
  1. ビューに表示(これもほんとはphpファイル)
view.html
<!-- 進捗状況表示 -->
<div id="progressVal">
    <span id="progress"></span>
</div>
<div id="progressArea">
</div>
  1. 進捗バーのCSS(デザインに合わせて調整してください)
style.css
.progress {
  width: 60%;
  height: 30px;
  background-color: #F5F5F5;
  border-radius: 4px;
  box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
}

.progressBarBody {
  transition: width 0.5s linear;
  height: 100%;
  background-color: #337AB7;
  border-radius: 4px;
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webアプリケーションの自動化をやってみよう

はじめに

Webアプリケーションに対してある種の繰り返しの操作を行ったり、定型処理を定期的に自動実行したい場合がよくあります。
大きくわけてWebアプリケーションの自動化には3種類のやり方が存在します。

1つ目はブラウザのGUI上の操作をプログラム上で真似して自動化する方法
2つ目はブラウザから送信しているデータを真似する方法
3つ目はWebアプリケーションが提供しているAPIを利用する方法

1つ目のブラウザのGUI上の操作をプログラム上で真似して自動化する方法は直観的にわかりやすいと言われますが、実際は最も難しい自動化の方法になります。また、アプリケーションのバージョンアップに伴い自動化用のプログラムが動作しなくなる可能性があります。

2つ目のブラウザから送信しているデータを真似する方法はプログラムで実装しやすいやり方ではありますが、Webアプリケーションがどのようなデータを送信しているかなどを調べる必要があります。また、1つ目と同様にアプリケーションのバージョンアップに伴い自動化用のプログラムが動作しなくなる可能性があります。

3つ目のWebアプリケーションが提供しているAPIを利用する方法が最も簡単でかつ正確に自動操作を行えます。ただし、WebアプリケーションのAPIが提供されているかどうかは自動操作対象のWebアプリケーションの仕様次第です。

実験環境

Windows10+PowerShell5.1
Visual Studio 2019 + .NET Framework 4.6
Python 3.7.4
NodeJs v10.16.0
UiPath 2019.8.0-beta 83

ブラウザのGUI上の操作をプログラム上で真似して自動化する方法

Windowsの場合、ブラウザを操作して自動化する方法も大きく4つの方法があります。

1つ目の方法はInternetExploreのCOMを利用する方法です。WindowsのInternetExploreに対してならば、なにもインストールすることなく、ブラウザの自動操作が可能になっています。ただし、InternetExploreの寿命自体が、長く持たない可能性があるので注意が必要です。(InternetExplore自体が持っても、対応するWebアプリケーションがInternetExploreのサポートを切る可能性が高いです)

2つ目の方法としてSeleniumを使用する方法です。外部のライブラリが必要になりますが、多くのブラウザがサポートされています。

3つ目の方法としてはブラウザが提供している拡張機能を作成する方法です。ChromeとFirefoxの場合、JavaScriptで作成できるので、外部のライブラリを導入する必要はありません。

4つ目の方法としてRPAツールを使用する方法です。ブラウザ上の要素について深く考えなくても、GUIベースで自動化が行えますが、コストの面で問題になります。

HTMLの要素を調べ方

どのような方法でブラウザを操作するとしても、HTMLがどのような要素で構成されているかを調べる必要があります。
ここではChromeでGoogleで検索する場合を例として画面上の要素を調べる方法を説明します。

image.png

1.ChromeにてF12キーを押下して開発者ツールを開きます。その後、「Elements」タブを選択してください。

image.png

2.[CTRL]+[Shift]+[C]を押下するか、下記のアイコンをクリックします。
image.png

3.調べたい要素にマウスを移動させます。
image.png

4.Elementsタブに選択した要素の内容が表示されます。今回の場合、以下のような内容が表示されます。

<input class="gLFyf gsfi" maxlength="2048" name="q" type="text" jsaction="paste:puy29d" aria-autocomplete="both" aria-haspopup="false" autocapitalize="off" autocomplete="off" autocorrect="off" role="combobox" spellcheck="false" title="検索" value="" aria-label="検索" data-ved="0ahUKEwiC0u6iu4nlAhXwyIsBHWwTBHcQ39UDCAQ">

inputタグの属性が以下のようになっていることがわかります。

属性
class gLFyf gsfi
name q

自動操作を行う場合、id、name、classなどを利用して要素を指定することになるので、属性値をメモしておきましょう。

5.同様にボタンについても属性を調べます。その結果は以下のようになります。

<input class="gNO89b" value="Google 検索" aria-label="Google 検索" name="btnK" type="submit" data-ved="0ahUKEwiC0u6iu4nlAhXwyIsBHWwTBHcQ4dUDCAo">
属性
class gNO89b
name btnK

ここで調べた属性を利用して要素を特定して自動操作を行うことになります。。
また、今回はChromeでのやり方を紹介しましたが、他のブラウザでも同様のことが可能です。同じWebアプリケーションを使用していてもブラウザによって出力される内容が異なる可能性もあるので、自動操作を行うブラウザを使用して要素を調べるようにしましょう。

InternetExplore11の場合

IE11でもF12キーを押すことで開発者ツールが表示されます。
そこで「DOM Explore」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

Edgeの場合

Edgeは将来Chromeベースのものに置き換わる可能性があります。
今回は旧EdgeとChromeベースの新Edge両方について説明します。

旧Edge

F12キーで開発者ツールが表示されます。
そこで「要素」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

新Edge

2019年10月時点でベータ版としてリリースされているEdgeの場合、F12キーで開発者ツールが表示されます。
Chromeと同様の操作で要素の属性を調べることが可能です。

image.png

Firefoxの場合

F12キーで開発者ツールが表示されます。
そこで「インスペクター」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

HTMLの要素を調べる方のまとめ

多くのブラウザは開発者ツールをサポートしており、要素の属性を調べることが可能です。
Webアプリケーションの自動化では、この要素を特定して操作する必要があるので、お使いのブラウザでの要素の調べ方は覚えておきましょう。

実際の自動操作の例は次章から解説します。

Internet ExploreのCOMを使用した自動化

Internet ExploreのCOMであるmshtmlを経由することでInternetExploreを自動操作できます。

COM(Component Object Model)
Microsoftが提唱した再利用を目的とした技術で、COMを用いて開発した部品はプログラム言語に依存せずに利用できるようになります。たとえば、説明したInternetExploreの操作やOfficeアプリケーションなどが外部から利用できるのは、COMのおかげです。

IE操作の単純な例

実際のGoogleのトップページで任意の単語を検索するサンプルを見てみましょう。

WSHやVBAでのIE操作の単純な例

WSHのVBScritpで以下のように実装可能です。
以下コードはWScript.EchoをDebug.Print等に置き換えることでVBAでも流用できると思います。

Dim ie
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = True 
call ie.Navigate("https://www.google.com/")

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100        
Loop

Dim doc
Set doc = ie.Document
Dim txt
Set txt = doc.getElementsByName("q")
txt.item(0).value = "ドリフターズ"

Dim btn
Set btn = doc.getElementsByName("btnK")
btn.item(0).click()

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100        
Loop
Set doc = ie.Document

Dim list
Do While True
    Set list = doc.getElementsByClassName("LC20lb")

    If Not list is Nothing Then
        If list.length > 0 Then
            Exit Do
        End If
    End If
    WScript.Sleep 100 
Loop

Dim item
For Each item In list
    WScript.Echo item.innerText
Next
ie.Quit

このサンプルではNavigateで指定のURLに移動したのち、getElementsByNameを使用して属性を取得して、値の設定とクリック操作を行っています。
その後,BusyreadyStateを監視してページの切り替え完了を待ちます。
その後、検索結果の要素が出現するまで待機して、その要素の内容を出力します。

PowerShellでのIE操作の単純な例

実はPowerShellやC#などの.NETでの実装は厄介です。結論から言えばやめといた方がいいです。
たとえば、よく見かける実装でGoogleとはてなブックマークを検索するスクリプトieng1.ps1とieng2.ps1を用意しました。

Googleの検索

ieng1.ps1
$ie = New-Object -ComObject InternetExplorer.Application  # IE起動

$ie.Visible = $true
$ie.Navigate("https://www.google.com/")


# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc=$ie.Document

# 検索文字入力
$txt=$doc.getElementsByName("q")
$txt[0].value = "ドリフターズ"

# ボタン押下
$btn=$doc.getElementsByName("btnK")
$btn[0].click()

# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc = $ie.Document

# LC20lbが表示されるまで待機
while($true) {
  $list = $doc.getElementsByClassName('LC20lb')
  if($list -and $list.length -ge 1) { break }
  Start-Sleep -Milliseconds 100
}

foreach($i in $list){
  Write-Host($i.innerText)
}
$ie.Quit()
Write-Host("OK")

はてなの検索

ieng2.ps1
$ie = New-Object -ComObject InternetExplorer.Application  # IE起動

$ie.Visible = $true
$ie.Navigate("https://b.hatena.ne.jp/")


# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc=$ie.Document

# 検索文字入力
$txt=$doc.getElementsByName("q")
$txt[0].value = "ドリフターズ"

# ボタン押下
$btn=$doc.getElementsByClassName("gh-search-button")
$btn[0].click()

# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc = $ie.Document

# LC20lbが表示されるまで待機
while($true) {
  $list = $doc.getElementsByClassName('centerarticle-entry-title')
  if($list -and $list.length -ge 1) { break }
  Start-Sleep -Milliseconds 100
}

foreach($i in $list){
  Write-Host($i.innerText)
}
$ie.Quit()
Write-Host("OK")

この実装はいくつか問題を起こす可能性があります。

PowerShellでのIE操作の問題点
環境によっては動作しない

同じコードをPowerShell2.0+Windows7で動かそうとしたところ下記のエラーを表示して動作しません。

PS C:\share\webctrl> $ie.Document.getElementsByName("q")
"getElementsByName" のオーバーロードで、引数の数が "1" であるものが見つかりません。
発生場所 行:1 文字:31
+ $ie.Document.getElementsByName <<<< ("q")
    + CategoryInfo          : NotSpecified: (:) []、MethodException
    + FullyQualifiedErrorId : MethodCountCouldNotFin

この問題は下記のフォーラムで議論されていますが、解決はしていません。

COMの解放処理を行っていない

上記のコードはCOMを.NETから利用しているにも関わらずReleaseComObjectを行っていません。
類似の問題として以下を参照してください。

.NETを使ったOfficeの自動化が面倒なはずがない―そう考えていた時期が俺にもありました。
https://qiita.com/mima_ita/items/aa811423d8c4410eca71

解放処理を行ったコードは以下のようになります。

    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://www.google.com/")


    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=$doc.getElementsByName("btnK")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    # LC20lbが表示されるまで待機
    while($true) {
      $list = $doc.getElementsByClassName('LC20lb')
      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue


    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

同一プロセスで複数起動した場合にエラーになる

先にあげた2つのスクリプトはPowerShellを再起動した直後には、それぞれ動作しますが以下のように続けて実行するとエラーになります。

正常に動作する
>powershell ./ieng1.ps1
>powershell ./ieng2.ps1

2つ目のスクリプトの実行でエラーになる
>./ieng1.ps1
>./ieng2.ps1

エラーの内容は下記の通りです。

HRESULT からの例外:0x800A01B6
発生場所 C:\dev\ps\webctrl\ieng2.ps1:14 文字:1
+ $txt=$doc.getElementsByName("q")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], NotSupportedException
    + FullyQualifiedErrorId : System.NotSupportedException

この問題は以下の記事で言及されています。

getElementsByNameをIHTMLDocument3_getElementsByNameに置き換えて、getElementsByClassNameをInvokeMemberで呼び出すようにすれば回避できるようです。

Googleの検索(修正版)

ieok1.ps1
    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://www.google.com/")


    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.IHTMLDocument3_getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=$doc.IHTMLDocument3_getElementsByName("btnK")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    # LC20lbが表示されるまで待機
    while($true) {
      $list = [System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "LC20lb")

      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue


    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

はてなの検索(修正版)

ieok2.ps1
    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://b.hatena.ne.jp/")


    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.IHTMLDocument3_getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=[System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "gh-search-button")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    #表示されるまで待機
    while($true) {
      $list = [System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "centerarticle-entry-title")

      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue


    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

PowerShellでのInternetExploreの操作のまとめ

辞めといた方がいいでしょう。

回避策を書いているページは色々見つかりますが、何故その事象が発生しているか、そしてなぜ回避することができたかを説明できているサイトは見つからず、ためしてみたら動作した以上のものではありません。
固定の環境で動かすスクリプトならばともかく、不特定多数の環境で動作するスクリプトでは採用を避けるべきでしょう。また、多くの場合、VBSやVBAで代替できるのでPowerShellにこだわる理由はないでしょう。

どうしても、PowerShellで行いたい場合は、IE操作をしたプロセスを終了するようにすると安定しそうです。

すでに起動しているブラウザの操作方法

すでに起動しているIEを操作するにはIEのウィンドウに対してRegisterWindowMessageでWM_HTML_GETOBJECTメッセージを定義して送信することでHTMLDocumentを取得することになります。

Cant create HTML document from Hwnd using C#
https://stackoverflow.com/questions/20873885/cant-create-html-document-from-hwnd-using-c-sharp

Win32 APIを直接実行できないWSHでは実現が難しいです。

VBAでの実装例

下記を参考にしてください。

VBAでInternetExplore上のJavaScriptを無理やり動かすよ!
https://qiita.com/mima_ita/items/fdff129a8db1153c9940

C#での実装例

.NET経由になるのでCOMの解放処理を入れる必要があります。

(1)参照マネージャーのCOMタブでMicrosoft HTML Object Libraryを追加します。
image.png

これにより「Interop.MSHTML.dll」が作成されます。
Interop~.dllはtlbImpコマンドを使用することでコマンドラインで作成できますが、VisualStudioなどの開発ツールをインストールしていないと使えないと思います。

(2)以下のような実装をします。下記のコードは.NET2.0でも動作します。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using MSHTML;
using System.Diagnostics;

namespace iesample
{
    class Program
    {


        [DllImport("user32.dll", EntryPoint = "GetClassNameA")]
        public static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount);

        /*delegate to handle EnumChildWindows*/
        public delegate int EnumProc(IntPtr hWnd, ref IntPtr lParam);


        [DllImport("user32.dll")]
        public static extern int EnumWindows(EnumProc lpEnumFunc, ref IntPtr lParam);


        [DllImport("user32.dll")]
        public static extern int EnumChildWindows(IntPtr hWndParent, EnumProc lpEnumFunc, ref IntPtr lParam);

        [DllImport("user32.dll", EntryPoint = "RegisterWindowMessageA")]
        public static extern int RegisterWindowMessage(string lpString);

        [DllImport("user32.dll", EntryPoint = "SendMessageTimeoutA")]
        public static extern int SendMessageTimeout(IntPtr hwnd, int msg, int wParam, int lParam, int fuFlags, int uTimeout, out UIntPtr lpdwResult);

        [DllImport("OLEACC.dll")]
        public static extern int ObjectFromLresult(UIntPtr lResult, ref Guid _riid, int wParam, ref MSHTML.IHTMLDocument2 _ppvObject);

        public const int SMTO_ABORTIFHUNG = 0x2;
        public Guid IID_IHTMLDocument = new Guid("626FC520-A41E-11CF-A731-00A0C9082637");


        private static List<IHTMLDocument2> cacheList = new List<IHTMLDocument2>();

        public static MSHTML.IHTMLDocument2 FindBrowser(string title)
        {
            MSHTML.IHTMLDocument2 ret;
            if (cacheList.Count == 0)
            {
                RefreshBrowserCache();
            }
            ret = FindBrowserInCache(title);
            if (ret != null)
            {
                return ret;
            }
            RefreshBrowserCache();
            return FindBrowserInCache(title);
        }
        public static MSHTML.IHTMLDocument2 FindBrowserInCache(string title)
        {
            MSHTML.IHTMLDocument2 ret;
            foreach(MSHTML.IHTMLDocument2 item in cacheList)
            {
                if (item.title.Contains(title))
                {
                    return item;
                }
            }
            return null;
        }

        public static void RefreshBrowserCache()
        {
            foreach (IHTMLDocument2 item in cacheList)
            {
                Marshal.ReleaseComObject(item);
            }
            cacheList.Clear();
            EnumProc proc = new EnumProc(EnumIEWndProc);
            IntPtr lparam = IntPtr.Zero;
            EnumWindows(proc, ref lparam);
        }

        private static int EnumIEWndProc(IntPtr hWnd, ref IntPtr lParam)
        {
            StringBuilder className = new StringBuilder(128);
            GetClassName(hWnd, className, className.Capacity);
            EnumProc proc = new EnumProc(EnumIEServerWndProc);
            if (className.ToString().Equals("IEFrame") || className.ToString().Equals("TabWindowClass"))
            {
                IntPtr lparam = IntPtr.Zero;
                EnumChildWindows(hWnd, proc, ref lparam);
            }
            return 1;
        }
        private static int EnumIEServerWndProc(IntPtr hWnd, ref IntPtr lParam)
        {
            StringBuilder className = new StringBuilder(128);
            GetClassName(hWnd, className, className.Capacity);
            if (className.ToString().Equals("Internet Explorer_Server"))
            {
                IHTMLDocument2 doc = GetHTMLDocument(hWnd);
                if (doc != null)
                {
                    cacheList.Add(doc);
                }
            }

            return 1;
        }
        private static IHTMLDocument2 GetHTMLDocument(IntPtr hWnd)
        {
            int nMsg = RegisterWindowMessage("WM_HTML_GETOBJECT");
            if (nMsg == 0)
            {
                return null;
            }
            UIntPtr lRes;
            SendMessageTimeout(hWnd, nMsg, 0, 0, SMTO_ABORTIFHUNG, 1000, out lRes);
            if (lRes == UIntPtr.Zero)
            {
                return null;
            }

            Guid IID_IHTMLDocument = new Guid("626FC520-A41E-11CF-A731-00A0C9082637");
            IHTMLDocument2 doc = null;
            int hr = ObjectFromLresult(lRes, ref IID_IHTMLDocument, 0, ref doc);
            return doc;
         }

        static void Main(string[] args)
        {
            IHTMLDocument3 doc = FindBrowser("Google") as IHTMLDocument3;
            IHTMLInputElement txt = doc.getElementsByName("q").item(0) as IHTMLInputElement;
            txt.value = "ドリフターズ";

            IHTMLElement btn = doc.getElementsByName("btnK").item(0) as IHTMLElement;
            btn.click();



            Marshal.ReleaseComObject(btn);
            Marshal.ReleaseComObject(txt);
            Marshal.ReleaseComObject(doc);

        }
    }
}

様々なコントロールを含むサンプルの例

様々なコントロールを含む以下のページの入力の自動化について考えてみます。
操作対象として以下のページを使用します。
http://needtec.sakura.ne.jp/auto_demo/form1.html

このページは「登録する」ボタンを押下することで確認メッセージが表示されて、「OK」の場合に登録処理を行うものとします。※
※実際はSleepしているだけでなにもしていないです。

WSHやVBSでの操作例は以下のようになります。

Dim ie
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = True 
call ie.Navigate("http://needtec.sakura.ne.jp/auto_demo/form1.html")

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100
Loop

Dim doc
Set doc = ie.Document
' INPUTBOX
doc.getElementsByName("name").item(0).value = "名前太郎"
doc.getElementsByName("mail").item(0).value = "test@co.jp"

' テキストエリア
doc.getElementsByName("comment").item(0).innerText = "猫猫子猫" & vbCrLf & "犬犬子犬"

' チェックボックス
doc.getElementsByName("q1[]").item(0).Checked = True
doc.getElementsByName("q1[]").item(1).Checked = True

' ラジオボタン
doc.getElementsByName("men").item(1).Checked = True

' 複数選択リスト
Dim objSelect
Set objSelect = doc.getElementsByName("osi[]").item(0)
objSelect.getElementsByTagName("option").item(1).Selected = True
objSelect.getElementsByTagName("option").item(2).Selected = True

' ボタン押下
' 確認メッセージ処理を偽造する
doc.parentWindow.ExecScript("confirm = function () { return true; }")
doc.getElementsByTagName("input").item(8).click()

確認ダイアログを突破する方法は色々ありますが、上記でやったようなJavaScirptのconfirmやalertを上書きしてしまうのが最も楽だと思います。

確認メッセージの処理を上書きしたくない場合

UIAutomation等を使用します。
確認メッセージが表示されるまで待機してボタンを押下するような処理を別スレッドかプロセスで起動します。WSHやVBAの場合は別プロセスでやった方が楽です。

まず、確認メッセージを監視するようなスクリプトを記載します。
これはPowerShellで書いた方が楽だと思います。

wait_confirm.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 GetWindowByTitle(string title) {
        var rootCond = new PropertyCondition(AutomationElement.ClassNameProperty, "Alternate Modal Top Most");
        var cond = new PropertyCondition(AutomationElement.NameProperty, title);
        var elementCollection = RootElement.FindAll(TreeScope.Children, rootCond);
        foreach(AutomationElement mainForm in elementCollection) {
            var win =  mainForm.FindFirst(TreeScope.Children, cond);
            if (win != null) {
                return win;
            }
        }
        return null;
    }

    public static AutomationElement WaitWindowByTitle(string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = GetWindowByTitle(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]

# ウィンドウ以下で指定の条件に当てはまるコントロールを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)
}

# 指定の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()
}


#
$dialog = [AutomationHelper]::WaitWindowByTitle("Web ページからのメッセージ", 30)
if ($dialog -eq $null) {
    Write-Host "タイムアウト"
} else {
    pushButtonById $dialog "1"
    Write-Host "終了"
}

あとはWSH側のボタン押下処理を以下のように修正します。

' ボタン押下
' 確認メッセージ処理を別プロセスで行う
Dim shell
Set shell = CreateObject("WScript.Shell")
shell.Run "C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File C:\dev\ps\webctrl\wait_confirm.ps1", 0, False

doc.getElementsByTagName("input").item(8).click()

Internet ExploreのCOMを使用した自動化のまとめ

VBAやVBSなどの何処でもはいっていそうなプログラミング言語で自動化できるのは強みです。
PowerShellでも行えますが、.NET経由だとCOMの解放処理が面倒だったり、動作が安定しない環境もあるので環境を制御できる場合のみに使用したほうがいいでしょう。

また、InternetExploreのサポートがいつまで続くかわからない以上、外部のツールを導入可能である場合は、お勧めしません。

Seleniumを使用した自動化

様々なOS上の様々なブラウザを様々なプログラミング言語で自動操作するためのツールです。
Webアプリケーションをブラウザ操作で自動化する場合、もっともよく使われるツールになります。
Selenium実践入門などの良書が流通しているので、この章は飛ばしてそっちを読んだ方がいいと思います。

SeleniumIDEを使用する例

ブラウザの操作を記憶する録画機能が提供されておりGUIベースで自動操作の処理を記載できます。録画した操作内容はスクリプトとして記録され、後で修正が可能です。また、別のプログラム言語で記載されたテストコードに変換することもできます。

以下はGoogle検索の操作をキャプチャした例になります。

auto.gif

SeleniumIDEはChromeまたはFirefoxの拡張機能として提供されています。

「あれ?SeleniumIDEって終わったんじゃなかったっけ?」という人は下記の経緯を参照してください。

Webブラウザ自動化ツール「Selenium IDE」の今までとこれから
https://www.valtes.co.jp/qbookplus/509

ChromeでSeleniumIDEを使用する

(1)Chrome用のSeleniumIDEを拡張機能として追加します。

(2)ブラウザの上部にSeleniumIDEのアイコンが表示されるのでクリックします。
image.png

(3)SeleniumIDEのポップアップが表示されるので「Record a new test in new project」を選択します。
image.png

(4)「Name your new project」ダイアログが表示されるので任意のプロジェクト名を入力して「OK」ボタンを押します。
image.png

(5)「Set your projects's base URL」ダイアログが表示されるので操作元になるURLを入力します。たとえばGoogle検索をする例だと「https://www.google.com」を入力して「START RECORDING」ボタンを押下します。

image.png

(6)操作の記録が始まると右下に「Selenium IDE is recording...」と書かれた新しいブラウザが開きます。
image.png
このブラウザを使用して記録したい任意の操作を行います。

(7)操作の記録を終了したい場合、「Selenium IDE」ウィンドウの右上の「Stop recording」アイコンを押します。
image.png

(8)「Name your new test」ポップアップが表示されるので任意のテスト名を入力して「OK」を押します。
image.png

(9)SeleniumIDE ウィンドウに今回操作した内容がスクリプトとして記録されます。
image.png

(10)記録したスクリプトは「Run Current Test」アイコンを押すことで再実行可能です。
image.png

また、JUnit や pytest、 JavaScript Mochaといった他のプログラミング言語のユニットテストとしてエクスポートすることが可能です。

FirefoxでSeleniumIDEを使用する

Firefox用のSeleniumIDEを拡張機能として追加します。
あとの操作はChromeと同じです。

ただし、記録される操作はFirefoxとChromeで差異があります。
Firefoxの場合、ブラウザのスクロール操作が記録されていましたが、Chromeでは記録されていませんでした。
image.png

なお、手で同じコマンドを追加すると、再生はChromeでも動作しました。

プログラムからSeleniumを利用する

C#の場合

まず、NuGetでSelenium.WebDriverとSelenium.Supportに加えて操作したいブラウザのDriverを入手します。今回はChromeを操作したいので、Selenium.Chrome.WebDriverを入手します。

image.png

image.png

C#でのSeleniumの操作例は以下のようになります。

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using System;
using System.IO;
using System.Reflection;

namespace chromeSample
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var driver = new ChromeDriver(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)))
            {
                driver.Url = "http://needtec.sakura.ne.jp/auto_demo/form1.html";
                driver.FindElementByName("name").SendKeys("名前太郎");
                driver.FindElementByName("mail").SendKeys("test@co.jp");
                driver.FindElementByName("comment").SendKeys("猫猫子猫\n\r犬犬子犬");

                // チェックボックス
                var chks = driver.FindElements(By.Name("q1[]"));
                chks[0].Click();
                chks[2].Click();

                // オプションボタン
                var opts = driver.FindElements(By.Name("men"));
                opts[1].Click();

                // 複数選択
                var sel = new OpenQA.Selenium.Support.UI.SelectElement(driver.FindElement(By.Name("osi[]")));
                sel.DeselectAll();
                sel.SelectByIndex(1);
                sel.SelectByIndex(2);

                // 登録ボタン押下 
                driver.FindElement(By.XPath("//input[@value='登録する']")).Click();
                // OKボタンを押す
                var confirm = driver.SwitchTo().Alert();
                confirm.Accept();

                // 結果の出力
                var results = driver.FindElementsByTagName("tr");
                foreach(var rec in results)
                {
                    Console.WriteLine(rec.Text);
                }
                Console.ReadLine();
                driver.Quit();
            }
        }
    }
}

ページの切り替え時に待機処理をいれていませんが、暗黙的にタイムアウトまでDOMをポーリングしています。このタイムアウトについては下記を参照してください。

The default value of timeouts on selenium webdriver
https://stackoverflow.com/questions/30114976/the-default-value-of-timeouts-on-selenium-webdriver

PowerShellの場合

PowerShellでもC#と同様な実装が可能です。
下記のページのSelenium Client & WebDriver Language BindingsからC#のクライアントと操作したいブラウザのWebDriverをダウンロードしてください。

https://www.seleniumhq.org/download/

image.png

image.png

クライアントをダウンロードすると以下のようなファイルが入っています。

  • Selenium.Support.3.14.0.nupkg
  • Selenium.WebDriver.3.14.0.nupkg
  • Selenium.WebDriverBackedSelenium.3.14.0.nupkg

これは圧縮されているファイルなので拡張子をzipに変更すればDLLを取り出せます。
サポートしているバージョンが.NET3.5以上なのでWindows7に初期から入っているPowerShellでは動作しません。

PowerShellでのサンプルは以下のようになります。

# 以下参考
# https://tech.mavericksevmont.com/blog/powershell-selenium-automate-web-browser-interactions-part-i/
$dllPath = Split-Path $MyInvocation.MyCommand.Path
Add-Type -Path "$dllPath\WebDriver.dll"
Add-Type -Path "$dllPath\WebDriver.Support.dll"

# chromedriver.exeがあるディレクトリを指定
$driver = New-Object OpenQA.Selenium.Chrome.ChromeDriver("C:\tool\selenium\chromedriver_win32\")
$driver.Url = "http://needtec.sakura.ne.jp/auto_demo/form1.html"
$driver.FindElementByName("name").SendKeys("名前太郎")
$driver.FindElementByName("mail").SendKeys("test@co.jp")

# テキストエリア
$comment = @"
猫猫子猫
犬犬子犬
"@
$driver.FindElementByName("comment").SendKeys($comment)

# チェックボックス
$chks = $driver.FindElements([OpenQA.Selenium.By]::Name("q1[]"))
$chks[0].Click()
$chks[2].Click()

#オプションボタン
$opts = $driver.FindElements([OpenQA.Selenium.By]::Name("men"))
$opts[1].Click()

#複数選択
$selElem = $driver.FindElement([OpenQA.Selenium.By]::Name("osi[]"))
$sel = New-Object OpenQA.Selenium.Support.UI.SelectElement -ArgumentList $selElem
$sel.DeselectAll();
$sel.SelectByIndex(1);
$sel.SelectByIndex(2);

# ボタン押下
$driver.FindElement([OpenQA.Selenium.By]::XPath("//input[@value='登録する']")).Click()
$confirm = $driver.SwitchTo().Alert();
$confirm.Accept()

# 結果表示
$results = $driver.FindElementsByTagName("tr");
foreach($rec in $results)
{
    Write-Host $rec.Text
}
$driver.Quit()
$driver.Dispose()

Write-Host("OK")

Pythonの場合

PythonでもSeleniumは使用可能です。まずpipコマンドでseleniumをインストールします。

pip install -U selenium

サンプルコードは以下のようになります。

from selenium import webdriver
from selenium.webdriver.support.ui import Select

driver = webdriver.Chrome("C:\\tool\\selenium\\chromedriver_win32\\chromedriver.exe")
driver.get("http://needtec.sakura.ne.jp/auto_demo/form1.html")
driver.find_element_by_name("name").send_keys("名前太郎");
driver.find_element_by_name("mail").send_keys("test@co.jp");
driver.find_element_by_name("comment").send_keys("猫猫子猫\n\r犬犬子犬");

# チェックボックス
chks = driver.find_elements_by_name("q1[]")
chks[0].click()
chks[2].click()

# オプションボタン
opts = driver.find_elements_by_name("men")
opts[1].click()

# 選択
sel = Select(driver.find_element_by_name("osi[]"))
sel.deselect_all()
sel.select_by_index(1)
sel.select_by_index(2)

# ボタン押下
driver.find_element_by_xpath("//input[@value='登録する']").click()
driver.switch_to.alert.accept()

# 結果
results = driver.find_elements_by_tag_name("tr")
for rec in results:
    print(rec.text)

driver.close()

NodeJsの場合

NodeJsでもSeleniumの操作は可能です。npmコマンドを使用してseleniumをインストールします。

npm install selenium-webdriver

簡単なサンプルは以下のようになります。

// 以下参考
// https://qiita.com/tonio0720/items/70c13ad304154d95e4bc
// https://stackoverflow.com/questions/26191142/selenium-nodejs-chromedriver-path
// https://seleniumhq.github.io/selenium/docs/api/javascript/index.html
const webdriver = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const path = 'C:\\tool\\selenium\\chromedriver_win32\\chromedriver.exe';
const service = new chrome.ServiceBuilder(path).build();
chrome.setDefaultService(service);

(async () => {
  const driver = await new webdriver.Builder()
                            .withCapabilities(webdriver.Capabilities.chrome())
                            .build();
  await driver.get('http://needtec.sakura.ne.jp/auto_demo/form1.html');
  await driver.findElement(webdriver.By.name("name")).sendKeys("名前太郎");
  await driver.findElement(webdriver.By.name("mail")).sendKeys("test@co.jp");
  await driver.findElement(webdriver.By.name("comment")).sendKeys("猫猫子猫\n\r犬犬子犬");

  // チェックボックス
  let chks = await driver.findElements(webdriver.By.name("q1[]"));
  await chks[0].click();
  await chks[2].click();

  // オプションボタン
  let opts = await driver.findElements(webdriver.By.name("men"));
  await opts[1].click();

  // 複数選択
  let sel = await driver.findElements(webdriver.By.xpath("//select[@name='osi[]']/option"));
  await sel[1].click();
  await sel[2].click();

  // ボタン押下
  await driver.findElement(webdriver.By.xpath("//input[@value='登録する']")).click();
  await driver.switchTo().alert().accept();

  // 結果取得
  let results = await driver.findElements(webdriver.By.tagName("tr"));
  for (let i = 0; i < results.length; ++i) {
    console.log(await results[i].getText());
  }

  driver.quit();
})();

Seleniumで既に起動しているブラウザの操作は行えるか?

Seleniumを介さず起動していたブラウザの自動操作については公式にはサポートしていません。
下記に幾つかの回避法が紹介されていますが、あくまで非公式の内容になります。

Can Selenium interact with an existing browser session?
https://stackoverflow.com/questions/8344776/can-selenium-interact-with-an-existing-browser-session

Seleniumを使用する方法のまとめ

Seleniumを使用することで様々なブラウザを様々なプログラミング言語で操作できることができます。
またSeleniumIDEを使用することでプログラミングをせずにブラウザの自動操作がおこなえます。

ただし、Flashページなどの画像認識を必要とする操作の場合は他の方法を検討してください。
たとえば以下のような方法があります。

Sikulix1.1.4を使って画面の自動操作をする
https://qiita.com/mima_ita/items/8f653042ac9140e5023f

C#やPowerShellで画面上の特定の画像の位置をクリックする方法
https://qiita.com/mima_ita/items/f7d2c38767bda8b35cbd

拡張機能を作成する方法

ChromeやFirefoxで利用できる拡張機能を使用して表示中のページを自動操作することが可能です。

Chromeの拡張機能
https://developer.chrome.com/extensions

Firefoxの拡張機能
https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension

以下はChromeの拡張機能を使用してページを自動操作後、操作結果をメッセージボックスで表示しています。

auto2.gif

操作対象のページ
http://needtec.sakura.ne.jp/auto_demo/form1.html

拡張機能を使った自動操作の仕組みは下記の通りです。
image.png

default_popupからcontent_scriptsに対して自動操作の開始指示をメッセージを使用して行います。
入力ページのcontent_scriptsは項目の入力とボタンの押下を行います。
出力ページのcontent_scriptsは出力ページの内容を取得してメッセージを使用してdefault_popupに内容を送信します。

なお、ChromeとFirefoxの拡張機能は同じような実装で作成できます。

Chromeの拡張機能で自動操作

Chrome拡張機能での自動操作のサンプル
https://github.com/mima3/auto_sample/tree/master/auto_chrome_sample

以下が実際の自動操作を行っているcontent_scriptになります。

content_input.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(request);
    let nameElem = document.getElementsByName('name');
    nameElem[0].value = '名前太郎';
    let mailElem = document.getElementsByName('mail');
    mailElem[0].value = 'test@co.jp';
    let commentElem = document.getElementsByName('comment');
    commentElem[0].value = '猫猫子猫\n犬犬子犬';
    // チェックボックス
    let chkElem = document.getElementsByName('q1[]');
    chkElem[0].click();
    chkElem[2].click();
    // ラジオボタン
    let radioElem = document.getElementsByName('men');
    radioElem[1].click();
    // 選択項目
    var itr = document.evaluate("//select[@name='osi[]']/option", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );
    var node = itr.iterateNext();
    while(node) {
      if (node.textContent === '千鶴さん' || node.textContent === 'さおりん') {
        node.selected = true;
      }
      node = itr.iterateNext();
    }
    // ボタン押下
    // contents.jsでwindows.confirmを書き換えてもブラウザ側の処理に影響を与えない
    // そのため、window.confirmを書き換えるscritpをタグとして挿入する
    // 考え方は以下を参考
    // https://qiita.com/suin/items/5e1aa942e654bce442f7
    let scr = document.createElement("script");
    scr.setAttribute('type', 'text/javascript');
    scr.innerText = 'window.confirm = function () { return true; }';
    document.body.appendChild(scr); 
    setTimeout(function(){ 
      //ここでやってもブラウザ上のwindow.confirmは影響ない。
      var btnElem = document.evaluate("//input[@value='登録する']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
      btnElem.singleNodeValue.click();
    }, 0);
  }
);

JavaScriptのDOM操作で入力項目を設定後、登録ボタンを押下します。
この際、confirmで確認ダイアログが表示されるため、window.confirmの処理を上書きしてダイアログが出ないようにしています。
content_scriptからブラウザで使用しているJavaScriptを更新するため、scriptタグを埋め込んでいます。
この考え方は下記を参考にしました。

Chrome拡張開発: 拡張からページにJavaScriptを送り込みたい
https://qiita.com/suin/items/5e1aa942e654bce442f7

Firefoxの拡張機能で自動操作

Firefox拡張機能での自動操作のサンプル
https://github.com/mima3/auto_sample/tree/master/auto_firefox_sample

拡張機能での自動操作のまとめ

ブラウザの拡張機能を利用することでブラウザの自動操作が行えます。
この方法のメリットとしてはインターネットの接続がなくてもテキストエディタのみで自動操作を行うためのスクリプトが作成できることです。(ブラウザを開発者モードで動かしていいという条件は必要)

もし、自動操作中にネイティブのアプリと連携が必要になった場合はNative Messagingを使用してください。このNaitiveMessageを使用したサンプルは以下にあります。

RPAツールを使用する方法

お高いRPAツールはブラウザの操作をサポートしている商品が多いです。
今回は小規模事業や個人利用なら無料でしようできるUiPath Communityを利用してChromeの操作を行います。

ChromeをUiPathで操作する場合、Chromeの拡張機能をインストールする必要があるので、下記を参考にインストールしてください。
https://docs.uipath.com/studio/lang-ja/docs/installing-the-chrome-extension

(1)UIPathで新規プロジェクトを作成します。言語はC#を選択します。
image.png

(2)「ブラウザを開く」アクティビティを追加します。
image.png

プロパティ
url "http://needtec.sakura.ne.jp/auto_demo/form1.html"
ブラウザの種類 Chrome

(3)「文字を入力」アクティビティを追加して「画面上で指定」でブラウザ上のテキスト入力項目を指定します。

image.png

image.png

image.png

(4)「文字を入力」アクティビティのプロパティを設定します。
image.png

(5)(3)~(4)を繰り返して「名前:」、「メールアドレス:」、「コメント:」を入力します。
image.png

プロパティ
表示名 文字を入力 'INPUT-名前'
テキスト "名前太郎"
フィールド内を削除 ON
ウィンドウメッセージを送信 OFF
入力をシミュレート OFF
プロパティ
表示名 文字を入力 'INPUT-メールアドレス'
テキスト "test@mail.co.jp"
フィールド内を削除 ON
ウィンドウメッセージを送信 ON ※デフォルトの挙動だとIMEが有効となり全角で入力されてしまう
入力をシミュレート OFF
プロパティ
表示名 文字を入力 'TEXTAREA-コメント'
テキスト "猫猫子猫\n\r犬犬子犬"
フィールド内を削除 ON
ウィンドウメッセージを送信 OFF
入力をシミュレート OFF

(6)「クリック」アクティビティを追加して「画面上で指定」でブラウザ上のクリックが必要な項目をを指定します。
image.png

image.png

image.png

(7)「クリック」アクティビティのプロパティを設定します。
image.png

(8)(5)~(6)を繰り返して「その1」、「その3」、「そば」をクリックします。
image.png

プロパティ
表示名 クリック 'INPUTーその1'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF
プロパティ
表示名 クリック 'INPUTーその3'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF
プロパティ
表示名 クリック 'INPUTーそば'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF

(9)リストを複数選択するために「JSスクリプトを挿入」アクティビティを追加します。
image.png

選択したJSスクリプトは下記の通りです。

select_multi.js
function selectmulti(e, aryStr) {
  var ary = JSON.parse(aryStr);
  var itr = document.evaluate("//select[@name='osi[]']/option", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );
  var node = itr.iterateNext();
  while(node) {
    if (ary.indexOf(node.textContent) >= 0) {
      node.selected = true;
    }
    node = itr.iterateNext();
  }
}
プロパティ
スクリプトコード select_multi.js
入力パラメータ "[\"千鶴さん\",\"さおりん\"]"

CTRLキーを押しながらのリスト項目をクリックする操作や、「複数の項目を選択」アクティビティを使用した実装だと動作しない場合がありました。

Web上のリストボックスで複数選択したい
https://forum.uipath.com/t/web/113531/9

また、ここで指定したJavaScript中で日本語やハングルは使用しないでください。文字化けします。日本語などが必要な場合は引数で渡すようにしてください。
たとえば「alert("千鶴さん");」とかいうコードを埋め込むと以下のようになります。

image.png

(10)登録ボタンを押下するために「クリック」アクティビティを追加します。
image.png

プロパティ
表示名 クリック 'INPUT-登録'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF

(11)登録ボタン押下後の確認メッセージを閉じるために「画像をクリック」アクティビティを追加します。

image.png

なお、「select_multi.js」に以下のコードを追加してconfirm関数を上書きして確認メッセージを表示させないことも可能です。

  let scr = document.createElement("script");
  scr.setAttribute('type', 'text/javascript');
  scr.innerText = 'window.confirm = function () { return true; }';
  document.body.appendChild(scr);

(12)登録後のページのデータを取得するために「データスクレイピング」を行います。

「データスクレイピング」アイコンを押下します。
image.png

「取得ウィザード」で「次へ」ボタンを押下します。
image.png

要素の選択が可能になるのでテーブルのセルを選択します。
image.png

「表全体からデータを抽出しますか?」の確認メッセージには「はい」を選択します。
image.png

「取得ウィザード」で「終了」ボタンを押下します。
image.png

「次へのリンクを指定」の確認メッセージには「いいえ」を選択します。
image.png

「データスクレイピング用のアクティビティが追加されます。
image.png

(13)「構造化データを抽出」アクティビティの「出力」プロパティに対してCTRL+Kを押下してresult変数を追加します
image.png
image.png

(14)「繰り返し(各行)」アクティビティを追加します。この際、コレクションには「result」変数を指定してください。
image.png

(15)「繰り返し(各行)」アクティビティに「一行を書き込み」アクティビティを追加します。

image.png

プロパティ
Text row[0].ToString() + " " + row[1].ToString()

(16)これまでの操作を再生すると以下のようになります。
auto3.gif

UiPathでのブラウザの自動操作のまとめ

UiPathを使用したメリットは以下の通りです。
・要素を画面から選択できる
・画像認識による自動操作ができる
・ブラウザ以外の自動操作が同じ操作感で行える。
・今回は説明してませんがUiPath Orchestratorで資産管理が容易になる

逆にデメリットは以下の通りです。
・GUIでのプログラミングになるので、複雑な実装が困難である
・GUIなので変更点の差分を見るのが困難で、コードレビューが負担になる。※結局はxamlなのでテキストで差分はとれるが…
・なれないとハマるポイントが多い。
・UiPathの操作でDOMの要素を変更したりしているのでシステム試験等で使用する場合、妥当性を考える必要がある。
例:UiPathで操作した要素には以下のように「uipath_custom_id」という属性が追加されている。
image.png

UiPathは外部プログラムを呼び出す機能やPowerShellの実行が可能なのでブラウザの操作は別の手法で行うことも可能です。

なお、RPAで未経験者でもお手軽自動操作とかいう言説が大きくなっていますが、正直、WebアプリケーションやWindowsアプリケーションを組んだことのない人が簡単に使えると言われると大きな疑問が残ります。
逆にRPAツール不要論もありますが、UiPath Orchestratorの存在や、簡単なフローが頻繁に変わる業務形態における仕事の分担という観点で、そのRPA不要論についても絶対的な真理とは言えないでしょう。

状況にあわせた組み合わせが必要と思います。

ブラウザのGUI上の操作をプログラム上で真似して自動化する方法のまとめ

ここまでで、ブラウザのGUI上の操作をプログラム上で真似して自動化する方法について紹介しました。

RPAツールを使える環境の場合、RPAツールは自動化を行う上で便利ではありますが、それを使うことを目的にしない方がいいです。必要に応じて別の方法をミックスして使うようにしましょう。

外部ライブラリを使用できる環境の場合、Seleniumを採用するのが一番楽だと思います。ChromeやFirefoxならSeleniumIDEで録画機能もついているので生産性は高いでしょう。

外部ライブラリが使用できない環境の場合、InternetExploreのCOM又は、ブラウザの拡張機能を使用することになります。
IEを使用する場合、操作対象のWebアプリケーションのサポート状況をよく確認しましょう。

いずれの方法でブラウザ操作を自動化するにせよ以下の点は気をつけてください。

安易なSleepを使用しない

たとえば、適当に2秒待つという処理をいれた場合、ネットワークやPCの負荷状況によって動作しない可能性があります。
Sleepよりも以下で判断するようにしましょう。

・ document.readyStatusなどを活用する
・ 特定の要素が出現したか、消えたかを見て判断する。
 →SeleniumやUiPathでは、要素の出現消滅の検出をサポートする機能が提供されている

テキスト入力は手動入力と異なる挙動をする可能性がある

テキスト入力を行う場合、手動で入力した場合と異なる挙動をする可能性があります。
たとえばキーボード操作のイベントで何らかの処理していたり、フォーカスの移動で何らかの処理をしていたりする場合です。
UiPathの場合、入力モードに以下の3種類があるので必要に応じて使いわけてください。

・デフォルト:デバイスドライバ経由なので手入力にもっとも近い
・WindowsMessage:Windowsのメッセージを利用してテキストを入力している。
・Simulate:コントロールを直接操作している

それ以外の場合は、JavaScriptのコード上でアプリが期待するイベントを無理やり起こす必要があります。

必要に応じてJavaScriptを利用する

複雑なUIの場合、画面要素を一々クリックするより、WebアプリケーションのJavaScriptを直接実行した方が早い場合があります。
また、いままでの例にでてきたように、alertやconfirmのような自動操作がし辛いポップアップの出現を抑止することが可能になります。

テストの自動化についてはテスト方針をよく確認する

ツールをつかったテストは強力ですが、それはユーザが動かしたものと全く同一にならないことに注意してください。
たとえば、先にあげたJavaScriptを呼び出して処理を行った場合、それがテストとして妥当かどうかはテストの方針や観点しだいになります。

UIの軽微な変更で動作しなくなることを忘れないこと

ブラウザの自動テストはUIの軽微な変更で簡単に動かなくなります。
たとえばリストの2番目と3番目の項目を選択するという実装だと、リストの項目が追加された場合に簡単に動作しなくなります。
これがなるべく影響を受けないような書き方をすることも可能ですが、限界はあります。

もしブラウザの自動化スクリプトを重要な業務で使用している場合は、前に動いたスクリプトだからと安心せずWebアプリケーションの変更にともなって定期的に以前に書いた自動化スクリプトが動作するか確認するようにしてください。

ブラウザから送信しているデータを真似する方法

ブラウザの送受信データの確認方法

ブラウザから送信しているデータを真似して自動化する前にブラウザからどんなデータを送受信しているか調べる方法を説明します。

下記のページで登録ボタンを押した場合にどのようなデータを送信しているか確認してみましょう。
http://needtec.sakura.ne.jp/auto_demo/form1.html

Chromeでの送受信データの確認方法

(1)F12で開発者ツールを開き、「Network」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
net1.gif

(3)「regist1.php」などの受信ファイルを選択後に、Headersタブを選択すると送信データが確認できます。
image.png

(4)Responseタブを選択すると受信内容が確認できます。
image.png

Firefoxでの送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します。
image.png

(4)パラメータタブでFormの送信情報を確認できます。
image.png

(5)応答タブでサーバーからのレスポンスデータを確認できます。
image.png

IE11での送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します
image.png

(4)本文タブを選択し、さらに「要求本文」タブを選択するとFormの送信情報を確認できます。
image.png

(5)本文タブを選択し、さらに「応答本文」タブを選択するとサーバーからのレスポンスデータを確認できます。
image.png

旧Edgeでの送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します
image.png

(4)本文タブを選択し、さらに「要求本文」タブを選択するとFormの送信情報を確認できます。
image.png

(5)本文タブを選択し、さらに「応答本文」タブを選択するとサーバーからのレスポンスデータを確認できます。
image.png

新Edgeでの送受信データの確認方法

Chromeと同じです。
image.png

単純なFormデータの送信例

下記のページのような単純なフォームのデータの送信例を説明します。
http://needtec.sakura.ne.jp/auto_demo/form1.html

curlコマンド

macやlinux系のOSならcurlコマンドを使用することで単純なフォームデータをPOSTすることが可能です。
windos10でもプリインストールされるようになったようですが、文字コードの問題があるので注意が必要です。また、PowerShellを使用しているとcurlコマンドが利用できますが、これはInvoke-WebRequestの別名です。

以下はCentOS7でcurlコマンドを実行した例となります。

>curl  -F "name=名前太郎" -F "mail=test@co.jp" -F "comment=コメント" -F "q1[]=その1" -F "q1[]=その3"  -F "men=soba" -F "osi[]=千鶴さん" -F "osi[]=さおりん"   http://needtec.sakura.ne.jp/auto_demo/regist1.php
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>sample</title>
</head>
<body>
<table border="1">
  <tr>
    <td>名前</td><td>名前太郎</td>
  </tr>
  <tr>
    <td>メールアドレス</td><td>test@co.jp</td>
  </tr>
  <tr>
    <td>コメント</td><td>コメント</td>
  </tr>
  <tr>
    <td>チェック</td>
    <td>
        その1, その3<br>    </td>
  </tr>
  <tr>
    <td>めん</td><td>soba</td>
  </tr>
  <tr>
    <td>おし</td><td>千鶴さん,さおりん</td>
  </tr>
</table>
</body>
</html>

powershellの例

PowerShellではInvoke-WebRequestを利用してFormデータを送信可能です。

    $data = @{
      name='名前太郎';
      mail='test';
      comment=@"
    猫猫子猫
    犬犬子犬
"@;
      'q1[0]'='その1';
      'q1[1]'='その3';
      men='soba';
      'osi[0]'='千鶴さん';
      'osi[1]'='さおりん'
    }
    $ret = Invoke-WebRequest http://needtec.sakura.ne.jp/auto_demo/regist1.php -Method POST -Body $data -ContentType "application/x-www-form-urlencoded"
    $html = $ret.ParsedHtml
    $list = $html.getElementsByTagName("tr")
    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null
    Remove-Variable html -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

ParsedHtmlはmshtmlになっているので、構文解析を容易に行えます。
なお、mshtmlはCOMなのでReleaseComObjectを実施して解放処理をしておいた方が無難です。

なお、ページによっては文字化けする場合があります。この場合は以下のように文字コードを変換して出力します。

# 以下参考
# https://qiita.com/zaki-lknr/items/1ae3258d7b77c5e2a2ba
$ret = Invoke-WebRequest http://needtec.sakura.ne.jp/auto_demo/form1.html
$content = [System.Text.Encoding]::UTF8.GetString( [System.Text.Encoding]::GetEncoding("ISO-8859-1").GetBytes($ret.Content) )
Write-Host $content

VBAまたはVBSの場合

MSXML2.XMLHTTPを利用することでFormデータを送信可能です。
受信したHTMLはMSHTML.HTMLDocumentで解析しています。

以下はVBSのサンプルになっていますがWScript.EchoをDebug.Print等に置き換えることでVBAでも流用できると思います。

' 参考:
' https://outofmem.tumblr.com/post/63052619242/vbaexcel-vba%E3%81%A7http%E9%80%9A%E4%BF%A1
' https://stackoverflow.com/questions/9931429/parse-html-file-using-mshtml-in-vbscript
Dim httpReq
Set httpReq = CreateObject("MSXML2.XMLHTTP")

Call httpReq.Open("POST", "http://needtec.sakura.ne.jp/auto_demo/regist1.php", False)
Call httpReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
Dim postData
postData = "name=名前太郎&mail=test@co.jp&comment=猫猫" & vbCrLf & "子犬&q1[]=その1&q1[]=その2&men=soba&osi[]=千鶴さん&osi[]=さおりん"
Call httpReq.Send(postData)

Dim objHtml
Set objHtml = CreateObject("htmlfile")
call objHtml.write(httpReq.responseText)

Dim list
Set list = objHtml.getElementsByTagName("tr")
Dim item 
For Each item In list
  WScript.Echo item.innerText
Next

Pythonの例

http.clientを使用してFormデータを送信して結果を受信可能です。html.parserを利用することでHTMLの解析も行えます。おそらく、python3xが入っている環境ならどこでも使えると思います。

import http.client, urllib.parse
from html.parser import HTMLParser

# 結果ページを解析するパーサー
class ResultParser(HTMLParser):
  def __init__(self):
      HTMLParser.__init__(self)
      self.flag = False

  def handle_starttag(self, tag, attrs):
      if tag == "td":
          self.flag = True

  def handle_data(self, data):
      if self.flag:
          print (data)
          self.flag = False

conn = http.client.HTTPConnection('needtec.sakura.ne.jp')

headers = {
  'Content-type': ' application/x-www-form-urlencoded'
}

data = {
  'name': '名前太郎',
  'mail': 'test@co.jp',
  'comment' : '猫猫子猫\n\r犬犬子犬',
  'q1[0]' : 'その1',
  'q1[1]' : 'その3',
  'men' : 'soba',
  'osi[0]' : '千鶴さん',
  'osi[1]' : 'さおりん'
}
#r = requests.post('http://needtec.sakura.ne.jp/auto_demo/regist1.php', data=data)
#print(r)
#print(r.text)

params = urllib.parse.urlencode(data)

conn.request('POST', '/auto_demo/regist1.php', params, headers)
response = conn.getresponse()
print(response.status, response.reason)
parser = ResultParser()
# trの内容を出力
parser.feed(response.read().decode())
conn.close()

外部ライブラリを使う場合

requestsパッケージを使うとデータの送受信が、Beautiful Soupを使うとHTMLの解析が楽になります。

import requests
from bs4 import BeautifulSoup

data = {
  'name': '名前太郎',
  'mail': 'test@co.jp',
  'comment' : '猫猫子猫\n\r犬犬子犬',
  'q1[0]' : 'その1',
  'q1[1]' : 'その3',
  'men' : 'soba',
  'osi[0]' : '千鶴さん',
  'osi[1]' : 'さおりん'
}
r = requests.post('https://needtec.sakura.ne.jp/auto_demo/regist1.php', data=data)
print(r.status_code, r.reason)
soup = BeautifulSoup(r.text)
for tr in soup.find_all('tr'):
  print('------------------------')
  print(tr.text)

認証があるページの例

単純なフォームの送信例はPOSTを一回送信するだけで済みましたが、認証処理やページの不正遷移防止が行われているWebアプリについてはサーバからの情報を受け取ってそれを基にデータを送信する必要があります。

今回はbitnamiから取得できるRedmineのVMでチケット登録を行うサンプルを見てみます。
VMのもろもろの設定はTestLinkで設定したときと同様に行えます。

Redmineでログインしてチケットを登録するには以下の手順を踏む必要があります。

  • ログインページを取得する。
  • サーバーはヘッダーにセッションID、HTML中に認証トークン文字を埋め込んでログイン用のページを返す。
  • セッションIDをリクエストヘッダに設定し、リクエストボディに認証トークン、ユーザ名、パスワード、ログイン後の遷移ページ(チケット登録画面)を指定してログイン処理を行う。
  • サーバーはログインに成功したらチケット登録画面を返す。この際、認証トークン文字が新しいものに変更される。
  • セッションIDをリクエストヘッダに設定し、リクエストボディに認証トークン、チケット情報を指定してチケット登録処理を行う。

PowerShellの例

PowerShellでRedmineのチケットを登録するには以下のようになります。

# エラーが起きたらとめる
$ErrorActionPreference = "Stop"

# サーバから取得したCookieの値からキーを指定して値を取得する
function get_key_value($value, $key) {
  $tmp = $value.substring($value.indexof($key) + $key.length)
  $ret = $tmp.substring(0, $tmp.indexof(';'))
  return $ret
}

# DOMを解析して指定の名前の指定の属性を取得する
function get_attribyte_value($html, $elem_name, $attr_name) {
  $elems = $html.getElementsByName($elem_name)
  $elem = $elems[0]
  $attrs = $elem.attributes
  $attr = $attrs[$attr_name]
  $ret = $attr.value
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($elems) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($elem) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($attrs) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($attr) | Out-Null
  return $ret
}

##################################
#Redmineの設定値
##################################
$redmine_host = "192.168.0.200"  # サーバ名
$redmine_project = "test1"       # プロジェクト名
$username = "user"
$password = "pass"

##################################
# ログインページを初回アクセスしてセッションIDとcsrf-tokenを取得する
##################################
$ret = Invoke-WebRequest "http://$redmine_host/login" -Method GET

# セッションID取得
$cookie = $ret.Headers['Set-Cookie']
$session_id = get_key_value $ret.Headers['Set-Cookie'] '_redmine_session='

# ログインページのcsrf-token取得
$html = $ret.ParsedHtml
$csrf_token = get_attribyte_value $html 'csrf-token' 'content'
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

# セッション情報作成
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$cookie = New-Object System.Net.Cookie 
$cookie.Name = "_redmine_session"
$cookie.Value = $session_id
$cookie.Domain = $redmine_host
$session.Cookies.Add($cookie);

##################################
# ログイン処理。
# ログイン後はチケット登録画面へ
##################################
$login_data = @{
  authenticity_token = $csrf_token;
  back_url = "http://$redmine_host/projects/$redmine_project/issues/new";
  username = $username;
  password = $password;
}
$ret = Invoke-WebRequest  "http://$redmine_host/login" -Method POST -WebSession $session -Body $login_data -ContentType "application/x-www-form-urlencoded"

# csrf-token取得
$html = $ret.ParsedHtml
$csrf_token = get_attribyte_value $html 'csrf-token' 'content'
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

##################################
# チケット登録
##################################
Write-Host "チケット登録........................................................"
$title = Get-Date -format "yyyyMMddHHmmss"
$ticket_data = @{
  'utf8' = '✓';
  authenticity_token = $csrf_token;
  'issue[is_private]' = 0;
  'issue[tracker_id]' = 1;
  'issue[subject]' = "自動登録 $title";
  'issue[description]' = "わっふるわっふる";
  'issue[status_id]' = 1;
  'was_default_status' = 1;
  'issue[priority_id]' = 2;
  'issue[start_date]' =  '2019-10-10';
  'issue[due_date]' =  '';
  'issue[done_ratio]' = 0;
  'commit' = '作成'
}
$ret = Invoke-WebRequest  "http://$redmine_host/projects/$redmine_project/issues" -Method POST -WebSession $session -Body $ticket_data -ContentType "multipart/form-data"
$html = $ret.ParsedHtml
Write-Host $html.title
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

Pythonの例

HTMLの解析がしんどいのでBeautiful Soupを使用した方がいいでしょう。

import requests
from bs4 import BeautifulSoup
import datetime

##############################################
# redmineの情報
##############################################
redmine_host = "192.168.0.200"  # サーバ名
redmine_project = "test1"       # プロジェクト名
username = "user"
password = "password"

# セッションの作成
session = requests.session()

# ログインページの取得
ret = session.get('http://' + redmine_host + '/login')
print(ret.status_code, ret.reason)
ret.raise_for_status()

session_id = ret.cookies['_redmine_session']

soup = BeautifulSoup(ret.text, 'html.parser')
elem_token = soup.find('meta', {'name': 'csrf-token'})
csrf_token = elem_token['content']

# ログイン処理
cookies = {
  redmine_host : session_id
}
login_data = {
  'authenticity_token' : csrf_token,
  'back_url' : "http://' + redmine_host + '/projects/' + redmine_project + '/issues/new",
  'username' : username,
  'password' : password
}
ret = session.post('http://' + redmine_host + '/login', data=login_data, cookies=cookies)
print(ret.status_code, ret.reason)
ret.raise_for_status()

# チケット登録
print('チケット登録................................')
soup = BeautifulSoup(ret.text, 'html.parser')
elem_token = soup.find('meta', {'name': 'csrf-token'})
csrf_token = elem_token['content']

ticket_data = {
  'utf8' : '✓',
  'authenticity_token' : csrf_token,
  'issue[is_private]' : 0,
  'issue[tracker_id]' : 1,
  'issue[subject]' : "自動登録 " + str(datetime.datetime.now()),
  'issue[description]' : "わっふるわっふる",
  'issue[status_id]' : 1,
  'was_default_status' : 1,
  'issue[priority_id]' : 2,
  'issue[start_date]' :  '2019-10-10',
  'issue[due_date]' : '',
  'issue[done_ratio]' : 0,
  'commit' : '作成'
}
ret = session.post('http://' + redmine_host + '/projects/' + redmine_project + '/issues', data=ticket_data, cookies=cookies)
print(ret.status_code, ret.reason)
ret.raise_for_status()

soup = BeautifulSoup(ret.text, 'html.parser')
print(soup.title)

ブラウザから送信しているデータを真似する方法のまとめ

ブラウザを介さないのでブラウザより簡単にかつ高速で自動操作が行えます。
同時にそれは、デメリットになる場合があります。
たとえば、JavaScriptでクライアントサイドで動的にページを作成している場合、その処理は動作しません。つまり、ブラウザで操作したときと同様のDOMの構成が返ってくるとは限りません。

この手法を結合試験やシステム試験で使用する場合は、注意してください。
仮にテストデータの入力に使う場合であっても、システム上本来作成できないデータが作成されてしまう場合があるからです。試験の観点に合わせて慎重に導入してください。

また、ブラウザの自動操作と同様にWebアプリケーションの変更によって今まで動いていた自動化スクリプトが動作しなくなるリスクはあるので注意してください。

Webアプリケーションが提供しているAPIを利用する方法

もっともリスクの少ないWebアプリケーションの自動化の方法です。
ただしWebアプリケーションがAPIを提供しているかどうかは個別の仕様次第になります。

Redmineのチケット登録の例

これまでにRedmineでチケット登録を行うサンプルをいくつか記述しました、Redmineが提供しているAPIを利用することでシンプルに実装することができます。

まずRedmineの管理画面でRESTAPIを有効にしてください。
image.png

すると個人設定画面でAPIキーが表示されます。このAPIを使用してRedmineを操作します。
image.png

PowerShellの例

チケット用のXMLを作成してPOSTするだけです。
この際、APIキーをヘッダに付与して送信します。

##################################
#Redmineの設定値
##################################
$redmine_host = "192.168.0.200"  # サーバ名
$apikey = "60076966cebf71506ae3f2391da649235a2b1d46"

$title = Get-Date -format "yyyyMMddHHmmss"
$xml = @"
<issue>
    <project_id>1</project_id>
    <subject>RESTAPIテスト $title</subject>
    <description>試験</description>
</issue>
"@
# セッション情報作成
$headers = @{
  'X-Redmine-API-Key' = $apikey;
  'Content-Type' = 'text/xml';
}

# 文字化けして登録されるなら以下をいれる
$sendData = [System.Text.Encoding]::UTF8.GetBytes($xml)

$ret = Invoke-WebRequest http://$redmine_host/issues.xml -Headers $headers -Method POST -WebSession $session -Body $sendData
Write-Host $ret.Content

Pythonの場合

import requests
import datetime

xml = """
<issue>
    <project_id>1</project_id>
    <subject>RESTAPIテスト python {0}</subject>
    <description>試験</description>
</issue>
""".format(str(datetime.datetime.now()))

headers = {
  'X-Redmine-API-Key' : '60076966cebf71506ae3f2391da649235a2b1d46',
  'Content-Type' : 'text/xml'
}

r = requests.post('http://192.168.0.200/issues.xml', data=xml.encode('utf-8'), headers=headers)
print(r.status_code, r.reason)
print(r.text)

なお、PythonでやるならPython Redmineあたりのライブラリを使用したほうが楽だと思います。

共通的な注意事項

ここまででWebアプリケーションの自動化の方法についていくつか方法を説明しました。
最後に共通的な注意事項を述べておきたいと思います。

できることと、やっていいことは違う

おそらくここまでで、多くのWebアプリケーションを自動で操作することが可能になったと思います。
しかしながら、できることと、やっていいことは違うということを常に心がけてください。

API経由以外の自動操作はWebアプリケーション側が想定していない操作になる可能性があります。つまり、いつ動かなくなってもおかしくありませんし、仮に動くからといってやっていい操作とは限りません。
場合によっては規約違反に問われることになります。たとえば広告ブロックして云々とか、複数人で遊ぶブラウザゲームの自動化は、かなりの確率で規約違反になります。
Webアプリケーションを自動化する際は必ず規約を確認してから行うようにしましょう。

また、そういった規約が明記されておらず、不正に当たらないと考えられる場合であっても自動操作はWebアプリケーション側に想定外の負荷を与えることがあります。たとえば、2010年には情報取得目的に図書館の蔵書検索システムに高頻度(1秒に1アクセス程度)のリクエストを送信して偽計業務妨害容疑で逮捕された岡崎市立中央図書館事件があったことは心に留めておくべきでしょう。

特に社内システムの場合、品質が悪い傾向があるので、根回しをしつつやっておくか、すぐに停止てきる状況かで実施し始めた方が無難です。

武器や流派にこだわるな

「武器や流派にこだわるな」という格言は、およそ20年前の名著「アジャイルソフトウェア開発 (The Agile Software Development Series) 」の「付録B3 武蔵」の項目にでてきた格言です。

今回、色々な自動化の方法を紹介はしましたが、それは自動化を行うための選択肢を増やして「こだわりを捨ててもらう」意図がありました。

RPAツールは素晴らしく自動化の手助けになります。しかしながら、あきらかに別の方法でやったほうが楽な場合でもRPAツールにこだわるケースがよくみられます。例えばブラウザ画面を介しての自動操作に慣れ親しんだ人はcurlコマンドで済むようなことまで慣れ親しんだという理由だけで困難な技法を選択してしまうケースをよく見かけます。
逆にcurlコマンドでは行うのが困難なことを、それだけでやろうとするケースも同じくらいよく見ます。

普段は使用しない技法であっても必要があるなら採用すべきですし、逆に最も自分が慣れ親しんだ技法であっても状況にそぐわなければ捨てるべきです。

自動化スクリプトの管理方法を考えよう

1度動かせばすむスクリプトなのか、定期的に動かすスクリプトなのかによって、スクリプトの管理方法が変わります。
定期的に動かすスクリプトの場合、常にWebアプリケーションのバージョンアップで動作しなくなるというリスクがあります。
このリスクをどう扱うか考えましょう。

たとえば、実際やってエラーとなった時点で修正する時間的余裕のあるものであれば、その時に考えればいいでしょう。
しかし、そういう時間的余裕が確保できないようなスクリプトの場合は、事前にそれを検出する必要があります。
定期的にスモークテストを行う計画を立てるか、Webアプリケーションのリリースノートをチェックする工数をとるか、いずれにせよなんらかの対策が必要になります。

あとは、自動化スクリプトの意図を複数の人間が理解して、メンテナンスできる体制を作るよう必要があります。人間は割と簡単にいなくなります。一誰も意図が分からない自動化スクリプトが動き続ける状態にならないように気を付けましょう。

自動化のコストを見積もる場合、これらの作ったあとのメンテナンスのコストについて忘れずに考えておきましょう。

自動化を目的にするのはやめよう

慣れてくると、なんらかの方法で多くのことが自動化できるようになりますが、それを目的とするのはやめましょう。
重大な障害対応を放置して、優先度の低い自動化スクリプトを書いても意味はありません。
全体の状況をみて、効果のありそうなところを自動化しましょう。

無理ならあきらめよう

どうしても自動化できないこともあります。
素直にあきらめて別の事をしましょう。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webアプリケーションを自動で操作してみよう

はじめに

Webアプリケーションに対してある種の繰り返しの操作を行ったり、定型処理を定期的に自動実行したい場合がよくあります。
大きくわけてWebアプリケーションの自動化には3種類のやり方が存在します。

1つ目はブラウザのGUI上の操作をプログラム上で真似して自動化する方法
2つ目はブラウザから送信しているデータを真似する方法
3つ目はWebアプリケーションが提供しているAPIを利用する方法

1つ目のブラウザのGUI上の操作をプログラム上で真似して自動化する方法は直観的にわかりやすいと言われますが、実際は最も難しい自動化の方法になります。また、アプリケーションのバージョンアップに伴い自動化用のプログラムが動作しなくなる可能性があります。

2つ目のブラウザから送信しているデータを真似する方法はプログラムで実装しやすいやり方ではありますが、Webアプリケーションがどのようなデータを送信しているかなどを調べる必要があります。また、1つ目と同様にアプリケーションのバージョンアップに伴い自動化用のプログラムが動作しなくなる可能性があります。

3つ目のWebアプリケーションが提供しているAPIを利用する方法が最も簡単でかつ正確に自動操作を行えます。ただし、WebアプリケーションのAPIが提供されているかどうかは自動操作対象のWebアプリケーションの仕様次第です。

実験環境

Windows10+PowerShell5.1
Visual Studio 2019 + .NET Framework 4.6
Python 3.7.4
NodeJs v10.16.0
UiPath 2019.8.0-beta 83

ブラウザのGUI上の操作をプログラム上で真似して自動化する方法

Windowsの場合、ブラウザを操作して自動化する方法も大きく4つの方法があります。

1つ目の方法はInternetExploreのCOMを利用する方法です。WindowsのInternetExploreに対してならば、なにもインストールすることなく、ブラウザの自動操作が可能になっています。ただし、InternetExploreの寿命自体が、長く持たない可能性があるので注意が必要です。(InternetExplore自体が持っても、対応するWebアプリケーションがInternetExploreのサポートを切る可能性が高いです)

2つ目の方法としてSeleniumを使用する方法です。外部のライブラリが必要になりますが、多くのブラウザがサポートされています。

3つ目の方法としてはブラウザが提供している拡張機能を作成する方法です。ChromeとFirefoxの場合、JavaScriptで作成できるので、外部のライブラリを導入する必要はありません。

4つ目の方法としてRPAツールを使用する方法です。ブラウザ上の要素について深く考えなくても、GUIベースで自動化が行えますが、コストの面で問題になります。

HTMLの要素を調べ方

どのような方法でブラウザを操作するとしても、HTMLがどのような要素で構成されているかを調べる必要があります。
ここではChromeでGoogleで検索する場合を例として画面上の要素を調べる方法を説明します。

image.png

1.ChromeにてF12キーを押下して開発者ツールを開きます。その後、「Elements」タブを選択してください。

image.png

2.[CTRL]+[Shift]+[C]を押下するか、下記のアイコンをクリックします。
image.png

3.調べたい要素にマウスを移動させます。
image.png

4.Elementsタブに選択した要素の内容が表示されます。今回の場合、以下のような内容が表示されます。

<input class="gLFyf gsfi" maxlength="2048" name="q" type="text" jsaction="paste:puy29d" aria-autocomplete="both" aria-haspopup="false" autocapitalize="off" autocomplete="off" autocorrect="off" role="combobox" spellcheck="false" title="検索" value="" aria-label="検索" data-ved="0ahUKEwiC0u6iu4nlAhXwyIsBHWwTBHcQ39UDCAQ">

inputタグの属性が以下のようになっていることがわかります。

属性
class gLFyf gsfi
name q

自動操作を行う場合、id、name、classなどを利用して要素を指定することになるので、属性値をメモしておきましょう。

5.同様にボタンについても属性を調べます。その結果は以下のようになります。

<input class="gNO89b" value="Google 検索" aria-label="Google 検索" name="btnK" type="submit" data-ved="0ahUKEwiC0u6iu4nlAhXwyIsBHWwTBHcQ4dUDCAo">
属性
class gNO89b
name btnK

ここで調べた属性を利用して要素を特定して自動操作を行うことになります。。
また、今回はChromeでのやり方を紹介しましたが、他のブラウザでも同様のことが可能です。同じWebアプリケーションを使用していてもブラウザによって出力される内容が異なる可能性もあるので、自動操作を行うブラウザを使用して要素を調べるようにしましょう。

InternetExplore11の場合

IE11でもF12キーを押すことで開発者ツールが表示されます。
そこで「DOM Explore」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

Edgeの場合

Edgeは将来Chromeベースのものに置き換わる可能性があります。
今回は旧EdgeとChromeベースの新Edge両方について説明します。

旧Edge

F12キーで開発者ツールが表示されます。
そこで「要素」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

新Edge

2019年10月時点でベータ版としてリリースされているEdgeの場合、F12キーで開発者ツールが表示されます。
Chromeと同様の操作で要素の属性を調べることが可能です。

image.png

Firefoxの場合

F12キーで開発者ツールが表示されます。
そこで「インスペクター」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

HTMLの要素を調べる方のまとめ

多くのブラウザは開発者ツールをサポートしており、要素の属性を調べることが可能です。
Webアプリケーションの自動化では、この要素を特定して操作する必要があるので、お使いのブラウザでの要素の調べ方は覚えておきましょう。

実際の自動操作の例は次章から解説します。

Internet ExploreのCOMを使用した自動化

Internet ExploreのCOMであるmshtmlを経由することでInternetExploreを自動操作できます。

COM(Component Object Model)
Microsoftが提唱した再利用を目的とした技術で、COMを用いて開発した部品はプログラム言語に依存せずに利用できるようになります。たとえば、説明したInternetExploreの操作やOfficeアプリケーションなどが外部から利用できるのは、COMのおかげです。

IE操作の単純な例

実際のGoogleのトップページで任意の単語を検索するサンプルを見てみましょう。

WSHやVBAでのIE操作の単純な例

WSHのVBScritpで以下のように実装可能です。
以下コードはWScript.EchoをDebug.Print等に置き換えることでVBAでも流用できると思います。

Dim ie
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = True 
call ie.Navigate("https://www.google.com/")

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100        
Loop

Dim doc
Set doc = ie.Document
Dim txt
Set txt = doc.getElementsByName("q")
txt.item(0).value = "ドリフターズ"

Dim btn
Set btn = doc.getElementsByName("btnK")
btn.item(0).click()

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100        
Loop
Set doc = ie.Document

Dim list
Do While True
    Set list = doc.getElementsByClassName("LC20lb")

    If Not list is Nothing Then
        If list.length > 0 Then
            Exit Do
        End If
    End If
    WScript.Sleep 100 
Loop

Dim item
For Each item In list
    WScript.Echo item.innerText
Next
ie.Quit

このサンプルではNavigateで指定のURLに移動したのち、getElementsByNameを使用して属性を取得して、値の設定とクリック操作を行っています。
その後,BusyreadyStateを監視してページの切り替え完了を待ちます。
その後、検索結果の要素が出現するまで待機して、その要素の内容を出力します。

PowerShellでのIE操作の単純な例

実はPowerShellやC#などの.NETでの実装は厄介です。結論から言えばやめといた方がいいです。
たとえば、よく見かける実装でGoogleとはてなブックマークを検索するスクリプトieng1.ps1とieng2.ps1を用意しました。

Googleの検索

ieng1.ps1
$ie = New-Object -ComObject InternetExplorer.Application  # IE起動

$ie.Visible = $true
$ie.Navigate("https://www.google.com/")


# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc=$ie.Document

# 検索文字入力
$txt=$doc.getElementsByName("q")
$txt[0].value = "ドリフターズ"

# ボタン押下
$btn=$doc.getElementsByName("btnK")
$btn[0].click()

# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc = $ie.Document

# LC20lbが表示されるまで待機
while($true) {
  $list = $doc.getElementsByClassName('LC20lb')
  if($list -and $list.length -ge 1) { break }
  Start-Sleep -Milliseconds 100
}

foreach($i in $list){
  Write-Host($i.innerText)
}
$ie.Quit()
Write-Host("OK")

はてなの検索

ieng2.ps1
$ie = New-Object -ComObject InternetExplorer.Application  # IE起動

$ie.Visible = $true
$ie.Navigate("https://b.hatena.ne.jp/")


# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc=$ie.Document

# 検索文字入力
$txt=$doc.getElementsByName("q")
$txt[0].value = "ドリフターズ"

# ボタン押下
$btn=$doc.getElementsByClassName("gh-search-button")
$btn[0].click()

# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc = $ie.Document

# LC20lbが表示されるまで待機
while($true) {
  $list = $doc.getElementsByClassName('centerarticle-entry-title')
  if($list -and $list.length -ge 1) { break }
  Start-Sleep -Milliseconds 100
}

foreach($i in $list){
  Write-Host($i.innerText)
}
$ie.Quit()
Write-Host("OK")

この実装はいくつか問題を起こす可能性があります。

PowerShellでのIE操作の問題点
環境によっては動作しない

同じコードをPowerShell2.0+Windows7で動かそうとしたところ下記のエラーを表示して動作しません。

PS C:\share\webctrl> $ie.Document.getElementsByName("q")
"getElementsByName" のオーバーロードで、引数の数が "1" であるものが見つかりません。
発生場所 行:1 文字:31
+ $ie.Document.getElementsByName <<<< ("q")
    + CategoryInfo          : NotSpecified: (:) []、MethodException
    + FullyQualifiedErrorId : MethodCountCouldNotFin

この問題は下記のフォーラムで議論されていますが、解決はしていません。

COMの解放処理を行っていない

上記のコードはCOMを.NETから利用しているにも関わらずReleaseComObjectを行っていません。
類似の問題として以下を参照してください。

.NETを使ったOfficeの自動化が面倒なはずがない―そう考えていた時期が俺にもありました。
https://qiita.com/mima_ita/items/aa811423d8c4410eca71

解放処理を行ったコードは以下のようになります。

    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://www.google.com/")


    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=$doc.getElementsByName("btnK")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    # LC20lbが表示されるまで待機
    while($true) {
      $list = $doc.getElementsByClassName('LC20lb')
      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue


    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

同一プロセスで複数起動した場合にエラーになる

先にあげた2つのスクリプトはPowerShellを再起動した直後には、それぞれ動作しますが以下のように続けて実行するとエラーになります。

正常に動作する
>powershell ./ieng1.ps1
>powershell ./ieng2.ps1

2つ目のスクリプトの実行でエラーになる
>./ieng1.ps1
>./ieng2.ps1

エラーの内容は下記の通りです。

HRESULT からの例外:0x800A01B6
発生場所 C:\dev\ps\webctrl\ieng2.ps1:14 文字:1
+ $txt=$doc.getElementsByName("q")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], NotSupportedException
    + FullyQualifiedErrorId : System.NotSupportedException

この問題は以下の記事で言及されています。

getElementsByNameをIHTMLDocument3_getElementsByNameに置き換えて、getElementsByClassNameをInvokeMemberで呼び出すようにすれば回避できるようです。

Googleの検索(修正版)

ieok1.ps1
    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://www.google.com/")


    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.IHTMLDocument3_getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=$doc.IHTMLDocument3_getElementsByName("btnK")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    # LC20lbが表示されるまで待機
    while($true) {
      $list = [System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "LC20lb")

      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue


    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

はてなの検索(修正版)

ieok2.ps1
    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://b.hatena.ne.jp/")


    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.IHTMLDocument3_getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=[System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "gh-search-button")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    #表示されるまで待機
    while($true) {
      $list = [System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "centerarticle-entry-title")

      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue


    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

PowerShellでのInternetExploreの操作のまとめ

辞めといた方がいいでしょう。

回避策を書いているページは色々見つかりますが、何故その事象が発生しているか、そしてなぜ回避することができたかを説明できているサイトは見つからず、ためしてみたら動作した以上のものではありません。
固定の環境で動かすスクリプトならばともかく、不特定多数の環境で動作するスクリプトでは採用を避けるべきでしょう。また、多くの場合、VBSやVBAで代替できるのでPowerShellにこだわる理由はないでしょう。

どうしても、PowerShellで行いたい場合は、IE操作をしたプロセスを終了するようにすると安定しそうです。

すでに起動しているブラウザの操作方法

すでに起動しているIEを操作するにはIEのウィンドウに対してRegisterWindowMessageでWM_HTML_GETOBJECTメッセージを定義して送信することでHTMLDocumentを取得することになります。

Cant create HTML document from Hwnd using C#
https://stackoverflow.com/questions/20873885/cant-create-html-document-from-hwnd-using-c-sharp

Win32 APIを直接実行できないWSHでは実現が難しいです。

VBAでの実装例

下記を参考にしてください。

VBAでInternetExplore上のJavaScriptを無理やり動かすよ!
https://qiita.com/mima_ita/items/fdff129a8db1153c9940

C#での実装例

.NET経由になるのでCOMの解放処理を入れる必要があります。

(1)参照マネージャーのCOMタブでMicrosoft HTML Object Libraryを追加します。
image.png

これにより「Interop.MSHTML.dll」が作成されます。
Interop~.dllはtlbImpコマンドを使用することでコマンドラインで作成できますが、VisualStudioなどの開発ツールをインストールしていないと使えないと思います。

(2)以下のような実装をします。下記のコードは.NET2.0でも動作します。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using MSHTML;
using System.Diagnostics;

namespace iesample
{
    class Program
    {


        [DllImport("user32.dll", EntryPoint = "GetClassNameA")]
        public static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount);

        /*delegate to handle EnumChildWindows*/
        public delegate int EnumProc(IntPtr hWnd, ref IntPtr lParam);


        [DllImport("user32.dll")]
        public static extern int EnumWindows(EnumProc lpEnumFunc, ref IntPtr lParam);


        [DllImport("user32.dll")]
        public static extern int EnumChildWindows(IntPtr hWndParent, EnumProc lpEnumFunc, ref IntPtr lParam);

        [DllImport("user32.dll", EntryPoint = "RegisterWindowMessageA")]
        public static extern int RegisterWindowMessage(string lpString);

        [DllImport("user32.dll", EntryPoint = "SendMessageTimeoutA")]
        public static extern int SendMessageTimeout(IntPtr hwnd, int msg, int wParam, int lParam, int fuFlags, int uTimeout, out UIntPtr lpdwResult);

        [DllImport("OLEACC.dll")]
        public static extern int ObjectFromLresult(UIntPtr lResult, ref Guid _riid, int wParam, ref MSHTML.IHTMLDocument2 _ppvObject);

        public const int SMTO_ABORTIFHUNG = 0x2;
        public Guid IID_IHTMLDocument = new Guid("626FC520-A41E-11CF-A731-00A0C9082637");


        private static List<IHTMLDocument2> cacheList = new List<IHTMLDocument2>();

        public static MSHTML.IHTMLDocument2 FindBrowser(string title)
        {
            MSHTML.IHTMLDocument2 ret;
            if (cacheList.Count == 0)
            {
                RefreshBrowserCache();
            }
            ret = FindBrowserInCache(title);
            if (ret != null)
            {
                return ret;
            }
            RefreshBrowserCache();
            return FindBrowserInCache(title);
        }
        public static MSHTML.IHTMLDocument2 FindBrowserInCache(string title)
        {
            MSHTML.IHTMLDocument2 ret;
            foreach(MSHTML.IHTMLDocument2 item in cacheList)
            {
                if (item.title.Contains(title))
                {
                    return item;
                }
            }
            return null;
        }

        public static void RefreshBrowserCache()
        {
            foreach (IHTMLDocument2 item in cacheList)
            {
                Marshal.ReleaseComObject(item);
            }
            cacheList.Clear();
            EnumProc proc = new EnumProc(EnumIEWndProc);
            IntPtr lparam = IntPtr.Zero;
            EnumWindows(proc, ref lparam);
        }

        private static int EnumIEWndProc(IntPtr hWnd, ref IntPtr lParam)
        {
            StringBuilder className = new StringBuilder(128);
            GetClassName(hWnd, className, className.Capacity);
            EnumProc proc = new EnumProc(EnumIEServerWndProc);
            if (className.ToString().Equals("IEFrame") || className.ToString().Equals("TabWindowClass"))
            {
                IntPtr lparam = IntPtr.Zero;
                EnumChildWindows(hWnd, proc, ref lparam);
            }
            return 1;
        }
        private static int EnumIEServerWndProc(IntPtr hWnd, ref IntPtr lParam)
        {
            StringBuilder className = new StringBuilder(128);
            GetClassName(hWnd, className, className.Capacity);
            if (className.ToString().Equals("Internet Explorer_Server"))
            {
                IHTMLDocument2 doc = GetHTMLDocument(hWnd);
                if (doc != null)
                {
                    cacheList.Add(doc);
                }
            }

            return 1;
        }
        private static IHTMLDocument2 GetHTMLDocument(IntPtr hWnd)
        {
            int nMsg = RegisterWindowMessage("WM_HTML_GETOBJECT");
            if (nMsg == 0)
            {
                return null;
            }
            UIntPtr lRes;
            SendMessageTimeout(hWnd, nMsg, 0, 0, SMTO_ABORTIFHUNG, 1000, out lRes);
            if (lRes == UIntPtr.Zero)
            {
                return null;
            }

            Guid IID_IHTMLDocument = new Guid("626FC520-A41E-11CF-A731-00A0C9082637");
            IHTMLDocument2 doc = null;
            int hr = ObjectFromLresult(lRes, ref IID_IHTMLDocument, 0, ref doc);
            return doc;
         }

        static void Main(string[] args)
        {
            IHTMLDocument3 doc = FindBrowser("Google") as IHTMLDocument3;
            IHTMLInputElement txt = doc.getElementsByName("q").item(0) as IHTMLInputElement;
            txt.value = "ドリフターズ";

            IHTMLElement btn = doc.getElementsByName("btnK").item(0) as IHTMLElement;
            btn.click();



            Marshal.ReleaseComObject(btn);
            Marshal.ReleaseComObject(txt);
            Marshal.ReleaseComObject(doc);

        }
    }
}

様々なコントロールを含むサンプルの例

様々なコントロールを含む以下のページの入力の自動化について考えてみます。
操作対象として以下のページを使用します。
http://needtec.sakura.ne.jp/auto_demo/form1.html

このページは「登録する」ボタンを押下することで確認メッセージが表示されて、「OK」の場合に登録処理を行うものとします。※
※実際はSleepしているだけでなにもしていないです。

WSHやVBSでの操作例は以下のようになります。

Dim ie
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = True 
call ie.Navigate("http://needtec.sakura.ne.jp/auto_demo/form1.html")

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100
Loop

Dim doc
Set doc = ie.Document
' INPUTBOX
doc.getElementsByName("name").item(0).value = "名前太郎"
doc.getElementsByName("mail").item(0).value = "test@co.jp"

' テキストエリア
doc.getElementsByName("comment").item(0).innerText = "猫猫子猫" & vbCrLf & "犬犬子犬"

' チェックボックス
doc.getElementsByName("q1[]").item(0).Checked = True
doc.getElementsByName("q1[]").item(1).Checked = True

' ラジオボタン
doc.getElementsByName("men").item(1).Checked = True

' 複数選択リスト
Dim objSelect
Set objSelect = doc.getElementsByName("osi[]").item(0)
objSelect.getElementsByTagName("option").item(1).Selected = True
objSelect.getElementsByTagName("option").item(2).Selected = True

' ボタン押下
' 確認メッセージ処理を偽造する
doc.parentWindow.ExecScript("confirm = function () { return true; }")
doc.getElementsByTagName("input").item(8).click()

確認ダイアログを突破する方法は色々ありますが、上記でやったようなJavaScirptのconfirmやalertを上書きしてしまうのが最も楽だと思います。

確認メッセージの処理を上書きしたくない場合

UIAutomation等を使用します。
確認メッセージが表示されるまで待機してボタンを押下するような処理を別スレッドかプロセスで起動します。WSHやVBAの場合は別プロセスでやった方が楽です。

まず、確認メッセージを監視するようなスクリプトを記載します。
これはPowerShellで書いた方が楽だと思います。

wait_confirm.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 GetWindowByTitle(string title) {
        var rootCond = new PropertyCondition(AutomationElement.ClassNameProperty, "Alternate Modal Top Most");
        var cond = new PropertyCondition(AutomationElement.NameProperty, title);
        var elementCollection = RootElement.FindAll(TreeScope.Children, rootCond);
        foreach(AutomationElement mainForm in elementCollection) {
            var win =  mainForm.FindFirst(TreeScope.Children, cond);
            if (win != null) {
                return win;
            }
        }
        return null;
    }

    public static AutomationElement WaitWindowByTitle(string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = GetWindowByTitle(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]

# ウィンドウ以下で指定の条件に当てはまるコントロールを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)
}

# 指定の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()
}


#
$dialog = [AutomationHelper]::WaitWindowByTitle("Web ページからのメッセージ", 30)
if ($dialog -eq $null) {
    Write-Host "タイムアウト"
} else {
    pushButtonById $dialog "1"
    Write-Host "終了"
}

あとはWSH側のボタン押下処理を以下のように修正します。

' ボタン押下
' 確認メッセージ処理を別プロセスで行う
Dim shell
Set shell = CreateObject("WScript.Shell")
shell.Run "C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File C:\dev\ps\webctrl\wait_confirm.ps1", 0, False

doc.getElementsByTagName("input").item(8).click()

Internet ExploreのCOMを使用した自動化のまとめ

VBAやVBSなどの何処でもはいっていそうなプログラミング言語で自動化できるのは強みです。
PowerShellでも行えますが、.NET経由だとCOMの解放処理が面倒だったり、動作が安定しない環境もあるので環境を制御できる場合のみに使用したほうがいいでしょう。

また、InternetExploreのサポートがいつまで続くかわからない以上、外部のツールを導入可能である場合は、お勧めしません。

Seleniumを使用した自動化

様々なOS上の様々なブラウザを様々なプログラミング言語で自動操作するためのツールです。
Webアプリケーションをブラウザ操作で自動化する場合、もっともよく使われるツールになります。
Selenium実践入門などの良書が流通しているので、この章は飛ばしてそっちを読んだ方がいいと思います。

SeleniumIDEを使用する例

ブラウザの操作を記憶する録画機能が提供されておりGUIベースで自動操作の処理を記載できます。録画した操作内容はスクリプトとして記録され、後で修正が可能です。また、別のプログラム言語で記載されたテストコードに変換することもできます。

以下はGoogle検索の操作をキャプチャした例になります。

auto.gif

SeleniumIDEはChromeまたはFirefoxの拡張機能として提供されています。

「あれ?SeleniumIDEって終わったんじゃなかったっけ?」という人は下記の経緯を参照してください。

Webブラウザ自動化ツール「Selenium IDE」の今までとこれから
https://www.valtes.co.jp/qbookplus/509

ChromeでSeleniumIDEを使用する

(1)Chrome用のSeleniumIDEを拡張機能として追加します。

(2)ブラウザの上部にSeleniumIDEのアイコンが表示されるのでクリックします。
image.png

(3)SeleniumIDEのポップアップが表示されるので「Record a new test in new project」を選択します。
image.png

(4)「Name your new project」ダイアログが表示されるので任意のプロジェクト名を入力して「OK」ボタンを押します。
image.png

(5)「Set your projects's base URL」ダイアログが表示されるので操作元になるURLを入力します。たとえばGoogle検索をする例だと「https://www.google.com」を入力して「START RECORDING」ボタンを押下します。

image.png

(6)操作の記録が始まると右下に「Selenium IDE is recording...」と書かれた新しいブラウザが開きます。
image.png
このブラウザを使用して記録したい任意の操作を行います。

(7)操作の記録を終了したい場合、「Selenium IDE」ウィンドウの右上の「Stop recording」アイコンを押します。
image.png

(8)「Name your new test」ポップアップが表示されるので任意のテスト名を入力して「OK」を押します。
image.png

(9)SeleniumIDE ウィンドウに今回操作した内容がスクリプトとして記録されます。
image.png

(10)記録したスクリプトは「Run Current Test」アイコンを押すことで再実行可能です。
image.png

また、JUnit や pytest、 JavaScript Mochaといった他のプログラミング言語のユニットテストとしてエクスポートすることが可能です。

FirefoxでSeleniumIDEを使用する

Firefox用のSeleniumIDEを拡張機能として追加します。
あとの操作はChromeと同じです。

ただし、記録される操作はFirefoxとChromeで差異があります。
Firefoxの場合、ブラウザのスクロール操作が記録されていましたが、Chromeでは記録されていませんでした。
image.png

なお、手で同じコマンドを追加すると、再生はChromeでも動作しました。

プログラムからSeleniumを利用する

C#の場合

まず、NuGetでSelenium.WebDriverとSelenium.Supportに加えて操作したいブラウザのDriverを入手します。今回はChromeを操作したいので、Selenium.Chrome.WebDriverを入手します。

image.png

image.png

C#でのSeleniumの操作例は以下のようになります。

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using System;
using System.IO;
using System.Reflection;

namespace chromeSample
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var driver = new ChromeDriver(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)))
            {
                driver.Url = "http://needtec.sakura.ne.jp/auto_demo/form1.html";
                driver.FindElementByName("name").SendKeys("名前太郎");
                driver.FindElementByName("mail").SendKeys("test@co.jp");
                driver.FindElementByName("comment").SendKeys("猫猫子猫\n\r犬犬子犬");

                // チェックボックス
                var chks = driver.FindElements(By.Name("q1[]"));
                chks[0].Click();
                chks[2].Click();

                // オプションボタン
                var opts = driver.FindElements(By.Name("men"));
                opts[1].Click();

                // 複数選択
                var sel = new OpenQA.Selenium.Support.UI.SelectElement(driver.FindElement(By.Name("osi[]")));
                sel.DeselectAll();
                sel.SelectByIndex(1);
                sel.SelectByIndex(2);

                // 登録ボタン押下 
                driver.FindElement(By.XPath("//input[@value='登録する']")).Click();
                // OKボタンを押す
                var confirm = driver.SwitchTo().Alert();
                confirm.Accept();

                // 結果の出力
                var results = driver.FindElementsByTagName("tr");
                foreach(var rec in results)
                {
                    Console.WriteLine(rec.Text);
                }
                Console.ReadLine();
                driver.Quit();
            }
        }
    }
}

ページの切り替え時に待機処理をいれていませんが、暗黙的にタイムアウトまでDOMをポーリングしています。このタイムアウトについては下記を参照してください。

The default value of timeouts on selenium webdriver
https://stackoverflow.com/questions/30114976/the-default-value-of-timeouts-on-selenium-webdriver

PowerShellの場合

PowerShellでもC#と同様な実装が可能です。
下記のページのSelenium Client & WebDriver Language BindingsからC#のクライアントと操作したいブラウザのWebDriverをダウンロードしてください。

https://www.seleniumhq.org/download/

image.png

image.png

クライアントをダウンロードすると以下のようなファイルが入っています。

  • Selenium.Support.3.14.0.nupkg
  • Selenium.WebDriver.3.14.0.nupkg
  • Selenium.WebDriverBackedSelenium.3.14.0.nupkg

これは圧縮されているファイルなので拡張子をzipに変更すればDLLを取り出せます。
サポートしているバージョンが.NET3.5以上なのでWindows7に初期から入っているPowerShellでは動作しません。

PowerShellでのサンプルは以下のようになります。

# 以下参考
# https://tech.mavericksevmont.com/blog/powershell-selenium-automate-web-browser-interactions-part-i/
$dllPath = Split-Path $MyInvocation.MyCommand.Path
Add-Type -Path "$dllPath\WebDriver.dll"
Add-Type -Path "$dllPath\WebDriver.Support.dll"

# chromedriver.exeがあるディレクトリを指定
$driver = New-Object OpenQA.Selenium.Chrome.ChromeDriver("C:\tool\selenium\chromedriver_win32\")
$driver.Url = "http://needtec.sakura.ne.jp/auto_demo/form1.html"
$driver.FindElementByName("name").SendKeys("名前太郎")
$driver.FindElementByName("mail").SendKeys("test@co.jp")

# テキストエリア
$comment = @"
猫猫子猫
犬犬子犬
"@
$driver.FindElementByName("comment").SendKeys($comment)

# チェックボックス
$chks = $driver.FindElements([OpenQA.Selenium.By]::Name("q1[]"))
$chks[0].Click()
$chks[2].Click()

#オプションボタン
$opts = $driver.FindElements([OpenQA.Selenium.By]::Name("men"))
$opts[1].Click()

#複数選択
$selElem = $driver.FindElement([OpenQA.Selenium.By]::Name("osi[]"))
$sel = New-Object OpenQA.Selenium.Support.UI.SelectElement -ArgumentList $selElem
$sel.DeselectAll();
$sel.SelectByIndex(1);
$sel.SelectByIndex(2);

# ボタン押下
$driver.FindElement([OpenQA.Selenium.By]::XPath("//input[@value='登録する']")).Click()
$confirm = $driver.SwitchTo().Alert();
$confirm.Accept()

# 結果表示
$results = $driver.FindElementsByTagName("tr");
foreach($rec in $results)
{
    Write-Host $rec.Text
}
$driver.Quit()
$driver.Dispose()

Write-Host("OK")

Pythonの場合

PythonでもSeleniumは使用可能です。まずpipコマンドでseleniumをインストールします。

pip install -U selenium

サンプルコードは以下のようになります。

from selenium import webdriver
from selenium.webdriver.support.ui import Select

driver = webdriver.Chrome("C:\\tool\\selenium\\chromedriver_win32\\chromedriver.exe")
driver.get("http://needtec.sakura.ne.jp/auto_demo/form1.html")
driver.find_element_by_name("name").send_keys("名前太郎");
driver.find_element_by_name("mail").send_keys("test@co.jp");
driver.find_element_by_name("comment").send_keys("猫猫子猫\n\r犬犬子犬");

# チェックボックス
chks = driver.find_elements_by_name("q1[]")
chks[0].click()
chks[2].click()

# オプションボタン
opts = driver.find_elements_by_name("men")
opts[1].click()

# 選択
sel = Select(driver.find_element_by_name("osi[]"))
sel.deselect_all()
sel.select_by_index(1)
sel.select_by_index(2)

# ボタン押下
driver.find_element_by_xpath("//input[@value='登録する']").click()
driver.switch_to.alert.accept()

# 結果
results = driver.find_elements_by_tag_name("tr")
for rec in results:
    print(rec.text)

driver.close()

NodeJsの場合

NodeJsでもSeleniumの操作は可能です。npmコマンドを使用してseleniumをインストールします。

npm install selenium-webdriver

簡単なサンプルは以下のようになります。

// 以下参考
// https://qiita.com/tonio0720/items/70c13ad304154d95e4bc
// https://stackoverflow.com/questions/26191142/selenium-nodejs-chromedriver-path
// https://seleniumhq.github.io/selenium/docs/api/javascript/index.html
const webdriver = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const path = 'C:\\tool\\selenium\\chromedriver_win32\\chromedriver.exe';
const service = new chrome.ServiceBuilder(path).build();
chrome.setDefaultService(service);

(async () => {
  const driver = await new webdriver.Builder()
                            .withCapabilities(webdriver.Capabilities.chrome())
                            .build();
  await driver.get('http://needtec.sakura.ne.jp/auto_demo/form1.html');
  await driver.findElement(webdriver.By.name("name")).sendKeys("名前太郎");
  await driver.findElement(webdriver.By.name("mail")).sendKeys("test@co.jp");
  await driver.findElement(webdriver.By.name("comment")).sendKeys("猫猫子猫\n\r犬犬子犬");

  // チェックボックス
  let chks = await driver.findElements(webdriver.By.name("q1[]"));
  await chks[0].click();
  await chks[2].click();

  // オプションボタン
  let opts = await driver.findElements(webdriver.By.name("men"));
  await opts[1].click();

  // 複数選択
  let sel = await driver.findElements(webdriver.By.xpath("//select[@name='osi[]']/option"));
  await sel[1].click();
  await sel[2].click();

  // ボタン押下
  await driver.findElement(webdriver.By.xpath("//input[@value='登録する']")).click();
  await driver.switchTo().alert().accept();

  // 結果取得
  let results = await driver.findElements(webdriver.By.tagName("tr"));
  for (let i = 0; i < results.length; ++i) {
    console.log(await results[i].getText());
  }

  driver.quit();
})();

Seleniumで既に起動しているブラウザの操作は行えるか?

Seleniumを介さず起動していたブラウザの自動操作については公式にはサポートしていません。
下記に幾つかの回避法が紹介されていますが、あくまで非公式の内容になります。

Can Selenium interact with an existing browser session?
https://stackoverflow.com/questions/8344776/can-selenium-interact-with-an-existing-browser-session

Seleniumを使用する方法のまとめ

Seleniumを使用することで様々なブラウザを様々なプログラミング言語で操作できることができます。
またSeleniumIDEを使用することでプログラミングをせずにブラウザの自動操作がおこなえます。

ただし、Flashページなどの画像認識を必要とする操作の場合は他の方法を検討してください。
たとえば以下のような方法があります。

Sikulix1.1.4を使って画面の自動操作をする
https://qiita.com/mima_ita/items/8f653042ac9140e5023f

C#やPowerShellで画面上の特定の画像の位置をクリックする方法
https://qiita.com/mima_ita/items/f7d2c38767bda8b35cbd

拡張機能を作成する方法

ChromeやFirefoxで利用できる拡張機能を使用して表示中のページを自動操作することが可能です。

Chromeの拡張機能
https://developer.chrome.com/extensions

Firefoxの拡張機能
https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension

以下はChromeの拡張機能を使用してページを自動操作後、操作結果をメッセージボックスで表示しています。

auto2.gif

操作対象のページ
http://needtec.sakura.ne.jp/auto_demo/form1.html

拡張機能を使った自動操作の仕組みは下記の通りです。
image.png

default_popupからcontent_scriptsに対して自動操作の開始指示をメッセージを使用して行います。
入力ページのcontent_scriptsは項目の入力とボタンの押下を行います。
出力ページのcontent_scriptsは出力ページの内容を取得してメッセージを使用してdefault_popupに内容を送信します。

なお、ChromeとFirefoxの拡張機能は同じような実装で作成できます。

Chromeの拡張機能で自動操作

Chrome拡張機能での自動操作のサンプル
https://github.com/mima3/auto_sample/tree/master/auto_chrome_sample

以下が実際の自動操作を行っているcontent_scriptになります。

content_input.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(request);
    let nameElem = document.getElementsByName('name');
    nameElem[0].value = '名前太郎';
    let mailElem = document.getElementsByName('mail');
    mailElem[0].value = 'test@co.jp';
    let commentElem = document.getElementsByName('comment');
    commentElem[0].value = '猫猫子猫\n犬犬子犬';
    // チェックボックス
    let chkElem = document.getElementsByName('q1[]');
    chkElem[0].click();
    chkElem[2].click();
    // ラジオボタン
    let radioElem = document.getElementsByName('men');
    radioElem[1].click();
    // 選択項目
    var itr = document.evaluate("//select[@name='osi[]']/option", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );
    var node = itr.iterateNext();
    while(node) {
      if (node.textContent === '千鶴さん' || node.textContent === 'さおりん') {
        node.selected = true;
      }
      node = itr.iterateNext();
    }
    // ボタン押下
    // contents.jsでwindows.confirmを書き換えてもブラウザ側の処理に影響を与えない
    // そのため、window.confirmを書き換えるscritpをタグとして挿入する
    // 考え方は以下を参考
    // https://qiita.com/suin/items/5e1aa942e654bce442f7
    let scr = document.createElement("script");
    scr.setAttribute('type', 'text/javascript');
    scr.innerText = 'window.confirm = function () { return true; }';
    document.body.appendChild(scr); 
    setTimeout(function(){ 
      //ここでやってもブラウザ上のwindow.confirmは影響ない。
      var btnElem = document.evaluate("//input[@value='登録する']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
      btnElem.singleNodeValue.click();
    }, 0);
  }
);

JavaScriptのDOM操作で入力項目を設定後、登録ボタンを押下します。
この際、confirmで確認ダイアログが表示されるため、window.confirmの処理を上書きしてダイアログが出ないようにしています。
content_scriptからブラウザで使用しているJavaScriptを更新するため、scriptタグを埋め込んでいます。
この考え方は下記を参考にしました。

Chrome拡張開発: 拡張からページにJavaScriptを送り込みたい
https://qiita.com/suin/items/5e1aa942e654bce442f7

Firefoxの拡張機能で自動操作

Firefox拡張機能での自動操作のサンプル
https://github.com/mima3/auto_sample/tree/master/auto_firefox_sample

拡張機能での自動操作のまとめ

ブラウザの拡張機能を利用することでブラウザの自動操作が行えます。
この方法のメリットとしてはインターネットの接続がなくてもテキストエディタのみで自動操作を行うためのスクリプトが作成できることです。(ブラウザを開発者モードで動かしていいという条件は必要)

もし、自動操作中にネイティブのアプリと連携が必要になった場合はNative Messagingを使用してください。このNaitiveMessageを使用したサンプルは以下にあります。

RPAツールを使用する方法

お高いRPAツールはブラウザの操作をサポートしている商品が多いです。
今回は小規模事業や個人利用なら無料でしようできるUiPath Communityを利用してChromeの操作を行います。

ChromeをUiPathで操作する場合、Chromeの拡張機能をインストールする必要があるので、下記を参考にインストールしてください。
https://docs.uipath.com/studio/lang-ja/docs/installing-the-chrome-extension

(1)UIPathで新規プロジェクトを作成します。言語はC#を選択します。
image.png

(2)「ブラウザを開く」アクティビティを追加します。
image.png

プロパティ
url "http://needtec.sakura.ne.jp/auto_demo/form1.html"
ブラウザの種類 Chrome

(3)「文字を入力」アクティビティを追加して「画面上で指定」でブラウザ上のテキスト入力項目を指定します。

image.png

image.png

image.png

(4)「文字を入力」アクティビティのプロパティを設定します。
image.png

(5)(3)~(4)を繰り返して「名前:」、「メールアドレス:」、「コメント:」を入力します。
image.png

プロパティ
表示名 文字を入力 'INPUT-名前'
テキスト "名前太郎"
フィールド内を削除 ON
ウィンドウメッセージを送信 OFF
入力をシミュレート OFF
プロパティ
表示名 文字を入力 'INPUT-メールアドレス'
テキスト "test@mail.co.jp"
フィールド内を削除 ON
ウィンドウメッセージを送信 ON ※デフォルトの挙動だとIMEが有効となり全角で入力されてしまう
入力をシミュレート OFF
プロパティ
表示名 文字を入力 'TEXTAREA-コメント'
テキスト "猫猫子猫\n\r犬犬子犬"
フィールド内を削除 ON
ウィンドウメッセージを送信 OFF
入力をシミュレート OFF

(6)「クリック」アクティビティを追加して「画面上で指定」でブラウザ上のクリックが必要な項目をを指定します。
image.png

image.png

image.png

(7)「クリック」アクティビティのプロパティを設定します。
image.png

(8)(5)~(6)を繰り返して「その1」、「その3」、「そば」をクリックします。
image.png

プロパティ
表示名 クリック 'INPUTーその1'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF
プロパティ
表示名 クリック 'INPUTーその3'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF
プロパティ
表示名 クリック 'INPUTーそば'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF

(9)リストを複数選択するために「JSスクリプトを挿入」アクティビティを追加します。
image.png

選択したJSスクリプトは下記の通りです。

select_multi.js
function selectmulti(e, aryStr) {
  var ary = JSON.parse(aryStr);
  var itr = document.evaluate("//select[@name='osi[]']/option", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );
  var node = itr.iterateNext();
  while(node) {
    if (ary.indexOf(node.textContent) >= 0) {
      node.selected = true;
    }
    node = itr.iterateNext();
  }
}
プロパティ
スクリプトコード select_multi.js
入力パラメータ "[\"千鶴さん\",\"さおりん\"]"

CTRLキーを押しながらのリスト項目をクリックする操作や、「複数の項目を選択」アクティビティを使用した実装だと動作しない場合がありました。

Web上のリストボックスで複数選択したい
https://forum.uipath.com/t/web/113531/9

また、ここで指定したJavaScript中で日本語やハングルは使用しないでください。文字化けします。日本語などが必要な場合は引数で渡すようにしてください。
たとえば「alert("千鶴さん");」とかいうコードを埋め込むと以下のようになります。

image.png

(10)登録ボタンを押下するために「クリック」アクティビティを追加します。
image.png

プロパティ
表示名 クリック 'INPUT-登録'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF

(11)登録ボタン押下後の確認メッセージを閉じるために「画像をクリック」アクティビティを追加します。

image.png

なお、「select_multi.js」に以下のコードを追加してconfirm関数を上書きして確認メッセージを表示させないことも可能です。

  let scr = document.createElement("script");
  scr.setAttribute('type', 'text/javascript');
  scr.innerText = 'window.confirm = function () { return true; }';
  document.body.appendChild(scr);

(12)登録後のページのデータを取得するために「データスクレイピング」を行います。

「データスクレイピング」アイコンを押下します。
image.png

「取得ウィザード」で「次へ」ボタンを押下します。
image.png

要素の選択が可能になるのでテーブルのセルを選択します。
image.png

「表全体からデータを抽出しますか?」の確認メッセージには「はい」を選択します。
image.png

「取得ウィザード」で「終了」ボタンを押下します。
image.png

「次へのリンクを指定」の確認メッセージには「いいえ」を選択します。
image.png

「データスクレイピング用のアクティビティが追加されます。
image.png

(13)「構造化データを抽出」アクティビティの「出力」プロパティに対してCTRL+Kを押下してresult変数を追加します
image.png
image.png

(14)「繰り返し(各行)」アクティビティを追加します。この際、コレクションには「result」変数を指定してください。
image.png

(15)「繰り返し(各行)」アクティビティに「一行を書き込み」アクティビティを追加します。

image.png

プロパティ
Text row[0].ToString() + " " + row[1].ToString()

(16)これまでの操作を再生すると以下のようになります。
auto3.gif

UiPathでのブラウザの自動操作のまとめ

UiPathを使用したメリットは以下の通りです。
・要素を画面から選択できる
・画像認識による自動操作ができる
・ブラウザ以外の自動操作が同じ操作感で行える。
・今回は説明してませんがUiPath Orchestratorで資産管理が容易になる

逆にデメリットは以下の通りです。
・GUIでのプログラミングになるので、複雑な実装が困難である
・GUIなので変更点の差分を見るのが困難で、コードレビューが負担になる。※結局はxamlなのでテキストで差分はとれるが…
・なれないとハマるポイントが多い。
・UiPathの操作でDOMの要素を変更したりしているのでシステム試験等で使用する場合、妥当性を考える必要がある。
例:UiPathで操作した要素には以下のように「uipath_custom_id」という属性が追加されている。
image.png

UiPathは外部プログラムを呼び出す機能やPowerShellの実行が可能なのでブラウザの操作は別の手法で行うことも可能です。

なお、RPAで未経験者でもお手軽自動操作とかいう言説が大きくなっていますが、正直、WebアプリケーションやWindowsアプリケーションを組んだことのない人が簡単に使えると言われると大きな疑問が残ります。
逆にRPAツール不要論もありますが、UiPath Orchestratorの存在や、簡単なフローが頻繁に変わる業務形態における仕事の分担という観点で、そのRPA不要論についても絶対的な真理とは言えないでしょう。

状況にあわせた組み合わせが必要と思います。

ブラウザのGUI上の操作をプログラム上で真似して自動化する方法のまとめ

ここまでで、ブラウザのGUI上の操作をプログラム上で真似して自動化する方法について紹介しました。

RPAツールを使える環境の場合、RPAツールは自動化を行う上で便利ではありますが、それを使うことを目的にしない方がいいです。必要に応じて別の方法をミックスして使うようにしましょう。

外部ライブラリを使用できる環境の場合、Seleniumを採用するのが一番楽だと思います。ChromeやFirefoxならSeleniumIDEで録画機能もついているので生産性は高いでしょう。

外部ライブラリが使用できない環境の場合、InternetExploreのCOM又は、ブラウザの拡張機能を使用することになります。
IEを使用する場合、操作対象のWebアプリケーションのサポート状況をよく確認しましょう。

いずれの方法でブラウザ操作を自動化するにせよ以下の点は気をつけてください。

安易なSleepを使用しない

たとえば、適当に2秒待つという処理をいれた場合、ネットワークやPCの負荷状況によって動作しない可能性があります。
Sleepよりも以下で判断するようにしましょう。

・ document.readyStatusなどを活用する
・ 特定の要素が出現したか、消えたかを見て判断する。
 →SeleniumやUiPathでは、要素の出現消滅の検出をサポートする機能が提供されている

テキスト入力は手動入力と異なる挙動をする可能性がある

テキスト入力を行う場合、手動で入力した場合と異なる挙動をする可能性があります。
たとえばキーボード操作のイベントで何らかの処理していたり、フォーカスの移動で何らかの処理をしていたりする場合です。
UiPathの場合、入力モードに以下の3種類があるので必要に応じて使いわけてください。

・デフォルト:デバイスドライバ経由なので手入力にもっとも近い
・WindowsMessage:Windowsのメッセージを利用してテキストを入力している。
・Simulate:コントロールを直接操作している

それ以外の場合は、JavaScriptのコード上でアプリが期待するイベントを無理やり起こす必要があります。

必要に応じてJavaScriptを利用する

複雑なUIの場合、画面要素を一々クリックするより、WebアプリケーションのJavaScriptを直接実行した方が早い場合があります。
また、いままでの例にでてきたように、alertやconfirmのような自動操作がし辛いポップアップの出現を抑止することが可能になります。

テストの自動化についてはテスト方針をよく確認する

ツールをつかったテストは強力ですが、それはユーザが動かしたものと全く同一にならないことに注意してください。
たとえば、先にあげたJavaScriptを呼び出して処理を行った場合、それがテストとして妥当かどうかはテストの方針や観点しだいになります。

UIの軽微な変更で動作しなくなることを忘れないこと

ブラウザの自動テストはUIの軽微な変更で簡単に動かなくなります。
たとえばリストの2番目と3番目の項目を選択するという実装だと、リストの項目が追加された場合に簡単に動作しなくなります。
これがなるべく影響を受けないような書き方をすることも可能ですが、限界はあります。

もしブラウザの自動化スクリプトを重要な業務で使用している場合は、前に動いたスクリプトだからと安心せずWebアプリケーションの変更にともなって定期的に以前に書いた自動化スクリプトが動作するか確認するようにしてください。

ブラウザから送信しているデータを真似する方法

ブラウザの送受信データの確認方法

ブラウザから送信しているデータを真似して自動化する前にブラウザからどんなデータを送受信しているか調べる方法を説明します。

下記のページで登録ボタンを押した場合にどのようなデータを送信しているか確認してみましょう。
http://needtec.sakura.ne.jp/auto_demo/form1.html

Chromeでの送受信データの確認方法

(1)F12で開発者ツールを開き、「Network」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
net1.gif

(3)「regist1.php」などの受信ファイルを選択後に、Headersタブを選択すると送信データが確認できます。
image.png

(4)Responseタブを選択すると受信内容が確認できます。
image.png

Firefoxでの送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します。
image.png

(4)パラメータタブでFormの送信情報を確認できます。
image.png

(5)応答タブでサーバーからのレスポンスデータを確認できます。
image.png

IE11での送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します
image.png

(4)本文タブを選択し、さらに「要求本文」タブを選択するとFormの送信情報を確認できます。
image.png

(5)本文タブを選択し、さらに「応答本文」タブを選択するとサーバーからのレスポンスデータを確認できます。
image.png

旧Edgeでの送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します
image.png

(4)本文タブを選択し、さらに「要求本文」タブを選択するとFormの送信情報を確認できます。
image.png

(5)本文タブを選択し、さらに「応答本文」タブを選択するとサーバーからのレスポンスデータを確認できます。
image.png

新Edgeでの送受信データの確認方法

Chromeと同じです。
image.png

単純なFormデータの送信例

下記のページのような単純なフォームのデータの送信例を説明します。
http://needtec.sakura.ne.jp/auto_demo/form1.html

curlコマンド

macやlinux系のOSならcurlコマンドを使用することで単純なフォームデータをPOSTすることが可能です。
windos10でもプリインストールされるようになったようですが、文字コードの問題があるので注意が必要です。また、PowerShellを使用しているとcurlコマンドが利用できますが、これはInvoke-WebRequestの別名です。

以下はCentOS7でcurlコマンドを実行した例となります。

>curl  -F "name=名前太郎" -F "mail=test@co.jp" -F "comment=コメント" -F "q1[]=その1" -F "q1[]=その3"  -F "men=soba" -F "osi[]=千鶴さん" -F "osi[]=さおりん"   http://needtec.sakura.ne.jp/auto_demo/regist1.php
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>sample</title>
</head>
<body>
<table border="1">
  <tr>
    <td>名前</td><td>名前太郎</td>
  </tr>
  <tr>
    <td>メールアドレス</td><td>test@co.jp</td>
  </tr>
  <tr>
    <td>コメント</td><td>コメント</td>
  </tr>
  <tr>
    <td>チェック</td>
    <td>
        その1, その3<br>    </td>
  </tr>
  <tr>
    <td>めん</td><td>soba</td>
  </tr>
  <tr>
    <td>おし</td><td>千鶴さん,さおりん</td>
  </tr>
</table>
</body>
</html>

powershellの例

PowerShellではInvoke-WebRequestを利用してFormデータを送信可能です。

    $data = @{
      name='名前太郎';
      mail='test';
      comment=@"
    猫猫子猫
    犬犬子犬
"@;
      'q1[0]'='その1';
      'q1[1]'='その3';
      men='soba';
      'osi[0]'='千鶴さん';
      'osi[1]'='さおりん'
    }
    $ret = Invoke-WebRequest http://needtec.sakura.ne.jp/auto_demo/regist1.php -Method POST -Body $data -ContentType "application/x-www-form-urlencoded"
    $html = $ret.ParsedHtml
    $list = $html.getElementsByTagName("tr")
    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null
    Remove-Variable html -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

ParsedHtmlはmshtmlになっているので、構文解析を容易に行えます。
なお、mshtmlはCOMなのでReleaseComObjectを実施して解放処理をしておいた方が無難です。

なお、ページによっては文字化けする場合があります。この場合は以下のように文字コードを変換して出力します。

# 以下参考
# https://qiita.com/zaki-lknr/items/1ae3258d7b77c5e2a2ba
$ret = Invoke-WebRequest http://needtec.sakura.ne.jp/auto_demo/form1.html
$content = [System.Text.Encoding]::UTF8.GetString( [System.Text.Encoding]::GetEncoding("ISO-8859-1").GetBytes($ret.Content) )
Write-Host $content

VBAまたはVBSの場合

MSXML2.XMLHTTPを利用することでFormデータを送信可能です。
受信したHTMLはMSHTML.HTMLDocumentで解析しています。

以下はVBSのサンプルになっていますがWScript.EchoをDebug.Print等に置き換えることでVBAでも流用できると思います。

' 参考:
' https://outofmem.tumblr.com/post/63052619242/vbaexcel-vba%E3%81%A7http%E9%80%9A%E4%BF%A1
' https://stackoverflow.com/questions/9931429/parse-html-file-using-mshtml-in-vbscript
Dim httpReq
Set httpReq = CreateObject("MSXML2.XMLHTTP")

Call httpReq.Open("POST", "http://needtec.sakura.ne.jp/auto_demo/regist1.php", False)
Call httpReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
Dim postData
postData = "name=名前太郎&mail=test@co.jp&comment=猫猫" & vbCrLf & "子犬&q1[]=その1&q1[]=その2&men=soba&osi[]=千鶴さん&osi[]=さおりん"
Call httpReq.Send(postData)

Dim objHtml
Set objHtml = CreateObject("htmlfile")
call objHtml.write(httpReq.responseText)

Dim list
Set list = objHtml.getElementsByTagName("tr")
Dim item 
For Each item In list
  WScript.Echo item.innerText
Next

Pythonの例

http.clientを使用してFormデータを送信して結果を受信可能です。html.parserを利用することでHTMLの解析も行えます。おそらく、python3xが入っている環境ならどこでも使えると思います。

import http.client, urllib.parse
from html.parser import HTMLParser

# 結果ページを解析するパーサー
class ResultParser(HTMLParser):
  def __init__(self):
      HTMLParser.__init__(self)
      self.flag = False

  def handle_starttag(self, tag, attrs):
      if tag == "td":
          self.flag = True

  def handle_data(self, data):
      if self.flag:
          print (data)
          self.flag = False

conn = http.client.HTTPConnection('needtec.sakura.ne.jp')

headers = {
  'Content-type': ' application/x-www-form-urlencoded'
}

data = {
  'name': '名前太郎',
  'mail': 'test@co.jp',
  'comment' : '猫猫子猫\n\r犬犬子犬',
  'q1[0]' : 'その1',
  'q1[1]' : 'その3',
  'men' : 'soba',
  'osi[0]' : '千鶴さん',
  'osi[1]' : 'さおりん'
}
#r = requests.post('http://needtec.sakura.ne.jp/auto_demo/regist1.php', data=data)
#print(r)
#print(r.text)

params = urllib.parse.urlencode(data)

conn.request('POST', '/auto_demo/regist1.php', params, headers)
response = conn.getresponse()
print(response.status, response.reason)
parser = ResultParser()
# trの内容を出力
parser.feed(response.read().decode())
conn.close()

外部ライブラリを使う場合

requestsパッケージを使うとデータの送受信が、Beautiful Soupを使うとHTMLの解析が楽になります。

import requests
from bs4 import BeautifulSoup

data = {
  'name': '名前太郎',
  'mail': 'test@co.jp',
  'comment' : '猫猫子猫\n\r犬犬子犬',
  'q1[0]' : 'その1',
  'q1[1]' : 'その3',
  'men' : 'soba',
  'osi[0]' : '千鶴さん',
  'osi[1]' : 'さおりん'
}
r = requests.post('https://needtec.sakura.ne.jp/auto_demo/regist1.php', data=data)
print(r.status_code, r.reason)
soup = BeautifulSoup(r.text)
for tr in soup.find_all('tr'):
  print('------------------------')
  print(tr.text)

認証があるページの例

単純なフォームの送信例はPOSTを一回送信するだけで済みましたが、認証処理やページの不正遷移防止が行われているWebアプリについてはサーバからの情報を受け取ってそれを基にデータを送信する必要があります。

今回はbitnamiから取得できるRedmineのVMでチケット登録を行うサンプルを見てみます。
VMのもろもろの設定はTestLinkで設定したときと同様に行えます。

Redmineでログインしてチケットを登録するには以下の手順を踏む必要があります。

  • ログインページを取得する。
  • サーバーはヘッダーにセッションID、HTML中に認証トークン文字を埋め込んでログイン用のページを返す。
  • セッションIDをリクエストヘッダに設定し、リクエストボディに認証トークン、ユーザ名、パスワード、ログイン後の遷移ページ(チケット登録画面)を指定してログイン処理を行う。
  • サーバーはログインに成功したらチケット登録画面を返す。この際、認証トークン文字が新しいものに変更される。
  • セッションIDをリクエストヘッダに設定し、リクエストボディに認証トークン、チケット情報を指定してチケット登録処理を行う。

PowerShellの例

PowerShellでRedmineのチケットを登録するには以下のようになります。

# エラーが起きたらとめる
$ErrorActionPreference = "Stop"

# サーバから取得したCookieの値からキーを指定して値を取得する
function get_key_value($value, $key) {
  $tmp = $value.substring($value.indexof($key) + $key.length)
  $ret = $tmp.substring(0, $tmp.indexof(';'))
  return $ret
}

# DOMを解析して指定の名前の指定の属性を取得する
function get_attribyte_value($html, $elem_name, $attr_name) {
  $elems = $html.getElementsByName($elem_name)
  $elem = $elems[0]
  $attrs = $elem.attributes
  $attr = $attrs[$attr_name]
  $ret = $attr.value
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($elems) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($elem) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($attrs) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($attr) | Out-Null
  return $ret
}

##################################
#Redmineの設定値
##################################
$redmine_host = "192.168.0.200"  # サーバ名
$redmine_project = "test1"       # プロジェクト名
$username = "user"
$password = "pass"

##################################
# ログインページを初回アクセスしてセッションIDとcsrf-tokenを取得する
##################################
$ret = Invoke-WebRequest "http://$redmine_host/login" -Method GET

# セッションID取得
$cookie = $ret.Headers['Set-Cookie']
$session_id = get_key_value $ret.Headers['Set-Cookie'] '_redmine_session='

# ログインページのcsrf-token取得
$html = $ret.ParsedHtml
$csrf_token = get_attribyte_value $html 'csrf-token' 'content'
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

# セッション情報作成
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$cookie = New-Object System.Net.Cookie 
$cookie.Name = "_redmine_session"
$cookie.Value = $session_id
$cookie.Domain = $redmine_host
$session.Cookies.Add($cookie);

##################################
# ログイン処理。
# ログイン後はチケット登録画面へ
##################################
$login_data = @{
  authenticity_token = $csrf_token;
  back_url = "http://$redmine_host/projects/$redmine_project/issues/new";
  username = $username;
  password = $password;
}
$ret = Invoke-WebRequest  "http://$redmine_host/login" -Method POST -WebSession $session -Body $login_data -ContentType "application/x-www-form-urlencoded"

# csrf-token取得
$html = $ret.ParsedHtml
$csrf_token = get_attribyte_value $html 'csrf-token' 'content'
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

##################################
# チケット登録
##################################
Write-Host "チケット登録........................................................"
$title = Get-Date -format "yyyyMMddHHmmss"
$ticket_data = @{
  'utf8' = '✓';
  authenticity_token = $csrf_token;
  'issue[is_private]' = 0;
  'issue[tracker_id]' = 1;
  'issue[subject]' = "自動登録 $title";
  'issue[description]' = "わっふるわっふる";
  'issue[status_id]' = 1;
  'was_default_status' = 1;
  'issue[priority_id]' = 2;
  'issue[start_date]' =  '2019-10-10';
  'issue[due_date]' =  '';
  'issue[done_ratio]' = 0;
  'commit' = '作成'
}
$ret = Invoke-WebRequest  "http://$redmine_host/projects/$redmine_project/issues" -Method POST -WebSession $session -Body $ticket_data -ContentType "multipart/form-data"
$html = $ret.ParsedHtml
Write-Host $html.title
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

Pythonの例

HTMLの解析がしんどいのでBeautiful Soupを使用した方がいいでしょう。

import requests
from bs4 import BeautifulSoup
import datetime

##############################################
# redmineの情報
##############################################
redmine_host = "192.168.0.200"  # サーバ名
redmine_project = "test1"       # プロジェクト名
username = "user"
password = "password"

# セッションの作成
session = requests.session()

# ログインページの取得
ret = session.get('http://' + redmine_host + '/login')
print(ret.status_code, ret.reason)
ret.raise_for_status()

session_id = ret.cookies['_redmine_session']

soup = BeautifulSoup(ret.text, 'html.parser')
elem_token = soup.find('meta', {'name': 'csrf-token'})
csrf_token = elem_token['content']

# ログイン処理
cookies = {
  redmine_host : session_id
}
login_data = {
  'authenticity_token' : csrf_token,
  'back_url' : "http://' + redmine_host + '/projects/' + redmine_project + '/issues/new",
  'username' : username,
  'password' : password
}
ret = session.post('http://' + redmine_host + '/login', data=login_data, cookies=cookies)
print(ret.status_code, ret.reason)
ret.raise_for_status()

# チケット登録
print('チケット登録................................')
soup = BeautifulSoup(ret.text, 'html.parser')
elem_token = soup.find('meta', {'name': 'csrf-token'})
csrf_token = elem_token['content']

ticket_data = {
  'utf8' : '✓',
  'authenticity_token' : csrf_token,
  'issue[is_private]' : 0,
  'issue[tracker_id]' : 1,
  'issue[subject]' : "自動登録 " + str(datetime.datetime.now()),
  'issue[description]' : "わっふるわっふる",
  'issue[status_id]' : 1,
  'was_default_status' : 1,
  'issue[priority_id]' : 2,
  'issue[start_date]' :  '2019-10-10',
  'issue[due_date]' : '',
  'issue[done_ratio]' : 0,
  'commit' : '作成'
}
ret = session.post('http://' + redmine_host + '/projects/' + redmine_project + '/issues', data=ticket_data, cookies=cookies)
print(ret.status_code, ret.reason)
ret.raise_for_status()

soup = BeautifulSoup(ret.text, 'html.parser')
print(soup.title)

ブラウザから送信しているデータを真似する方法のまとめ

ブラウザを介さないのでブラウザより簡単にかつ高速で自動操作が行えます。
同時にそれは、デメリットになる場合があります。
たとえば、JavaScriptでクライアントサイドで動的にページを作成している場合、その処理は動作しません。つまり、ブラウザで操作したときと同様のDOMの構成が返ってくるとは限りません。

この手法を結合試験やシステム試験で使用する場合は、注意してください。
仮にテストデータの入力に使う場合であっても、システム上本来作成できないデータが作成されてしまう場合があるからです。試験の観点に合わせて慎重に導入してください。

また、ブラウザの自動操作と同様にWebアプリケーションの変更によって今まで動いていた自動化スクリプトが動作しなくなるリスクはあるので注意してください。

Webアプリケーションが提供しているAPIを利用する方法

もっともリスクの少ないWebアプリケーションの自動化の方法です。
ただしWebアプリケーションがAPIを提供しているかどうかは個別の仕様次第になります。

Redmineのチケット登録の例

これまでにRedmineでチケット登録を行うサンプルをいくつか記述しました、Redmineが提供しているAPIを利用することでシンプルに実装することができます。

まずRedmineの管理画面でRESTAPIを有効にしてください。
image.png

すると個人設定画面でAPIキーが表示されます。このAPIを使用してRedmineを操作します。
image.png

PowerShellの例

チケット用のXMLを作成してPOSTするだけです。
この際、APIキーをヘッダに付与して送信します。

##################################
#Redmineの設定値
##################################
$redmine_host = "192.168.0.200"  # サーバ名
$apikey = "60076966cebf71506ae3f2391da649235a2b1d46"

$title = Get-Date -format "yyyyMMddHHmmss"
$xml = @"
<issue>
    <project_id>1</project_id>
    <subject>RESTAPIテスト $title</subject>
    <description>試験</description>
</issue>
"@
# セッション情報作成
$headers = @{
  'X-Redmine-API-Key' = $apikey;
  'Content-Type' = 'text/xml';
}

# 文字化けして登録されるなら以下をいれる
$sendData = [System.Text.Encoding]::UTF8.GetBytes($xml)

$ret = Invoke-WebRequest http://$redmine_host/issues.xml -Headers $headers -Method POST -WebSession $session -Body $sendData
Write-Host $ret.Content

Pythonの場合

import requests
import datetime

xml = """
<issue>
    <project_id>1</project_id>
    <subject>RESTAPIテスト python {0}</subject>
    <description>試験</description>
</issue>
""".format(str(datetime.datetime.now()))

headers = {
  'X-Redmine-API-Key' : '60076966cebf71506ae3f2391da649235a2b1d46',
  'Content-Type' : 'text/xml'
}

r = requests.post('http://192.168.0.200/issues.xml', data=xml.encode('utf-8'), headers=headers)
print(r.status_code, r.reason)
print(r.text)

なお、PythonでやるならPython Redmineあたりのライブラリを使用したほうが楽だと思います。

共通的な注意事項

ここまででWebアプリケーションの自動化の方法についていくつか方法を説明しました。
最後に共通的な注意事項を述べておきたいと思います。

できることと、やっていいことは違う

おそらくここまでで、多くのWebアプリケーションを自動で操作することが可能になったと思います。
しかしながら、できることと、やっていいことは違うということを常に心がけてください。

API経由以外の自動操作はWebアプリケーション側が想定していない操作になる可能性があります。つまり、いつ動かなくなってもおかしくありませんし、仮に動くからといってやっていい操作とは限りません。
場合によっては規約違反に問われることになります。たとえば広告ブロックして云々とか、複数人で遊ぶブラウザゲームの自動化は、かなりの確率で規約違反になります。
Webアプリケーションを自動化する際は必ず規約を確認してから行うようにしましょう。

また、そういった規約が明記されておらず、不正に当たらないと考えられる場合であっても自動操作はWebアプリケーション側に想定外の負荷を与えることがあります。たとえば、2010年には情報取得目的に図書館の蔵書検索システムに高頻度(1秒に1アクセス程度)のリクエストを送信して偽計業務妨害容疑で逮捕された岡崎市立中央図書館事件があったことは心に留めておくべきでしょう。

特に社内システムの場合、品質が悪い傾向があるので、根回しをしつつやっておくか、すぐに停止てきる状況かで実施し始めた方が無難です。

武器や流派にこだわるな

「武器や流派にこだわるな」という格言は、およそ20年前の名著「アジャイルソフトウェア開発 (The Agile Software Development Series) 」の「付録B3 武蔵」の項目にでてきた格言です。

今回、色々な自動化の方法を紹介はしましたが、それは自動化を行うための選択肢を増やして「こだわりを捨ててもらう」意図がありました。

RPAツールは素晴らしく自動化の手助けになります。しかしながら、あきらかに別の方法でやったほうが楽な場合でもRPAツールにこだわるケースがよくみられます。例えばブラウザ画面を介しての自動操作に慣れ親しんだ人はcurlコマンドで済むようなことまで慣れ親しんだという理由だけで困難な技法を選択してしまうケースをよく見かけます。
逆にcurlコマンドでは行うのが困難なことを、それだけでやろうとするケースも同じくらいよく見ます。

普段は使用しない技法であっても必要があるなら採用すべきですし、逆に最も自分が慣れ親しんだ技法であっても状況にそぐわなければ捨てるべきです。

自動化スクリプトの管理方法を考えよう

1度動かせばすむスクリプトなのか、定期的に動かすスクリプトなのかによって、スクリプトの管理方法が変わります。
定期的に動かすスクリプトの場合、常にWebアプリケーションのバージョンアップで動作しなくなるというリスクがあります。
このリスクをどう扱うか考えましょう。

たとえば、実際やってエラーとなった時点で修正する時間的余裕のあるものであれば、その時に考えればいいでしょう。
しかし、そういう時間的余裕が確保できないようなスクリプトの場合は、事前にそれを検出する必要があります。
定期的にスモークテストを行う計画を立てるか、Webアプリケーションのリリースノートをチェックする工数をとるか、いずれにせよなんらかの対策が必要になります。

あとは、自動化スクリプトの意図を複数の人間が理解して、メンテナンスできる体制を作るよう必要があります。人間は割と簡単にいなくなります。一誰も意図が分からない自動化スクリプトが動き続ける状態にならないように気を付けましょう。

自動化のコストを見積もる場合、これらの作ったあとのメンテナンスのコストについて忘れずに考えておきましょう。

自動化を目的にするのはやめよう

慣れてくると、なんらかの方法で多くのことが自動化できるようになりますが、それを目的とするのはやめましょう。
重大な障害対応を放置して、優先度の低い自動化スクリプトを書いても意味はありません。
全体の状況をみて、効果のありそうなところを自動化しましょう。

無理ならあきらめよう

どうしても自動化できないこともあります。
素直にあきらめて別の事をしましょう。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む