2019/05/20(Mon)
○[Windows] PowerShellで生活するために - Import-Csvコマンドレット編(その1)
前回はForEach-Objectコマンドレットの正体はawk(1)であると看破したけど(おい)、PowerShellはMicrosoftで生まれました、ベル研でもCSRGでもMITの発明品じゃありません、Jeffrey Snoverフェローのオリジナルです、しばし遅れをとりましたが今や巻き返しの時です。
しかし実際のところ世間一般でawk(1)の代替はImport-Csvコマンドレットの方だと思われているかもね、ただawk(1)ってやつは区切り文字でテキストを分割しての処理に適してるとはいうけど、CSVってやつはもうちょっと複雑なデータ形式でクオート文字の中に区切り文字や改行コードまで含めることが可能だから、本当はawk(1)向きではないのだ。
ワイはそもそもshebang含めて3行超えそうならPerlで書いてしまうおじさんなので *1、Text::CSV_XSあたりCPANから拾ってささっと書いとったのだが、世の中にはGNU awkの拡張(FPATとか)を駆使して頑張る人もいるらしい。
いっぽうImport-Csvはこのようなクオート中の区切り文字や改行コードも、どうぞ回してみてください…いい音でしょう?余裕の処理だ、馬力が違いますよ。
とりあえず書き方のサンプルはこんな感じ。
Import-Csv -Delimiter '`t' -Encoding OEM -Path '1.tsv', '2.tsv', ... | ForEach-Object { Write-Host $_.カラム名 }
- awk(1)なら-Fオプションあるいは組込変数FSで指定するデリミタは-Delimiterオプションで行う
- awk(1)なら$nにn番目のフィールドの値が入ってるけど、行=オブジェクトなので列はプロパティ名でアクセスする必要がある
- awk(1)も文字コードについては実装を躊躇しています、そのような快挙を手際よくやりおおせた-Encodingオプションは我らの誇りです、なお
- Unicode系以外の文字コードはOEMコードページのみなのでちょい不便
- 6.2以降は番号で任意のコードページを指定できるようになったらしい
- 読込むCSVファイルは複数指定可能、ただしフィールド数が一致しない場合は例外が飛ぶ
うん、Import-CsvそのものはCSV形式のファイルをオブジェクトに変換してパイプラインに流すだけだからやっぱりForEach-Objectとセットでawk(1)やね、Import-Csvは特殊なcat(1)相当でしかないっすわ。
そんでこのプロパティ名って何を元に決めてるのかというと、1行目がヘッダ行として扱われそこでの値が使われるのですよな。
"アクセスURL","参照元URL","アクセス日時","ホスト名","User-Agent"
"/","","2019/05/01 00:01:16 JST","host1-113-0-203.example.com","NCSA_Mosaic/2.0"
...
なんてCSVファイルであれば
Import-Csv -Path 'access.log' | ForEach-Object { Write-Host $_.ホスト名 }
と書けば特定の列、このコード例なら「ホスト名」にアクセスできるわけ、またの名を源氏名。
これがもしヘッダ行の無いCSVなら(そっちの方が多いよね…)
Import-Csv -Path 'access.log' -Header "アクセスURL","参照元","アクセス日時","ホスト名","User-Agent" | ForEach-Object { Write-Host $_.ホスト名 }
のようにいちいち-Headerオプションで明示的に指定する必要があるのがちょっとめんどくせえなという感じ。
そんでこれは文法の話に脱線しちゃうけど、カラム名に「-」や「$」などの特殊文字やスペース等が含まれる場合
$_.{User-Agent}
$_.'User-Agent'
$_."User-Agent"
とクオートしてやらんと演算子や変数と間違われて予期しない結果になるのは注意。
なおSelect-Objectの-Propertyオプションの引数ではクオートしなくてよいとか
$_ | Select-Object -Property User-Agent
なーんか一貫性が無いよな…
はいお次、最後の列だけ取り出すにもawkなら組込変数NFを使えばいいのだけれどもPowerShellではちょいと厄介だ。 ヘッダを明示的に指定しているパターンであれば、配列の最後の要素は添字に-1を指定することで取り出せるので
$header = "アクセスURL","参照元","アクセス日時","ホスト名","ユーザーエージェント"
Import-Csv -Path 'access.log' -Header $header | ForEach-Object { Write-Host $_.($header[-1]) }
とすることで何とかなるのだけど、CSVファイル中のヘッダ行を使う場合には
Import-Csv -Path 'access.log' | ForEach-Object { $_.($_.PSObject.Properties.Name[-1]) }
とだいぶ薄汚れたコードを書かないとあかんっぽい、なんやこのクソ言語…
@次回
まだまだImpotent-Csvへの文句は続くよ…