2015/12/10

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

はじめに

前編では、Invoke-WebRequestコマンドレットやWebClientクラスを用いて、WebページからHTMLの文字列を取得するところまで説明しました。

後編の今回は、取得したHTML文字列をパースして、オブジェクトとして利用可能しやすい形に変換する話です。

IEエンジンによるHTMLパース(DOM)

前編でも触れましたが、Invoke-WebRequestコマンドレットは、レスポンス文字列を取得すると同時に、HTMLをパース(構文解析)し、結果をオブジェクトとして構造化してくれます。

実はこのHTMLパース、内部的にInternet Explorerのエンジンを呼び出すことで実現されています。(ちなみに後で説明しますが、-UseBasicParsingパラメータを付与すると、IEエンジンを使わずごく基本的なパースのみ行うようになります。)

Invoke-WebRequestコマンドレットの出力であるHtmlWebResponseObjectオブジェクトのParsedHtmlプロパティを経由することで、HTMLパースされたオブジェクトを、DOM(Document Object Model)に従ってアクセスすることができます。(-UseBasicParsing指定時は不可)

HTMLのtable要素を切り出し、table各行を1オブジェクト、各セルをプロパティとして、オブジェクト配列化する例を以下に示します。

$response = Invoke-WebRequest http://winscript.jp/powershell/301

# DOMを利用して1つ目のtable要素を取得
$table = $response.ParsedHtml.getElementsByTagName("table")| select -First 1

# tableの1行目をプロパティ名として取得
$properties = ($table.rows| select -first 1).Cells| foreach {$_.innerText}

# tableの残りの行に対して、各セルのinnerTextをプロパティ値としてオブジェクト化
$objs = foreach($row in ($table.rows| select -skip 1))
{
    $row.Cells| foreach -Begin {
        $index = 0
        $obj = [ordered]@{}
    } -Process {
        $obj += @{$properties[$index] = $_.innerText}
        $index++
    } -End {
        [pscustomobject]$obj
    }
}

$objs| Format-List

ところで前編で軽く触れましたが、IEエンジンによるパースは、Invoke-WebRequestコマンドレットを用いずとも、以下のようにして直接IEのCOMインターフェースを呼ぶことで利用可能です。

$client = New-Object System.Net.WebClient
$content = $client.DownloadString("http://winscript.jp/powershell/301")
$parsedHtml = New-Object -com "HTMLFILE"
$parsedHtml.IHTMLDocument2_write($content)
$parsedHtml.Close()
$table = $parsedHtml.getElementsByTagName("table")| select -First 1
# 以下同様…

というより実際に試すと直接IEエンジンを呼び出す方がずっと速いです。理由はよく分かりませんが…。

HTML要素コレクションの取得

Invoke-WebRequestコマンドレットを用いると、DOMとは別に、すべての要素(AllElementsプロパティ)、input要素(InputFieldsプロパティ)、img要素(Imagesプロパティ)、a要素(Linksプロパティ)、script要素(Scriptsプロパティ)を含むコレクションを、HtmlWebResponseObjectオブジェクトの対応するプロパティからそれぞれ取得することができます。

コレクションに含まれる各要素は、innerText(タグ内の文字列)、innerHTML(タグ内のHTML)、tagName(タグ名)等のプロパティが共通して利用可能です。また要素の属性(たとえばa要素ならリンク先を示すhref属性)に、プロパティとしてアクセス可能となります。

以下はBingでWeb検索した結果から、ページタイトルとURLを抜き出す例です。HtmlWebResponseObjectのLinksプロパティでa要素の配列を取ってきて、次に検索結果では無いっぽいURLを、hrefプロパティの値を見てwhereで除外し、最後にinnerTextプロパティとhrefプロパティをTitle、Urlとリネームしてから値を出力しています。泥臭い処理が混じってますが、この泥臭さがスクレイピングなのかもなぁと思います。

$searchWord = "PowerShell 配列"
$notSearchResults = "/","#","javascript:","http://go.microsoft.com/"
$response = Invoke-WebRequest "https://www.bing.com/search?q=$([Uri]::EscapeDataString($searchWord))"
$response.Links | 
    where {
        $href = $_.href
        !($notSearchResults|? {$href.StartsWith($_)})
    }|
    select @{L = "Title"; E = "innerText"}, @{L = "Url"; E = "href"}|
    Format-List

form要素についてもほぼ同様にFormsプロパティからコレクションを取得できますが、このコレクションにはFormObjectという特別なオブジェクトが含まれます。FormObjectのFieldsプロパティは、Key=パラメータ名、Value=パラメータ値が格納された連想配列となっています。この連想配列は書き替えが可能なので、前編で説明した、ログオンを要するWebサイト等で用いると便利かと思います。

以下に、HtmlWebResponseObjectオブジェクトのプロパティをまとめます。(×印は使用不可を表す)

プロパティ名 説明 -UseBasicParsing
指定時
AllElements 本文に含まれるすべての要素のコレクション ×
Forms フォーム(form要素)のコレクション ×
InputFields 入力フィールド(input要素)のコレクション  
Images 画像(img要素)のコレクション  
Links リンク(a要素)のコレクション  
Scripts スクリプト(script要素)のコレクション ×

このように、一部のプロパティについては-UseBasicParsing指定時でも利用可能です。サーバーOS等でIEエンジンが利用できない場合には-UseBasicParsingパラメータが必須となりますが、その場合でも最低限のパースはしてくれるわけです。

HTML要素のコレクションを利用する方法は、DOMを使う方法に比べると自由度は少ないですが、「ページから画像のリストを取得したい」等の処理は簡便に行うことができます。

その他のHTMLパース手法

最後に、Invoke-WebRequestコマンドレットとIEエンジン以外のHTMLパース手法について軽くご紹介します。

XMLとしてパース(XHTML限定)

XHTMLというのはごくかいつまんで言うと、HTMLをXMLで定義したものです。XHTMLはXMLなので、XMLとしてパースして用いることができます。

PowerShellは[xml](XmlDocument)型アクセラレータと型アダプタにより、XML要素への簡便なアクセス手段を提供しています。以下のように、[xml]型アクセラレータを用い、取得したXHTML文字列を[xml]型に変換すると、以降は型アダプタの機能により、ドット演算子で要素を辿っていくことができます。

$client = New-Object System.Net.WebClient
$content = $client.DownloadString("XHTMLなページ")
$xml = [xml]$content
$xml.html.body.h2.'#text'

ただ世の中のWebページ上のXHTML文書が、すべてXML文書としてvalidなものであるかと言われると、現実はかなり厳しいです。そしてXML文書としてエラーがある場合は、型アクセラレータの処理は容赦なく失敗します。なのでこの手法は「使えたら強いが、大抵使えない」レベルのものと思って頂ければいいと思います。

SgmlReader

標準機能にこだわらなければ、.NET製のHTMLパーサーを使うのが楽かと思います。SgmlReaderは通常のHTML文書(当然、XHTMLに限らず)をXmlDocumentへとパースしてくれるので、PowerShellと相性が良いのではないかと思います。

以下にサンプルを載せておきます。

Add-Type -Path .\SgmlReaderDll.dll

function Get-HTMLDocument
{
    param([uri]$Uri)
    $sgmlReader = New-Object Sgml.SgmlReader -Property @{
        Href = $Uri.AbsoluteUri
        CaseFolding = [Sgml.CaseFolding]::ToLower
    }
    $doc = New-Object System.Xml.XmlDocument
    $doc.Load($sgmlReader)
    $doc
}

$xml = Get-HTMLDocument http://winscript.jp/
$xml.html.body.div|? id -eq outer|% div|? id -eq main|% {$_.p.innerText}

ぎたぱそ氏も以前SgmlReaderを取り上げておられるので、そちらも参考にして下さい。:Html Agility Pack と SgmlReader を使って PowerShell でスクレイピングしてみる - tech.guitarrapc.cóm

正規表現等で自前パース

これまではHTMLパースを既存のコマンドやライブラリを用いて行ってきましたが、対象のHTMLが非常にシンプルである場合とか、HTMLですらなく単なるテキストの場合だとか、対象ページは分量が多いものの必要箇所はごくわずかで、かつピンポイントに取得可能な場合等々は、むしろ自前でパースするコードを書いた方が手っ取り早いこともあります。

例えばYAMAHAのルーターで、管理Webのシステム情報レポートからグローバルIPアドレスを取ってくる、みたいなことは、

$response = Invoke-WebRequest http://サーバー/detail/status.html -UseBasicParsing -Credential $credential
if($response.Content -match "PP IP Address Local\: (.+)\,")
{
    $ipAddress = $Matches[1]
}

のようなコードで十分かと思います。

ConvertFrom-String

これはまだ検証してないんですが、PowerShell5.0の新機能、Auto-Generated Example-Driven Parsingの実装であるConvertFrom-Stringコマンドレットを用いて、HTMLパースができないかな、と考えています。

ConvertFrom-Stringについては過去記事参照:[v5] Auto-Generated Example-Driven Parsing について - PowerShell Scripting Weblog

まとめ

前後編に渡って、PowerShellでのWebスクレイピングの手法について解説しました。スクレイピングはWeb APIが用意されていない場合の苦肉の策ですが、背に腹は代えられない場合というのは稀によくあると思います。そういうときに今回の記事が参考になれば幸いです。

次回あたりには、Web APIがちゃんと用意されてる場合に、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の機能を全部網羅したわけではなく、使用頻度が高そうなものと個人的ハマリポイントがあるところだけです。なので詳しくはリファレンスを見て下さい。というかハマリポイントたぶんまだまだ一杯あると思います。

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

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/04/30

はじめに

IE6〜11まで、要するに現行のIEすべてを対象とするやばげなゼロデイ脆弱性がみつかり、パッチ公開までIE以外のブラウザを使いましょうという通達が出たり出なかったりしている昨今のようです。

その文脈で、IE以外のWebブラウザをダウンロードするのにIEを使うしかない!もう死ぬしか…みたいな(本気なのか冗談なのか判断が付きかねる)反応を散見します。

実際のところはWebブラウザを探してダウンロードする位はIE使えばいいと思いますが、IE使わないでいかにWebブラウザをダウンロードするか、を考えるのが、Twitter等で一種の大喜利のようになっています。

ftp.exeを使う、wgetなどのコマンドラインツールを使う、ペイントのファイルダイアログを使う(?)等々いろいろな案がありますが、皆なぜPowerShellを使わないんだ。ということで、書きます。

なお、WebブラウザのダウンロードURLはご自分でお調べください。こちらの方の記事が参考になるかと。

Invoke-WebRequestコマンドレットを使う

一番普通のやり方としては、Invoke-WebRequestコマンドレットを使う方法ですね。

Invoke-WebRequest https://アドレス/setup.exe -OutFile setup.exe

これを実行するとファイルがダウンロードされて、カレントディレクトリにsetup.exeというファイルが生成されます。フルパス指定でももちろんOKです。

なお、Invoke-WebRequestコマンドレットはHTMLのDOMパース時のみIEコンポーネントを用いるのですが、-OutFileパラメータ使用時はパースしないのでおそらくIEとは無関係で実行できると思います。(注:IEコンポーネントによるDOMパースを抑制するには、-UseBasicParsingパラメータを付加します)

Invoke-WebRequestコマンドレットはPowerShell 3.0から追加されたコマンドレットなので、3.0が同梱されているWindows 8、4.0が同梱されているWindows 8.1では特に何もせずに利用可能です。

ちなみにInvoke-WebRequestコマンドレットはデフォルトエイリアスとしてiwrが定義されているので、iwrでも呼び出せます。PowerShell 4.0ではそれに加えてwget、curlもエイリアス定義されていたりします。(このエイリアス定義は賛否両論ですけどね)

Windows 7の場合はPowerShellのデフォルトのバージョンは2.0なので、3.0以上を追加で入れる必要があります。Vistaは2.0までしか入らないので残念でした。

Start-BitsTransferコマンドレットを使う

Windows 7以上であればBITS(バックグラウンド インテリジェント転送サービス)の機能がPowerShellコマンドレットから利用できます。BITSはその名の通り、ネットワーク帯域の空き部分を有効活用してファイルを転送するかしこいサービスで、Windows Update等で用いられています。ファイル転送のプロトコルとしてはSMBとHTTP(S)をサポートしてるので、Webサイトからファイルをダウンロードするという用途で使うことができます。

ダウンロードを開始するには、Start-BitsTransferコマンドレットを使います。

Import-Module BitsTransfer
Start-BitsTransfer https://アドレス/setup.exe setup.exe

Windows 7標準のPowerShell 2.0だと、Cmdlet Auto Discoveryの機能が働かないため、上記のようにImport-Moduleコマンドレットによる明示的なモジュールロードが必要ですが、PowerShell 3.0以降では不要です。

BitsTransferモジュールには他にもコマンドレットがあり、非同期転送等できたりするので興味のある方はヘルプをみてください。こちらの記事も参考になるかと:PowerShell: ◆Bits転送2

WebClientオブジェクトを使う

今回の大喜利(?)でPowerShellを用いてファイルダウンロードする方法というのもいくつか見かけたのですが、WebClientを使う方法が殆んどだったように思います。まあ今回の記事を書いた動機としては、今はもうWebClient使わなくても標準のコマンドレットでできるよ!ということなんで敢えてここでは書きません。次のツイートを参照して下さい:Twitter / tanakh: IEを使わずにFirefoxをダウンロードする方法、Powe ...

おまけ:chocolateyを使う

あまりPowerShellとは関係ないのですが、そもそもWindowsにはapt-getみたいなパッケージ管理システムはないんかい、という意見をみかけたので紹介します。ChocolateyというNuGetベースのWindows用アプリケーションのパッケージ管理ツールとリポジトリです。

このツール自体のインストールはコマンドプロンプトに、サイトトップに書いてあるコマンドをコピーペーストして実行するだけです。(このコマンド内でPowerShellを呼び出しているので関係あるっちゃある…)

あとはコマンドプロンプトで cinst GoogleChrome とかしてやるとダウンロードとインストールをしてくれると思います。

ところでchocolateyは現在のところコマンドプロンプトベースであり外部ツールですが、次期バージョンのPowerShell 5.0ではOneGetと呼ばれるパッケージ管理システムが追加され、コマンドレットでchocolateyをはじめとする様々なリポジトリからファイルを取得してインストールできるようになる予定なのでご期待ください。

2013/01/20

PowerShellでFizzBuzz問題をいかに短く書くかというのは、人類にとっての太古からの命題であり、色々な方がチャレンジしています。

以下は国内でのチャレンジを、 日時、チャレンジャー名、コード文字数(半角スペース消去後)、初出アドレス で時系列にまとめたものです。

2007/11/06 牟田口 89文字 リンク
2007/11/07 囚人さん 86文字 リンク
2007/11/07 よこけんさん 75文字 リンク
2007/11/13 よこけんさん 57文字 リンク
2013/01/19 guitarrapcさん 57文字 リンク

私の現在の最短コードはこれです。PowerShell 3.0でしか動きませんが、51文字です。

1..100|%{($t="fizz"*!($_%3)+"buzz"*!($_%5))+$_[$t]}

PowerShell 2.0でも動くバージョンは以下。54文字です。

1..100|%{($t="fizz"*!($_%3)+"buzz"*!($_%5))+@($_)[$t]}

きっと解説は不要だと思いますが蛇足を承知で少しばかり。

$_%3 は、剰余を求める演算子%を使っているので、$_が3の倍数のとき0を返します。

!(0)とすると、0はboolに型変換され$falseとなり、その論理否定なので!(0)は$trueになります。

”fizz”*$true とすると右辺はintに型変換されるので”fizz”*1が評価され、”fizz”を返します。PowerShellでは「文字列*整数値」で文字列を整数値回繰り返した文字列を返すことを利用しています。

同じことを”buzz”に対しても行い、結果を+で連結します。このとき、”fizz”か”buzz”か”fizzbuzz”か””(空の文字列)のいずれかを返します。得られた値を@とします。

($t=@) とすると$tに@の値を入れつつ、@の値を返します。

@($_)[$t] とすると、$tが””(空の文字列)の場合は型変換され@($_)[0]が評価されます。よって、$tが””のときは@($_)の0番目の要素、$_、すなわち元の数値が取り出されます。最後に””と元の数値を+で連結したものが出力されるので、結果として数値のみが出力されます。

$tが”fizz”か”buzz”か”fizzbuzz”の場合は@($_)[$t]は配列の範囲外なので$nullを返します。よって$t+$null、すなわち$tの文字列がそのまま出力されます。

PowerShell 3.0だと非配列変数でも[]演算子を使用することができます。よって$_[0]は$_と等しく、$_[文字列]は$nullです。これによって@($_)のように配列化する必要がなく、3文字短縮できたわけです。

2011/03/24

Wake on LANでリモートコンピュータを起動する
http://gallery.technet.microsoft.com/scriptcenter/58ea4272-eb3f-45ff-9ff8-d2a90d03b7c4

前回、リモートコンピュータをシャットダウンするスクリプトを書きましたが、今回はWake on LANを使ってリモートコンピュータを起動するスクリプトを投稿しました。ActiveDirectoryの特定OUに属するPC/サーバーすべてに対してWMIを用いてMACアドレスを収集するスクリプトも付属しています。リモートシャットダウンスクリプトとあわせてご活用ください。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/03/24/197834.aspx

2009/10/17

最近、わんくまブログサーバーが高負荷のため繋がりにくくなっております。そのため、負荷分散の意味も込めて私のブログのミラーを用意しました。ミラーには過去記事を上げてありますし、今後はこちらに投稿した記事と同じものを同時に上げて行きます。なのでRSSなんかはミラーの方を登録するといいかもです。

なお、コメントはこちら(わんくまの元記事)の方にお願いします。

ミラーのアドレスはこちら

http://winscript.jp/tech_blog/

よろしくお願いします。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2009/10/17/182170.aspx

2008/12/03

Outlook2007は、メールアドレスに.の連続(hoge...hoge@example.com)や@の前に.がある場合(hoge.hoge.@example.com)にはメールを送信することができません。受信トレイに「システム管理者」から送信不能と通知メッセージが入ります。

これは別にOutlook2007が悪いんじゃなくて、RFCに準拠しているためです。でもDocomoやauなど、このRFCに準拠していないキャリアがあって、結構、よく見ます。その辺の詳しい事情はこちらなどをどうぞ。

で、Outlookユーザーはじゃあこれらのメアドにはメールを送れないのか、と言うとそうではなく、実は裏技があって、@の前の部分を""(ダブルクォーテーション)でくくることで送信可能になります。

"hoge...hoge"@example.comや"hoge.hoge."@example.comなどのようにします。これはOKとRFCで規定されてるらしいですね。

でも毎回これを手で入力するのはメンドイ!

ということでなんとかしてみました。

Private Sub Application_ItemSend(ByVal Item As Object, Cancel As Boolean)
    Dim oMailItem As MailItem
    Dim bExistInvalidAddress As Boolean
    
    If TypeName(Item) = "MailItem" Then
        Set oMailItem = Item
        
        Dim reAddress As New RegExp
        reAddress.Pattern = "([a-zA-Z0-9\-\_\.]+)\@([a-zA-Z0-9\-\_\.]+)"
        reAddress.Global = True
            
        sMailAddresses = Split(oMailItem.To, ";")
        
        For I = 0 To UBound(sMailAddresses)
            If (InStr(sMailAddresses(I), "..") Or InStr(sMailAddresses(I), ".@")) And _
            InStr(sMailAddresses(I), """") = 0 Then
                sMailAddresses(I) = reAddress.Replace(sMailAddresses(I), """$1""@$2")
                bExistInvalidAddress = True
            End If

        Next
        
        If bExistInvalidAddress Then
            oMailItem.To = Join(sMailAddresses, ";")
            Cancel = True
        End If
        
    End If
End Sub

このようなコードをThisOutlookSessionに埋め込みます。VBエディタでF2キーを押してMicrosoft VBScript Regular Expression 1.0と5.5を参照設定してください。ただしマクロなんで毎回起動時にマクロを有効にするか聞かれます。(以上の意味が分からない方は使わないほうが無難です)

すると、問題あるメールアドレスのメールを送信しようとすると、正しくクォーテーションがつけられたメールアドレスに変換します。ただしTo:だけです。Cc:やBcc:にも対応してもよかったんですがそれは宿題ということで。

Cancel=Trueは要らないと思ったんですが、これを取るとなんかさらに''でクォートされて送信トレイに残骸が残ってしまいます。ので、一度メール編集画面に戻るようにしています。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2008/12/03/162596.aspx

2008/11/22

ご無沙汰してます。牟田口です。近況は省略(えー mixiついったーブログその他などを。

さて、最近、spamコメントが鬱陶しくて色々対策を考えてるんですが、手っ取り早く効果的なのはBBQを使うことですね。某巨大掲示板群サイトで荒らしに使われた、おもに公開プロキシのリストをDNSを引いて持ってくるというものです。Perlの実装はこのページにあります。PHPの実装も見かけました

意外と.NET,C#な実装を見かけないので、最近覚えたC#でさくっとかいてみました。

using System;
using System.Net;
using System.Net.Sockets;

namespace CheckBBQ
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine("BBQ判定プログラムの使い方:\r\ncheckbbq.exe IPAddressもしくはHostName\r\n戻り値:\r\n-1:失敗\r\n0:串ではない\r\n1:串である");
                return;
            }
            IPAddress[] addresses= Dns.GetHostAddresses(args[0]);
            if (addresses.Length == 0)
            {
                Console.WriteLine("-1");
                return;
            }
            string ipAddress = addresses[0].ToString();
            string[] ipAddressParts = ipAddress.Split('.');
            Array.Reverse(ipAddressParts);

            string hostName = string.Join(".", ipAddressParts) + ".niku.2ch.net";
            try
            {
                addresses = Dns.GetHostAddresses(hostName);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("0");
                return;
            }
            catch (Exception ex)
            {
                Console.WriteLine("-1");
                return;
            }

            if (addresses.Length == 0)
            {
                Console.WriteLine("-1");
                return;
            }

            ipAddress = addresses[0].ToString();
            if (ipAddress == "127.0.0.2")
            {
                Console.WriteLine("1");
            }
            else
            {
                Console.WriteLine("-1");
            }
        }
    }
}

使い方はコードを見てください。これは単に引数のホストをBBQにかけ、結果を標準出力に出すコンソールアプリですが、メソッド化してもaspxに組み込んでもWebサービスにしてもまぁ好きなように使ってくださいませ。

でも、BBQは万能選手じゃありません。本来書き込めるべき人が書き込めないことも多々あります。なので、BBQをまず通してOKならOK、NGならCAPTCHA認証をやってもらう、とかがいいと思います。

トラックバックspam対策もいろいろ考えたんですが、まだ国産のは少ないので、送信データが英字のみをはじく、でも効果は結構あります。このへんmixiでメモったのでコピペ。

トラックバックって
2008年10月11日20:04

黒歴史になるんだろうか
オートディスカバリーはないと微妙だしあるとspamの温床になるし。
何らかの認証の共通規格を設ける?
RSSは2.0で一応落ち着いたけどトラックバックは発展しないままだなー
私はいま、現行規格のまま、オートディスカバリーを有効にしてかつspamトラックバックが送られないようにする方法を考え中
送り元を見に行くというはてな方式も一つの方法論であるんだけど、なんかこう納得できないものがある

コメント
むたぐち 2008年10月11日 20:07
送信元ホワイトリスト方式もなんだか微妙です
むたぐち 2008年10月11日 20:18
excerpt,titleなどの文字列で弾くのはよくある対策だけど根治法じゃない
いたちごっこだー
BBQは使えない。理由は少し考えれば分かりますので略。
やっぱりはてな方式なんかな。urlの先をまず見に行って、そこにこちらへのリンクがなければまず速効NG。
あとはお好みで、引用がなければNGとか。
でもこれって莫大なコストがかかるし送り元を見に行くというのがそもそもなんか根本的にどうなんっておもう。
むたぐち 2008年10月11日 20:38
トラックバックは性善説ベースで作られた規格
引用元を見に行くのは性悪説ベースの対処
両極端すぎる
むたぐち 2008年10月11日 21:08
送り元を見に行く方式の問題はまだあって、A→Bにトラックバックを送信された場合、B→Aに「AにBのURLが存在するか」を確認しに行く。
一見合理的だけど、このトラックバックって誰でも送れるんだよねー。Aを書いた人じゃなくても。つまり、AともBとも関係ない悪意を持つxがいて、A→Bへのトラックバックを乱射した場合どうなるか。B→AのDoS攻撃みたいなんが成立しちゃう。同ドメインへの確認間隔を制御する必要がでてくる。でもそれはもちろん正しくサービスを利用する人にとっては不便になるわけで。
むたぐち 2008年10月11日 21:28
送信元を見に行く方式で、トラックバックを送った人のリモートホストと、urlに指定されたWebサイトのドメインが一致するかまず調べるという方法もあるけど(たぶんはてなではこれをやっている)、これは正しい使い方をしていても一致しないことも当然あるわけで(手動でトラックバック打つときとかね)。だけどこの辺が確かに手の打ちどころではあるようには思う。手動で打つ時はオートディスカバリー関係ないしな。ただ、私のようにDNSの逆引きができないというか正逆で結果が異なるようなサーバーの場合は泣いてもらうしかないかなー。
むたぐち 2008年10月11日 21:32
つまり、オートディスカバリー用(Blog提供サービスが見に行く用)のtrackback ping URLと、手動で打つ場合のtrackback ping URLをまず分ける。
前者は、トラックバックを送った人のリモートホストと、urlに指定されたWebサイトのドメインが一致するかまず調べ、一致しない場合ははじく。一致した場合は送信元を見に行って、こちらへのリンクがある場合は通す。それ以外ははじく。
後者は、trackback ping URLを用意するがあるところまではコピペできるが、それに数文字、画像にかかれた文字をつけたすようにする(認証の代わり)。
この辺が落としどころかなー。
むたぐち 2008年10月11日 21:35
追加文字列は定期的に変わるようにする。
むたぐち 2008年10月11日 21:37
オートディスカバリーのほうはチェック間隔に制限を設ける。
JZ5 2008年10月11日 22:38
SPAM温床はメールと同じようなもんじゃないだろか。
バーベキューってなんですか?
手動のトラックバックURL(使わないけど)は、JavaScriptなんかで生成するのはどう? Server側とClient側で共通のアドレス作れるけど、直接ファイル読み込んでJavaScript実行してないと正しいURLがない。
http://katamari.jp/blog/index.php?UID=1163941454
むたぐち 2008年10月11日 22:54
いえねー、トラックバックってわりと新しい規格なのになんでspam温床になることを考えて規格作らなかったのかと。
eメールは昔々作られたものだから仕方ないとして。
JavaScriptいいですねー。やってみます。
トラックバックspam対策って結局同じところに辿りつくのねw>URL
むたぐち 2008年10月11日 22:55
BBQについてはこちら
http://bbq.uso800.net/

JZ5さん事後承諾でごめんなさいだけどコピペさせていただきました。

あと、BBXを使うのも一つの案かな。こっちは広告爆撃ブラックリストなので近いものがあるかと。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2008/11/22/161939.aspx

2008/08/12

Const olFolderContacts = 10
Set Outlook = CreateObject("Outlook.Application")
Set Fs = CreateObject("Scripting.FileSystemObject")
Set ts = Fs.CreateTextFile("Address.lst")
For Each oItem In Outlook.GetNamespace("MAPI").GetDefaultFolder(olFolderContacts).Items
	If oItem.Email1Address<>"" Then
		ts.WriteLine oItem.Email1Address & vbTab & oItem.FullName
	End If
	If oItem.Email2Address<>"" Then
		ts.WriteLine oItem.Email2Address & vbTab & oItem.FullName
	End If
	If oItem.Email3Address<>"" Then
		ts.WriteLine oItem.Email3Address & vbTab & oItem.FullName
	End If
Next
ts.Close

さくっと。他のメーラーにも無論カスタマイズして使ってください。npopqのアドレス帳ほどシンプルなのはたぶんないと思いますがw(メールアドレス<タブ>名前)

元記事:http://blogs.wankuma.com/mutaguchi/archive/2008/08/12/152820.aspx

次のページへ

Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

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

Awards

Books

Twitter