Selenium WebDriver を使う (C#) (2)

前回 は WebDriver を使ってブラウザを起動する方法を確認しました。
今回は HTML 要素の取得について確認します。

WebDriver で行えることは、「Web ページの操作」です。Web ページの操作の大部分は HTML の要素を操作することです。
つまり HTML の要素を扱うことが WebDriver で行うことのメインになります。

HTML 要素の操作の第一段階として、まずは HTML 要素に相当するオブジェクトを得る必要があります。
そのオブジェクトは、ドライバクラスの FindElement/FindElements メソッドを使用して取得します。

どの HTML 要素を取得するかをどうやって指定するか、その指定の仕方には、HTML タグ名や CSS セレクタXPath などがあり、そのどれを使うかは、FindElementXXX メソッドを使い分けるか、By クラスを使用して使い分けるかのいずれかになります。

// ID属性を指定して取得 FindElementById メソッドを使う
var element = driver.FindElementById("hoge");
// 他にも
// CSSセレクタで取る driver.FindElementByCssSelector とか
// name 属性で取る driver.FindElementByName とか
// XPath で取る FindElementByXPath とかがある

// XPath を指定して取得 By クラスを使う
var element2 = driver.FindElement(By.Id("hoge"));
// 他にも By.CssSelector とか By.Name とか、FindElementByXXX に対応した By メソッドがある
// というよりも FindElementByXXX の方が By を使わないですむ糖衣構文のようなもの

上記の2つは同じことをしていますが、ID 属性を指定して取得する専用のメソッド を使用するか、By オブジェクトによって方法を伝えるか の違いがあります。

いずれの方法を用いても、これにより IWebElement のオブジェクトを取得することができます。これが1つの HTML 要素に対応するオブジェクトです。
このオブジェクトを通じて、HTML 要素を操作します。

// タグ名を取得
var tagName = element1.TagName;

// innerText を取得
var text = element1.Text;

// class 属性の値を取得
var classValue = element1.GetAttribute("class");

// 要素をクリック
element1.Click();




Cloudflare で SSL

某違法サイトも利用していたという CDN のサービスが Cloudflare です。

CDN はコンテンツ配信のためのサーバネットワークで、閲覧者に近いサーバからコンテンツを配信することにより、読み込み速度向上や通信リソース節約などを目的とするものです。

この仕組みを利用すると、代理となるサーバを経由してコンテンツを配信することになるため、もともとのサーバが隠れることになります。

これはどういうことかというと、

hoge.test というドメインでサイトを運用していたとして、ブラウザがそのサイトのページを表示するときには、DNShoge.test の IP アドレスを引いて、その IP アドレス(仮に 192.0.2.1 とします)を使用して通信を行うことになります。
IP アドレスがわかれば、その IP アドレスを管理している運営会社からサイトの運営者をたどることができます。*1
しかし Cloudflare を利用している場合は、hoge.test ドメインの IP アドレスを引くと CDN のサーバの IP アドレスが得られるように設定します。
これにより、実際のコンテンツを配信するのは 192.0.2.1 のサーバではなく、CDN のサーバ=別の IP アドレスのサーバになります。
そのドメインで運用されている本当のサーバの IP アドレスが隠れてしまうということになるわけです。

話がそれそうになってしまいましたが、つまり CDN を導入すると以下のような構成になるということです。

導入前:ブラウザ <--(1)--> 本当のサーバ
導入後:ブラウザ <--(2)--> CDN のサーバ <--(3)--> 本当のサーバ


さてここで SSL について考えてみます。SSL(/TLS) はブラウザなどとサーバの間の通信を暗号化したり実在性の認証を行ったりするものです。

上記でいえば、本当のサーバに SSL の設定をしていたとすると、それは (1) と (3) の部分になります。

導入後は、ブラウザと直接やり取りをする相手は CDN のサーバになるため、(2) の部分で SSL の設定を行う必要があります。

Cloudflare では、(2)、(3) の部分をどのようにするか設定を行うことができます。大きく分けて以下の2つがあります。

  • Flexible
  • Full

上記いずれも (2) の部分は SSL 化してくれます。
それぞれの違いは、Flexible は (3) の部分で SSL 通信を行わず、Full は (3) の部分で SSL 通信を行う というものです。

Flexible を利用すれば、本当のサーバで SSL を導入していなくてもブラウザに対しては SSL 化できる、ということになります。

ただし、Full を利用できるのであれば Full を利用した方がよいでしょう。セキュリティ的な側面も1つですが、サイトの設定周りに大きく影響する可能性があります。
Flexible を利用する場合は以下のような点に注意が必要です。

  • HTTP でアクセスされてきたときに HTTPS にリダイレクトしている
  • ページに http://~ をハードコーディングしている
  • HTTP か HTTPS かを見て何かを切り替えている (Wordpress など)

Cloudflare の場合、X-Forwarded-Proto というヘッダに "https" を設定して送信してくれるので、これを見て処理を行うようにする必要があるかもしれません。



*1:ドメイン名の取得者からたどる方法もあると思います

Selenium WebDriver を使う (C#) (1)

WebDriver を使って、ブラウザ(Chrome)の起動と簡単な操作を行ってみます。
環境は Windows Visual Studio 2017 です。

WebDriver は NuGet で入れられます。Selenium.WebDriver と Selenium.Chrome.WebDriver を入れます。

f:id:venturenet:20181228154853p:plain

Selenium.WebDriver が主なライブラリです。これの他に、ドライバ(chromedriver.exe)が必要になります。それが Selenium.Chrome.WebDriver になります。


ブラウザを起動させるのは以下のようになります。

var options = new ChromeOptions();
options.AddExcludedArgument("ignore-certifcate-errors");
options.AddArgument("--disable-infobars");
options.AddArgument("--renderer-process-limit=1");
options.AddArgument("--user-data-dir=./profile");

var service = ChromeDriverService.CreateDefaultService();
service.SuppressInitialDiagnosticInformation = true;
service.HideCommandPromptWindow = true;
            
var driver = new ChromeDriver(service, options);

driver.Quit();


ChromeOptions クラスで、Chrome に対するいくつかのオプションを指定することができます。例えば--renderer-process-limit で起動する Chrome のプロセス数を抑制しています。

ChromeDriverService は chromedriver.exe と対応するもので、それに対するオプションを指定することができます。

詳細は公式サイトで確認できます。

new ChromeDriver(service, options); のところで Chrome が起動します。Chrome とともに chromedriver.exe も起動します。
driver.Quit() で Chrome と chromedriver.exe が終了します。

なお、Chrome が異常終了したり、driver.Quit() を呼べないでプログラムが終了してしまった場合には、chromedriver.exe のプロセスが残ってしまうことがあり、この状態だと次に new ChromeDriver(service, options) したときに例外が出ることがあります。
そのような場合は、chromedriver.exe のプロセスを終了させて、しばらく待ってから実行するとうまくいくかもしれません。

また Chrome は頻繁にバージョンアップがありますが、古い chromedriver.exe ではうまく動かない場合が多々あります。chromedriver.exe はまめに最新版を入れたおいた方がよいでしょう。




Selenium WebDriver を使う

めっきり冷え込んできた今日このごろです。
年末年始は強い寒波に見舞われるということで、防寒や交通などには注意する必要がありそうです。

さて、最近では Selenium という名前はだいぶ世間に浸透しているのではないかと思いますし、実際にご利用になられている方も多いのではないかと思います。
この Selenium は、ブラウザ自動操作(automation)のためのツールスイートと説明されています。

automation といえば、MA(Marketing Automation) や RPA(Roboting Process Aumation)、SFA(Sales Force Automation) など、AI とも絡んで様々な自動化戦略がすすめられています。

そちらの難しい話はおいておくとして、Selenium はもともと Web アプリケーションのテストのために生み出されたものです。

ソフトウェア/システムのテストには様々な粒度があり、言語の関数やライブラリのレベルでは、JUnit に代表される XUnit テスティングフレームワークによってテストの自動化が普及しました。

最終的にできあがるシステムは、大抵は人が何らかの画面を操作して使うものです。この段階のテストでは、人が手作業でテストシナリオにそってシステムを操作して、期待した結果が得られることを確かめる、ということが一般的であったと思います。
しかしいかんせん人手で行われるため、見落としや、逼迫したスケジュールでの工数不足、リグレッションテストが省略される、といったことが現れてくることもあります。

Web アプリケーションも、HTTP リクエストを入力として HTTP レスポンスを出力する関数のようなものであるととらえ、HTTP レスポンスの内容を自動で検証すればよいという考えも生まれました。
その考え方に基づいて Cactus などのテスティングフレームワークが誕生しましたが、生の HTTP レスポンスに対しての検証となると、JavaSript が絡んできたときにテストしきれないため、あまり浸透しなかった印象があります。
機能の配置場所がサーバからクライアントにシフトし始め、RIA だのと言われ始めた頃とも重なるかもしれません。

そんな状況を踏まえてなのかはわかりませんが、Web アプリケーションでも「だったらブラウザを自動で操作して検証すればよい」ことを行うために、Selenium が登場しました。


このように、Selenium はテストのためのツールスイートなわけですが、ブラウザの自動操作は、テストのみならず何らかの作業効率の改善のために利用することもできます。




今更ながら JSON のおさらい

一昔前(もっと前かもしれませんが..)には、データ交換のフォーマットといえば XML 、という時期がありました。

その流れで SOAPWSDLBPEL 、ESB などが生まれましたが、複雑すぎたのか、堅すぎたのか、最近ではあまり見ることがありません。(大規模な基幹システムなどでは採用されているのかもしれませんが)

最近の Web ベースの API でよく見るフォーマットは JSON です。

JSON は、JavaScript Object Notation の略であり、もともとは JavaScript のオブジェクトをあらわすものであったわけですが、その仕様は JavaScript の言語仕様に強く結びついているわけではないため、様々なデータを表現するのにばっちりはまりました。
API のコールを JavaScript で行う場合、レスポンスが JSON の場合はレスポンスの取り扱いが非常に容易であることもその一因かと思います。

そこで今更ながら、JSON の基本をおさらいしたいと思います。
厳密な仕様は http://json.org/ へ。

以下の6種類。

  • 文字列
  • 数値
  • 真偽値
  • null
  • 配列
  • オブジェクト


書き方

文字列 ダブルクォーテーションでくくる。
文字列にダブルクォーテーションを含める場合は、バックスラッシュでエスケープする。
"hoge"
"f\"oo"
など。
数値 数字をそのまま書く。
123
-234
12.6
など。
真偽値 true か false と書く。
null null と書く。
配列 [ と ] でくくり、カンマで各要素を区切る。すべての種類の値を要素にできる。
[1, 2, "hoge", false, null, [3, 2]]
など。
オブジェクト オブジェクトはキーと値のペア。キーは文字列でなければならない。値にはすべての種類の値を指定できる。
キーと値はコロンで区切り、キーと値のセットはカンマで区切る。
{ "price":1200, "title":"ほげほげ" }
など。


値の種類を組み合わせた例

{
  "hoge": [1, 2, {"foo":"aaa", "bar":"bbb"}],
  "piyo": { "key1":100, "key2":200 },
  "piyopiyo": 60.3
}




WebSocket を試す

Web といえば HTTP なわけですが、HTTP はあくまでプル型の機能です。つまり、自分から取りに行ってはじめて何らかの情報を得ることができます。

近年はいろんな API(Webサービス) が登場していて、それを利用することもあるかと思いますが、よくある REST API なども自分からコールしてその結果を得るものです。

RSSフィードにしても、Eメール(POP3)にしても、自分から取りに行く必要があります。
とはいえRSSフィードはそんなに頻繁に取得する必要はないですし、POP3 などは、短くても分単位でポーリングしておけば十分でしょう。

ただ、もっと速く、もっと短い間隔で、リアルタイムに情報を取得したいケースもあります。 しかしそんなときに1秒おきにリクエストを送信するなどして対応したら、攻撃とみなされてしまうかもしれません。

こういうケースでは、クライアントがデータを取りに行くのではなく、サーバ側起点で情報を送ってくれた方が効率的です。
プル型ではなくプッシュ型の方式ということです。

こういった要件を満たすために、ロングポーリングなどの方法が考え出されました。
WebSocket も、プッシュ型の通信を実現するものです。

最近のブラウザではだいたい WebSocket が使用できますので、この機能を試してみたいと思います。

データの取得先は、リアルタイムといえば取引ということで、仮想通貨取引所 Bitmex の WebSocketAPI を使ってみます。

<!DOCTYPE html>
<meta charset="utf-8" />
<script>
const ws = new WebSocket('wss://www.bitmex.com/realtime?subscribe=quote:XBTUSD');

ws.onopen = event => {
   console.log(event);
};
ws.onmessage = event => {
   console.log(event);
};
ws.onclose = event => {
   console.log(event);
};
</script>
<button onclick='ws.close();'>Close</button>

WebSocket のインタフェースはシンプルです。これだけで、コンソールログに情報がずらずらと流れます。




コマンドラインで Route53

EC2 のサーバで Web サイトを運用する場合、DNS には Route53 を利用される方もいらっしゃるかと思います。

Route53 は「可用性が高くスケーラブルなクラウドドメインネームシステム (DNS) ウェブサービス」(本家サイトより) です。


いくつかのドメインの Web サイトを EC2 インスタンスで運用していたのですが、そのインスタンスには Elastic IP アドレスを設定していませんでした。
つまり、停止 → 起動 を行うと、IP アドレスが変わってしまう状態でした。

あるとき、このインスタンスの能力を増強するためにインスタンスタイプの変更を行うことになったのですが、インスタンスタイプの変更を行うためには一度インスタンスを停止する必要があります。

インスタンスタイプの変更自体は問題なく行えたのですが、IP アドレスが変わってしまいました。

そのインスタンスにひもづいているドメインDNS レコードは Route53 で管理しており、インスタンスの IP アドレスが変わったので、各ドメインの A レコードを Route53 で変更しなければなりません。

1つ1つ修正する方法もありますが、数が多かったため、コマンドで変更することにしました。以下がそのコマンドです。(1ドメイン分)


aws route53 change-resource-record-sets --hosted-zone-id <ゾーンID> --change-batch '{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"<ドメイン名>","Type":"A","TTL": 300,"ResourceRecords":[{"Value":"<IPアドレス>"}]}}]}'


便利ですね。普通のドメイン管理サービスではなかなかこうはいきません。

ドメインのゾーンIDもコマンドで取得することができます。

aws route53 list-hosted-zones | jq -r '.HostedZones[]|[.Name,.Id]|@csv'



aws コマンドを使用するためにはそれなりに(もしかしたら面倒な)準備が必要ですが、それを補ってあまりある便利さです。