【PowerShell】を使ってCSVファイルを編集する(理論編)

# PowerShellを使ってCSVを編集する(理論編)

2023/10 MSの発表により将来的にWindowsからVBScriptが廃止になるとのことで
その後継はPowerShellということになるそうです。
PowerShellがVBScriptより使いやすいのかはどうかは思案の余地があるとしても、
PowerShellのCSV加工は凄く便利だったのでゼロから勉強しながら使ってみることにします。

この記事は、それについてほうぼうで調べてきてものを纏めたもので、
他のプログラミング言語には触れている人レベルを想定します。

**実際に動作するサンプルについては[実用編](https://qiita.com/Tabito/items/7eb35d3df0f874d22376)へどうぞ**

:::note warn
* Windows 11 & PowerShell 5.1で検証しています。
:::

## コマンド：Import-Csv

PowerShellにおいてはImport-CsvコマンドでCSVファイルを読み取ることができます。
特筆点として、低め水準の言語で手でCSV解析を書いた場合、たいてい面倒になって諦めることになる「改行を含む文字データ」が正しく取得できるのが便利です。もちろんダブルクォートやそのエスケープにも対応してくれます。

``` powershell
# 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になります」ということになりそうです。

<details><summary>古いPSでのShift-JIS指定</summary>

ただし、古いPowerShellのバージョンでは``default``を使う、という記事もあります。
一方で、最近のバージョンでは`default`が``utf8``になっているとの話もあります。
結論としては、oemでダメだったらdefaultという感じのようです。
（ちなみに私の環境では、defaultもoemもどちらもShift-JISになりました）
他、厳密にやりたい場合は、エンコーディングコードを指定する方法もあるようです。

</details>
<details><summary>戻り値はレコードの配列とは限らない</summary>

格納されるCSVデータは、PSCustomObjectと言うオブジェクトないしそれの配列です。
PSCustomObjectについて雑に説明すると、プロパティを追加できるクラスみたいなものらしいのですが、当座は１レコード分が格納される連想配列存在と捉えてOKです。
‥‥そんなことより「オブジェクトないし、それの配列」であることは注意が必要で、つまり、これ、**得られる結果のオブジェクトは、CSVファイルが1レコードしかなかった場合オブジェクトそのもの、複数のレコードある場合は、オブジェクトの配列になります。つまり状況によって型が違います**。幸いにして~~というかややこしいことに~~後述するforeachやExport-CSVなどは、どちらを渡してもイメージ通りに処理してくれるので、この事実はある程度は気にしなくてよいのですが、より込み入った事をやる場合はこの件を気にする必要があります。

</details>

`-Header`指定なしで取り込んだ場合、CSVファイルの1行目の内容がヘッダ項目として扱われます。
もし、CSVファイルにヘッダ項目がない1行目からデータがあるような場合、`-Header`をオプションを指定して、別に項目の列名を設定します。

``` powershell
$srcs = Import-Csv -Encoding utf8 -Path "source.csv" -Header "ID","名前","年齢","コメント"
```

## コマンド：Export-Csv

``` powershell
$dests | Export-Csv -Encoding utf8 -NoTypeInformation -Path "destination.csv"
```
出力。CSVデータ``$dests``をファイルに吐き出します。
エンコーディングはImport-Csvのものと同じものなので割愛。
``-NoTypeInformation``は、1行目に型情報を出力しなくするためのオプションです。
というか無いと勝手に出てしまうので、常にこの呪文を付けるものと思ってください。

## コマンド：ft出力
``` powershell
$csvs | ft
```
Format-Tableの略とかそんなの。これを使うと標準出力に表形式で出してくれます。

~~~
ID   名前     年齢 コメント
--   ----     ---- --------
0001 アリス   18   がんばります
0002 ベティ   15
0003 クリス   17
0004 ディック 21   コメントには...
0005 エリン   22   "エリ"と呼んでください
~~~

デバッグにはとても便利です。

## コマンド：Select-Object

列のコントロールと行のコントロールができます。

### 列の追加と削除と並べ替え

``` powershell
$dests = $srcs | Select-Object -Property ("名前","所属","ID")
```

元が`ID`,`名前`,`年齢`,`コメント`のCSVデータの場合、`名前`と`ID`が入れ替わります。
そして`所属`は空データのまま追加され、`年齢`と`コメント`の列は削除されます。

### 件数による行の絞り込み

``` powershell
## 先頭２レコードだけ
$dests = $srcs | Select-Object -First 2
## 最後２レコードだけ
$dests = $srcs | Select-Object -Last 2
## 最初の２レコードを飛ばして、３レコード目以降を全部
$dests = $srcs | Select-Object -Skip 2
## 最初の１レコードを飛ばして、そこから３レコードだけ
$dests = $srcs | Select-Object -Skip 1 -First 3
```

大量データを処理するときはこれを駆使する感じになりますが、そもそも$srcが全件読んじゃってるので、真の巨大CSVには通用しないのではないかとも思います。

### 重複削除

``` powershell
$dests = $srcs | Select-Object -Unique -Property @("名前")
```

``-Unique``をつけると、重複するデータが削除されます。
この例だと同じ名前は１つにまとまります。

## コマンド：Where-Object

``` powershell
$dest = $srcs | Where-Object {$_."名前" -eq "アリス"}
```
絞り込みを掛けます。``{}``内にある``$_``が実際の$srcsの中にあるレコードそれぞれを意味しており、ここでは、そのレコードの「名前」が「アリス」であるレコードのみを選択します。

<details><summary>PowerShellの比較演算子</summary>

話は逸れますが、PowerShellで比較するときは`==`とかではなく`-ceq`などを使います。
　`-eq`、`-ceq` # 等しい
　`-ne`、`-cne` # 等しくない
cが付いている方はアルファベットの大文字小文字を別の文字として扱います。
逆に言うと`-eq`ではABCとabcが区別されませんのであしからず。

</details>

## コマンド：Sort-Object

WhereがあるならOrderByもあるんじゃ。はい、ありましたよ

``` powershell
#昇順
$dests = $srcs | Sort-Object -Property "年齢"
#降順
$dests = $srcs | Sort-Object -Descending -Property "年齢"
```
ただし、この書き方は文字列比較になるため"100"より"11"の方が小さい扱いになります。
``` powershell
$dests = $srcs | Sort-Object -Property {[int]$_."年齢"}
```
int型に変換して比較したい場合、その書き方はこう。
実は{}内は数式が書けますので確認日-作成日なんかとかやっても計算可能のようです。

## コマンド：New-Object

新規のレコードを作成したい場合は、このようにします。

``` powershell
$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で回すことができます。

``` powershell
foreach ($src in $srcs){
  $src."年齢" = $src."年齢" + "歳";
}
```
と書くと、これだけだとなんの感慨もありませんが‥‥

<details><summary>CSVデータはレコードの配列とは限らない</summary>

先の通り「Import-Csv等で取得されるものは、1件ならオブジェクト、複数件なら配列になっている」わけで``$srcs``は配列のケースと配列でないケースがあります。実は、foreachはどちらも処理してくれていて、走らせてみると上述の$srcはちゃんと1レコードが入ってくれます。

なので。

``` powershell
for ($i=0; $i -lt $srcs.Length; $i++){
  $srcs[$i]."年齢" = $srcs[$i]."年齢" + "歳";
  Write-Host "[Info] :" $srcs[$i]."年齢"
}
```
上とこれは**等価ではない**です。
CSVデータが1レコードしかない場合``$srcs.Length``が**空**を返すので、下例のほうはこのループ句は何もしないで終了します。エラーにもならないので見落としがちです。基本foreachを使いましょう。
ついでに言うと、０件の場合はなぜか``$srcs.Length``はゼロを返します。この時$srcsはnullなのですが、nullにLengthをつけると0になるみたいで。ただこの場合上例も下例にループ句は素通りするので希望通りにはなります。
この辺バージョンにもよりそうですけども。

</details>

## その他TIPSというかメモ

### パラメータでの配列の指定

``` powershell
$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をつけて
``` powershell
$srcs = Import-Csv -Encoding utf8 -Path "source.csv" -Header "ID","名前" | Select-Object -Skip 1
```
とするとCSVファイル1行目のヘッダ行データを捨てられます。

### CSVファイルのダブルクォートを取り除く

取り込み先のシステムでダブルクォートを受けれてくれないシステムがあるかもしれません。
``` powershell
$dests | Export-Csv -UseQuotes AsNeeded -Encoding utf8 -NoTypeInformation -Path "destination.csv"
```
``-UseQuotes AsNeeded``を使うことでクォートが取り除けます‥‥が、これはPowerShell 7からです。

``` powershell
$srcs | convertto-csv -NoTypeInformation -Delimiter "," | % {$_ -replace '"',''} | Set-Content "destination.csv"
```
そうでない場合は、``convertto-csv``で一度テキストに変換してダブルクォートを取り除き``Set-Content``で保存するような手しか取れ無いようです。もちろん、一括置換なので、データの中身のダブルクォートや改行混じりのデータを持つCSVは壊れます。

----

あと必要なときに加筆予定

----
独学のため正確でない可能性があります。
（っ・ｘ・）っ きゅ