しばたテックブログ

気分で書いている技術ブログです。

PowerShellとSelenium WebDriver(Chrome)を使ってブログのMixed Contentをチェックする

つい先日、本ブログをホストしているはてなブログが独自ドメインのサイトに対してHTTPS対応したので本ブログをHTTPS化しました。

staff.hatenablog.com

本ブログはこれまで241エントリ公開しており、そこまで数が多くないため、今回は全エントリを目視で確認してMixed Contentがあったエントリを手動で直す対応を取りました。

Mixed Contentとなったケースは殆ど

  • 古いエントリの画像がHTTPのCDNを向いていた
  • 一部oEmbedのリンクがHTTPになっていた

であり、単純にエントリを更新しなおすことで対応できたので作業自体は非常に楽でした。
HTTPS対応とは別に古いエントリのリンク切れなども修正できたのでブログの棚卸しにちょうど良かった感じです。

本エントリの動機

当座の対応としてはこれで良かったのですが、Mixed Contentをちゃんと全滅させたか確認したくなり、これは手動ではなく機械的に全エントリをチェックすべきだろうと思ったのが本エントリの動機になります。

Mixed Contentの確認方法

私は正直WEB系の知識には疎いのですが、ざっと調べた限りMixed Contentを機械的に確認するには「それ専用のサイトやツールを利用する」「ヘッドレスブラウザを使い警告やエラーを検知する」方法がある様で、今回は後者の方法を選びました。

基本的にはこちらのブログの方式に倣っています。

こちらはPHP + Selenium WebDriver + Chromeを使い、ブラウザコンソールログからMixed Contentの警告・エラーログを検出するものとなっています。
普段PHPを使う方はこちらのブログを参考にすると良いでしょう。

私はPowerShell使いですし、ブログのネタとしても美味しいのでPowerShell + Selenium WebDriver + Chromeで同様のことをやってみることにします。

【補足】他ツール

他に

github.com

というツールも見つけました。
Nodeを使っている方であればこのツール単体でMixed Contentをチェックできます。
(実装を見る限り同じ方式を採っています。内部でpuppeteerを使っているのでサイズはそこそこ大きい様です。)

前準備

事前に以下のソフトウェアを用意しておく必要があります。

Google Chrome

www.google.co.jp

最新バージョンをインストールしておけば良いでしょう。
最低限バージョン60以降であれば全プラットフォームにおいてヘッドレスで動作させることができる様です。

今回はバージョン66を使っています。

Selenium WebDriver (C#)

こちらのダウンロードページからC#のSelenium Client & WebDriver Language Bindingsをダウンロードするか、Nugetから手動でnupkgをダウンロードしてください。
.nupkgはただのZipファイルですので、解凍して中にあるWebDriver.dllを後述するフォルダに配置します。

実行環境がWindows PowerShellの場合は、net45フォルダにあるWebDriver.dllを、PowerShell Core 6.0の場合はnetstandard2.0フォルダにあるWebDriver.dllを使います。
現時点での最新バージョンはVer.3.12です。

f:id:stknohg:20180613214317p:plain

ChromeDriver - WebDriver for Chrome

から最新バージョンのChromeDriverをダウンロードし、Zipファイルの中にあるchromedriver.exeを後述するフォルダに配置します。
現時点での最新バージョンはVer.2.40です。

PowerShellとSelenium WebDriver(Chrome)を使ってMixed Contentをチェックする

ここから本題に入ります。
今回使用するスクリプトをGistに上げました。

gist.github.com

このスクリプトを任意のフォルダに保存し、同じフォルダにWebDriver.dllchromedriver.exeを配置します。
図にするとこんな感じです。

f:id:stknohg:20180613214648p:plain

動作確認

今回はPowerShell Core 6.0(6.0.2)で動作確認してみます。
スクリプトに記載してある

はGoogleによるMixed Contentの確認用サンプルサイトです。
Chromeでアクセスすると下図の様になり、開発者ツールからコンソールログを確認するとMixed Content等の警告・エラーログが出力されていることが分かります。

f:id:stknohg:20180613231303p:plain

PowerShell Coreを起動し、このスクリプトを実行してみると下図の様にブラウザのコンソールログからMixed Contentに関するものを出力してくれます。

f:id:stknohg:20180613231742p:plain

また、接続先を変えてMixed Contentが無いサイトの場合だとこの様になります。

f:id:stknohg:20180613232128p:plain

手元の環境で試した限りではいい感じにログを出力してくれています。

ただ、ブラウザ上で見るコンソールログとSelenium WebDriverから取得するログが完全に一致しないケースがある様です。
原因は解明できていないのですが、ブラウザ上で重複してるログがSelenium WebDriverではフィルタされて取得されない様に見受けられます。
とりあえず今回の用途には問題ないだろうとの判断をしていますが、厳密なログの検証などが必要な場合は注意が要りそうです。

最後に

Selenium WebDriverのおかげで思ってた以上にお手軽にMixed Contentのチェックができました。

本エントリの例は単一のURLに対するチェックですが、スクリプトを改造して複数URLに対応するのは簡単です。
例えばはてなブログであればAtomPub APIsitemap.xmlからエントリの一覧を取得できますので、これらを組み合わせてやるとブログ全体をチェックするスクリプトも比較的容易に作成することができます。

Get-ChildItemの動作に関する問題についての指摘事項

本エントリはブログに書くべきものなのか迷いましたが、文章量が多くなったためここに記します。
また、出題者を責める意図は無いためリンクは張りませんのでご了承ください。

出題された問題

先日Twitter上でPowerShellに関する以下の問題を見かけました。

問題

Windows Powershellにおいて、以下のコマンドを実行した。
実行結果はどうなる?

PS > (Get-ChildItem).Length

回答欄

  1. オブジェクトが無いのでエラーになる
  2. ディレクトリ配下のファイルの総容量が表示される
  3. ディレクトリ配下のファイルの数が表示される
  4. コマンドが存在しないためエラーになる

出題者の意図としては3. ディレクトリ配下のファイルの数が表示されるを選ばせたかったのでしょうが、残念ながらこの選択肢は全て正しくありません。
(前提条件次第では回答欄の内容にマッチする場合もあるのですが、本エントリでは回答欄として不適切の意味で"正しくない"と表現しています)

以下、何が正しくないのかについて順に指摘していきます。

PowerShellにおけるロケーション

まず、DocsにあるGet-ChildItemのヘルプを確認してみると、

Gets the items and child items in one or more specified locations.

とあり、このコマンドレットは「特定の ロケーション にある要素および子要素を取得する」ものです。

blog.shibata.tech

でも触れたとおり、PowerShellにおいて ロケーション はファイルシステム以外のデータ構造も対象とするより抽象度の高い概念です。

このためGet-ChildItemGet-DirectoryItemの様な名前でなくGet-ChildItemが選ばれているといえます。
(とはいえ、ファイルシステムを扱うのが主であるため、現実としては利便性のためにdirlsといったエイリアスが設定されていますが...)

先述の問題の場合、例えばロケーションをレジストリにすると

> cd HKCU:\Software\
> (Get-ChildItem).Length

で取得できるのは「レジストリの値とサブキーの総数」になります。

この問題を正しく出題するためにはロケーションをドライブレベルで確定する必要があります。

PowerShellのバージョン間での挙動の違い

PowerShellにおけるLengthプロパティはバージョン間で挙動が違います。

PowerShell 2.0まで

LengthプロパティはSystem.Arrayのプロパティであり、配列でないスカラ値には使用できません。

例えば

# PowerShell 2.0ではスカラ値にLengthは使えない
> (123).Length
> 

といった指定は何も返しませんし、Strict Modeのレベルによってはエラーとなります。

> Set-StrictMode -Version 2.0
> (123).Length
. : このオブジェクトにプロパティ 'Length' が見つかりません。このプロパティが存在することを確認してください。
発生場所 行:1 文字:7
+ (123). <<<< Length
    + CategoryInfo          : InvalidOperation: (.:OperatorToken) []、RuntimeException
    + FullyQualifiedErrorId : PropertyNotFoundStrict

PowerShell 3.0以降

配列でないスカラ値に対してLengthプロパティが拡張され、

# スカラ値のLengthは1
> (123).Length 
> 1

# $nullのLengthは0
> $null.Length
> 0

といった値を返す様になっています。

最初の問題に戻ると、出題時にバージョンを指定していないので挙動が不定となり回答を確定することが出来ません。
ロケーションの明示もないため、(Get-ChildItem).Lengthが返しうる結果としては、

  • エラー
  • $null
  • 0
  • 1以上の数

のいずれかになります。

敢えて選択肢から答えを選ぶとするなら

  • 1 オブジェクトが無いのでエラーになる
  • 3 ディレクトリ配下のファイルの数が表示される

のいずれか、が近い感じでしょうか。

System.IO.FileInfo.Length プロパティ

【2018/06/11追記】

一点指摘漏れがありました。
(ついさっきまで完全に失念していました...)

例として適当なディレクトリで単一のファイルを作成するケースを考えます。

mkdir sample
cd .\sample\
# 適当なファイル(BOM付きUTF-16)を作成
"Hello World!" | Out-File -FilePath .\sample.txt

ここで(Get-ChildItem).Lengthを実行すると、

> (Get-ChildItem).Length
30

sample.txtのファイルサイズを返します。

これは対象フォルダ内にファイルが一つしかない場合はGet-ChildItemの戻り値はSystem.IO.FileInfo型となり、この型にあるLengthプロパティが使われるためです。

この場合2. ディレクトリ配下のファイルの総容量が表示されるが正答になります。

ちなみに、

mkdir sample2
cd .\sample2\
# 適当なサブディレクトリを作成
mkdir subdir

とサブディレクトリ1つだけの場合はGet-ChildItemの戻り値はSystem.IO.DirectoryInfo型となり、この型にはLengthプロパティはありませんので、(Get-ChildItem).Lengthの結果は

  • エラー
  • $null
  • 1

のいずれかになります。

【追記ここまで】

どう出題すればよかったか?

この問題で不足していたのは環境定義です。

PowerShellの環境はその気になればいくらでもカスタマイズできるので抜け道を全て埋めるのは難しいのですが、一般的な余興レベルとしては

問題

Windows 10においてWindows PowerShellを起動し、直ちに以下のコマンドを実行した。
実行結果はどうなる?

PS > (Get-ChildItem).Length

回答欄

  1. オブジェクトが無いのでエラーになる
  2. ディレクトリ配下のフォルダ、ファイルの総容量が表示される
  3. ディレクトリ配下のフォルダ、ファイルの数が表示される
  4. コマンドが存在しないためエラーになる

とし、

Windows 10

  • PowerShell 5.0~5.1 であることが確定している

Windows PowerShellを起動し、直ちに以下のコマンドを実行した。

  • 環境をカスタマイズしない限りはユーザープロファイルのフォルダ、またはC:\WINDOWS\system32(管理者として実行した場合)がカレントロケーションとなる

の条件を暗示するくらいで良いでしょう。

補足

ちなみに、PowerShellの言語モードや、対象となるロケーションのアクセス権などによっても(Get-ChildItem).Lengthの結果が変わってしまいますが、さすがにそれは穿ち過ぎなので指摘の対象から外しています。

PowerShellの"罠"と呼ばれるモノについて

私は普段ブログのネタ探しのためにいくつかのSNSやフォーラムなどを巡回しているのですが、その際に

PowerShellの罠にハマる

といった表現をそれなりに目にしてきました。

大抵のツールに初学者にとってハマりやすい"罠"はあると思いますが、PowerShellにも"罠"と言われても仕方のない部分は結構あります。
本エントリではこの"罠"について思うところを書き連ねていきます。

"罠"の一覧

PowerShellはPowerShell 1.0がリリースされてからもうすぐ12年目を迎えつつある歴史の長いツールです。
このため"罠"の数もそれなりにあり、既に有志によって一覧はまとめられています。

github.com

個々の"罠"の詳細についてはこちらのリポジトリをご覧いただければ十分でしょう。

また、日本語の情報としては、

がPowerShellを使う上でハマりやすいポイントを良く解説してくれており参考になります。

PowerShell 3.0 での破壊的変更

こちらは"罠"と少し違うのですが、PowerShellはPowerShell 3.0でランタイムが刷新され、それに合わせて幾つかの破壊的変更が生じています。
この破壊的変更の一覧はWMF 3.0のリリースノートに軽く記載されているだけで詳細な解説がありません。

情報の得にくさとしてはこれも"罠"といっても良いかもしれません。*1

【2018/12/01追記】

PowerShell 3.0での破壊的変更の一覧をブログにまとめました。

blog.shibata.tech

【追記ここまで】

"罠"を感じる心理的な問題

あくまで私の経験上ですが、PowerShellを学ぶ人が"罠"を感じるのには心理的な要因もあるのではないかと思っています。

非常に大事な前提なのですが、

PowerShellはコマンドプロンプトでは無いし*shでも無い。

この前提を誤解して"罠"と見做されるケースが多い様に感じます。
これは一見当たり前の様に見えますが非常に大切なことです。

まず、PowerShellは時代的な経緯からコマンドプロンプトの後継の様に誤解されることが多いのですが、後発ではあるものの後継ではありませんしコンセプトが異なるツールです。
また、Bash等のシェルから影響を受けている部分があるため、いくつか概念や語彙に重なる部分はあるのですが、大原則としてPowerShellはBashなど*shなシェルとはまるで異質のものです。

PowerShellと他のシェルを混同してしまっているため(他のシェルに対しての)期待した動作を得られず「PowerShellには"罠"がある。」と言われるケースを多く見てきました。

PowerShellの異質さ

PowerShellの異質さについては私が以前発表したスライドを見てもらうと理解して頂けるかと思います。

これらで触れている

PowerShellはAPIを志向している

PowerShellは.NET Frameworkのオブジェクトを扱うシェルである

といった点は他のシェルに慣れた方からするとかなり異質に感じることでしょう。
とはいえ、これはPowerShellを扱う上で必須の前提であり、この前提に立てば"罠"と感じることも減ると思います。*2

私的"罠"四天王

心理的要因とは関係なく普通にハマってしまうポイントも当然あります。
私自身もいくつもの"罠"を体験してPowerShellを学習してきました。

最初の項の内容とかぶる部分もありますが、私がハマってきたいくつかの"罠"を最後に紹介します。

1. 文字コード

2018年現在、PowerShell Coreが登場してPowerShellはクロスプラットフォームなアプリケーションとなっていますが、元々はWindowsの.NET Framework上で動作するアプリケーションでした。

このためPowerShellで取り扱える文字コードは.NET FrameworkおよびWindowsの影響を大きく受けています。

リダイレクト演算子やファイル操作に関わるコマンドレットの既定のエンコーディングがBOM付きUTF-16であることやUTF-8がBOM付きなのは、基本的には.NET Frameworkにおけるエンコーディングがそうである影響です。*3

また、非英語圏におけるエンコーディングは俗にANSIエンコーディングとも呼ばれ、OSのロケール設定に依存し日本語環境であればSHIFT-JIS(CP932)です。
PowerShellではコンソール上で扱える文字種に影響を及ぼしています。

コマンドプロンプトを常用する人であれば文字コードにSHIFT-JISを期待するでしょうし、Bashを常用する人であればBOM無しUTF-8を期待するでしょう。
PowerShellでは扱う文字コードを常に意識しておくと"罠"を回避できます。

2. -Pathパラメーターにおけるワイルドカード

これは罠を超えてPowerShell最大最悪の失敗仕様であり、多くの方にとってPowerShellへのヘイトを募らせる要因となっているかと思います。

PowerShellはコマンドプロンプトからの影響を受け、パラメーターに*?[]の4種の文字をワイルドカードとして使用できます。
それぞれ、

  • * : 0文字以上の任意の文字列
  • ? : 1文字の任意の文字
  • [ ] : []内で指定したパターンにマッチする文字列

となっています。

各コマンドレットがワイルドカード検索に対応するかどうかは一応任意なのですが、開発ガイドラインとしては

  • A cmdlet should support wildcard characters if possible.

  • The name of the parameter should be Path, with an alias of PSPath. Additionally, the Path parameter should support wildcard characters. If support for wildcard characters is not required, define a LiteralPath parameter.

となっており、Microsoft製のコマンドレットであれば-Name-Pathパラメーターはほぼ100%ワイルドカード検索をサポートしています。

ここで-Pathパラメーターが大問題で、Windowsの仕様では[]がディレクトリ・ファイル名として使用可能です。*4
またディレクトリ・ファイルを扱うコマンドレットにおいて-Pathパラメーターが既定となっており、例えば

mkdir .\[Test]
cd [Test]

なんてことをすると、ディレクトリ名にある[]を正しく認識できずエラーになってしまいます。
(ワイルドカードとして扱われてしまう)

この場合は

cd -LiteralPath [test]

の様に-LiteralPathを使うか

cd '.\`[Test`]\'

の様にワイルドカード文字をエスケープする必要があります。

この点に関しては、

ディレクトリ・ファイルを扱うコマンドレットでは原則 -LiteralPath パラメーターを使う。

ことを徹底するのをお勧めします。

3. 配列の平坦化 (パイプライン文)

PowerShellではコマンドレットなどの実行結果を変数に代入する際、実行結果のオブジェクトが複数ある場合はオブジェクトの配列(Object[])として扱われ、単一のオブジェクトの場合は配列ではなくそのオブジェクト自身を取得します。
挙動だけ見ると配列を期待した処理に対して平坦化が行われている様に見え、非常に直感的でない挙動に感じてしまうでしょう。

この挙動はパイプライン文(Pipeline statements)と呼ばれるPowerShell独自の構文に依るものです。
パイプライン文は名前の通りパイプライン(|)を扱うために用意された文なのですが、PowerShellのパイプラインは.NET Frameworkのオブジェクトを扱うオブジェクトパイプラインであり、他のシェルのそれとは完全に異質のものです。

わかりやすい表現をすると、これは厳密には間違っているのですが、PowerShellにおいてコマンドレットなどの実行結果は最終的にパイプライン文に行きつき、このパイプライン文は評価されたオブジェクトを一つずつストリーム(他のシェルの標準出力に近いもの)に放流する動作をします。
ストリームに放流されたオブジェクトはパイプライン演算子(|)でパイプすることができます。
放流されたオブジェクトは、途中で代入やパイプされなかった場合は最終的にPowerShell内部で| Out-Defaultと暗黙的にパイプされコンソールにその結果を表示する様になっています。

オブジェクトの取得(代入)はストリームに放流されたオブジェクトを途中で浚うイメージです。
(あくまでイメージであり正確な動作ではありません)
浚ったオブジェクトが単一であればそのまま扱い、複数個あれば配列(Object[])になる配列化が実際の挙動に近く、このため、必ず配列が欲しいといった場合は配列部分式演算子(@())などによる明示的なキャストが必要になります。

このパイプライン文の挙動は非常にわかりにくく、私もまだ厳密に正しい挙動を押さえることができていません...
ただ、パイプライン文やストリームといったものがあることを認識するだけでもPowerShellに対する理解はかなり変わると思います。

これらの詳細については本ブログの以下のエントリが役に立つでしょう。

blog.shibata.tech

blog.shibata.tech

加えて、PowerShellの関数もこのパイプライン文の影響下にあり独特の挙動をします。
こちらについては以下のエントリをご覧ください。

blog.shibata.tech

4. 配列に対する-eq演算子

最後の罠です。

blog.shibata.tech

で触れているのですが、配列に対して-eq演算子はフィルターとして動作します。

これを回避するには

if ($null -eq $array) {
    # do something
}

の様にヨーダ記法にする必要があります。

この点に関して私はハマる前に気が付けたので実害は被っていないのですが、罠としてはかなり凶悪な部類に入ると思います。

最後に

思うがままにPowerShellの"罠"について書いてきました。

PowerShellは他のシェルと比べて似て非なるツールであり誤解を生みやすい部分は残念ながらあります。
この点に関してPowerShellの設計は「悪い」としか言いようがありません。

しかしながら、PowerShellの前提・仕様を正しく押さえ、"罠"があることを知ってしまえば回避は容易です。

PowerShellは設計に悪い部分はありますが使いこなすと十分な実利を得ることができるツールだと私は思っています。
本エントリの内容が"罠"を回避してPowerShellで実利を得る一助になれば幸いです。

*1:時間があれば本ブログでもまとめたいですね...

*2:ただし仕様の理不尽さが無くなるとは言わないです...

*3:なお、最新のPowerShell Core 6.0ではクロスプラットフォーム化の影響もあり既定のエンコーディングはBOMなしUTF-8になっています

*4:*と?は不可