The Man Who Fell From The Wrong Side Of The Sky:2019年5月25日分

2019/5/25(Sat)

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

前回はImport-Csvというのは awk(1)というより、CSVファイルを読込んでオブジェクト(=PSCustomObject)のストリームとして流す特殊な cat(1)だと書いた。

なお特殊ではないcat(1)に最も近いコマンドレットには Get-Contentというのがある。

Get-Content -Path 'unko.csv' -Encoding OEM | ForEach-Object { Write-Host $_ }

こちらはファイルをやはりオブジェクト、つまりString *1に変換してパイプに流す。

細かいことをいえばcat(1)が「con CAT inate(結合)」であるのに対し「Get Content(中身をとりだす)」という思想の違いがあるけど、そこは目を瞑っておこう。 そもそもcat(1)でファイル結合ができた時代なんてのは遠い過去の歴史上の話だ、CSVみたいな古色蒼然としたファイルフォーマットですらヘッダ行が存在するなら結合時に読み飛ばさんとおかしなことになる。

なによりテキストファイルの文字コードがUS-ASCIIしかない時代ならまだしも、現代では数えきれないほどの文字コードが存在し異なる文字コードのファイルをcat(1)したら即文字化けだ。 Unicodeですら複数CESが存在してあまつさえBOMなんてシロモノがある時点で論外なんやで。

ということでファイルフォーマットの数だけcat(1)が増えるのは致し方ない、三毛とか鯖虎とかね…

@UNIX哲学の負の側面

そんでcat(1)というと思い出すのが Useless Use Of Cat、つまり「無駄にネコさまの手をわずらわせる」と呼ばれる性能問題なんですわ。

簡単な例としては

$ cat unko1.txt unko2.txt | awk -F',' '{ print $NF }'

というやつ、awk(1)では

$ awk -F',' '{ print $NF }' unko1.txt unko2.txt

と引数でファイルを指定できるので、ここでのネコさま登場させるのはパーフェクトに無駄骨でありプロセス生成とプロセス間通信のコストだけ性能は確実に劣化するわけ。

そんでこちらもまったく意味の無いファイルの最後5行を表示するのにネコの尻尾踏んづけるコード

$ cat unko.txt | tail -n 5

尻尾ひとふりするだけで終わる仕事なんよな。

$ tail -n 5 unko.txt

他にも

$ cat unko.txt | sort | uniq

なんて書かずに

$ sort -u unko.txt

とsort(1)だけで書けるよとかね。

この問題はcat(1)にとどまらないので、はつみみですというネコは米Yahoo!でスケーラビリティーに関する仕事に携わってた、NなんとかBSDの開発者による「 Useless Use of *」というプレゼンでも読んでみてどうぞ。

そんでよくUNIX哲学と関数型言語は似ているなんていわれるけど、それはすなわち同じ欠点を持ってるってことなのだ。Useless Use Of Higher-Order Functionとでも呼ぶんすかねこれ。

これはPowerShellも同じで

Get-Content unko.txt | Select-Object -First 5 | ForEach-Object {
	Write-Host $_
}

なんてコードを書かずに

  • -TotalCount … 実質head(1)コマンド
  • -Tail … 実質tail(1)コマンド

とSelect-Objectの-First/-Lastオプションに相当するfilterが実装されてるのでそちらを使った方が性能的に有利なはず。

Get-Content -TotalCount 5 | ForEach-Object {
	Write-Host $_
}

同じ理屈はそのまんまImport-Csvにも当てはまるんだけど、こっちには読込む行数を指定するオプションが存在しないのよね、あくまでパイプで渡した先のfilter側で件数を絞らざるをえない。

Import-Csv 'unko.csv' -Header "Column1", "Column2" | Select-Object -First 5 | ForEach-Object {
	Write-Host $_.Column1
}

まぁパイプで繋いでるのだから

  • Select-Objectがパイプラインから5レコード読んだら
  • SIGPIPEだかバグパイプ的なものがピーヒャララと飛んで
  • それを受取ったImport-Csvは読込処理をそこで終了

するだろうからCSVファイル全体を読み込むわけでもなし、そもそもコマンドレットはプロセスではないからUNIXシェルスクリプトよかコストは格段に低いとは思うけど。

ただ 過去回でさらっと触れたforeach vs ForEach-Object対決をみる限り、ほんとうに無視できるほどのコストなんですかね?って疑ってしまうのですな。

ちなみに性能測定にはMeasure-Commandというコマンドレットがあるけど、困ったことにImport-CsvはCSVファイルをパイプラインから読み込むという動作ができんので

Measure-Command {
	Get-Content 'unko.txt' | Import-Csv | Select-Object -First 5
}

Measure-Command {
	Get-Content -TotalCount 5 'unko.txt' | Import-Csv
}

の差を測定するみたいな性能テストができないのよね、PowerShellはオープンソースなので ソース落としてきて読み込む行数を指定するオプションを自分で実装してどれくらい改善するか調べりゃいいんだが、あまり読みたいコードじゃねぇんだよなぁ…

@パイプを流れるストリームがテキストではなくオブジェクトであることの弊害

そしてここでもうひとつImport-Csvの、ひいてはPowerShellそのもののバッドデザインが明らかになりましたね、そうImport-CsvはパイプラインからCSVファイルを読み込めないんですわ。

例えばcat(1)はパイプからもデータを読み込める、だから下のような完全に意味の無い多頭飼いネコの数珠繋ぎができる、これにはムカデ人間のヨーゼフ・ハイター博士もニッコリ。

$ cat - | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat

まぁこの節操の無さこそがUseless Use Of *問題を生むんだけどね!

しかしPowerShellにおいてGet-ContentやImport-Csvのようなコマンドレットは、ファイルをオブジェクトに変換してパイプにストリームとして流す起点にはなれるんだけど、自身がパイプからストリームを読み込むができないんですわ。

これすげー地味に利便性悪く、インターネットから例えば「非国民の休日.csv」なんてものをダウンロードしてきて処理するのに

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

みたいに書けないのよね、いちいち一時ファイルとして保存して

[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 2019 } | ForEach-Object {
	Write-Host $_.{国民の祝日・休日月日}
}
Remove-Item -Force $tmpfile

と余計な処理が増えるのがいろいろ制約でてきてつらい。

UNIX哲学においてはパイプを流れるのはテキストという汎用インタフェースなのだけど、PowerShellにおいてはオブジェクトなのでImport-CsvもCSVファイルを読込んでせっせとオブジェクトを作ってパイプに流すけど 便所に紙以外のもの流すと詰まるよなーという話、Microsoftのオフィスのトイレはベル研より配管が太いのだろう、まぁベル研は音響カプラで300ボーという便所だったしな…

そういえばGoogle翻訳は「トイレ スッポン」を「Toilet Suppon」と訳すので、こっちもさぞや詰まらない立派な便所があるんだろう。 ちなみに正解は「Toilet Plunger」、でも「Toilet Softshell Turtle」と訳されて動物愛護団体と深刻な文化摩擦を引き起こされりよかマシか…。 なお「すっぽん」だとなぜか「Happy」と訳されるので、Googleオフィスはみな裸族であり川とか流れててそこで用を足してると推察される。

@次回

もうちょっとだけImport-Csvへの文句は続く、そして今度はCSVをファイルに出力するExport-Csvの話までいけたらいいね。

*1:あるいは-AsByteStreamオプションでByte、6.0以降より