tech.guitarrapc.cóm

Technical updates

PowerShellでJSONを触ってみる

かずき先生から謎APIをご提供いただいたので触ってみましたw 何分初めて真面目にapiを叩いたり、JSON触ったので寄り道しまくりで。 無理やりJSON無視して-replaceで抜き出したりとか、JSON Serializationでやってみたりとかしました。 ====

どんなAPI?

期間限定で用意していただいたので、キャプチャだけ。 http://guitarrapc.azurewebsites.net/api/people

http://guitarrapc.azurewebsites.net/api/people/200

なんか、2000号まで量産されました。

まずはAPI叩いてみよう

Wheatherほにゃららの時同様に、New-WebServiceProxyコマンドレットでどうかなと。

New-WebServiceProxy cmdlet - TechNet - Microsoft

えいやっと。

New-WebServiceProxy "http://guitarrapc.azurewebsites.net/api/people"

…あれ?WSDLドキュメントじゃないよー、とエラーが出てしまい使えないようです。

New-WebServiceProxy : URL http://guitarrapc.azurewebsites.net/api/people のドキュメントは既知のドキュメントの種類として認識されませんでした。
それぞれの既知の種類に関するエラー メッセージを参照して問題を解決してください。
- 'WSDL ドキュメント' からのレポート: 'XML ドキュメント (1,1) でエラーが発生しました。'
  - ルート レベルのデータが無効です。 行 1、位置 1。
- 'XML スキーマ' からのレポート: 'ルート レベルのデータが無効です。 行 1、位置 1。'
- 'DISCO ドキュメント' からのレポート: 'ルート レベルのデータが無効です。 行 1、位置 1。'
発生場所 行:1 文字:1
+ New-WebServiceProxy "http://guitarrapc.azurewebsites.net/api/people"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	+ CategoryInfo          : InvalidOperation: (http://guitarrapc.net/api/people:Uri) [New-WebServiceProxy]、InvalidOperationException
	+ FullyQualifiedErrorId : InvalidOperationException,Microsoft.PowerShell.Commands.NewWebServiceProxy

気を取り直して、System.Net.WebClientではどうでしょうか。

MSDN - WebClient クラス
$uri = "http://guitarrapc.azurewebsites.net/api/people"

$client = New-Object System.Net.WebClient
$stream = $client.OpenRead($uri)
$stream

無事に接続できました。

CanTimeout   : True
ReadTimeout  : 300000
WriteTimeout : 300000
CanRead      : True
CanSeek      : False
CanWrite     : False
Length       : 
Position     : 

後は、System.IO.StreamReader().ReadLine()を使って読み取るだけですね。

$uri = "http://guitarrapc.azurewebsites.net/api/people"
$encode_utf8 = [Text.Encoding]::GetEncoding("utf-8")

$client = New-Object System.Net.WebClient
$stream = $client.OpenRead($uri)
$streamReader = New-Object System.IO.StreamReader($stream,$encode_utf8)
$dataSream = $streamReader.ReadLine()

[Text.Encoding]::GetEncoding()を使って、読み取り時にEncodingとして"Utf-8"を指定します。 "Shift-JIS"では日本語化けします。 ここまでをいったん纏めて、データを取得する簡単なfunctionを書いてみました。

function Get-guitarrapcDataSream{

	[CmdletBinding()]
	param(
	[string]$uri
	)

	$encode_utf8 = [Text.Encoding]::GetEncoding("utf-8")

	$client = New-Object System.Net.WebClient
	$stream = $client.OpenRead($uri)
	$streamReader = New-Object System.IO.StreamReader($stream,$encode_utf8)
	$dataSream = $streamReader.ReadLine()

	return $dataSream

}

URIを指定して取得してみましょう。

$uri = "http://guitarrapc.azurewebsites.net/api/people"
Get-guitarrapcDataSream -uri $uri

わざわざJSONフォーマットで取得されました 

JSONをxmlにシリアライズ

PowerShellはJSONをそのまま扱うより、一旦xmlにバイパスすることで、簡単にデータアクセスできます。 そこで、この記事を参考にWCF DataContractJsonSerializerでXMLにバイパスさせてみました。

JSON Serialization/Deserialization in PowerShell
function Get-guitarrapcDataSream{

	[CmdletBinding()]
	param(
	[string]$uri
	)

	$encode_utf8 = [Text.Encoding]::GetEncoding("utf-8")

	$client = New-Object System.Net.WebClient
	$stream = $client.OpenRead($uri)
	$streamReader = New-Object System.IO.StreamReader($stream,$encode_utf8)
	$dataSream = $streamReader.ReadLine()

	return $dataSream

}

function Get-HashTableToCustomObject{

	[CmdletBinding()]
	param(
	[object[]]$xmldata
	)

	$output = $xmldata `
			| %{
				[PSCustomObject]@{
				Id=[int]$_.id.InnerText;
				Name=[string]$_.Name.InnerText}}

	return $output

}

function Convert-guitarrapcJsonToXml{
	
	[CmdletBinding()]
	param(
	[int]$setId=-1
	)

	#region Uri add Setid parameter exist
	$uri = "http://guitarrapc.azurewebsites.net/api/people"

	if ($setId -ne -1)
	{
		$uri = $uri + "/" + $setId
	}
	#endregion

	#obtain Json data from uri
	$dataSream = Get-guitarrapcDataSream -uri $uri

	# Encode data ToBytes
	$bytes = [Text.Encoding]::UTF8.GetBytes($dataSream)

	# Create JsonReader
	$quotas = [System.Xml.XmlDictionaryReaderQuotas]::Max
	$JSONReader = [System.Runtime.Serialization.Json.JsonReaderWriterFactory]::CreateJsonReader($bytes,$quotas)

	# Convert Json to XML
	$xml = New-Object System.Xml.XmlDataDocument
	$xml.Load($JSONReader)
	$JSONReader.Close()

	if ($xml.root.Item -is [System.Object[]])
	{
		# Convert HashTable to CustomObject
		$output = $output = Get-HashTableToCustomObject -xmldata $xml.root.item

		# sort data by id(cause HashTable not sorted)
		return $output.GetEnumerator() | sort id

	}
	else
	{
		# Convert HashTable to CustomObject
		$output = Get-HashTableToCustomObject -xmldata $xml.root

		# only one array return, no sorting required
		return $output
	}
}

[byte][char]では日本語が含まれるとエラーが出る

少し引っかかったので。

[byte[]][char[]]"あ"

ようは、「byteは0から255までだから、日本語を1byteには変換できない」のが原因と。

値 "あ" を型 "System.Byte" に変換できません。エラー: "符号なしバイト型に対して値が大きすぎるか、または小さすぎます。"
発生場所 行:1 文字:1
+ [byte[]][char[]]"あ"
+ ~~~~~~~~~~~~~~~~~~~
	+ CategoryInfo          : InvalidArgument: (:) []、RuntimeException
	+ FullyQualifiedErrorId : InvalidCastIConvertible

そこで、[Text.Encoding]::UTF8.GetBytes()を使ってマルチバイト(日本語)をバイト シーケンスにエンコードしています。

Encoding.GetBytes メソッド
[[Text.Encoding]::UTF8.GetBytes("あ")

無事にいきました。

227
129
130

[PSCustomObject]を使ってNew-Obejctの注意

HashTableは、Dictionaryなので「順序」が保障されていません。 そのため、以下のコードでは-eqでの指定はできても-ltなどで比較ができません。

$output = $xmldata `
			| %{
				[PSCustomObject]@{
				Id=[int]$_.id.InnerText;
				Name=[string]$_.Name.InnerText}}
$output

そこで、.GetEnumerator()してからsortをすることで、指定したプロパティをキーにきっちり並びます。

$output.GetEnumerator() | sort id

XMLにバイパスした際の値の指定

複数のデータが含まれている時と、一つの時では、$xmlでデータを指定する際に微妙に異なります。

$xml.root.item #複数のデータが含まれたJSONをxmlにバイパス時
$xml.root #一つのデータが含まれたJSONをxmlにバイパス時

この違いは、GetType()すると判定できます。

($xml.root.Item).gettype().fullname
System.Object[] #複数のデータが含まれたJSONをxmlにバイパス時
System.Management.Automation.PSParameterizedProperty #一つのデータが含まれたJSONをxmlにバイパス時

あとは、データが一つの時は、$output.GetEnumerator() | sort idではなく$outputですね。

実行してみる

ででんと。

Convert-guitarrapcJsonToXml| ?{$_.id -eq 200} | Format-Table -AutoSize
Convert-guitarrapcJsonToXml | ?{$_.Name -match ".*100.*"} | Format-Table -AutoSize
Convert-guitarrapcJsonToXml | ?{$_.id -lt 10} | Format-Table -AutoSize
Convert-guitarrapcJsonToXml -setId 1 | Format-List

上手くデータを指定できていますね。

 Id Name       
 -- ----       
200 量産型ぎたぱそ200号



  Id Name        
  -- ----        
 100 量産型ぎたぱそ100号 
1000 量産型ぎたぱそ1000号
1001 量産型ぎたぱそ1001号
1002 量産型ぎたぱそ1002号
1003 量産型ぎたぱそ1003号
1004 量産型ぎたぱそ1004号
1005 量産型ぎたぱそ1005号
1006 量産型ぎたぱそ1006号
1007 量産型ぎたぱそ1007号
1008 量産型ぎたぱそ1008号
1009 量産型ぎたぱそ1009号
1100 量産型ぎたぱそ1100号



Id Name     
-- ----     
 1 量産型ぎたぱそ1号
 2 量産型ぎたぱそ2号
 3 量産型ぎたぱそ3号
 4 量産型ぎたぱそ4号
 5 量産型ぎたぱそ5号
 6 量産型ぎたぱそ6号
 7 量産型ぎたぱそ7号
 8 量産型ぎたぱそ8号
 9 量産型ぎたぱそ9号




Id   : 1
Name : 量産型ぎたぱそ1号

Json楽しいです!

無理やり力技

アンチパターンとして自戒の念を込めて晒しておきます。 勉強が足りませんね。

#Required -Version -3.0

#return $dataSream from api
function Get-guitarrapcDataSream{

	[CmdletBinding()]
	param(
	[string]$uri
	)

	$encode_utf8 = [Text.Encoding]::GetEncoding("utf-8")
	$replaceSream = "[`[{}`]]"

	$client = New-Object System.Net.WebClient
	$stream = $client.OpenRead($uri)
	$streamReader = New-Object System.IO.StreamReader($stream,$encode_utf8)
	$dataSream = $streamReader.ReadLine()

	return $dataSream

}

#return split $dataSream to each row
function Split-guitarrapcDataSream{

	[CmdletBinding()]
	param(
	[Parameter(ValueFromPipeline=$true)]
	[string]$streamData
	)

	$splitStream = "},{"
	$replaceSream = "[`[{}`]]"

	return $streamData -split $splitstream -replace $replaceSream,""
}

#return formatted api data
function Get-guitarrapcApi{
	
	[CmdletBinding()]
	param(
	[Parameter(ValueFromPipeline=$true)]
	[int]$setId=-1
	)

	#region Uri add Setid parameter exist
	$uri = "http://guitarrapc.azurewebsites.net/api/people"

	if ($setId -ne -1)
	{
		$uri = $uri + "/" + $setId
	}
	#endregion

	#obtain data from uri
	$dataSream = Get-guitarrapcDataSream -uri $uri

	#split $dataSream to each row
	$dataSplit = Split-guitarrapcDataSream -streamData $dataSream

	#region create PScustomObject from string
	$pattern = "`"Id`":(?<id>.*),`"Name`":`"(?<Name>.*)`""
	$customData = $dataSplit `
		| %{
			$_ -cmatch $pattern `
			| %{
				[PSCustomObject]@{
				id=$Matches.id;
				Name=$Matches.Name
				}
			}
	}
	#endregion

	return $customData

}

実行してみます。

Get-guitarrapcApi -setId 1998 | select id

Get-guitarrapcApi | select -First 2 | Format-Table -AutoSize

Get-guitarrapcApi `
	| ?{$_.id -eq 100} `
	| select Name `
	| Format-List

一応、取れてますが、まったく応用が利かないので没です。

id
--
1998



id Name     
-- ----     
1  量産型ぎたぱそ1号
2  量産型ぎたぱそ2号




Name : 量産型ぎたぱそ100号

おまけ1

ただデータ取得するだけです。

function Get-guitarrapcToFile{

	[CmdletBinding()]
	param(
	[string]$uri="http://guitarrapc.azurewebsites.net/api/people",
	[string]$path,
	[string]$filename
	)

	$savefile = $path + "\" + $filename
	$client = New-Object System.Net.WebClient
	$client.DownloadFile($uri,$savefile)
}

Get-guitarrapcToFile -path ((Get-Location).Path) -filename "sample2.JSON"

おまけ2

ダウンロードしたJSONを適当に整形するカジュアルワンライナー (反省しています

Get-Content .\sample.JSON | ForEach-Object{$_ -replace "\[" , "[`n`t" -replace "{`"" , "{`n`t`t`"" -replace ",`"" , ",`n`t`t`"" -replace "`"}" , "`"`n`t}" -replace "},{" , "},`n`t{" -replace "\]" , "`n]"} `
	| Out-File .\sample_formatted.JSON

一応改行するとこうです。

Get-Content .\sample.JSON `
	| ForEach-Object{$_ `
		-replace "\[" , "[`n`t" `
		-replace "{`"" , "{`n`t`t`"" `
		-replace ",`"" , ",`n`t`t`"" `
		-replace "`"}" , "`"`n`t}" `
		-replace "},{" , "},`n`t{" `
		-replace "\]" , "`n]" `
	} `
	| Out-File .\sample_formatted.JSON

全く流用できませんね…… 

ODATAはどうした

いやー、サンプルサイトでは問題なく書けたのですが……こんかいの謎APIでは上手くいかなくて……すいません><

参考サイト

Windows PowerShell を使用して株価をすばやく確認す​る方法はありますか PowerShell Scripting Weblog - PowerShellでJScript.NETを利用してJSONをパースする JSON Serialization/Deserialization in PowerShell ODATAサンプルサイト OData原文仕様