Not only is the Internet dead, it's starting to smell really bad.:2020年12月下旬

2020/12/24(Thu)

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

いまさら1年半前の記事の続きを書くのもアレだけどリハビリがてらに。

前回はImport-Csvの欠点として

ってとこまで書いた、これまで長年UNIXタッパーウェア哲学に染まってたワイとしては「標準入出力もまたファイルである」が当然なので、面食らってしまうわけだ。 まぁPowerShellの流儀において邪教であるUNIX哲学なぞ捨ててしまえいわれりゃそれまでではある。

じゃあどうするのが正しいのかというと、答えは簡単Import-Csvとは別にConvertFrom-CsvというStringを処理する専用のコマンドレットが存在するのだ。

PS C:\Users\tnozaki> "a,b,c,d`naa,bb,cc,dd`naaa,bbb,ccc,ddd" | ConvertFrom-Csv -Header col1,col2,col3,col4


col1 col2 col3 col4
---- ---- ---- ----
a    b    c    d   
aa   bb   cc   dd  
aaa  bbb  ccc  ddd

なるほどね。

でもそれだったら

Import-Csv $env:TEMP\syukujitsu.csv

というコードは

Get-Content $env:TEMP\syukujitsu.csv | ConvertFrom-Csv

と書けるわけで、なんならConvertFrom-Csvに-Pathオプションを用意すればGet-Contentも必要も無いよね。 やっぱりPowerShellっていろいろと設計が変だよなぁと感じてしまうわけだ、クソ言語ソムリエとしてはビンビン感じますね…

ちなみにコマンドレットの命名規則には「動詞 + 名詞」というルールがあり 承認されている動詞としてまとめられてる。

ここからConvertFromの持つ意味を調べると

1 種類のプライマリ入力 (コマンドレットの名詞が入力を示します) を、1 つ以上のサポートされている出力の種類に変換します。

とあり、イメージ的に「オブジェクト形式をパイプの前後で変換するフィルタ」だという事がわかる。

そんでImportの方はというと

永続的なデータ ストア (ファイルなど) に、またはインターチェンジ形式で格納されているデータから、リソースを作成します。

とあり、イメージ的には「データベースへの接続をオープンする処理」ってところだろうか。

しかしだな、面白いことにCSVと似たようなテキストフォーマットに関連して

は存在するんだけど

は存在しないのだうーんこの、やっぱりお前らImport-Csvは失敗だったと気づいてるだろ!おい!

ちょいと話脱線するけど同じテキストフォーマットのXMLのためのConvertFrom-Xmlは存在しない。 なぜならXMLには専用のSystem.XML.XMLDocument型があるので、文字列からの変換にわざわざフィルタを経由する必要が無いのだ。

PS C:\Users\tnozaki> function walk($node) {
	Write-Host $node.GetType()
	$node.ChildNodes | ForEach-Object {
		walk($_)
	}
}
walk([System.Xml.XmlDocument]"<root/>")


System.Xml.XmlDocument
System.Xml.XmlElement

文字列からキャストだけで変換できる。

つまりConvertFrom-*は専用型を持たないテキストフォーマットを汎用のPSCustomObjectにパックせなあかんから必要ってことだ。

PS C:\Users\install> "{`"foo`":`"bar`"}" | ConvertFrom-Json | ForEach-Object {
	Write-Host $_.GetType()
}


System.Management.Automation.PSCustomObject

なるほどね。

では前回サンプルで書いたImvoke-WebRequestで取得したCSVを一時ファイルに保存してからImport-Csvで読み込むというコード

[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$tmpfile = New-TemporaryFile
Invoke-WebRequest -Method Get -Uri 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv' -OutFile $tmpfile
Import-Csv -Encoding OEM $tmpfile | Where-Object { ([DateTime]$_.{国民の祝日・休日月日}).Year -eq 2020 } | ForEach-Object {
	Write-Host $_.{国民の祝日・休日月日}
}
Remove-Item -Force $tmpfile

を修正して、Invoke-RestMethodとConvertFrom-Csvを使って一時ファイルを使わないように書き直してみよう。

[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Invoke-RestMethod -Method Get -Uri 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv' | `
ConvertFrom-Csv | Where-Object { ([DateTime]$_.{国民の祝日・休日月日}).Year -eq 2020 } | ForEach-Object {
	Write-Host $_.{国民の祝日・休日月日}
}

こんなかんじ。

ところがこれやってみるとわかるけど正常に動きません。 なぜなら別記事で書いた Invoke-WebRequestの文字化け問題と同じ話で、サーバーのレスポンスヘッダのcontent-type charsetを元に文字コード変換が行われるせい。 指定がないためShift_JISのCSVがISO-8859-1と解釈されて文字化けしてしまう。

基本PowerShellで扱える文字コードはユニコードなので、Shift_JISなCSVファイルを扱うにはImport-CsvやGet-Contentに-Encoding OEMオプション与えて変換するんだけど、Invoke-RestMethodにはそんな便利機能無いのよね。

ということでkludgeだけど文字コード変換かます必要がある。

[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$res = Invoke-RestMethod -Method Get -Uri 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv'
$bytes = [System.Text.Encoding]::GetEncoding('ISO-8859-1').GetBytes($res)
[System.Text.Encoding]::GetEncoding('Shift_JIS').GetString($bytes) | `
ConvertFrom-Csv | Where-Object { ([DateTime]$_.{国民の祝日・休日月日}).Year -eq 2020 } | ForEach-Object {
	Write-Host $_.{国民の祝日・休日月日}
}

うーんこれじゃCSVのサイズが巨大だったりすると性能問題が発生しちゃうよ、場合によってはメモリ足りなくなるわな。 やはり一時ファイルに保存してImport-Csvが安全ですかね…

結論、やっぱりPowerShellは捨てろ(ぉ

2020/12/25(Fri)

[Unix] Ancient Unix

The Unix Heritage SocietyThe Unix Tree、しれっと今年の4月に8~10th Unixなんて増えてるんですけお…(遅い)

うんPlan 9まで繋がるミッシングリンクが補完されてしまったので人類は滅亡する。