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

# PowerShellを使ってCSVを編集する(実践編)

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

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

**個々のコマンドの使い方や注意点は[理論編](https://qiita.com/Tabito/items/c97003b7a2b787ab320d)へどうぞ**

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

## PowerShell起動バッチ

とりま、PowerShellを手で呼び出すのは面倒なので、起動バッチファイルを作ります。
名前はとりあえずCsvConvert.batとしておきます。batなので**SJIS**で保存します。
引数でファイル名を渡しておきましょう。

~~~ Bat 
@ECHO OFF
SETLOCAL
SET BASE_DIR=%~dp0
CD /d "%BASE_DIR%"

SET IN_FILE=source.csv
SET OUT_FILE=destination.csv
powershell -NoProfile -ExecutionPolicy Unrestricted .\CsvConvert.ps1 "%IN_FILE%" "%OUT_FILE%"

TIMEOUT 5 > nul 2>&1
ENDLOCAL
EXIT /b 0
~~~

私の[過去記事](https://qiita.com/Tabito/items/c18cdf33cf82a617bf0e)にあるテンプレートを使用しています。

## サンプルCSVファイル
本記事のsource.csvの内容がこちらになります。

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

以降このサンプルを使います。こいつの文字コードは**UTF-8**です。

## PowerShellスクリプト・ベース
呼び出されるCsvConvert.ps1を作成していきます。
文字コードは**UTF-8**で保存してください。

~~~ PowerShell
Set-PSDebug -Strict
# 引数取得とチェック
$srcFile  = $args[0]
$destFile = $args[1]
Write-Host "[Info] Input :" $srcFile
Write-Host "[Info] Output:" $destFile
if(([string]::IsNullOrEmpty($srcFile)) -or ([string]::IsNullOrEmpty($destFile)))
{
  Write-Host '[Error] Filename is empty.'
  exit
}
## 開始
Write-Host '[Info] Execute Start'
## 前回出力時の変換後のファイルがあれば削除する
if(Test-Path $destFile){
  Remove-Item -Path $destFile
}
## CSV取込
$srcs = Import-Csv -Encoding utf8 -Path $srcFile
## #############################################################################
$dests = $srcs
## #############################################################################
## CSV保存
$dests | Export-Csv -Encoding utf8 -NoTypeInformation -Path $destFile
$dests | ft
Write-Host '[Info] Execute End'
~~~

実用ということで、引数が空だったら抜ける処理と、失敗時に前のファイルが残らないようにしておきます。
本体は``## CSV取込``と``## CSV保存``の部分です。

~~~
ID   名前     年齢 コメント
--   ----     ---- --------
0001 アリス   18   がんばります
0002 ベティ   15
0003 クリス   17
0004 ディック 21   コメントには...
0005 エリン   22   "エリ"と呼んでください
~~~
これは標準出力(ft)側。別にdestination.csvが生成されます。

<details><summary>destination.csv</summary>

```
"ID","名前","年齢","コメント"
"0001","アリス","18","がんばります"
"0002","ベティ","15",""
"0003","クリス","17",""
"0004","ディック","21","コメントには
力を込めんと"
"0005","エリン","22","""エリ""と呼んでください"
```
</details>


ファイル側では各項目がダブルクォートされます。
ポイントは4行目のコメントで、出力では省略されているものの、ファイル取込・保存ではちゃんと処理されていることが分かります。

## 一部の項目を加工する

※上のPowerShellスクリプト・ベース内の``$dests = $srcs``の置換する形で実装します。

~~~ PowerShell
$dests = $srcs
foreach ($dest in $dests){
  $dest."年齢" = $dest."年齢" + "歳";
}
~~~

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

<details><summary>destination.csv</summary>

```
"ID","名前","年齢","コメント"
"0001","アリス","18歳","がんばります"
"0002","ベティ","15歳",""
"0003","クリス","17歳",""
"0004","ディック","21歳","コメントには
力を込めんと"
"0005","エリン","22歳","""エリ""と呼んでください"
```
</details>

ループを回しながら処理を行います。

## 改行を取り除く

※上のPowerShellスクリプト・ベース内の``$dests = $srcs``の置換する形で実装します。

改行文字を取り除きます。

~~~ PowerShell
## そのまま複製
$dests = $srcs
## 改行を取り除く処理
foreach ($dest in $dests){
  foreach ($col in @("名前","コメント")){
    if($dest.$col){
      $dest.$col = ($dest.$col).Replace("`r`n", " ")
      $dest.$col = ($dest.$col).Replace("`n", " ")
      $dest.$col = ($dest.$col).Replace("`r", " ")
    }
  }
}
~~~

レコードごとに、各ヘッダ項目ごとにReplaceを掛けます。
（PowerShellでの改行コードは `` `n ``と表現されます）

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

<details><summary>destination.csv</summary>

```
"ID","名前","年齢","コメント"
"0001","アリス","18","がんばります"
"0002","ベティ","15",""
"0003","クリス","17",""
"0004","ディック","21","コメントには 力を込めんと"
"0005","エリン","22","""エリ""と呼んでください"
```
</details>

改行コードが無くなれば、他の言語でも安全に取り込めますね。

## 新しいCSVを作り直す

※上のPowerShellスクリプト・ベース内の``$dests = $srcs``の置換する形で実装します。

元々のCSVデータから各レコードを取り出し、
空っぽのレコードを作成してデータを設定して登録していく処理です。

~~~ PowerShell
## 出力結果の項目名を配列で定義
$destHeaders = @("ID","名前","年齢","出身")
## 出力結果を配列で準備
$dests = @()
## 変換元のデータを１レコードごとに処理する
foreach ($src in $srcs){
  if($src."コメント" -cne ""){
    # 空のレコードを作成
    $dest = New-Object PSObject | Select-Object $destHeaders
    # 値をセットしていく
    $dest."ID" = $src."ID"
    $dest."名前" = $src."名前"
    $dest."年齢" = [int]($src."年齢") + 5
    if($dest."名前" -ceq "アリス"){
      $dest."出身" = "オーサカ"
    }else{
      $dest."出身" = "トーキョー"
    }
    # レコードを結果データに登録
    $dests += $dest
  }
}
~~~

コメントのある人のみ「年齢」に+5して「出身」を作成したものを登録するという出力。

~~~
ID   名前     年齢 出身
--   ----     ---- ----
0001 アリス     23 オーサカ
0004 ディック   26 トーキョー
0005 エリン     27 トーキョー
~~~

<details><summary>destination.csv</summary>

```
"ID","名前","年齢","出身"
"0001","アリス","23","オーサカ"
"0004","ディック","26","トーキョー"
"0005","エリン","27","トーキョー"
```
</details>


変換後が変換前とかなり異なる場合は、こうした方がスッキリするかもしれません。

### 新しいCSVを作り直す２

似たようなことがforeach文ひとつで出来きます。
速度はこちらが速いのでこちらが推奨であると思う。
~~~ PowerShell
## 変換元のデータを１レコードごとに処理したものを配列に保存
$dests = foreach ($src in $srcs) {
    $area = "トーキョー"
    [PSCustomObject]@{
        "ID"   = $src."ID"
        "名前" = $src."名前"
        "年齢" = [int]($src."年齢") + 1
        "出身" = $area
    }
}
~~~

## マスタデータ参照

source.csvに、master.csv の内容をくっつけたいとします。
~~~
ID,出身
0001,"オーサカ"
0003,"ナゴヤ"
0003,"サッポロ"
~~~

※上のPowerShellスクリプト・ベース内の``$dests = $srcs``の置換する形で実装します。

~~~ PowerShell
## マスタ持込
$masters = Import-Csv -Encoding utf8 -Path "master.csv"
## 出力結果の項目名を配列で定義
$destHeaders = @("ID","名前","出身")
## 項目の抽出
$dests = $srcs | Select-Object $destHeaders
## データを１レコードごとに処理する
foreach ($dest in $dests){
  # マスタ取得
  $master = $masters | Where-Object {$_."ID" -ceq  $dest."ID" } | Select-Object -First 1
  if($master){
    $dest."出身" = $master."出身"
  }
}
~~~

~~~
ID   名前     出身
--   ----     ----
0001 アリス   オーサカ
0002 ベティ
0003 クリス   ナゴヤ
0004 ディック
0005 エリン
~~~

<details><summary>destination.csv</summary>

```
"ID","名前","出身"
"0001","アリス","オーサカ"
"0002","ベティ",
"0003","クリス","ナゴヤ"
"0004","ディック",
"0005","エリン",
```
</details>

マスタに0003が２つありますが、`Select-Object -First 1`で先頭１行に絞っています。
このSelectがないと``0003 クリス   {ナゴヤ, サッポロ}``という配列で表示され、
出力ファイルでは``"0003","クリス","System.Object[]"``と出てきてしまいます。

----

というわけで理論編と実践編でまとめてみました。
基本的にはこれの亜種で大抵のCSVファイルの加工がBAT+PowerShellで変換ができると思います。
PorwerShellのメンターの方々、ここをこうした方がいいというのがありましたらご教授ください。

----

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

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