2017/12/24

この記事には「 独自研究 」に基づいた記述が含まれているおそれがあります。

この記事はPowerShell Advent Calendar 2017の24日目です。

一般的なプログラミング言語では、文(statement)と式(expression)の違いは、値を返すのが式で、返さないのが文、という説明がされることが多いと思います。しかし、PowerShellではこの説明は成り立たたず、文が値を返したりしてるように見えて良く分かりません。そこでPowerShellにおける文と式とはそもそも何なのかということを、仕様書(PowerShell 3.0のものですが)やAST(ShowPSAstモジュールが便利!)を眺めながら考えてみたので軽くまとめようと思います。

文(statement)と式(expression)の定義

PowerShellでは言語要素として、文(statement)と式(expression)が明確に定義されています。すなわち、言語要素の何が文であって、何が式であるかという定義は仕様できちんと決まっていて、ある言語要素が、状況によって文になったり式になったりと変化する、ということはありません。

パイプライン、代入、ifやforやfunctionなどは文です。

変数、数値/文字リテラル、オブジェクトのメンバ呼び出し、スクリプトブロック、単項または二項算術演算子で構成される式、カンマ演算子で構成される配列などは式です。

ただ、仕様書での定義と、実際に構築されるASTに齟齬があることはあります。

例えば「$a=1」のような代入については、ASTではAssignmentStatementAstとなります。一方、言語仕様上はassignment-expressionと書かれています。厳密には、言語仕様書のgrammer節によれば、assignment-expressionはexpressionではなくpipelineであるということになっています(お前は何を言っているんだ)。いずれにせよパイプラインは文であるので、AST通り、代入は文であるという解釈で良いと思います。

※仕様書にはassignment expressionとはっきり書いてあるんだから代入は式だろ!という意見を否定するものではないです。が、代入は文であると考えたほうが他の文法と整合性を取りやすいので、そういう立場をとりました。

しばたさんが9日目に書かれた記事で取り上げられているように、配列に関しても同様の齟齬があります。いずれにせよ「1,2,3」のような配列は、式と考えて良いと思います。

文と式の構造

PowerShellには文と式が存在することは分かりました。では文と式は何が違うのか? それを考察するために、具体的にいくつかの文と式の構造を取り上げて見ていきます。

パイプライン(文)

パイプラインといえば「Get-Process | Where-Object Handles -gt 100 | Select-Object ProcessName」みたいな|で繋ぐやつのことでしょ、と思われがちですが、言語仕様上は以下のようなものはすべてパイプラインです。

Get-Process -Name PowerShell # @コマンド1つだけ
1 # A数値リテラルだけ
1 + 1  # B算術演算子で構成される算術式
$a = 1 # C代入
gps | where Handles -gt 100 | select ProcessName # Dパイプ記号でコマンドを連結したもの
1 + 1 | Write-Host # E算術式をパイプラインでコマンドと繋げたもの

要はパイプラインというのは、一般的なプログラミング言語で、;で終わる一文に相当するものと考えればだいたい間違いないと思います。ただしPowerShellだと文末の;は必須ではなく、改行でもOKです。

パイプラインは以下のような構造を取ります。[]は省略可能を意味します。

パイプライン要素1 [ | パイプライン要素2] [ | パイプライン要素3] ...

または、

代入文

すなわち、代入文(後述)を除くパイプラインは1個以上のパイプライン要素から構成されており、複数のパイプライン要素が存在する場合はパイプ演算子|で連結されます。

パイプライン要素には式とコマンド(Get-Processとか)が存在します。ただし式は1つ目のパイプライン要素にのみ許可されます。

以上を踏まえると、@、Dはコマンドのみで構成されるパイプライン、A、Bは単独の式のみで構成されるパイプライン、Eは1つの式とコマンドで構成されるパイプライン、Cは代入文であることが分かります。

代入文

代入文はパイプラインなので、文です。代入文は以下のような構造を取ります。(ここでは+=などの複合代入演算子については省略)

式 = 文

ただし、左辺の式は、変数やプロパティなど、代入が可能な式である必要があります。

よって以下のような記述が可能です。

$a = $b # @変数
$a = 1 + 1 # A算術式
$a = Get-Process # Bパイプライン(コマンド1つ)
$a = gps | where Handles -gt 100 # Cパイプライン(コマンド複数)
$a = if($true){"a"}else{"b"} # Dif文
$a = $b = 1 # E代入文の結果を更に代入

言語仕様上、代入文の右辺には文であれば何でも書けるのですが、実際に代入が行われるのは、パイプライン、if文、for文、switch文といった、パイプラインに値を出力する文と代入文に限られます。ちなみにDのようにパイプラインと代入文以外の文を右辺に指定できるようになったのは、PowerShell2.0からです。

ところで上記@やAは右辺が式です。代入文の右辺は文じゃなかったの?と思われると思いますが、パイプラインの節で述べた通り、単独の式もパイプラインであり、パイプラインは文なので、三段論法でいくと式は文として扱われることになります。

※AST上では$a = $bの右辺はPipelineAst/CommandExpressionAst/VariableExpressionAstではなく、いきなりCommandExpressionAst/VariableExpressionAstとなっているので、この説明はASTの実装とはかみ合わないかもしれません。AssignmentStatementAst.Rightは確かにStatementAstを取るのですが、CommandExpressionAstはStatementAstから派生しているクラスなので、式の代入は問題なく行えます。

代入文は上記Eのようなことができることから分かる通り、値を返す文ですが、パイプラインには値を出力しません。値は返すがパイプライン出力がないものは、インクリメント演算子で構成される式($a++等)も同様です。

if文

パイプラインと代入文以外の文は色々あるわけですが、代表的なものとしてif文を取り上げます。一番シンプルなif文はこういう構造です。

if (パイプライン) {
    文1
    文2
....}

おそらく多くの人が誤解しているのではないかと思いますが、条件節に書くのは式ではなくパイプラインです。パイプラインを実行した結果、出力値がtrue、またはboolに型変換してtrueになる場合に、ブロック内の複数の文が実行されます。

よって以下のような記述が可能です。

if ($true) {} # @変数
if ($a -eq 1) {} # A論理演算子で構成された式
if ("a.txt" | Test-Path) {} # Bパイプライン
if ($a = 1) {} # C代入文

@とAは普通の書き方ですが、実際には、1つの式のみ有するパイプラインを実行し、出力される値が判定されています。

条件節はパイプラインなので、当然Bのような書き方もできるわけです。また、代入文もパイプラインであるので、Cの書き方もできてしまい、注意を要します。

条件節に指定できるのはパイプラインだけで他の文は許容されないので、
if (if($true){}){}
というような書き方はできません。

※といっても実はこう書くと文法上はvalidであり、条件節内は「ifコマンド、パラメータ値1($true)、パラメータ値2(スクリプトブロック)」という解釈になってしまいます。

また、条件節にはパイプラインは1つのみ指定可能で、複数文を書くことはできないので、
if ($a;$b) {}
という書き方はできません。(パーサーもエラーを出す)

丸括弧式

さて、普通のプログラミング言語だと、()はグループ化や演算子の優先順を変更するのに用いられるものの、別に文法そのものに影響を与えるものではないと思います。多くの場合、ASTでも()の情報はそぎ落とされます。

ところがPowerShellでの丸括弧()は文法的な意味を有しており、ASTにもParenExpressionAstとして存在する、立派な式です。丸括弧式の構造は以下の通りです。

(パイプライン)

これは要するに、「パイプラインに()を付けると式になる」、ということです。()内のパイプラインで出力された値が返される式となります。具体的にどういうところで使うのかを示します。

2 * (1 + 3) # @数値演算の優先順を変更する
($a = 1) # A値を返すがパイプラインには出力しない代入文の値を出力させる
$a[(Get-Hoge)] # B式は許容するがパイプラインは許容しない構文で、式に変換する
Get-Process -Name (Get-Hoge) # Cコマンドのパラメータにコマンド実行結果を指定する

@の使い方は普通です。ただし、()はパイプラインを生成するので、「(1+3)」は「1つの式のみ有するパイプラインを実行し、パイプラインに値を出力し、その値を返す」という見た目より複雑な処理になります。

※少なくともAST上はそうなりますが、実際は何らかの最適化処理が入ってる可能性はあります。

代入を重ねる場合にはAのような書き方は必要ないのですが、代入した結果をパイプラインの出力としたい場合は()を付ける必要があります。この場合、$aに1が代入され、コンソールにも1が出力されます。

Bで挙げている、式を許容するがパイプラインは許容しない言語要素というのは実はあまりないです。前述した通りif文の条件節は式じゃなくて、パイプラインを取るといった案配です。ただ、たとえば配列や連想配列の要素を取得するインデックス演算子[]は、式のみ許容されます。なのでコマンドなどのパイプラインの出力値を指定したい場合は()が必須となるわけです。

ちなみに、()内にはパイプライン以外の文(if文等)は指定できません。また、複数のパイプラインも指定できず、あくまで1つだけです。

※任意の文あるいは複数の文を式としたい場合には、部分式演算子$()または@()を用います。両者とも内部の文がパイプラインに出力した値を返す、「部分式」となります。両者とも複数値が出力されると配列になりますが、@()は出力値が1つでも要素数1の配列を返す点が異なります。

まとめ?

PowerShellの文と式は厳密に定義されています。文は複数の文と式で構成されるし、式は複数の文と式で構成されています。文や式の構成要素が取る文や式の種類についても、各々、きちんと定義されています。

ただし、PowerShellにおいて「値を返すか返さないか」、「パイプラインに出力されるかされないか」、「式であるか文であるか」という概念はすべて独立しています。そのため、PowerShellの文とは何である、式とは何である、ということを一言で説明することは難しいんじゃないかと思います。

なので、本記事でこれまで述べてきたとおり、「パイプラインは文で、要素として式やコマンドを取りますよ」とか、「ifは文で、条件節にはパイプラインを取りますよ」みたいな、各論でしか表現できないのではないかなぁと、私はそういう結論に至りました。

しかしここまで書いてちゃぶ台をひっくり返すようなことを言いますが、ある言語要素が文であるか式であるか、ここまで仕様書を読んだりASTを追ったりして把握するのは、まあ楽しくなくはないですが、知らなくても別に大丈夫だと思われます。別に、ifの条件節には条件式を書くのだと理解していても不都合は特にないかと。

効用があるとすれば、例えば「if ((Test-Path a.txt)) {}」とか「foreach ($i in (1..5)) {}」とかの、余分な()を取り除くのには文法の知識が役立ちます。それもまぁ、心配だから怪しい所には常に()付けておく or 何か変だったら()付けてみる とかでもそれ程問題にはならないかもしれません。

2017/12/10

この記事はPowerShell Advent Calendar 2017の10日目です。

PowerShellはオブジェクトを扱うシェルですが、別にテキストデータを扱えない訳ではありません。むしろ、PowerShellで取得したデータをテキストファイルとして保存したり、スクリプトで用いるデータをテキストファイルで保存しておくことは日常的に行われることだと思います。

ただし、PowerShellで扱うデータはオブジェクトであり、テキストファイルは文字通り文字列であることから、コマンドレットを用いる等、何らかの手段で変換が必要になります。また、テキストデータ形式にも様々な種類があり、それぞれメリット、デメリットが存在します。今回の記事では、PowerShellで用いるデータを保持しておく際のテキストデータ形式について比較をしてみます。

プレーンテキスト

プレーンテキスト、すなわち書式なしのテキストファイルです。もっともシンプルな使い方をする場合、文字列配列の各要素に含まれる文字列が、テキストファイルの1行と対応します。

書き出し
$lines | Set-Content -LiteralPath file.txt -Encoding UTF8

$linesは文字列変数です。

特に理由がなければ文字コードはUTF-8で良いと思います。

追記
$lines | Add-Content -LiteralPath file.txt -Encoding UTF8

Add-Contentは実行のたびにファイルを開いて、書き込んでから閉じるという動作をするので、1行ずつforeachで実行するのはNGです。

読み込み
$lines = @(Get-Content -LiteralPath file.txt -Encoding UTF8)
メリット
  • 文字列配列をテキストファイルに書き出すのは多分これが一番楽だと思います。
  • 書き出したデータは人間にも読みやすい。 編集もしやすい。
デメリット
  • 文字列だけを保存しておきたいというケースがそもそも少ない。
CSV

コンマ等の特別な文字で区切り、1行あたりに複数のデータを保存できる形式です。

PowerShellのコマンドレットで扱う場合、オブジェクトが持つプロパティがヘッダ列名に対応します。各行にオブジェクト配列1要素のプロパティ値が、カンマ区切りで保持されます。

書き出し
$objects | Export-Csv -LiteralPath file.csv -NoTypeInformation -Encoding Default

$objectsは任意のオブジェクト配列です。必要であればSelect-Objectコマンドレットを併用して、プロパティを絞り込みます。

文字コードはExcelでそのまま読み込み/書き出しができるDefault(日本語環境ではShift_JIS)がお勧めです。(最近のExcel2016ならUTF8も一応読めますが)

追記
$objects | Export-Csv -LiteralPath file.csv -Append -NoTypeInformation -Encoding Default
読み込み
$objects = Import-Csv -LiteralPath file.csv -Encoding Default
メリット
  • オブジェクトのプロパティ値が、すべて数値あるいは文字列で表現できる値を持つ場合に最も適合する。
  • 人間にも読みやすく、ある程度は編集もできる。
  • Excelで開ける。
デメリット
  • オブジェクトのプロパティが、数値と文字列以外のオブジェクトである場合、すなわち、階層構造を持つデータの保存には適さない。
  • 数値も文字列として読み込まれてしまうので、数値として扱いたい場合は変換が必要になる。
  • Export-CsvとImport-Csvで扱うCSVファイルはヘッダが必須。つまり、ヘッダなしのCSVファイルが既にあって、それを読み書きするという用途には適さない。(できなくはないが)
  • 書き出し時の列順を制御することができない。つまり、PowerShellで書き出したCSVを、列順が固定であるとの想定である他のプログラムで読み込むことは基本NG。
  • 書き出し時、1つ目の要素に存在しないプロパティは、2つ目以降では存在しないものとして扱われる。同種のオブジェクトで構成される配列なら通常は問題ないのだが、要素によって動的に追加されるプロパティがあったりなかったりすると厄介。(ADでありがち)
JSON

JavaScriptのような表記でデータを保持するデータ形式です。データの受け渡しに様々な言語で利用できます。Web APIでもよく利用されます。

PowerShellではv3からJSONを扱うコマンドレットが提供されています。

書き出し
$objects | ConvertTo-Json | Set-Content -LiteralPath file.json -Encoding UTF8
読み込み
$objects = Get-Content -LiteralPath file.json -Encoding UTF8 -Raw | ConvertFrom-Json
メリット
  • CSVと異なり、階層構造を持ったデータでも扱える。
  • CSVと異なり、数値は数値型のまま読み書き可能。 (整数値はint、小数値はdecimal)
  • 人間にもまぁまぁ読めるし、頑張れば編集できなくもない。
デメリット
  • -Depthパラメータによりプロパティを展開する階層の深さを指定はできるが、プロパティに応じて深さ指定を変化させるというようなことはできない。基本的には、自分で構築したPSCustomObjectを使うか、JSON化する前に自分で元オブジェクトを整形しておく必要がある。
  • 直接ファイルに書き出し、追記、ファイルから読み込みするコマンドレットはない。
  • 実は細かい話をしだすと色々と罠があります…。
CLIXML

PowerShellではPSリモーティング等、プロセス間でオブジェクトのやり取りを行う際に、CLIXML形式を介してシリアライズ/デシリアライズが実行されます。シリアライズ対象によっては、完全に元のクラスのオブジェクトに復元されます。(復元されないオブジェクトにはクラス名にDeserialized.との接頭辞が付与され、プロパティ値のみ復元される)

ユーザーもコマンドレットを用いて、任意のデータをCLIXML形式でシリアライズし、XMLファイルとして保存することができます。

書き出し
$objects | Export-Clixml -LiteralPath file.xml
読み込み
$objects = Import-Clixml -LiteralPath file.xml
メリット
  • 元のオブジェクトの構造、プロパティ値と型情報を含めてほぼ完全にテキストファイルに保存できる。
  • 復元したオブジェクトはプロパティ値を参照できるのはもちろん、オブジェクト全体が完全にデシリアライズされ、元の型に戻った場合には、メソッドを実行することも可能。
  • 例え元の型に戻らず、Deserialized.との接頭辞が付いた状態でも、コンソールに表示する場合は元の型のフォーマットが使われるので見やすい。
デメリット
  • すべてのオブジェクトが元の型に戻せるわけではない。戻せるかどうかは確認が必要。
  • 人間が読み書きするようなものではない。

ちなみに、ConvertTo-Xmlという似たようなコマンドレットがありますが、出力形式はCLIXMLではない上、復元の手段もなく、かといって別に読みやすいXMLというわけでもなく、正直何のために使うのかよく分かりません(適切なxsltでも用意すればいいのかな?)。まだConvertTo-Htmlの方が使えそうです。

psd1

psd1は「PowerShellデータファイル」で、モジュールマニフェストやローカライズデータに使われるファイル形式です。スクリプトファイルの1種ですが、数値や文字列リテラル、配列、連想配列、コメントなど基本的な言語要素のみ使用可能です。PowerShell 5.0以降ではImport-PowerShellDataFileコマンドレットを用いて、任意のpsd1ファイルのデータを読み込み、変数に格納することが可能です。

書き出し

書き出し用のコマンドレットはありません。

読み込み

例えば以下のような内容をbackup_setting.psd1として保存しておきます。ルート要素は必ず連想配列にします。

@{
	Directories = @(
		@{
			From = "C:\test1"       # コピー元
			To = "D:\backup\test1"  # コピー先
			Exclude = @("*.exe", "*.dll")
			Recurse = $true
		},
		@{
			From = "C:\test2"
			To = "D:\backup\test2"
			Exclude = @("*.exe")
			LimitSize = 50MB
		},
		@{
			From = "C:\test3"
			To = "D:\backup\test4"
		}
	)
	Start = "0:00"
}

なお、dataセクションで全体を括ってもいいですが、psd1で許容される言語要素はdataセクションより更に制限がきついので、敢えてしなくてもいいんじゃないかと思います。

このファイルは以下のように読み込めます。

$setting = Import-PowerShellDataFile -LiteralPath backup_setting.psd1

$settingには連想配列が格納され、以下のように値が参照できます。

$setting.Directories | foreach {Copy-Item -Path $_.From -Destination $_.To}
メリット
  • PowerShellの構文でデータを記述できる。
  • 通常のps1ファイルを呼び出すのとは異なり、式の評価やコマンド実行などはされない分、セキュアである。
  • 配列と連想配列の組み合わせにより、JSONライクな階層構造を持てる。型情報も保持される。
  • JSONとは違い、コメントが入れられる。
デメリット
まとめ

PowerShellで扱うデータをテキストファイルとして保存する際には、各テキストデータ形式の特性を理解し、メリット、デメリットを踏まえて選定する必要があります。

また、当然ながらテキストファイルに保持することが不適切なデータもありますので、そこは注意してください。(画像データを敢えてBase64とかでエンコードしてテキストファイル化する意味があるのか、とかですね)

個人的には…

ちょっとした作業ログ等を記録しておきたい→プレーンテキスト

.NETオブジェクトの一部のプロパティだけ抜き出してファイル化したい→CSV

自分で構築したPSCustomObjectをファイル化したい→JSON

.NETオブジェクト全体をファイル化したい→CLIXML

スクリプトで使う設定データを用意したい→psd1

みたいな感じでなんとなく使い分けていると思います。psd1はまだ採用例はないですが…。

今回はビルトインのコマンドレットで扱えるもののみ取り上げましたが、他にもyaml等のテキストデータ形式が存在し、有志によるモジュールを用いて扱うことが可能です。

2015/12/21

この記事はPowerShell Advent Calendar 2015の21日目の記事です。

PowerShellの属性

PowerShellには2.0から言語機能として「属性」機能が追加されました。PowerShellの属性はC#の属性とほぼ同じものですが、2.0〜4.0の時点では、高度な関数(Advanced Function)を作成するために関数に付与するCmdletBinding属性(記述上ではparamブロックに付与する形になる)、関数のパラメータに付与するParameter属性やAlias属性等、関数のパラメータや変数に付与する各種検証属性(Validate〜、Allow〜系)くらいしか使うことはなかったかと思います。

このうちパラメータ検証属性については、あえとす氏が以前詳しく解説しておられます。:パラメーターの検証属性について/前編 - 鷲ノ巣

PowerShellで属性クラスを作る

さて話は変わって、つい先日、WMF 5.0 / PowerShell 5.0がRTMしました。Windows 7/8.1、Windows Server 2008R2/2012/2012R2用のインストーラーが公開されたので、もう使っておられる方もいると思います。ぎたぱそ氏が一晩で解説記事を書いてくれてますね。

PowerShell 5.0では言語機能としてclass構文がついに追加されました。これでついにPowerShell言語も真の意味でオブジェクト指向言語の要素を完備したと言えるかと思います。

このclass構文、.NET Frameworkの既存のクラスを基底クラスとして継承するなんてことも、普通にできてしまいます。そこで私が考えたのは、もしかしてこれで属性も作れるんじゃない?ということでした。

v5のclass構文の基本的な部分の解説は、また別の機会にということにしまして、今回はいきなり、属性を作る話をしていきます。

ちなみにC#の属性については、属性 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C が参考になります。PowerShellで属性を作る時も基本はだいたい同じかと思います。

属性クラスの例

属性パラメータ(コンストラクタ引数)にDescription、名前付きパラメータとしてNoを指定でき、classに適用可能なTest属性はこんな感じです。

[AttributeUsage([AttributeTargets]::Class)]
class TestAttribute : Attribute
{
    [string]$Description

    [int]$No

    TestAttribute($Description)
    {
        $this.Description = $Description
    }
}

これを利用する際は、まずこのclassを含むスクリプトをドットソースで実行することで、グローバルに読みこむ必要があるようです(同一スクリプト内に属性定義と属性利用をまとめて書くとうまく動作しない)。

呼び出し側では以下のように指定します。

[TestAttribute("クラスの説明", No = 1)]
class Foo
{
    $X = 1
}

通常なら[Test()]のように"Attribute"の部分は省略できるはずなんですが、これも何故かフルネームで指定しないとダメなようでした。

クラスに属性が正しく指定されているかどうかはリフレクションで調べます。

[Attribute]::GetCustomAttributes([Foo])

結果は

Description  No TypeId
-----------  -- ------
クラスの説明  1 TestAttribute

こんな感じです。

パラメータ検証属性を作る

これだけでは面白くないので、少し実用になるかもしれない属性を書いてみましょう。

ところで前述のあえとす氏の記事の後編では、C#でPowerShellのパラメータ検証属性を作る方法について解説されています。ただ、C#でPowerShellの実行空間にアクセスして、操作を行ったり情報を取得したりするのは、少し知識が必要な部分かと思います。

そこで、PowerShellで使う属性なんだからPowerShellで書いたら楽になるんじゃない?という発想で、パラメータ検証属性をクラス構文で書いてみました。

このサンプルは、パラメータ(または変数)値の型が、指定の型セットに含まれているかどうかを検証するものです。複数型を取るようにするためにパラメータの型を[psobject]にすることが良くありますが、この属性を利用して、指定可能な型を絞り込めるようになります。

using namespace System.Management.Automation

[AttributeUsage(([AttributeTargets]::Property) -bor ([AttributeTargets]::Field))]
class ValidateTypeSetAttribute : ValidateEnumeratedArgumentsAttribute
{
    [Type[]]$Types

    ValidateTypeSetAttribute($Types)
    {
        $this.Types = $Types
    }

    [void]ValidateElement([object]$element)
    {
        if(!($element.GetType() -in $this.Types))
        {
            throw New-Object ValidationMetadataException `
                "$($element.GetType().FullName) は許容されない型です。値は $($this.Types) のいずれかの型である必要があります。"
        }
    }
}

使い方は以下の通り。パラメータ検証属性の場合は、定義と同一スクリプト内で呼び出しても、"Attribute"を省略しても問題ないようです。

function test
{
    param(
        [ValidateTypeSet(,([int],[string],[double]))]
        [psobject]
        $obj
    )
}

test "a" #OK
test (Get-Process)[0] #NG

class構文のコンストラクタで可変長引数を定義する方法が分からなかったので、呼び出しがちょっと不格好ですが、そこはご容赦を。

2015/12/04

この記事はPowerShell Advent Calendar 2015の4日目の記事です。

はじめに

今回はPowerShellでWebページのスクレイピングをする際の、ちょっとしたノウハウ集を前後編に分けて紹介したいと思います。

スクレイピングというのは、Webページから文字列を取ってきて、スクリプトから利用可能な形に加工する処理です。昨今は多くのWebサイトやサービスでWeb APIが公開されていて、スクレイピングをせずとも比較的簡単にデータを取得できます。PowerShellだとInvoke-RestMethodコマンドレット等が使えます(その話はまた次回とかにやります)。

しかし現実には、APIが公開されていない等の理由で、HTMLを取ってきて自前で解釈せざるを得ないケースが多々あります。さて、PowerShellではどうやりましょうか、というのが今回の話。様々な方々によってもう色々と語られている分野ではあるのですが、結構細かいハマりどころがあるのでちょっとまとめてみようと思いました。

前編ではまず、Webページからの文字列の取得方法ついてまとめます。

なお、スクレイピングには技術的な問題以外の、微妙な問題(著作権の問題とか、Webサイトへの攻撃と見なされる可能性とか)を含むものなので、その辺りは各自どうかご留意ください。この辺りの話はPowerShellに限った問題ではないので、ここでは詳説いたしません。参考記事:Webスクレイピングの注意事項一覧 - Qiita

Invoke-WebRequestコマンドレットで文字列を取得する

PowerShellでのスクレイピング、基本は何はなくともInvoke-WebRequestコマンドレットです。ただしこのコマンドレットはPowerShell 3.0で追加されたものなので、2.0環境にはないことに注意です。その場合は.NETのWebClientクラス等を使う方法があり、後で述べます。

基本は、

$response = Invoke-WebRequest -Uri "http://winscript.jp/"

のように、Invoke-WebRequestコマンドレットを実行する、だけです。「-Uri」は省略可能です。

このとき$responseにはHtmlWebResponseObjectオブジェクトが格納されています。このうち、指定URLのWebページに含まれているHTMLなどの文字列データは、Contentプロパティに格納されます。つまり、$response.Content に欲しいデータが格納されているので、あとはそれをよしなに利用すればいいわけです。

実はInvoke-WebRequestは、文字列データを取得すると同時に、HTMLの場合はパースしてタグの構造をオブジェクト化までしてくれます。が、それについては次回。

なお、Invoke-WebRequestコマンドレットでは文字列を取得する他、バイナリデータをダウンロードしてファイルとして保存する機能もあります。それについては過去記事をご参照ください。

リクエストにパラメータを付与する(GET)

GETメソッドを用いてクエリを指定する場合、要はhttps://www.google.co.jp/search?q=PowerShell のようなURLのデータを取得する場合は、Invoke-WebRequest "https://www.google.co.jp/search?q=PowerShell" のようにQueryStringを含んだURLをそのまま指定するだけでOKです。

ただし、動的にクエリを組み立てる場合は、URIエンコード(URIエスケープ)を考慮する必要があります。もっとも簡単なのは

$searchWord = "PowerShell 配列"
$response = Invoke-WebRequest "https://www.google.co.jp/search?q=$([Uri]::EscapeDataString($searchWord))"

のように、Uri.EscapeDataStringメソッドを使う方法かと思います。

リクエストにパラメータを付与する(POST)

POSTメソッドでリクエストボディにパラメータを付与するには、Invoke-WebRequestコマンドレットの-Methodパラメータに"Post"を指定し、-Bodyパラメータにリクエストボディに付与するデータを連想配列で指定します。

たとえばブログのトラックバックを手動で撃つにはこんな感じでいけます。

$body = @{title="テスト";url="http://example.com/";excerpt="テスト";blog_name="test"}
Invoke-WebRequest http://ご自分のブログのトラックバックpingURL -Method POST -Body $body

なお、リクエストボディに含めるパラメータの各値(連想配列の値)は、自動でURIエンコードしてくれます。

(12/16追記)
また、-Bodyには連想配列のみならず、任意の文字列(URIエンコード要)やバイト配列(バイナリを送信する場合)を指定することも可能です。

標準認証が必要なページを取得する

ページの取得に標準認証が必要な場合は、-Credentialパラメータにユーザー名とパスワードを指定したPSCredentialオブジェクトを指定すればOKです。

セキュリティのことは取りあえず置いておき、簡易的にスクリプトに生パスワードを直書きしてもいいかな、という場合には以下のように書くことができます。

$userName = "user"
$password = "pass"
$credential = New-Object PSCredential $userName, (ConvertTo-SecureString $password -AsPlainText -Force)
Invoke-WebRequest 認証が必要なページのURL -Credential $credential

しかしこの方法はもちろんお勧めできないので、スクリプトとして保存する場合は通常はパスワードを暗号化しておきます。

まず、Get-Credential ユーザー名 | Export-Clixml cred.xmlを、スクリプトを実行するコンピュータ上で、スクリプトを実行するアカウントと同じアカウントで実行します。パスワードを入力するダイアログが出るので、Webサイトにログオンする際のパスワードを入力します。すると、ユーザー名と暗号化されたパスワードがcred.xmlに出力されます。

スクリプトからは

$credential = Import-Clixml cred.xml
Invoke-WebRequest 認証が必要なページのURL -Credential $credential

のようにすると、cred.xmlからユーザー名と復号したパスワードを、そのまま-Credentialパラメータに渡すことが可能です。

なおcred.xmlに含まれる暗号化パスワードは、ConvertFrom-SecureStringコマンドレットと同様、Windows Data Protection API(DPAPI)を用いてWindowsアカウントのパスワードをキーに利用して暗号化されているので、他のユーザーが復号することはできません。

ちなみに同一スクリプトファイルに暗号化パスワードを含めておくこともできなくはないです。過去記事参照。あと本当は資格情報マネージャーを使うのがいいんですが、…略。参考:PowerShell で Windows の 資格情報マネージャー を利用する (Jenkins などでの Git Credentialなど) - tech.guitarrapc.com

セッション情報を引き継ぐ

多くのWebアプリケーションは、同一クライアントからの連続したアクセスを、セッションという単位で管理します。

サーバーはクライアント(普通はWebブラウザ)の初回アクセス時にセッションIDを含むcookieを返し、クライアントからの2回目のアクセス時に、サーバーはcookieにセッションIDが含まれているかどうかを確認し、同一クライアントからのアクセスかどうかを判断するわけです。(ざっくりした説明ですが)

WebブラウザではなくInvoke-WebRequestを使ったアクセスでも同様に、以下のようにすれば受けとったcookie等のセッション情報を次回アクセスに引き継ぐことができます。

$url = "https://ログオンが必要なサイト"
$body = @{リクエストボディ(例えばユーザー名とかパスワードとか)}
$response = Invoke-WebRequest $url -SessionVariable sv -Method POST -Body $body
Invoke-WebRequest $url -WebSession $sv

初回アクセス時に-SessionVariableパラメータに指定した変数名(sv)の変数($sv)にはWebRequestSessionオブジェクトが格納されます。この中に、サーバーから受け取ったcookie等の情報が格納されています。

次回アクセス時には、-WebSessionパラメータに、初回アクセス時に得られたWebRequestSessionオブジェクト($sv)を指定します。

さて、実際のWebアプリケーションではcookie以外にも、Formのhiddenフィールドの値などもセッション管理に用いていることがあります。その場合は、初回アクセスのレスポンスからFormに含まれるinput type="hidden"なフィールドを抽出し、次回アクセスのリクエストボディに含ませる必要が出てきます。この辺りの話は後編で述べるパースが必須になってくる(し、長くなる)ので今回は詳説しません。Invoke-WebRequestコマンドレットのリファレンスのExample2に、Facebookにログオンする例なんてのがあるので、そちらで雰囲気をつかんでください。(今でも動作するかは確認してないですが)

エラートラップ

さて、Invoke-WebRequestは、タイムアウトになった、名前解決ができなかった、ページが無かった(404エラー)等々、正常にWebページを取得できなかった場合は、System.Net.WebExceptionというエラーを出します。

コマンドレットの出すエラー(Errorストリームに出力されるErrorRecord)は、try...catchステートメントでは捕捉できない、というのが原則ですが、Invoke-WebRequestコマンドレットのエラーは一般的なコマンドレットと異なり、普通の.NETの例外(System.Net.WebException)なので、try...catchステートメントでエラートラップを行います。

とは言え、Invoke-WebRequestコマンドレットの仕様上、エラートラップをして適切な処理を行うのは非常にめんどいです。何故かというと、Invoke-WebRequestがエラーを出した時点で、HtmlWebResponseObjectオブジェクトの出力は行われないので、このオブジェクトから得られる様々な情報(レスポンス文字列、ステータスコード等々)が取得できないからです。

じゃあどうすればいいのかという話なんですけど、どうもWebExceptionオブジェクトのResponseプロパティを見るしかないようです。具体的にはこんな感じ。

try
{
    $response = Invoke-WebRequest http://存在しないページなど
}
catch [System.Net.WebException]
{
    # HTTPステータスコード取得
    $statusCode = $_.Exception.Response.StatusCode.value__

    # レスポンス文字列取得
    $stream = $_.Exception.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader $stream
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd()
}

せっかくInvoke-WebRequestコマンドレットは、生のレスポンスを利用しやすくHtmlWebResponseObjectという形で返してくれるのに、エラー発生時はその恩恵を受けることができず、泥臭い処理が必要になります。これはかなりいけてないですし、どうせここまで書かないといけないのであれば最初からWebClientクラスを使った方がいいと思います。

httpsで無効な証明書が使われている場合

(12/16追記)
Invoke-WebRequestコマンドレット(およびWebClient)では、httpsで始まるURLからもダウンロード可能ですが、サイトで用いられている証明書に問題がある場合(期限が切れている、暗号化形式に問題がある、いわゆるオレオレ証明書である等)には、「要求は中止されました。SSL/TLSセキュリティで保護されているチャネルを作成できませんでした」というエラーが出てしまいます。

これを回避するには、Invoke-WebRequestコマンドレット実行前に、

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}

という1文を記述しておきます。

ただし、証明書に問題があるということは、その通信相手が正当かどうか、通信内容が正しく秘匿されているかどうか、保証がされなくなるということですから、その点は念頭においてください。

文字化けの問題

Invoke-WebRequestコマンドレットのもう一つの悩ましい問題、それは文字コードです。実はInvoke-WebRequestコマンドレットには、Webページの文字コードを指定する方法がありません。(多分)

ではレスポンス文字列の文字コードがどのように決まるかというと、サーバーが返すレスポンスヘッダのContent-Typeフィールドで指定されているcharsetです。具体的には、$response.Headers["Content-Type"]の値が例えば"text/html; charset=UTF-8"であれば、$response.Contentの文字コードはUTF-8になります。

このときページ(HTML)を記述している文字コードと、レスポンスヘッダで指定されている文字コードが一致すれば全く問題はないのですが、異なる場合は容赦なく文字化けします。

異なる場合だけでなく、レスポンスヘッダのContent-Typeフィールドに文字コードの指定がない場合はASCIIと見なされるので、日本語のページの場合はやはり文字化けします。

この問題を回避する方法は、私はまだ見つけていません。よって文字化けが起きる場合は、諦めてWebClientを使って文字コードを指定するようにしています…。

WebClientを用いる

以上で述べてきたとおり、Invoke-WebRequestコマンドレットは、ページをさくっと取得して、さくっとパースするのには重宝するのですが、細かい所で融通が利かない印象があります。

そこで細かい処理が必要な場合(と、PowerShell 2.0環境)は、素直にWebClientクラスを用いるのがいいと思います。今回WebClientの使い方も入れようかと思いましたが、長くなったので詳しくは省略します。

基本は以下のような感じでDownloadStringメソッドを使って文字列を取得します。文字コードも指定できます。

$client = New-Object System.Net.WebClient
$client.Encoding = [System.Text.Encoding]::UTF8
$content = $client.DownloadString("http://アドレス")

なお、WebClientを用いた場合でも、Invoke-WebRequestと同等のHTMLパースを行う方法は存在するので、それは次回に。

おわりに

今回はまず、Webページから文字列データを取得する部分にフォーカスしてみました。といっても、Invoke-WebRequestの機能を全部網羅したわけではなく、使用頻度が高そうなものと個人的ハマリポイントがあるところだけです。なので詳しくはリファレンスを見て下さい。というかハマリポイントたぶんまだまだ一杯あると思います。

後編では、とってきた文字列データを「パース」して、扱いやすいデータ形式に変換する方法についてまとめようかと思います。

2015/04/14

JPPOSH(Japan PowerShell User Group)主催の、第5回PowerShell勉強会を5/9(土)に開催します。大阪での開催は3回目になります。会場はMicrosoft関西支店です。

セッションはお昼1時から4セッションあり、PowerShell大阪勉強会主催のwakaさんによる、「スクリプトの書き方」、私による「PowerShell 5.0 新機能」、ALM MVPのPosauneさんによる「C#er的Powershellの使い方」、岩城さんによる「PowerShellで行うOffice365の管理&メンテナンス」とのラインナップになっています。

今回は開発者視点の話が中心になりそうですが、基本的なスクリプトの書き方から実務への応用、そしてそろそろ全貌が見えてきた次バージョンの話まで、幅広く対応できる内容かと思います。ご興味のある方は、ぜひ、ご参加くださいませ。

今回の開催日は、アメリカで開催される大型カンファレンスである、Build 2015およびMicrosoft Igniteの直後ということもあって、私のセッションでは可能な限り両イベント開催中に新しく発表された情報を反映していこうと思っています。

先日発表になったばかりの、Windows Serverのコンテナ実行環境である、Nano Serverの詳細や、その上で動作するPowerShellの話なんかが出てくるんじゃないかなぁと思っていますが、どうでしょうね? お楽しみに。

2014/12/24

はじめに

この記事はPowerShell Advent Calendar 2014の24日目の記事です。

今回は、Windows 8から追加されたOSの機能である、「Windows 位置情報プラットフォーム」をPowerShellから呼び出して、位置情報(緯度、経度)を取得してみよう、という話になります。

Windows 位置情報プラットフォームとは

Windows 8から、「Windows 位置情報プラットフォーム」という機能が追加され、アプリケーションから現在位置情報(緯度、経度など)をAPIで取得できるようになっています。

Windows 位置情報プラットフォームでは、位置情報をGPSがあればGPSから、なければWi-Fiの位置情報あるいはIPアドレスなどから推定して取得します。すなわち、GPSがない場合でも位置情報を取得できる、いわば仮想GPSの機能がデフォルトで備わっているのがミソです。

(注:Windows 7にも「Windows センサー&ロケーションプラットフォーム」というのがありましたが、OSデフォルト機能としては仮想GPSはありませんでした。今は亡き、Geosense for Windowsというサードパーティー製アプリを追加すると仮想GPS使えたんですけどもね。あとWindows Phone?知らない子ですね…

PowerShellでWindows 位置情報プラットフォームを利用する

さて、Windows 位置情報プラットフォームをPowerShellで使うには、.NET4.0以上に含まれている、System.Device.Location名前空間配下に含まれるクラスの機能を用います。アセンブリ名としてはSystem.Deviceとなります。

以下のような関数Get-GeoCoordinateを定義します。

Add-Type -AssemblyName System.Device

function Get-GeoCoordinate
{
    param(
        [double]$Latitude,
        [double]$Longitude
    )

    if(0 -eq $Latitude -and 0 -eq $Longitude)
    {
        $watcher = New-Object System.Device.Location.GeoCoordinateWatcher
        $sourceId = "Location"
        $job = Register-ObjectEvent -InputObject $watcher -EventName PositionChanged -SourceIdentifier $sourceId
        $watcher.Start()
        $event = Wait-Event $sourceId
        $event.SourceEventArgs.Position.Location
        Remove-Event $sourceId
        Unregister-Event $sourceId
    }
    else
    {
        New-Object System.Device.Location.GeoCoordinate $Latitude,$Longitude
    }
}

関数実行前に、まずAdd-Type -AssemblyName System.Deviceを実行して必要なアセンブリをロードする必要があります。

関数本体ではまず、System.Device.Location.GeoCoordinateWatcherオブジェクトを生成します。このオブジェクトのStartメソッドを実行すると、Windows 位置情報プラットフォームにアクセスして、位置情報の変化を監視します。位置情報の変化を感知すると、PositionChangedイベントが発生し、取得した位置情報を、イベントハンドラの引数にGeoPositionChangedEventArgs<T>オブジェクトとして返します。

さて、PowerShellでは、.NETクラスのイベントを取得するには、Register-ObjectEventコマンドレットを用い、イベントを「購読」します。

イベントが発生するたびに何かの動作をする、というような場合では、Register-ObjectEvent -Action {処理内容}のようにして、イベントハンドラを記述するのが一般的です。が、今回は位置情報の変化の最初の一回(つまり、初期値の取得)さえPositionChangedイベントを捕まえればOKなので、-Actionは使用しません。

代わりにWait-Eventコマンドレットを用い、初回のイベント発生を待機するようにしています。Wait-Eventコマンドレットは、当該イベントを示すPSEventArgsオブジェクトを出力します。

PSEventArgsオブジェクトのSourceEventArgsプロパティには、当該イベントのイベントハンドラ引数の値(ここではGeoPositionChangedEventArgs<T>オブジェクト)が格納されているので、あとはそこから.Position.Locationと辿ることで、位置情報を格納したGeoCoordinateオブジェクトが取得できます。

(注:あとで知ったんですけど、GeoCoordinateWatcherクラスには、同期的に位置情報を取得するTryStartメソッドというのがあって、これを使えばイベント購読は実は不要でした…まぁいっか)

なお、関数のパラメータとしてLatitude(緯度)、Longitude(経度)を指定すると、現在位置ではなく、指定の位置を格納したGeoCoordinateオブジェクトを生成するようにしています。

Get-GeoCoordinate関数の使い方

事前にコントロール パネルの「位置情報の設定」で「Windows 位置情報プラットフォームを有効にする」にチェックを入れておきます。

あとはGet-GeoCoordinateをそのまま実行するだけです。

Latitude           : 34.799999
Longitude          : 135.350006
Altitude           : 0
HorizontalAccuracy : 32000
VerticalAccuracy   : NaN (非数値)
Speed              : NaN (非数値)
Course             : NaN (非数値)
IsUnknown          : False

このように現在位置が表示されるかと思います。といっても、緯度、経度が表示されたところでちゃんと取得できてるのかよく分からないので、以下のような簡単な関数(フィルタ)を定義しておきます。

filter Show-GoogleMap
{
    Start-Process "http://maps.google.com/maps?q=$($_.Latitude),$($_.Longitude)"
}

このフィルタを使うと、指定の緯度経度周辺の地図を、標準のWebブラウザで開いたGoogleマップ上に表示してくれます。使い方はこんな感じ。

Get-GeoCoordinate | Show-GoogleMap

現在位置が表示されましたでしょうか? 位置測定に用いたソースによってはkmオーダーでズレると思いますが、それでも何となく、自分がいる場所が表示されるのではないかと思います。

なお、先ほども書いたように、パラメータで任意の緯度、経度を指定することも可能です。この関数だけではあんまり意味を成しませんが…

Get-GeoCoordinate 35.681382 139.766084
まとめ

PowerShellでも「Windows 位置情報プラットフォーム」を使って現在位置が取れるよ、という話でした。あんまりPowerShellでSystem.Device.Locationとかを使っているサンプルを見かけないので、何かの参考になれば幸いです。あとPowerShellでのイベントの扱い方についても復習になるかと。

ところでこうやって取得した位置情報を使って、Web APIを呼び出して活用しよう、というようなネタを書くつもりだったんですが、長くなったんでまたの機会としましょう。ではでは。

2014/12/22

はじめに

この記事はPowerShell Advent Calendar 2014の22日目の記事です。

前回はMMLをパースし、音楽をBeepで再生するところまで作りました。

MMLはテキストデータなので、テキストエディタで入力しても良いのですが、どうせなら、PowerShellでエディタも作ってしまいましょう。

コード

いきなりですが、エディタの本体コードを。

function Enter-MusicEditor
{
    $keyMap=@{
        A="<G+4>"
        Z="<A4>"
        S="<A+4>"
        X="<B4>"
        C="C4"
        F="C+4"
        V="D4"
        G="D+4"
        B="E4"
        N="F4"
        J="F+4"
        M="G4"
        K="G+4"
        ","="A4"
        L="A+4"
        "."="B4"
        "/"=">C4<"
        ":"=">C+4<"
        "\"=">D4<"
        "]"=">D+4<"
        "R"="R4"
    }

    $mml=@()
    cls
    while($true)
    {  
        $k = [System.Console]::ReadKey($true)

        if($k.Key -eq [System.ConsoleKey]::Escape -or $k.Key -eq [System.ConsoleKey]::Enter)
        {
            cls
            break
        }
        elseif($k.Key -eq [System.ConsoleKey]::Backspace)
        {
            if($mml.length -eq 1)
            {
                $mml=@()
            }
            elseif($mml.length -ge 2)
            {
                $mml=$mml[0..($mml.length-2)]
            }
            cls
            [console]::write((-join $mml))
        }
        else
        {
            $key=$k.KeyChar.ToString().ToUpper()
        
            if($keyMap.Contains($key))
            {
                $mml+=$keyMap[$key]
                [console]::write($keyMap[$key])
                Invoke-Mml $keyMap[$key]
            }
        }
    }
    -join $mml
}
使い方

PowerShellコンソール上(ISEは不可)で、前回公開した、ConvertFrom-MML、Invoke-Beep、Invoke-Mmlの3つの関数をまず読み込み、続いて上記のEnter-MusicEditor関数を読み込みます。

この状態でコンソール上でEnter-MusicEditorを実行すると、MMLエディタモードに入ります。

MMLエディタモードでは、PCのキーボードを鍵盤代わりにして入力できます。

たとえば、Cキーを押下すると、「ド」の音がBeepで鳴り、コンソールに"C4"と出力されます。同様にFキー押下で「ド♯」が鳴り"C+4"と出力されます。どのキーがどの音に対応しているかは、上記コードの$keyMap変数に格納された連想配列を参照してください。

(なんとなくですけど、楽器の鍵盤の配置と合わせてあります)

Backspaceキーを押下すると、直前の入力を削除できます。

EnterキーかEscapeキーを押下すると、エディタモードを終了します。

出力したMMLは、コンソール上に表示が残るので、あとはコピーしてInvoke-Mml関数に渡してやると演奏することができます。

また、$mml=Enter-MusicEditorとしてやると、入力したMMLをそのまま変数に格納できます。この場合だとInvoke-Mml $mmlと実行すれば演奏できます。

解説

技術的には全然大したことをしてないですが、一応解説。

まず、[System.Console]::ReadKey()で入力したキーコードを判別します。引数に$trueを指定すると、入力したキー名をそのままコンソールに出力するのを抑制できます。

ReadKeyメソッドはConsoleKeyInfoオブジェクトを返します。アルファベットキーについてはKeyCharプロパティの値、特殊キーについてはConsoleKey列挙体を返すKeyプロパティの値で調べることができます。

あとは入力キーから対応する音名を連想配列$keyMapから取ってきて、その音名を[System.Console]::Write()でコンソール出力すると同時に、Invoke-Mml関数でその音をBeepとして鳴らしているだけです。

まとめ

入力も再生もできるようになったので、これはもう完全にシーケンサーですね! いや、やはり無理があるか…。

しかし、他のMMLコンパイラと併用して、ちょっと演奏しながらMML入力したいな、というときにもしかすると役に立つかもしれません。

ちょっと機能が少なすぎるんで、せめて↑で半音上げ↓で半音下げ、→で伸ばす←で短く、くらいはそのうち実装してみたいですね。

まあ、MML作成にはあんまり役に立たないかもしれませんが、PowerShellでコンソールの入出力を制御するのは、こういう単純なものなら意外と簡単にできるという、サンプルにはなるかと思います。

2014/10/12

昨日10/11のPowerShell勉強会#4にお越しいただいた方、どうもありがとうございました。スタッフ一同、これからも定期的に開催していこうと考えておりますので、ぜひともよろしくお願い致します。

私のセッション資料を公開します。わんくま横浜で行ったものとほとんど同じですが、v5関係のスライドを少しだけ修正しています。

今回は高度な関数とコマンドレットの作り方を主に取り上げていますが、要するに、PowerShellコマンドはPowerShellスクリプトでもC#でもほぼ同じようにして同じようなものが書ける、ということなのです。なので、基本を押さえればどちらにも対応可能です。

両者は状況に応じて使い分ければOKかと思います。PowerShellよりC#が得意、というならばコマンドレットを書くと良いですし、スクリプト的なお手軽なコードで書きたい場合は高度な関数、とかですね。

速度が求められる部分とか、.NETアセンブリを多用する部分だけコマンドレットにして、他を高度な関数にする、あるいは、主要部はコマンドレットにして、その他はコマンドレットのラッパー的な高度な関数を用意する、のように両者を組み合わせたモジュールもよくあります。

以下はデモ用のサンプルスクリプトです。高度な関数の雛型的なものになります。使い方はスライド本文を参照してください。

function Get-Foo
{
    [CmdletBinding()]
    param([string[]]$Name)
    end
    {
        foreach($n in $name)
        {
            $out = [pscustomobject]@{
                Name = $n
                No = 0 
            }
            # PSCustomObjectのインスタンスに型名を付ける
            $out.PSTypeNames.Insert(0, "Winscript.Foo")
            $out
        }
    }
}

function Set-Foo
{
    [CmdletBinding()]
    param(
        [parameter(ValueFromPipeLine=$true, Mandatory=$true, Position=0)]
        [PSObject[]]
        $InputObject,
        [parameter(Position=1)]
        [string]
        $Property,
        [parameter(Position=2)]
        [PSObject]
        $Value,
        [switch]
        $PassThru
    )
    process
    {
        foreach($o in $InputObject)
        {
            $o.$Property = $Value
            if($PassThru)
            {
                $o
            }
        }
    }
}

# C#によるクラス定義
Add-Type -TypeDefinition @"
using System;
namespace Winscript
{
    public class Foo2
    {
        private string _name;
        private int _no;
        public Foo2(string name)
        {
            _name = name;
            _no = 0;
        }
        public string Name
        {
            get{
              return _name;  
            }
            set{
               _name = value; 
            }
        }
        public int No
        {
            get{
              return _no;  
            }
            set{
               _no = value; 
            }
        }
    }
}
"@

function Get-Foo2
{
    [CmdletBinding()]
    param([string[]]$Name)
    end
    {
        foreach($n in $name)
        {
           New-Object Winscript.Foo2 $n
        }
    }
}

function Set-Foo2
{
    [CmdletBinding()]
    param(
        [parameter(ValueFromPipeLine=$true, Mandatory=$true, Position=0)]
        [Winscript.Foo2[]]
        $InputObject,
        [parameter(Position=1)]
        [string]
        $Property,
        [parameter(Position=2)]
        [PSObject]
        $Value,
        [switch]
        $PassThru
    )
    process
    {
        foreach($o in $InputObject)
        {
            $o.$Property = $Value
            if($PassThru)
            {
                $o
            }
        }
    }
}

以下はデモで用いたC#のコードです。コマンドレットクラスの雛型的なものになっています。ビルドの際はSDKに含まれるPowerShell関係のdllを参照設定してください(詳しくはスライド)。また使用する際は、Import-Module ビルドで生成したdllのフルパス を実行してインポートして下さい。以下の例だとGet-Baz、Set-Bazの2コマンドレットがインポートされます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Management.Automation;

namespace Winscript
{
    [Cmdlet(VerbsCommon.Get, "Baz")]
    public class GetBazCommand : Cmdlet
    {
        [Parameter(Mandatory = false, ValueFromPipeline = false, Position = 1)]
        public string[] Name { get; set; }

        protected override void ProcessRecord()
        {
            foreach (var n in Name)
            {
                WriteObject(new Baz(n));
            }
        }
    }

    [Cmdlet(VerbsCommon.Set, "Baz")]
    public class SetBazCommand : Cmdlet
    {
        [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)]
        public Baz[] InputObject { get; set; }

        [Parameter(Mandatory = true, ValueFromPipeline = false, Position = 1)]
        public string Property { get; set; }

        [Parameter(Mandatory = true, ValueFromPipeline = false, Position = 2)]
        public PSObject Value { get; set; }

        [Parameter(Mandatory = false, ValueFromPipeline = false)]
        public SwitchParameter PassThru { get; set; }

        protected override void ProcessRecord()
        {
            foreach(var o in InputObject)
            {
                if (Property == "No")
                {
                    o.No = (int)Value.BaseObject;
                }
                else if(Property == "Name")
                {
                    o.Name = (string)Value.BaseObject;
                }
                if (PassThru)
                {
                    WriteObject(o);
                }
            }
        }
    }

    public class Baz
    {
        private string _name;
        private int _no;
        public Baz(string name)
        {
            _name = name;
            _no = 0;
        }
        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
            }
        }
        public int No
        {
            get
            {
                return _no;
            }
            set
            {
                _no = value;
            }
        }
    }
}

2013/02/21

一般的な Web Programmer ならば、HTTP Status code はすべて暗記していると聞きました。

しかし、僕は初心者なので、なかなか覚えきれていないので、HTTPのステータスコードをさがすのに便利な PowerShell 関数を用意しました。Get-HttpStatusCode 関数です。インストール方法は以下のコードをprofile.ps1にコピーするなどです。

function Get-HttpStatusCode
{
    param(
        [string]$Code
    )
    begin
    {
        $enumType = [System.Net.HttpStatusCode]
        $codeNames = [enum]::GetNames($enumType)
        $codes = $codeNames |
        %{
            [pscustomobject]@{
                Code = [string][long][enum]::Parse($enumType, $_)
                Description = $_ -creplace "([a-z])([A-Z])",'$1 $2'
            }
        }
    }
    process
    {
        if(![string]::IsNullOrEmpty($code))
        {
            $codes|?{$_.Code.IndexOf($Code) -eq 0 -or $_.Description.IndexOf($Code) -eq 0}
        }
        else
        {
            $codes
        }
    }
}

使い方は以下のとおりです。

4xx なコードを列挙する。

PS> Get-HttpStatusCode 4

Code                                    Description
----                                    -----------
400                                     Bad Request
401                                     Unauthorized
402                                     Payment Required
403                                     Forbidden
404                                     Not Found
405                                     Method Not Allowed
406                                     Not Acceptable
407                                     Proxy Authentication Required
408                                     Request Timeout
409                                     Conflict
410                                     Gone
411                                     Length Required
412                                     Precondition Failed
413                                     Request Entity Too Large
414                                     Request Uri Too Long
415                                     Unsupported Media Type
416                                     Requested Range Not Satisfiable
417                                     Expectation Failed
426                                     Upgrade Required

40x なコードを列挙する

PS> Get-HttpStatusCode 40

Code                                    Description
----                                    -----------
400                                     Bad Request
401                                     Unauthorized
402                                     Payment Required
403                                     Forbidden
404                                     Not Found
405                                     Method Not Allowed
406                                     Not Acceptable
407                                     Proxy Authentication Required
408                                     Request Timeout
409                                     Conflict

500 ってなんだっけ?

PS> Get-HttpStatusCode 500

Code                                    Description
----                                    -----------
500                                     Internal Server Error

403 ってなんだっけ?

PS> Get-HttpStatusCode 403

Code                                    Description
----                                    -----------
403                                     Forbidden

Bad なんとかってなんだっけ?

PS> Get-HttpStatusCode Bad

Code                                    Description
----                                    -----------
400                                     Bad Request
502                                     Bad Gateway

とりあえず全部みるか。

PS> Get-HttpStatusCode

元ネタ:httpstatus コマンドで、HTTP のステータスコードをすばやくしらべる! - tokuhirom's blog.

ネタなんで蹴らないで…

2011/12/25

はじめに

PowerShell Advent Calendar 2011の25日目最終日の記事、そしてこれが私の記事では4回目となります。今回もバックグラウンドジョブについての話題です。今回はバックグラウンドジョブを使って並列処理をやってみようという試みです。

これまでの記事は以下になります。

2日目:バックグラウンドジョブの使い方・基本編

13日目:バックグラウンドジョブとの通信

19日目:PowerShell 3.0で追加されるバックグラウンドジョブ関係の新機能

ところでつい2日前、WMF3 CTP2 Windows PowerShell Workflow.pdfというpdfファイルが公開されました。これは19日目に書いたPS workflowについての詳しい説明(英語)です。構文だけでなくPSスクリプトとの違いやWFとの関係などが詳しく書かれています。ぜひ目を通しておくことをお勧めします。23日目のAhfさんの記事と併せて読むと理解が深まると思いますよ!

並列処理スクリプト

C#をご存知の方なら、PowerShellのバックグラウンドジョブ機能はC#4.0から使えるTaskオブジェクトとちょっと似てるかなーと思われるかもしれません。ではC#4.0でコレクションに対して並列処理でループを回すParallel.For()やParallel.Invoke()みたいなことはPowerShellでできないのか、という疑問が出てくるかと思います。

前回述べたようにPowerShell 3.0ならworkflowを使えば並列処理が可能で、for -parallelステートメントやparallelブロックでParallel.For()やParallel.Invoke()みたいなことが可能になります。しかしPowerShell 3.0がリリースされるのはまだ先ですし制限事項も多いので、なんとかPowerShell 2.0で、しかもworkflowのような制限なしで、並列処理のスクリプトは書けないものかと考えてみました。

function ParallelForEach-Object
{    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,Position=1)][scriptblock]$process,
        [scriptblock]$begin={},
        [scriptblock]$end={},
        [Parameter(ValueFromPipeline=$true)][psobject]$inputObject
    )
    begin
    {
        &$begin
        $jobs=@()
    }
    
    process
    {    
        $jobs|Receive-Job
        while(@($jobs|?{$_.State -eq "Running"}).Length -ge 5)
        {
            $jobs|Receive-Job
            start-sleep -Milliseconds 100
        }       
        
        $jobs += Start-Job $process -argumentList $inputObject
    }

    end
    {
        while(@($jobs |?{$_.State -eq "Running"}).Length -gt 0)
        {
            $jobs|Receive-Job
            start-sleep -Milliseconds 100
        }
        $jobs|Receive-Job
        $jobs|remove-job
        &$end
    }
}




$watch=new-object System.Diagnostics.Stopwatch

"ForEach-Object 開始"

$watch.Start()
1..10|ForEach-Object {
    "start: " + $_
    Start-Sleep -sec 5
    "end: " + $_
    
}
$watch.Stop()

"ForEach-Objectの場合:" + $watch.Elapsed.TotalSeconds + " sec"

$watch.Reset()

"ParallelForEach-Object 開始"

$watch.Start()
1..10|ParallelForEach-Object {
    "start: " + $args[0]
    Start-Sleep -sec 5
    "end: " + $args[0]
}
$watch.Stop()

"ParallelForEach-Objectの場合:" + $watch.Elapsed.TotalSeconds + " sec"

ParallelForEach-Object関数はパイプラインから渡されたコレクションの各要素について、並列にスクリプトブロックを実行させるものです。同等の処理をForEach-Objectを使って同期的に逐次処理した場合とかかる時間を比較しています。10個の要素があり、各要素につき5秒かかる処理なので、逐次的に処理すると当然50秒以上かかりますが、ParallelForEach-Object関数を使って並列処理させると環境にもよりますが20秒以内に完了します。

この関数では渡されたコレクション1要素に対し1つのジョブを割り当て、同時に5ジョブまで(呼び出し元を含めて同時稼働が6プロセスまで)を並列実行するようにしています。

ただこれはあくまでなんちゃって並列処理なので、並列化することで本当に処理が高速になるかどうかは環境次第かと思います。一応、うちのCore2Duo (2コアCPU)な環境だと、足し算を3万回ほどする処理を10回行う場合、逐次処理とこの関数を使った並列処理では54秒が39秒に短縮され、有意な実行時間差が出ました。

またジョブを開始するのに新しくプロセスを起動させるので、1ループあたりの実行時間がプロセス起動にかかる時間より短ければ、この関数による並列化で処理時間の短縮は見込めません。

処理の対象が複数のリモートPCである場合などは割と有効なのかなと思います。たとえば複数サーバーから別々のファイルを同時にダウンロードするときなど。

ここではParallel.For()やParallel.ForEach()相当の関数を書きましたが、Parallel.Invoke()のような関数も書けるかと思います。スクリプトブロックの配列をStart-Jobで順に走らせ、Wait-Job, Receive-Jobする感じですね。

あとここではやりませんでしたが、Start-Jobの代わりにInvoke-Commandを使い複数のリモートPCに処理を振り分ければ、なんちゃって分散処理もできるのかなあと思いました。

おわりに

実はこのスクリプトを書いたのはPS Workflowの調査前のことで、Workflowで同様のことが可能になることを知って少々愕然としたのですが、それなりに面白いスクリプトかと思ったので公開することにしました。ともあれ、これからのマルチコア、メニーコアの時代、非同期処理や並列処理はますます重要になるかと思います。管理スクリプトにおいてもこれらの概念を意識しないわけにはいかなくなるでしょう。全4回にわたってPowerShellのバックグラウンド機能を解説してきましたが、これらがあなたの非同期&並列スクリプトライフ(?)の一助になれば幸いです。

さてさて、これでPSアドベントカレンダー2011もおしまいです。楽しんでいただけたでしょうか? 私自身も自分で記事を書いていて楽しかったですし、他の方の記事を読むのも色々な発見があり、とても有意義な25日間でした。記事を書いて参加していただいた方々、そして読者の方々に厚く御礼申し上げます。これからもぜひ、PowerShellを活用し、楽しんでくださいませ。

それでは皆様、良いクリスマスをお過ごしください!


古い記事のページへ |

Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

mailto: mutaguchi at roy.hi-ho.ne.jp

Awards

Books

Twitter