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 何か変だったら()付けてみる とかでもそれ程問題にはならないかもしれません。

2016/12/20

この記事はPowerShell Advent Calendar 2016の20日目です。

はじめに

前々回はASTの概要について述べ、最後にAST.FindAllメソッドを使って、ASTから指定のASTノードを検索する方法について説明しました。

前回はASTを再帰的に検索して、木構造を視覚化してみました。

今回もASTを検索する話なのですが、静的解析機能を実装するためのAST Visitorを用いる方法について説明します。が、あらかじめお断りしておきますが、静的解析の実装までは今回はたどり着きません。静的解析ツールをどう作るかorどう作られているか、ということを雰囲気で味わっていただければと。

Visitorパターン

AST Visitorの説明をする前に、まず、Visitorパターンについて簡単に。

Visitorパターン[Wikipedia]というのは、オブジェクト指向言語におけるデザインパターンの1つで、対象オブジェクトを巡回する「訪問者」クラスを定義するものです。Visitorクラスでは、対象クラスごとに行う処理を、個別にvisitメソッドをオーバーロードさせることで定義します。共通のVisitor抽象クラスを継承することで、異なる機能を持ったVisitorクラスを作ることができます。

一方、処理対象クラスには、Visitorオブジェクトを引数に受け取る、acceptメソッドを定義します。acceptメソッドでは、引数として受け取ったVisitorオブジェクトのvisitメソッドを呼ぶことで、処理を実行させます。

なお、処理対象クラスが子要素クラスを持つ場合には、acceptメソッド内で、子要素クラスのacceptメソッドを呼ぶようにします。こうしておくことで、Visitorは処理対象を再帰的に巡回できるようになります。

このように処理対象クラスから、実際に処理を行う機能をVisitorクラスとして分離することで、処理対象クラスに手を加えることなく、Visitorクラスを追加して、処理内容を増やしたりすることが可能になります。

AST Visitorの呼び出し

Visitorパターンを念頭において、AST Visitorの呼び出し方を見ていきましょう。Ast抽象クラスには、以下の2つのVisitメソッドが定義されています。

説明に入る前に注意点。メソッド名は"Visit"となっていますが、Visitorパターンでいうところの"accept"メソッドのことです。なぜメソッド名がAcceptじゃないのかは不明ですが…。

ともかく、AstクラスのVisitメソッドは、AstVisitor抽象クラスを継承したクラスのオブジェクトか、ICustomAstVisitorインターフェースを実装したクラスのいずれかを引数に取ることで、ASTに対する処理を実施します。

AstVisitor抽象クラスを継承、もしくはICustomAstVisitorインターフェースを実装することで、ASTの種類に応じた巡回処理を行うクラスを、自分で定義していきます。

AstVisitor抽象クラス

AstVisitor抽象クラスは、Visitorとしての基本的な機能があらかじめ実装されています。具体的には既に以下の機能は用意されています。

  • ASTの種類に応じたVisitメソッドの定義
    すべての種類のASTに対応するVisitメソッド(50個以上)がVirtualメソッドとして定義されています(※)。例えば、IfStatementAstに対する処理を行うための、VisitIfStatementメソッドがあります。

    ※一般的なVisitorパターンでは、Visitメソッドを対象クラス分オーバーロードさせますが、PowerShellのAstVisitorは対象クラスに応じた別名のメソッドを定義する方式です。これも理由は分かりませんが、オーバーロードにするには多すぎるからかもしれません。

  • 子ノードの再帰的な巡回
    各Visitメソッドには、ASTの子ノードに対し、再帰的にVisitメソッドを呼ぶ仕掛けがあらかじめ備わっています。
  • ノード巡回の停止
    各VisitメソッドはAstVisitAction列挙型を返却します。以下のように返却する値によって、ノード巡回の継続、停止を制御できます。
    • Continue:ノード巡回を継続(デフォルト)
    • SkipChildren:子ノードの巡回を行わない
    • StopVisit:巡回を終了する
カスタムAstVisitorクラスを作成する

以上の基本的な機能を踏まえて、AstVisitor抽象クラスを実装したカスタムVisitorクラスを作ります。C#で書くのが一般的ですが、せっかくなのでPowerShell v5で追加された、クラス構文を使って書いてみましょう。

例えば、「利用しているコマンドのリストを取得する。ただし、コマンドのパラメータ内で別コマンドを呼び出している場合は除く。」というお題を解くことを考えます。

ASTのFindAllメソッドだと、配下に含まれるすべてのCommandAstを取得してしまうので、単純にはいきません。そこでカスタムAstVisitorクラスの出番です。

このお題を実現するVisitorクラスは以下のようになるでしょう。

using namespace System.Management.Automation.Language

class GetCommandNamesVisitor : AstVisitor
{
    [string[]]$CommandNames = @()

    [AstVisitAction]VisitCommand([CommandAst]$commandAst)
    {
        $this.CommandNames += $commandAst.CommandElements[0].Extent.Text
        return [AstVisitAction]::SkipChildren
    }
}

PowerShellのクラス構文において、Virtualメソッドのオーバーライドは、単に同名のメソッドを定義するだけですので、ここではVisitCommandメソッドをオーバーライドします。

プロパティとフィールドの区別はないので、コマンド名の一覧を格納するCommandNamesプロパティは上記のような定義になります。メソッド内でクラスメンバを参照する際には$thisを用います。

作成したGetCommandNamesVisitorクラスをインスタンス化し、解析対象スクリプトブロックのASTのVisitメソッドに引数として渡します。

$scriptBlock = {
    $files = Get-ChildItem -Path (Get-Location | Split-Path -Parent) -File
    $files | 
        Sort-Object -Property LastWriteTime -Descending | 
        Select-Object -First 5
}

$visitor = New-Object GetCommandNamesVisitor
$scriptBlock.Ast.Visit($visitor)
$visitor.CommandNames

実行すると、結果は

Get-ChildItem
Sort-Object
Select-Object

のようになるかと思います。

AstVisitorクラスの具体的な実装については、PSReadLinePowerShellEditorServicesにありますので、参考にしてみてください。

ICustomAstVisitorの実装

前項で述べた、AstVisitor抽象クラスを継承したカスタムAstVisitorクラスの場合、基本的な処理を実装する必要はないですし、目的とするASTクラスに対するVisitメソッドだけオーバーライドすればいいので、非常に簡便です。

ただ、本格的にPowerShellの構文解析を行いたい場合、ノードの巡回順だとか、その他もろもろをもっと細かく自分で実装したいケースが出てきます。

そういった場合にはICustomAstVisitorインターフェースを実装したクラスを作って対応します。ICustomAstVisitorインターフェースも、AstVisitor抽象クラス同様、各ASTクラスに応じたVisitメソッドが定義されているのですが、各VisitメソッドはAstVisitAction列挙体ではなく、object型のオブジェクトを返します。つまり、自分で好きなオブジェクトを返すように定義できるわけです。

Ast.Visit(ICustomAstVisitor)はAstVisitor抽象クラスを引数に取る場合と異なり、objectを返却するのですが、このとき返却されるのは、最初に実行されたVisitメソッドの戻り値になります。

ICustomAstVisitorはインターフェースですので、処理はすべて自分で定義しなくてはなりません(※)。ノードの再帰的探索も、必要ならもちろん自前で実装する必要があります(前回紹介した、JSON化スクリプトのような処理になるかと思います)。

※ISEだとインターフェースの実装を一発で行うリファクタリング機能はないので、今回みたく実装すべきメンバがたくさんある場合は、こんな感じのひな形を作るスクリプトを使うと良いでしょう。

今回はICustomAstVisitorインターフェースを実装したクラスの実例まではご紹介できませんでしたが、興味のある方は、PSScriptAnalyzerで用いられているので参考にしてみてください。

まとめ

PowerShellのASTについてきちんと解説している記事が英語圏を含めてもあまりないようでしたので、3回に渡って、一通りの基礎知識をまとめてみました。

普通にPowerShellを使っている分には、滅多に使うことはないと思いますが、たとえばPSScriptAnalyzerカスタムルールを自分で作る場合には、ASTの知識は必須になってきますので、必要に応じて参考にしていただければ幸いです。

2016/12/12

この記事はPowerShell Advent Calendar 2016の11日目です。遅刻してごめんなさい!

ASTとは

ASTとはAbstract Syntax Treeの略で、日本語では「抽象構文木」といいます。コードをパーサーが構文解析した結果から、言語の意味に関係のない要素(空白等)を除外し、木構造として構築したものです。

PowerShellでは3.0からASTの仕組みが取り入れられました。スクリプト実行時にはまずパーサーがスクリプトブロックからASTを生成し、コンパイラによってASTが解釈され、実行されるようになっています。

ASTを直接的に扱うのはコンパイラですが、実はPowerShellではパーサーが構築したASTを、PowerShellスクリプトから扱うことができます。

ASTの具体的な使い道としては、構文の静的解析が挙げられますが、その話は後でするとして、今回はまず、ASTの構成要素と構造を見ていきます。

ASTの構成要素

具体的には、{スクリプトブロック}.Astとして、ScriptblockオブジェクトのAstプロパティから、ScriptBlockAstオブジェクトにアクセスできます。このオブジェクトがASTのルートとなるノード(分岐点)を表します。このScriptBlockAstから、スクリプトブロック内部の構文要素が木構造として展開されていきます。

式(Expression)、文(Statement)といった構文要素は、各々対応したAstクラスが対応し、木構造における分岐点を形成します。また、分岐点の末端の葉では、当該の構文要素を構成するデータを示すオブジェクトが格納されます。

すべてのAstクラスは、Ast抽象クラス(System.Management.Automation.Language.Ast)を継承したクラスです。PowerShellでは50個程のAstクラスが存在します。各Astクラスは、抽象クラスで定義されている以下の2つのプロパティを持っています。

  • Parent
    親ノードを示すAstオブジェクトを返す
  • Extent
    当該のASTノードに含まれるコード文字列や、スクリプト全体から見たコード文字列の位置等の情報を持つ、IScriptExtentインターフェースを実装したクラスのオブジェクトを返す

また各Astクラスは、対象の構文要素に応じて、それぞれ異なったプロパティを持ちます。たとえばScriptBlockAstは以下のプロパティを持ちます。

子の分岐点を返すもの

  • UsingStatements
    Using節を表す、UsingStatementAstのコレクションを返す
  • Attributes
    スクリプトブロックに付与された属性を表す、AttributeAstのコレクションを返す
  • ParamBlock
    paramブロックを表す、ParamBlockAstを返す
  • BeginBlock、ProcessBlock、EndBlock、DynamicParamBlock
    各々、beginブロック、processブロック、endブロック、DynamicParamブロックを示すNamedBlockAstを返す

葉を返すもの

  • ScriptRequirements
    #Requires節の内容を表す、ScriptRequirementsを返す
ASTの構造

たとえば、

$scriptBlock = {
    param([int]$x,[int]$y)
    end
    {
        $out = $x + $y
        $out | Write-Host -ForegroundColor Red
    }
}

という、二つの整数値の和を赤字で表示するというスクリプトブロックならば、以下のようなASTが構築されます。(一部分岐点、葉は省略しています。また、分岐点のASTクラス名は、末尾の"Ast"を省略表記しています。)

PowerShell_AST

このスクリプトブロックのASTから、例えば「Red」というパラメータ値を表す、StringConstantExpressionAstまで辿るには、

$scriptBlock.Ast.EndBlock.Statements[1].PipelineElements[1].CommandElements[2]
StringConstantType : BareWord
Value              : Red
StaticType         : System.String
Extent             : Red
Parent             : Write-Host -ForegroundColor Red

のようにします。

基本的なASTの構造が頭に入っていれば、タブ補完を併用することで比較的簡単に目的のノードまで辿れますが、ASTノードの子に対し、ノード検索をかける方法もあります。

例えば、すべてのVariableExpressionAstを列挙するには、

$scriptBlock.Ast.FindAll({
    param($ast)
    $ast -is [System.Management.Automation.Language.VariableExpressionAst]
}, $true)

のように、FindAllメソッドを用います。

AST編はあと何回か続く予定です。

2016/01/03

PowerShellのパイプライン処理

まず、PowerShellのパイプライン処理について軽くおさらいします。

例えば、@、A、Bをそれぞれ何らかのコマンドとしたとき、

@|A|B

というパイプラインがあったら、処理の流れは、

@begin→Abegin→Bbegin→(@process→Aprocess→Bprocess→)×n→@end→Aend→Bend

の順に実行されます。(processブロックで「1入力に対し1出力する」場合以外は必ずしもこうならないですが)

さて、AかBのprocessブロック実行中に、何らかの条件を満たした時、パイプラインのprocessの後続処理を打ち切りたい場合はどうすれば良いでしょうか。

まずはbreakを使った駄目な例

ネットでよく見かける以下のようなコード、すなわち「パイプラインはbreakで処理を打ち切ることができる」というのは実は正しくないのです。

function Select-WhileTest
{
    [CmdletBinding()]             
    param (             
        [parameter(ValueFromPipeline=$true)] 
        [psobject[]]$InputObject,
        [parameter(Position=0)]           
        [scriptblock]$Predicate
    )

    process
    {
        if(!(&$Predicate))
        {
            break
        }
        $InputObject
    }
}

このコードはv2までではそもそも正しく動作しませんが、v3以降では条件によっては正しく動作しているように見えるのが、誤解の元なのかと思います。(というか私も誤解してました。)

例えば、

$result = "初期値"
$result = &{end{foreach($i in (1..5)){$i}}} | Select-WhileTest {$_ -lt 3}
Write-Host "`$resultは $result です。"

のようにすると、

$resultは 1 2 です。

のように、想定した通りの結果が得られます。このように、上流のスクリプトブロックのendブロック内にforeachなどループブロックが存在し、そのループブロック内で下流に値を出力している場合はうまくいきます。(ちなみに、スクリプトブロック直下に記述するのとendブロック内に記述するのは等価。)

しかし、上流にループブロックがない場合、例えば

$result = "初期値"
$result = 1..5 | Select-WhileTest {$_ -lt 3}
Write-Host "`$resultは $result です。"

とすると、コンソールに1と2が改行区切りで表示されますが、ホストに表示されるだけで$resultには値は格納されません。そしてスクリプト化して実行した場合は、Write-Hostが実行されることすらなく、スクリプトが終了してしまいます。

breakだとなぜうまくいかないのか

結局どういうことかというと、パイプライン下流のbreakは、パイプラインを打ち切る処理をするのではなく、単に一つ上流のブロックをbreakする処理に過ぎないのです。

パイプライン上流にループブロックがある場合は、そのループブロックをbreakしますが、それ以外の場合はスクリプトのbegin, process, endのいずれかのブロックがbreakされてしまい、結果としてスクリプトが終了してしまうわけですね。

そして、このSelect-WhileTest関数だと大丈夫ですが、processブロックの中にループブロックを記述し、その中でbreakを書くのは当然ダメです。単にそのループを抜けるだけなので、上流の出力は止まってくれません。

breakではなくcontinueを使う場合も基本は同じ結果です。しかもcontinueは所詮、その名の通り継続処理なので、上流に以下のような無限リストがあると無限ループに陥ってしまいます。

&{begin{$i = 0} process{while($true){$i++; $i}}}|Select-WhileTest {$_ -lt 3}

breakの代わりに、

throw (New-Object System.Management.Automation.PipelineStoppedException)

を実行する方法も見かけますが、これはループブロックがあっても強制的にスクリプトが終了するので余計ダメです。try...catchでエラートラップすればスクリプトの終了は回避できますが、「パイプラインが正常終了せずエラーを出した」扱いであることには代わりないので、やはり出力を変数に格納することができません。

ダミーループを用いる、取りあえずの解決策

前述のbreakを使った方法の問題点のうち、上流にループブロックがないとスクリプトが終了してしまい、出力を変数に代入することもできない問題は、とりあえず解決する方法があります。

以下のように、呼び出す時にパイプライン全体をダミーのループブロックでラップすれば良いのです。

$result = "初期値"
$result = do{
    1..5 | Select-WhileTest {$_ -lt 3}
}until($true)
Write-Host "`$resultは $result です。"

このようにしておけば、breakはパイプラインの外側のdo...untilを抜ける効果になるので、スクリプトが終了する心配も、値を出力しない問題も起こりません。

元々、パイプライン上流にループブロックが存在する場合でも、単にdoループ内の処理が1回走るだけなので、特に問題は起きません。1回だけ処理を実行するダミーループなら、for($i=0; $i -lt 1; $i++){}とかでも良いです。

ただ…この記述を美しいと思う人は多分いないですね。事情を知らないと意味不明ですし。そして、breakを記述する側の関数には、前述の通りのループブロック内では値を出力できないという制限は残ったままになります。

やはりbreakでパイプラインを打ち切るのは、本来想定された動作かと言われるとかなり微妙なところだと思います。(v3で一応動くようになったとはいえ)

この方法についての参考記事:Cancelling a Pipeline - Dreaming in PowerShell - PowerShell.com ? PowerShell Scripts, Tips, Forums, and Resources (ただしv2準拠の内容であることに注意)

ところで、Select-Object -Firstは…

さて、話は変わって、PowerShell 3.0からはSelect-Object -First の処理が変わったことについては、ご存知の方も多いかと思います。

具体的には、v2までは単にパイプライン処理をすべて終了してから、最初のn件を抽出する処理であったn件のパイプライン出力がされた後は、入力をすべてフィルタし出力に流さなくなる動作であったところが、v3からはn件のパイプライン出力がされた時点で、パイプラインの処理を打ち切るようになりました。(1/5修正)

つまり、

$result = 1..5| &{process{Write-Host "Process:$_"; $_}}| Select-Object -First 2
Write-Host "`$resultは $result です。"

というスクリプトは、v2までは

Process:1
Process:2
Process:3
Process:4
Process:5
$resultは 1 2 です。

のようにパイプライン出力は指示通り2件であるものの、上流の処理は結局、全部実行されてしまっています。

一方v3以降では、

Process:1
Process:2
$resultは 1 2 です。

のように、きちんと上流の処理を打ち切ってくれています。

つまり、ここまで述べてきたパイプライン処理の打ち切りは、実はv3以降のSelect-Object -Firstでは実現できているということです。これと同じことを我々も自作関数の中でやりたいわけです。

ではSelect-Object -Firstは具体的にどういう処理をしているかというと、StopUpstreamCommandsExceptionという例外をthrowすることでパイプライン処理の打ち切りを実現しています。この例外はまさに名前の通り、パイプライン上流の処理を中止するための例外です。この例外を自作関数でthrowしてやればうまくいきそうです。

しかし、この例外は非publicな例外クラスであることから、New-Objectコマンドレットなどでインスタンス化することはできません。リフレクションを駆使する必要がでてきます。
参考:PowerShell 3.0からはじめるTakeWhile - めらんこーど地階

(1/5追記)参考2:パイプラインの処理を途中で打ち切る方法のPowerShell版 - tech.guitarrapc.cóm(Add-TypeでC#経由でリフレクションしてます。)

頑張ればできなくはないですが、もっと楽な方法はないものか…と思ってあきらめかけたところ、いい方法を思いついたので紹介します。

Select-Object -Firstを利用する方法

Select-Object -Firstでできることが我々には(簡単には)できない。ならばどうするか。Select-Object -Firstを使えばいいじゃない。という発想です。

function Select-While
{
    [CmdletBinding()]             
    param (             
        [parameter(ValueFromPipeline=$true)] 
        [psobject[]]$InputObject,
        [parameter(Position=0)]           
        [scriptblock]$Predicate
    )

    begin
    {
        $steppablePipeline = {Select-Object -First 1}.GetSteppablePipeline()
        $steppablePipeline.Begin($true)
    }

    process
    {
        if(!(&$Predicate))
        {
            $steppablePipeline.Process($InputObject)
        }
        $InputObject
    }

    end
    {
        $steppablePipeline.End()
    }
}

scriptblockにはGetSteppablePipelineというメソッドが存在し、このメソッドによりSteppablePipelineオブジェクトが取得できます。これは何かというと、要は「スクリプトブロック内のbegin, process, endを個別に実行する」ための機能です。
参考:PowerShell: ◆パイプライン入力・パラメータ入力対応のGridView出力関数を作る(私自身も以前この記事で知りました。)

{Select-Object -First 1}というスクリプトブロックは、1回目に実行するprocessブロック内でStopUpstreamCommandsExceptionを出してくれます。

よって、自作関数のprocessブロック内のパイプライン処理を打ち切りたい箇所で、SteppablePipelineオブジェクトのProcessメソッドを使うことで、{Select-Object -First 1}のprocessブロックの処理を呼び出してやればいいわけです。

このようにして作成したSelect-While関数を以下のように実行してみると、

# 上流にループあり
$result1 = &{end{foreach($i in (1..5)){$i}}} | Select-WhileTest {$_ -lt 2}
Write-Host "`$result1は $result1 です。"

# 上流にループなし
$result2 =  1..5 | Select-While {$_ -lt 3}
Write-Host "`$result2は $result2 です。"

# 上流に無限リスト
$result3 = &{begin{$i = 0} process{while($true){$i++; $i}}} | Select-While {$_ -lt 4}
Write-Host "`$result3は $result3 です。"

結果は

$result1は 1 です。
$result2は 1 2 です。
$result3は 1 2 3 です。

となり、少なくとも今まで述べてきた諸問題はすべて解消していることが分かると思います。

このSelect-While関数は、スクリプトブロックで指定した条件を満たさなくなったときに、パイプライン処理を打ち切ってくれるものですが、この「Steppable Select -First 方式」を使えば他の自作関数でも、割と気楽に呼べるんじゃないかなと思います。ループブロック内で呼び出すことももちろん可能です。

ただし問題点はある

これはSelect-Object -FirstというかStopUpstreamCommandsExceptionあるいはPowerShellのパイプライン機構の仕様に由来する問題であると思われるので、どうにもならないことではあるんですが、一点だけ注意事項があります。

$result = 1..5| &{
    begin
    {
        Write-Host "Begin"
    }
    process
    {
        Write-Host "Process:$_"
        $_
    }
    end
    {
        Write-Host "End"
    }
}| Select-While {$_ -lt 2}

Write-Host "`$resultは $result です。"

これの結果は

Begin
Process:1
Process:2
$resultは 1 です。

となり、なんとendブロックが実行されていません。Select-While {$_ -lt 2} の代わりに Select-Object -Firstを使っても、同様にendは実行されません。

つまり、StopUpstreamCommandsExceptionというのはパイプライン処理を打ち切って、そこまでの出力値を正しくパイプライン出力として出してくれますが、やってくれるのはそこまでで、最後のendブロック処理はしてくれません。

これは十分注意が必要な点で、自作関数内でbeginブロックで確保したリソースをprocessブロックで利用して、endブロックで解放する…という、いかにも書いてしまいそうなパターンは、実はNGなんですね。何も上のようにマニアックなことをしなくても、単に下流でSelect-Object -Firstを使うだけでアウトです。

じゃあ、リソースの取り回しはどうするのが良いの、って話もありますが、それはまたの機会にしましょう。

(1/5追記)あえとすさんの記事が参考になります。:パイプライン処理の後始末をしよう - 鷲ノ巣 ただ、この方法ではパイプライン下流でthrowされた場合はトラップできないぽいですね。コマンドレットクラスの場合はIDisposable実装で良さそうです。

ここからは私見ですが、StopUpstreamCommandsExceptionが後付けかつ非パブリックなところとか、パイプラインを合法的に脱出するステートメントが今に至るまで用意されていないところとか、パイプラインを何とかして途中で打ち切っても、endは実行されないところとかを見ていると、そもそもPowerShellではパイプライン処理の中断というのは、あまり想定してない操作なのかなぁ、という気がしてきています。

上記のような裏技を使って回避するのも一案ではあるとは思いますが、そもそも「パイプライン処理の中断はイレギュラー」と考えて、そういう処理は避けて、必要に応じて別のアプローチを取ることも考えた方がいいのかもしれません。

2015/12/15

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

はじめに

前々回前回は、PowerShellによるWebスクレイピングの具体的手法についてまとめました。ただ、スクレイピングはあくまで最後の手段であり、Webから何らかの文字列情報を取得するには、Web APIを用いるのが本道かと思います。

今回はPowerShellでWeb APIを用いるお話です。

Web APIとは

Web APIというのは、その名の通り、プログラムからWeb上のデータを取得したり、何らかのサービスの機能を実行したりするための、呼び出し方式を定めた規約です。

Web APIでは、HTTPリクエストに呼び出したい機能の内容を指定し、結果をHTTPレスポンスとして受け取るというのが一連の流れになります。

Web APIの主な実装方式としてはSOAPとRESTがありますが、このうち、XMLでリクエストを組み立てるSOAPは最近は廃れてきた感じです。

(PowerShellではSOAP APIはNew-WebServiceProxyコマンドレットで対応しています。が、今回は略。参考:PowerShell: ◆空港の場所と天気を調べる(New-WebServiceProxy)

最近はWeb APIといえばREST(REpresentational State Transfer) APIを指すことが殆どです。REST APIでは操作の対象となるリソース=URI(エンドポイントという)、呼び出し方式=HTTPメソッド(GET:データの取得, POST:データの作成, PUT:データの更新, DELETE:データの削除)、操作に対するパラメータ=クエリストリング(GETの場合)もしくはリクエストボディ(POSTの場合)、結果の返却=HTTPレスポンス(JSON、XML等)となるのが基本です。

また、RESTの呼び出しは基本的にステートレスなものとなります。要はセッション情報を持たない≒cookieを使わない、ってことです。

PowerShellではREST APIを簡便に利用するためにInvoke-RestMethodコマンドレットが用意されています。(ただしPowerShell 3.0から)

Invoke-RestMethodコマンドレットのパラメータ指定

Invoke-RestMethodコマンドレットのパラメータについては、実は前々回に取り上げたInvoke-WebRequestコマンドレットと同じです(IEのパーサーを使うことはないので、-UseBasicParsingも無いですが)。ただしREST APIの形式は前述の通りなので、利用するパラメータは限られてきます。具体的には

データ取得の場合

$response = Invoke-RestMethod -Uri エンドポイント(パラメータを含む) -Method GET

データ作成、更新の場合

$response = 
 Invoke-RestMethod -Uri エンドポイント -Method POST -Body パラメータ(連想配列あるいはJSONやXML等)

となるかと思います。

その他にOAuth等の認証情報を指定する場合は、-Headers @{Authorization="認証情報"}のような指定も必要になることがあります。

Invoke-RestMethodコマンドレットのレスポンス

Invoke-RestMethodコマンドレットがInvoke-WebRequestコマンドレットと異なる最大のポイントは、レスポンス文字列の種類によって、自動的に出力オブジェクトの型が切り替わるところです。

私の調べた限りでは以下のような対応になっているようです。

レスポンス文字列の種類 出力型
XML XmlDocument
RSS/ATOM XmlElement
JSON PSCustomObject
プレーンテキスト string
利用の具体例
AED検索

Microsoft MVPのはつねさんが公開されている、AED検索はREST APIでAEDの所在地情報を検索し、JSONで結果を得ることができます。

例えば兵庫県芦屋市のAED一覧を取得するには、

$response = Invoke-RestMethod https://aed.azure-mobile.net/api/aedinfo/兵庫県/芦屋市/
$response | Format-Table Latitude, Longitude, LocationName,
    @{L = "Address"; E = {
        "$($_.Perfecture) $($_.City) $($_.AddressArea)"
    }} -AutoSize

のようにします。

ここで$responseには、JSON形式のデータをパースしてPSCustomObject化したデータが格納されるので、あとはFormat-Tableコマンドレットで見やすい形で出力してあげれば良いでしょう。

結果はこんな感じです。

image

AED検索APIと、去年のアドベントカレンダーで紹介した、Windows 位置情報プラットフォームを用いて現在位置を取得するGet-GeoCoordinate関数を併用して、「現在位置の最寄りにあるAEDをGoogle MAP上で表示する」なんてこともできます。

$location = Get-GeoCoordinate
$response = Invoke-RestMethod "https://aed.azure-mobile.net/api/NearAED?lat=$($location.Latitude)&lng=$($location.Longitude)"
Start-Process "http://maps.google.com/maps?q=$($response.Latitude),$($response.Longitude)"

ここではREST APIにQueryStringでパラメータ(経度、緯度)情報を渡しているところと、レスポンスから生成されたオブジェクトのプロパティ値をマップ表示の際のパラメータとして利用しているところに注目してください。

RSS取得

RSSやATOMもREST APIの一種と考えて良いと思います。

ここではこのブログのRSSを取得する例を示します。

$response = Invoke-RestMethod http://winscript.jp/powershell/rss2/
$response | select @{L = "Title"; E = "title"},
    @{L = "Url"; E = "link"},
    @{L = "PublishDate"; E = {[DateTime]::Parse($_.pubDate)}},
    @{L = "Description"; E = {
        ($_.description -replace "<.+?>").
        PadRight(50).Substring(0,50).TrimEnd() + "..."
    }}|
    Format-List

Descriptionの加工がやや適当(HTMLタグっぽいところを削除して50文字に切り詰めてるだけ)ですが、少し見やすくしています。結果は以下のように表示されます。

image

RSSの結果は、1エントリがXMLElement型のオブジェクトとして出力されるので、データの取扱いが比較的楽だと思います。

レスポンスがXMLなREST APIの良い例がなかったので省略してますが、基本的には前回取り上げた、XHTMLをXMLとしてパースする方法と同じやり方です。ただInvoke-RestMethodの場合は[xml]型アクセラレータによる変換は不要で、いきなりXmlDocumentオブジェクトが得られます。

現状の問題点

レスポンスがコールバック関数つきのJSONP形式であるとかで、JSON、XML、RSS/ATOMのいずれの形式にも適合しない場合はプレーンテキストとして出力されてしまいます。

その場合は、出力文字列を適宜加工した後に、[xml]型アクセラレータや、ConvertFrom-Jsonコマンドレット等により手動でオブジェクト化するようにしてください。もっとも、その場合は敢えてInvoke-RestMethodを使わずInvoke-WebRequestで充分ですが。

あとWeb APIというのは大抵(特にPOSTの場合)、認証を要するのですが、最近よくあるのはTwitter等でもおなじみのOAuth認証です。ところがOAuth認証は結構めんどくさい処理で、何らかのライブラリを使わないとしんどいです。残念ながらPowerShellの標準コマンドレットには存在しないので、自前で頑張って書くか、既存のライブラリやコマンドを利用することになるかと思います。

今回そこまで説明できませんでしたが、また機会があれば。

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/11/09

前編(前々回)中編(前回)の続きです。

分かち書きとは

中編で作ったGet-JpYomi関数は、JapanesePhoneticAnalyzerクラスの読み仮名取得機能にフォーカスを当てたラッパー関数でした。

今回は、JapanesePhoneticAnalyzerクラスの最大の使用目的と思われる、「分かち書き」を目的とした関数を作成します。分かち書きとは、文章を文節単位で分割することと考えて頂いて良いかと思います。

前々回作ったGet-JpWordは単語単位の分割を行うものでしたが、読みやすさや発音のしやすさを目的として文章を分割表記する場合は、単語単位では細かすぎると言えます。

よって単語単位ではなく、文を意味のあるまとまりとして区切ることのできる最小の単位である、文節単位で分割する方法を考えてみます。

Split-JpText関数

前編で作ったGet-JpWord関数をラップし、分かち書きに特化した関数Split-JpTextを作成しました。まずは以下にコードを示します。

function Split-JpText
{
    param(
        [parameter(ValueFromPipeline=$true)]
        [PSObject[]]
        $InputObject,

        [ValidateSet("Text", "Yomi", "Detail")]
        [string]
        $Format = "Text",

        [ValidateSet("ByWord", "ByPhrase")]
        [string]
        $SplitMode = "ByPhrase",

        [string]
        $Separator = " ",

        [switch]
        $ToArray
    )

    begin
    {
        if($Format -eq "Detail"){$ToArray = $true}
    }

    process
    {
        foreach($o in $InputObject)
        {
            $o.ToString() | Get-JpWord | 
            foreach -Begin {
                $phrases = @()
                $phrase = $null
            } -Process {
                if($_.IsPhraseStart)
                {
                    if($phrase){$phrases += $phrase}
                    $phrase = New-Object psobject |
                        Add-Member -MemberType ScriptProperty -Name Text -Value {
                            -join $this.Words.DisplayText} -PassThru |
                        Add-Member -MemberType ScriptProperty -Name Yomi -Value {
                            -join $this.Words.YomiText} -PassThru |
                        Add-Member -MemberType NoteProperty -Name Words -Value @() -PassThru
                }
                $phrase.Words += $_
            } -End {
                if($phrase){$phrases += $phrase}

                if($SplitMode -eq "ByPhrase")
                {
                    $out = switch($Format)
                    {
                        "Text"   {$phrases.Text}
                        "Yomi"   {$phrases.Yomi}
                        "Detail" {$phrases}
                    }
                }
                else
                {
                    $out = switch($Format)
                    {
                        "Text"   {$phrases.Words.DisplayText}
                        "Yomi"   {$phrases.Words.YomiText}
                        "Detail" {$phrases.Words}
                    }
                }

                if($ToArray)
                {
                    $out
                }
                else
                {
                    $out -join $Separator
                }
            }
        }
    }
}
パラメータの説明
パラメータ名 説明
InputObject 任意の型 入力テキスト。文字列以外の型の場合は文字列に変換して評価される。パイプライン入力可能。
Format string Text(デフォルト):文字列のみ出力する。
Yomi:文字列ではなく読みを出力する。
Detail:文節の文字列、読み、各文節に含まれる単語の配列を含んだオブジェクトの配列を出力する。(SplitMode=ByPhraseの時のみ)
SplitMode string ByPhrase(デフォルト):文を文節単位で分割する。
ByWord:文を単語単位で分割する。
Separator string 分割文字を指定。デフォルトは" "(半角スペース)。(Format=DetailもしくはToArray指定時には無効)
ToArray switch 指定すると、単一の文字列ではなく、文字列の配列を出力する。
使用法
  • 分かち書き(文節)
    例:Split-JpText "今日はいい天気ですね。"
    出力:今日は いい 天気ですね 。
  • 分割文字指定
    例:Split-JpText "今日はいい天気ですね。" -Separator /
    出力:今日は/いい/天気ですね/。
  • 分かち書き(単語)
    例:Split-JpText "今日はいい天気ですね。" -SplitMode ByWord
    出力:今日 は いい 天気 です ね 。
  • 文節単位で読み仮名を表示
    例: Split-JpText "今日はいい天気ですね。" -Separator / -Format Yomi
    出力:きょうは/いい/てんきですね/。
  • 分かち書きした文節を文字列配列として変数に格納
    例:$phrases = Split-JpText "今日はいい天気ですね。" -ToArray
解説

ちょっと長めの関数ですが、ポイントはJapanesePhoneticAnalyzerクラスのGetWordsメソッドが返すJapanesePhonemeオブジェクトのIsPhraseStartプロパティです。

IsPhraseStartプロパティは、当該単語(Phoneme)が文節(Phrase)の開始部分にあたる単語であればTrueを返します。すなわち、JapanesePhonemeコレクションを文頭から文末まで列挙していったとき、IsPhraseStartプロパティがFalseからTrueに変わる部分が文節の境界になるわけです。

Split-JpText関数では、単語を列挙していき、文頭もしくは文節の境界に遭遇すると、文節に含まれる文字列(Textプロパティ)とその読み(Yomiプロパティ)と単語の配列(Wordsプロパティ)を格納するオブジェクトを新たに作成し、$phrase変数に代入します。一方で$phrase変数に元々入っていたオブジェクトは、$phrases配列に追加します。

$phraseオブジェクトのWordsプロパティには、列挙中の単語を都度、追加していきます。

なお、$phraseオブジェクトのTextプロパティとYomiプロパティはスクリプトプロパティとして定義しておき、必要時に値を取得するようにしてあります。

まとめ

3回に渡って、JapanesePhoneticAnalyzerクラスの使用法を具体的なラッパー関数を作成して紹介しました。

個人的には、PowerShellからなら中編で挙げた、読みの取得が一番使いでがあるかな?と思いました。今回取り上げた分かち書きは、意外と応用例が思いつきませんでした。

前編のGet-JpWord関数を使って、何らかの文書群の単語リストをあらかじめインデックスとして出力しておき、単語検索コマンドを実装するのも面白そうですね。

ただ、残念ながら品詞情報が取れないので、JapanesePhoneticAnalyzerをmecabとかの形態素解析エンジンの代替にするのはちょっと厳しいかもしれないです。まあ、標準機能のみでちょっとしたものを作れるのは大きいかと思います。何か日本語の文章を解析する必要があるときには使ってみてはいかがでしょうか。

2015/10/17

はじめに

がりっち氏がWindows10 UWPに日本語解析のAPIが備わっていた件 | garicchi.com というエントリを上げていました。

実はWin10に限らず、Win8.1 / Server 2012R2以降であれば、Windows ランタイム(WinRT)のWindows.Globalization名前空間に含まれるJapanesePhoneticAnalyzerクラスを用いた形態素解析ができます。

形態素解析とは要するに文字列を単語(正確には形態素という、文字列の最小構成要素)ごとに分割し、それぞれの単語の品詞を判別する処理になります。(JapanesePhoneticAnalyzerクラスだと分割までで、品詞の情報は取得できない?ようですが…)

またJapanesePhoneticAnalyzerでは分割した単語の読み仮名を取得することができます。

WinRTならPowerShellからも使えるんじゃないかなーと思ってやったらできたので、紹介します。

WinRTのPowerShellからの利用法

WinRTについての説明は他サイト様に譲りますが、要はWindowsストアアプリやUWP(ユニバーサルWindowsプラットフォーム)アプリを動作させる実行環境とAPI群です。

じゃあデスクトップアプリであるPowerShellは関係ないのかというとそうではなくて、例えばストアアプリのサイドローディングを行うAppxモジュールというものがあります。

つまりWinRTは従来のデスクトップアプリからの相互運用もできるようになっています。(すべてのコンポーネントではない)

このあたりの話は、荒井さんの記事が参考になるかと思います。:特集:デスクトップでもWinRT活用:開発者が知っておくべき、ライブラリとしてのWindowsランタイム (1/5) - @IT

PowerShellからWinRTを利用するには.NET Frameworkに含まれるクラスを利用するのと基本は同じです。

ただし注意点としては、クラス名を指定する時は、クラスの「アセンブリ修飾名」を指定する必要があります。

今回の例ではJapanesePhoneticAnalyzerクラスを使いますが、アセンブリ修飾名はWindows.Globalization.JapanesePhoneticAnalyzer, Windows.Globalization, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime

となります。

このうち必須となるのは

Windows.Globalization.JapanesePhoneticAnalyzer:クラスの完全修飾名
Windows.Globalization:クラスの含まれる名前空間
ContentType=WindowsRuntime:WinRTのコンポーネントであること

の3つだけのようです。

つまり、PowerShellからは

[Windows.Globalization.JapanesePhoneticAnalyzer, Windows.Globalization, ContentType=WindowsRuntime]

とすればクラスを参照することができます。

ちなみに任意の型のアセンブリ修飾名を知るには、[型名].AssemblyQualifiedName のようにすればOKです。

なお、WinRTにはPowerShellからも利用価値の高いクラスが他にも色々あるようです。ぎたぱそ氏が認証系のクラスについて書かれていますので、参考にしてみてください。:PowerShell も Windows Store Apps 同様に Windows.Security.Credentials namespace を使って認証情報を管理できるようにしてみる - tech.guitarrapc.com

単語を分割する

早速、形態素解析をやってみましょう。具体的にはJapanesePhoneticAnalyzerのGetWordsメソッドを呼び出すだけです。

すべての基本になるので軽く関数としてラップしておきます。

function Get-JpWord
{
    param(
        [parameter(ValueFromPipeline=$true)]
        [ValidateLength(1,99)]
        [string[]]
        $Text,
        
        [switch]
        $MonoRuby
    )
    process
    {
        foreach($t in $Text)
        {
            [Windows.Globalization.JapanesePhoneticAnalyzer, Windows.Globalization, ContentType=WindowsRuntime]::GetWords($t, $MonoRuby)
        }
    }
}

GetWordsメソッドはスタティックメソッドなので、::演算子で呼び出します。戻り値はIReadOnlyList<JapanesePhoneme>というコレクションです。

GetWordsメソッドの第2引数にTrueを指定すると、漢字の含まれた単語をルビの振れる最小単位にまで分割する、Mono Rubyモードが有効になります。

なお、GetWordsメソッドはどうも文字数制限があるようです。だいたい100文字を超えると何も出力しない感じです。この制限値はリファレンスに書いてないようなので詳細不明ですが、一応関数では99文字までという制限を入れておきました。

例えば、Get-JpWord "最近急に寒くなってきました。" のようにすると結果は以下のように表示されます。

DisplayText          IsPhraseStart YomiText
-----------          ------------- --------
最近                          True さいきん
急に                          True きゅうに
寒くな                        True さむくな
って                         False って
き                            True き
ました                       False ました
。                            True 。

出力されるJapanesePhonemeオブジェクトは3つのプロパティを持ちます。

DisplayText 分割された単語
IsPhraseStart 単語が文節の開始であるかどうか
YomiText 単語の読み仮名

このように、入力した文章を単語単位に分割し、それぞれの単語の読み仮名を取得することができます。ただこれだけだと、「で、どうしろと」という感じなので、この出力結果を利用する、より実用的な関数を書いていきましょう。

長くなったので次回に続く。

2014/12/11

MMLとは

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

突然ですが、MMLってご存知ですか? MMLとはMusic Macro Languageの略で、その名の通り音楽演奏データを記述するための言語です。BASICのPLAY文で使うあれです。MIDIファイルに変換するコンパイラもありましたよね。

たとえば、「ねこふんじゃった」の最初の部分をMMLで書くとこんな感じです。MMLはいろんな記法があるのですが、よくあるのはこういうのです。

e-d-<g-4>g-rg-4e-d-<g-4>g-rg-4e-d-<g-4>g-4<e-4>g-4<d-4>frf4
e-d-<d-4>frf4e-d-<d-4>frf4e-d-<d-4>f4<e-4>f4<g-4>g-rg-4

ドの音をc、レの音をd、のように以下シの音をbまでアルファベット音階で割り当てます。半音上げる(シャープ)場合は音名のあとに+、半音下げる(フラット)場合は-を付与します。音の長さは4分音符なら4、8分音符なら8と書きます。付点は数字のあとに.を付けます。数字省略時は8分音符と解釈するのが普通のようです。オクターブを上げる場合は>、下げる場合は<と書きます(コンパイラによっては逆の解釈をする)。休符はrです。

本物のMMLでは、音色の指定だとかトラックの割り当てだとか、もっとたくさん命令があるんですが、基本はだいたいこんなもんです。

PowerShellで音を鳴らすには

PowerShellで音を鳴らすには、.NETのSystem.Console.Beepメソッドを使うのが一番手っ取り早いです。ビープ音ですね。比較的古いWindowsではハードウェアのビープスピーカーから、比較的新しいWindowsではサウンドデバイス上で鳴ります。今回のMMLシーケンサーも結局はBeepメソッドです。

そしてぎたぱそ先生がずっと以前に書いておられます。なので今回のはまあ、MMLシーケンサー部分を除けば二番煎じなんですけどもね。(そしてSmallBasicLibraryを使えば実はMML演奏もできるので、車輪の再発明でもあるんですが)

ちなみにwavやmp3なんかを鳴らすには、Windows Media PlayerをCOM経由で実行する方法や.NET(WPF)のSystem.Windows.Media.MediaPlayerクラスを使う方法などがあります。

音程の決め方

Beepメソッドでは第1引数にビープ音の周波数をHzで、第2引数に再生時間をミリ秒で指定します。つまりは音程(音の高さ)はHzで指定する必要があります。ここでドの音は何ヘルツで、みたいな対応表をどこかから探してきてそのデータを使ってもいいんですが、今回はせっかくなので、PowerShellで計算して求めてみます。

まず、ある音程の一つ上のオクターブの音程の間の周波数比は、1:2です。

ピアノ等の鍵盤楽器が近くにあれば、それを見てもらえば分かると思いますが、1オクターブには12の音程が含まれています。1オクターブの周波数比を12個均等に分割します(この周波数の決め方を平均律といいます)。つまり、隣り合う音程の周波数比は、1:12√2となるわけです。

さて、音程の周波数比はこれでわかりました。あとは基準となる音の周波数が分かれば、全ての音の周波数が計算できるわけです。時代や地域、演奏する楽器等によって微妙に違ってきますが、現代日本においては普通は、A(ラ)=440Hzが基準周波数です。

あとは単純にかけ算して周波数を求めて連想配列に入れておくだけです。コードにすればこんな感じですね。

$baseFrequency = 440.0
$pitches = @{
            "c" = $baseFrequency * [math]::Pow(2, 3/12)
            "d" = $baseFrequency * [math]::Pow(2, 5/12)
...
}

あと、+で半音上がる場合は[math]::Pow(2, 1/12)をかけ、-で半音下がる場合は割ればいいだけです。(別に$pitchesテーブルで最初から定義してもいいかもですが)

音価の決め方

音価というのは音の長さです。Beepメソッドの第2引数にはミリ秒で実際の時間を指定する必要があるので、これも計算で求めてやります。

4分音符というのは、1小節(全音符)を4分割、つまり1/4した音価を持ちます。同様に8分音符は1/8ですね。付点がつくと音価は1.5倍になります。

ではたとえば4分音符の具体的な音価はどうやって定めるのでしょうか。それを決めるのがテンポと拍子です。

テンポは1分間(つまり60000ミリ秒)の拍数(BPM、Bit Per Minuteともいう)で定義されます。今回作るシーケンサーは4/4拍子、すなわち1拍=4分音符とする4拍子固定とするので、1分間に4分音符がBPM回含まれることになります。

つまり、4分音符の音価=60000ミリ秒/BPMの値、全音符の音価=60000*4ミリ秒/BPMの値 となるわけです。

具体的なコードは…略します。単なるかけ算と割り算だけなんで。

MMLのパース

えっと時間がなくなってきたので、駆け足で説明します。

MMLのパースとは、要するに最初の例のようなMML(テキストデータ)から、実際に演奏する音程と音価の組み合わせのデータとして組み立てる作業になります。

今回のシーケンサーでは、繰り返し記号等はサポートしないので、単にMMLを一文字ずつ読み込んで、音名(cなど)を見つけたら、その後の文字を読んで、+があれば半音上げるなどして周波数を求め、その後に数字があればn分音符とみなし、先ほどの計算式から実際の音価を求めてやる。というのを全部の音でくり返すだけです。

この処理をやっているのがConvertFrom-MMLという関数です。MMLテキストを入力してやると、周波数(Frequency)や発音時間(Duration)などをプロパティとして含んだpscustomobject(Noteオブジェクト)を出力します。(Noteとは音符のことです)

MMLの演奏

パースしたMMLを実際に演奏するのがInvoke-Beep関数です。入力は、ConvertFrom-MMLで生成したNoteオブジェクトです。実際の処理は至って単純で、 [System.Console]::Beep($note.Frequency, $note.Duration)するだけです(えー)。あ、あと休符の時はDuration分だけSleepを入れてやっています。

MMLの>と<の意味を逆転する-Reverseスイッチ、BPMを指定する-Bpmパラメータ(デフォルト120)なんかも用意してます。

実際にはMMLから直接演奏できるように、Invoke-Mmlというラッパー関数を用意しています。

冒頭のねこふんじゃったのMMLを再生するにはこんな感じにします。

$s=@"
e-d-<g-4>g-rg-4e-d-<g-4>g-rg-4e-d-<g-4>g-4<e-4>g-4<d-4>frf4
e-d-<d-4>frf4e-d-<d-4>frf4e-d-<d-4>f4<e-4>f4<g-4>g-rg-4
"@
Invoke-Mml $s
コード
function ConvertFrom-MML
{
    [CmdletBinding()]
    param(
        [parameter(ValueFromPipeline=$true)][string[]]$mml,
        [int]$bpm = 120,
        [switch]$reverse = $false
    )
    begin
    {
        $baseFrequency = 440.0
        $scaleRatio = [math]::Pow(2, 1/12)
        $baseDuration = 60000 * 4 / $bpm
        $pitches = @{
            "c" = $baseFrequency * [math]::Pow(2, 3/12)
            "d" = $baseFrequency * [math]::Pow(2, 5/12)
            "e" = $baseFrequency * [math]::Pow(2, 7/12)
            "f" = $baseFrequency * [math]::Pow(2, 8/12)
            "g" = $baseFrequency * [math]::Pow(2, 10/12)
            "a" = $baseFrequency * [math]::Pow(2, 12/12)
            "b" = $baseFrequency * [math]::Pow(2, 14/12)
            "r" = -1
        }
        $currentOctave = 0
    }

    process
    {
        $chars = [string[]]$mml.ToCharArray()

        0..($chars.Length - 1)|%{
            if($null -ne $pitches[$chars[$_]])
            {
                $frequency = $pitches[$chars[$_]]
                $suffix = ""
            
                $denominator = ""
                $dot = $false

                if($_ -lt $chars.Length-1)
                {
                    switch($chars[($_ + 1)..($chars.Length - 1)])
                    {
                        {$pitches.Contains($_)}
                            {break}
                        "+"
                            {$frequency *= $scaleRatio; $suffix += $_}
                        "-"
                            {$frequency /= $scaleRatio; $suffix += $_}
                        {$_ -match "\d"}
                            {$denominator += $_}
                        "."
                            {$dot = $true}
                    }
                }

                $frequency *= [math]::Pow(2, $currentOctave)

                $denominator = [int]$denominator
                if($denominator -eq 0){$denominator = 8}
                $multiplier = 1 / $denominator
                
                if($dot){$multiplier *= 1.5}
                $duration = $baseDuration * $multiplier

                if($frequency -le 0){$on = $false}else{$on = $true}
                [pscustomobject]@{
                    Frequency = [int]$frequency
                    Duration = $duration
                    Note = "$($chars[$_])$suffix$denominator$(if($dot){"."})"
                    Octave = $currentOctave
                    Pitch = $chars[$_]
                    Suffix = $suffix
                    Denominator = "$denominator$(if($dot){"."})"
                    On = $on
                }
            }
            elseif($chars[$_] -eq ">")
            {
                if($reverse)
                {
                    $currentOctave--
                }
                else
                {
                    $currentOctave++
                }
            }
            elseif($chars[$_] -eq "<")
            {
                if($reverse)
                {
                    $currentOctave++
                }
                else
                {
                    $currentOctave--
                }
            }
        }
    }
}

function Invoke-Beep
{
    [CmdletBinding()]
    param([parameter(ValueFromPipeline = $true)][psobject[]]$note)
    
    process
    {
        if($note.On)
        {
            [System.Console]::Beep($note.Frequency, $note.Duration)
        }
        else
        {
            Start-Sleep -Milliseconds $note.Duration
        }
    }
}

function Invoke-Mml
{
    [CmdletBinding()]
    param(
        [string]$mml,
        [int]$bpm,
        [switch]$reverse
    )
    
    ConvertFrom-Mml @PSBoundParameters|Invoke-Beep
}
おわりに

今回はMMLをパースしてBeepを演奏するスクリプトをPowerShellで作ってみました。これをシーケンサーというのはおこがましいと思いますが、まあたまにはこういう柔らかいネタもいいんじゃないでしょうか。ドレミの音がどうやって決められてるのかというのも、もしかして話のネタくらいにはなるかも。

そしてこれ、シーケンサーといいつつ、入力機能がないですよね。それについては次回やります。

PowerShellアドベントカレンダー2014はまだまだ残席ございます。ぜひ、あなたのPowerShell話を聞かせて下さい。

2013/03/24

Windows 8[業務アプリ]開発読本:書籍案内|技術評論社

9784774156057

というわけで、私が企画協力させていただいた「Windows 8 [業務アプリ] 開発読本」という書籍が技術評論社さんから3/27に発売となります。この書籍は書名通り、Windows 8対応の業務アプリを作成するためのノウハウを、私のお友達を中心とした皆さんによって書かれたものとなります。執筆者を募集したときは特に意識はしてなかったのですが、結果的にMicrosoft MVPやMicrosoftエバンジェリストをはじめとしたエキスパートで占められるという何とも豪華な執筆陣となっています。

Windows 8の開発書籍ということで、Windowsストアアプリ開発法をメインに据えた構成となっているのはもちろんのこと、従来のデスクトップ(Windows Forms / WPF)アプリとの違いや移行方法、ストアアプリとデスクトップアプリをどう作り分けていくか(既存のデスクトップアプリのうちどこをストアアプリ化するのか)、といった視点も盛り込んでいます。

というのもストアアプリはタッチデバイスを想定した新しいプラットフォームではあるものの、業務システムすべてをストアアプリに置き換えるというのは得策とは言えないケースが多々あるためです。逆に業務システムのうちストアアプリ化すると効率が上がる部分も多く存在しており、本書では業務システムをグレードアップさせるためのストアアプリ開発を、概念、UI設計から具体的なコードにいたるまで詳しく取り上げています。

私もWindows 8業務アプリを開発する上で、DevOps(開発と運用の協調)を実現するためにPowerShellモジュールをアプリに組み込み、運用サイドでもアプリ操作の自動化を容易に行う方法についての記事を書かせていただいてます。

本書はストアアプリを中心にクラウド等の周辺テクノロジーを包括した、Windows 8時代の業務システム開発とはどのようなものであるか、どのように行っていけばいいのか、という指針を提示できているのではないかと思っています。Windows開発に携わる方はぜひお手にとって見ていただきたい一冊です。どうぞよろしくお願いします。

3/29追記。
本書のサポートページが公開になりました。特集1と私の記事のサンプルファイルがダウンロード可能になっていますので、ご活用ください。


古い記事のページへ |


Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー

Books

Twitter