PowerShellを使ってCSVを編集する(理論編)
2023/10 MSの発表により将来的にWindowsからVBScriptが廃止になるとのことで
その後継はPowerShellということになるそうです。
PowerShellがVBScriptより使いやすいのかはどうかは思案の余地があるとしても、
PowerShellのCSV加工は凄く便利だったのでゼロから勉強しながら使ってみることにします。
この記事は、それについてほうぼうで調べてきてものを纏めたもので、
他のプログラミング言語には触れている人レベルを想定します。
実際に動作するサンプルについては実用編へどうぞ
- Windows 11 & PowerShell 5.1で検証しています。
コマンド:Import-Csv
PowerShellにおいてはImport-CsvコマンドでCSVファイルを読み取ることができます。
特筆点として、低め水準の言語で手でCSV解析を書いた場合、たいてい面倒になって諦めることになる「改行を含む文字データ」が正しく取得できるのが便利です。もちろんダブルクォートやそのエスケープにも対応してくれます。
# UTF-8
$srcs = Import-Csv -Encoding utf8 -Path "source.csv"
# Shift-JIS
$srcs = Import-Csv -Encoding oem -Path "source.csv"
-Encoding
は、utf8
が指定できます‥‥というかUTF8ならこれでいいです。
Shift-JISのほうは、今の状況であれば「"MS-DOSの規定を使う" を指すoem
を使うと、いわゆるWindowsのSJISになります」ということになりそうです。
古いPSでのShift-JIS指定
ただし、古いPowerShellのバージョンではdefault
を使う、という記事もあります。
一方で、最近のバージョンではdefault
がutf8
になっているとの話もあります。
結論としては、oemでダメだったらdefaultという感じのようです。
(ちなみに私の環境では、defaultもoemもどちらもShift-JISになりました)
他、厳密にやりたい場合は、エンコーディングコードを指定する方法もあるようです。
戻り値はレコードの配列とは限らない
格納されるCSVデータは、PSCustomObjectと言うオブジェクトないしそれの配列です。
PSCustomObjectについて雑に説明すると、プロパティを追加できるクラスみたいなものらしいのですが、当座は1レコード分が格納される連想配列存在と捉えてOKです。
‥‥そんなことより「オブジェクトないし、それの配列」であることは注意が必要で、つまり、これ、得られる結果のオブジェクトは、CSVファイルが1レコードしかなかった場合オブジェクトそのもの、複数のレコードある場合は、オブジェクトの配列になります。つまり状況によって型が違います。幸いにしてというかややこしいことに後述するforeachやExport-CSVなどは、どちらを渡してもイメージ通りに処理してくれるので、この事実はある程度は気にしなくてよいのですが、より込み入った事をやる場合はこの件を気にする必要があります。
-Header
指定なしで取り込んだ場合、CSVファイルの1行目の内容がヘッダ項目として扱われます。
もし、CSVファイルにヘッダ項目がない1行目からデータがあるような場合、-Header
をオプションを指定して、別に項目の列名を設定します。
$srcs = Import-Csv -Encoding utf8 -Path "source.csv" -Header "ID","名前","年齢","コメント"
コマンド:Export-Csv
$dests | Export-Csv -Encoding utf8 -NoTypeInformation -Path "destination.csv"
出力。CSVデータ$dests
をファイルに吐き出します。
エンコーディングはImport-Csvのものと同じものなので割愛。
-NoTypeInformation
は、1行目に型情報を出力しなくするためのオプションです。
というか無いと勝手に出てしまうので、常にこの呪文を付けるものと思ってください。
コマンド:ft出力
$csvs | ft
Format-Tableの略とかそんなの。これを使うと標準出力に表形式で出してくれます。
ID 名前 年齢 コメント
-- ---- ---- --------
0001 アリス 18 がんばります
0002 ベティ 15
0003 クリス 17
0004 ディック 21 コメントには...
0005 エリン 22 "エリ"と呼んでください
デバッグにはとても便利です。
コマンド:Select-Object
列のコントロールと行のコントロールができます。
列の追加と削除と並べ替え
$dests = $srcs | Select-Object -Property ("名前","所属","ID")
元がID
,名前
,年齢
,コメント
のCSVデータの場合、名前
とID
が入れ替わります。
そして所属
は空データのまま追加され、年齢
とコメント
の列は削除されます。
件数による行の絞り込み
## 先頭2レコードだけ
$dests = $srcs | Select-Object -First 2
## 最後2レコードだけ
$dests = $srcs | Select-Object -Last 2
## 最初の2レコードを飛ばして、3レコード目以降を全部
$dests = $srcs | Select-Object -Skip 2
## 最初の1レコードを飛ばして、そこから3レコードだけ
$dests = $srcs | Select-Object -Skip 1 -First 3
大量データを処理するときはこれを駆使する感じになりますが、そもそも$srcが全件読んじゃってるので、真の巨大CSVには通用しないのではないかとも思います。
重複削除
$dests = $srcs | Select-Object -Unique -Property @("名前")
-Unique
をつけると、重複するデータが削除されます。
この例だと同じ名前は1つにまとまります。
コマンド:Where-Object
$dest = $srcs | Where-Object {$_."名前" -eq "アリス"}
絞り込みを掛けます。{}
内にある$_
が実際の$srcsの中にあるレコードそれぞれを意味しており、ここでは、そのレコードの「名前」が「アリス」であるレコードのみを選択します。
PowerShellの比較演算子
話は逸れますが、PowerShellで比較するときは==
とかではなく-ceq
などを使います。
-eq
、-ceq
# 等しい
-ne
、-cne
# 等しくない
cが付いている方はアルファベットの大文字小文字を別の文字として扱います。
逆に言うと-eq
ではABCとabcが区別されませんのであしからず。
コマンド:Sort-Object
WhereがあるならOrderByもあるんじゃ。はい、ありましたよ
#昇順
$dests = $srcs | Sort-Object -Property "年齢"
#降順
$dests = $srcs | Sort-Object -Descending -Property "年齢"
ただし、この書き方は文字列比較になるため"100"より"11"の方が小さい扱いになります。
$dests = $srcs | Sort-Object -Property {[int]$_."年齢"}
int型に変換して比較したい場合、その書き方はこう。
実は{}内は数式が書けますので確認日-作成日なんかとかやっても計算可能のようです。
コマンド:New-Object
新規のレコードを作成したい場合は、このようにします。
$headers = @("ID","名前","年齢","コメント")
$record = New-Object PSObject | Select-Object $headers
$record."ID" = "0006"
$record."名前" = "フレッド"
$record."年齢" = 16
$record."コメント" = "遅れてきた主役だ"
$dests += $record
New-Object PSObject
を使う事で空っぽの(列を何も持たない)レコードモドキができます。
それにSelect-Object
することで、全て列項目が追加された空のレコードが完成します。
あとは項目を設定して、もともとのCSVデータオブジェクトに追加する。
もちろん、最初に$dests = @()
として空の配列を用意すればゼロからCSVデータが作れます。
コマンド:foreach
各レコードをforeachで回すことができます。
foreach ($src in $srcs){
$src."年齢" = $src."年齢" + "歳";
}
と書くと、これだけだとなんの感慨もありませんが‥‥
CSVデータはレコードの配列とは限らない
先の通り「Import-Csv等で取得されるものは、1件ならオブジェクト、複数件なら配列になっている」わけで$srcs
は配列のケースと配列でないケースがあります。実は、foreachはどちらも処理してくれていて、走らせてみると上述の$srcはちゃんと1レコードが入ってくれます。
なので。
for ($i=0; $i -lt $srcs.Length; $i++){
$srcs[$i]."年齢" = $srcs[$i]."年齢" + "歳";
Write-Host "[Info] :" $srcs[$i]."年齢"
}
上とこれは等価ではないです。
CSVデータが1レコードしかない場合$srcs.Length
が空を返すので、下例のほうはこのループ句は何もしないで終了します。エラーにもならないので見落としがちです。基本foreachを使いましょう。
ついでに言うと、0件の場合はなぜか$srcs.Length
はゼロを返します。この時$srcsはnullなのですが、nullにLengthをつけると0になるみたいで。ただこの場合上例も下例にループ句は素通りするので希望通りにはなります。
この辺バージョンにもよりそうですけども。
その他TIPSというかメモ
パラメータでの配列の指定
$srcs = Import-Csv -Encoding utf8 -Path "source.csv" -Header "ID","名前"
$srcs = Import-Csv -Encoding utf8 -Path "source.csv" -Header @("ID","名前")
$headers = @("ID","名前")
$srcs = Import-Csv -Encoding utf8 -Path "source.csv" -Header $headers
どれも指定しても行けます。個人的に変数に入れるほうが好き
列名を変更する。
CSVの1行目に分かりにくいヘッダ名があって
COL1,COL2
0001 アリス
読み込む際に分かりやすいヘッダ名で取り込みたい、なんてときは
Import-Csvで-Header
をつけて取り込むと列名を差し替えることができますが、
元々の列名が1行目のレコードになってしまいます。なので、Select-Objectをつけて
$srcs = Import-Csv -Encoding utf8 -Path "source.csv" -Header "ID","名前" | Select-Object -Skip 1
とするとCSVファイル1行目のヘッダ行データを捨てられます。
CSVファイルのダブルクォートを取り除く
取り込み先のシステムでダブルクォートを受けれてくれないシステムがあるかもしれません。
$dests | Export-Csv -UseQuotes AsNeeded -Encoding utf8 -NoTypeInformation -Path "destination.csv"
-UseQuotes AsNeeded
を使うことでクォートが取り除けます‥‥が、これはPowerShell 7からです。
$srcs | convertto-csv -NoTypeInformation -Delimiter "," | % {$_ -replace '"',''} | Set-Content "destination.csv"
そうでない場合は、convertto-csv
で一度テキストに変換してダブルクォートを取り除きSet-Content
で保存するような手しか取れ無いようです。もちろん、一括置換なので、データの中身のダブルクォートや改行混じりのデータを持つCSVは壊れます。
あと必要なときに加筆予定
独学のため正確でない可能性があります。
(っ・x・)っ きゅ