The Man Who Fell From The Wrong Side Of Sky:2019年1月4日分

2019/1/4(Fri)

[Windows] PowerShellで生活するために - Invoke-WebRequest コマンドレット編(その1)

ここ数年脱UNIX(TM)文化を志してタッパーウェア捨てたり美容院を変えたりし、普段使いのシェルもCygwinからPowerShellにして頑張ってるんですがまぁハマりどころが多くてキレそうです(憤怒)。

まず最初に書いておくとPowerShellをわざわざ使うなんてのは正気の沙汰じゃない、よりにもよって暗黒期の横浜ベイスターズに自分からFA入団するようなものだ。

みんなプロになりたいんだろ、チャンスはあるよ。でも横浜だけはやめとけよ (by 門倉健)

という箴言を心に刻みこんで、PythonでもRubyでもなんでもいいからまともなプロと呼べる球団へ入ろう。Perlはうん…

ハマりどころの例を挙げると、ウェブページのスクレイピングをしたいという時、UNIX(TM)環境ならwget(1)やらcurl(1)、そしてNなんとかBSDならftp(1)あたりを使うところだけど *1、PowerShellではいろいろと問題がある。

こいつらと同じことをしたければPowerShell 3.0以降では Invoke-WebReqestというコマンドレットがある(ご丁寧にwgetがエイリアスとなってる、なおオプションに互換性は皆無だ)。 ところがWindows 7やWindows Server 2008 R2に標準添付のPowerShellのバージョンは2.0なのでバージョンアップが必要となる。さもなくば.Net Frameworkの System.Net.WebClientをCOM経由で操作せざるをえない羽目になる。

ちなみにPowerShellのバージョンアップはWindows Management Framework(WMF)を別途インストールすればいいだけ、最新は WMF5.1だ。

ところがそれだけじゃ終わらない、Invoke-WebRequestにはいくつか初心者には判りにくいハマりどころがある。

@スクリプト実行ポリシーの設定が必要

これはPowerShell自体の問題なんだけど、Invoke-WebRequestを使ってウェブページをファイルに保存したいだけであればプロンプトで

PS> C:\Users\tnozaki> Invoke-WebRequest 'https://example.com/' -OutFile unko.html

と叩くだけなんだけど、リクエストがPOSTとかリファラとかクッキーとか認証とか必要だったり、また取得したHTMLを整形した上で保存するみたいな後処理が必要になると、これはもうスクリプト化したくなる。

ところがPowerShellのデフォルトの設定はなんと「スクリプトはすべて実行禁止」なので

PS C:\Users\tnozaki> .\unko.ps1
.\unko.ps1 : このシステムではスクリプトの実行が無効になっているため、ファイル C:\Users\tnozaki\unko.ps1 を読み込むことができません。
詳細については、「about_Execution_Policies」(http://go.microsoft.com/fwlink/?LinkID=135170) を参照してください。
発生場所 行:1 文字:1
+ .\unko.ps1
+ ~~~~~~~~~~
  + CategoryInfo     : セキュリティ エラー: (: ) []、PSSecurityException
  + FullyQualifiedErrorId : UnauthorizedAccess

となり実行すら許されぬというめんどくささ、はーつっかえもうやめたらこの仕事。

まず現在の設定を確認するにはGet-ExecutionPolicyコマンドレットを使用する。

PS C:\Users\tnozaki> Get-ExecutionPolicy
Restricted

デフォルトでは「Restricted」なので全面禁止、これ変更するためにはまず管理者権限でPowerShellを起動しないとならない。

ということで管理者権限が無い人はここでグッバイ、自分の書いたスクリプトすら実行できないって馬鹿じゃねぇの。

ということで管理者権限でPowerShellを起動しポリシーを変更するためにSet-ExecutionPolicyコマンドレットを実行する、設定可能な引数は以下の通り。

実行場所 ローカル リモート
署名 あり なし あり なし
Restricted × × × ×
AllSigned × ×
RemoteSigned ×
Unrestricted △(警告)
Bypass

これらの意味について細かい説明をすると長くなるのでまたいずれ回を改めて書く。

とりあえず実行したければSet-ExecutionPolicyコマンドを使用し、署名すんのもめんどいので「RemoteSigned」くらいまで緩めとく。

PS C:\Users\tnozaki> Set-ExecutionPolicy RemoteSigned

Execution Policy Change
The execution policy helps protect you from scripts that you do not trust. Changing the execution policy might
expose you to the security risks described in the about_Execution_Policies help topic at
http://go.microsoft.com/fwlink/?LinkID=135170. Do you want to change the execution policy?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "N"):

変更していいか問われるので、YあるいはAとでも答えておく。

@Internet Explorer 11 の設定が必要

しかしこれだけじゃまだ動かない、Invoke-WebRequestは内部的にIE11をCOMオブジェクトとして動かしてるだけなので、そちらの設定が未了だと動かないのですわ。

なので一度もIE11を起動せずにChromeやFirefoxをスタンドアロンインストーラーで入れて使ってる人はいきなりここでハマる。 またWindows 10環境ではOS標準ブラウザがMicrosoft Edgeに変更されたのでIE11を一度も起動したことの無い可能性が高いのだけど、PowerShellが呼ぶのはあくまでEdgeでなくIE11。やっぱりハマるわけですわ。

ということでめんどくさいけど一度はIE11を起動して初回起動時に表示される「Internet Explorer 11 の設定」ダイアログで「お勧めのセキュリティと互換性の設定を使う」あるいは「推奨設定を使用しない」を選択して初期設定を済ませておく必要がある。

@TLS1.2 対応が必要

困ったことにInvoke-WebRequestはデフォルトで今時TLSv1.2に対応していない、当チラシの裏ですら TLSv1とv1.1を無効にするご時勢だというのに…

よって

if (-not ([Net.ServicePointManager]::SecurityProtocol -band [Net.SecurityProtocolType]::Tls12)) {
	[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
}

の3行を追加してセッション中でTLSv1.2を有効にする、あるいは KB2960358を適用して規定値を変更する必要がある(詳細については ここを読め)。

ちなみにKB2960358は月例のセキュリティおよび品質ロールアップには含まれていない上、Windows UpdateにもMicrosoft Update Catalogにも出てこないので前述のリンク先からダウンロードして手動適用すること。

@文字コード絡みのバグを抱えているので対策が必要

ここまでの設定でとりあえずInvoke-WebRequestは使えるようになるのだけど、文字コード周りに致命的なバグを抱えていてその対策が必要となる。

というのも、サーバーがレスポンスヘッダでcontent-typeにcharsetを返さない場合、Invoke-WebRequestはコンテンツのBOMやHTMLのmetaタグで指定されたcharsetを一切検証することなくISO-8859-1(あるいはASCII-8BIT)として扱うので文字化けするのだ。 もう長いことIssueになってるけど直らないということは直す気が無いということと思われる。

これ明示的に文字コードを指定する方法も存在しないので、コンテンツをいちどISO-8859-1からバイト列に戻した上でBOMやHTMLのmetaタグから正しい文字コードを取得し、もういちど文字列へ変換し直す必要がある。

これについては記事が長くなるのでまた次回コードつきで詳細に解説する予定。

*1:telnet(1)でhttp喋りますとかネタでも寒いから、openssl(1)使ってhttpsも喋れますとかもっと寒いから。