PowerShell で csv を扱う方法まとめ

本記事は PowerShell Advent Calendar 2019 の 10 日目です。

PowerShell で csv 形式のデータを取り扱うための基礎知識についてまとめてみました。

# 動作環境

Windows 10 にデフォルトで入っているバージョン（PowerShell v5.1）で動作確認してます。
PowerShell 6 だと一部の機能が動作しないですが、ほぼ同じように実行可能です。（たぶん、`Out-GridView` が使えないくらい）
いずれ正式リリースされる PowerShell 7 ではきっと全て上手く動きます。

# サンプルデータについて

こちらのサンプルデータを使います。
動作確認したい場合はこちらのファイルを `UTF-8` で保存してください。

```powershell:products.csv
"product_code","product_name","price"
"POSH00001","鉛筆","100"
"POSH00002","消しゴム","50"
"POSH00003","ノート","110"
"POSH00004","定規","90"
"POSH00005","蛍光ペン","70"
```

# csv 関連のコマンドレットの概要

まず、 csv ファイルや csv 形式の文字列と PowerShell オブジェクトとで変換を行うコマンドレットについて
主な機能を以下の表でまとめました。

PowerShell で csv データを操作する場合は、この 4 つのコマンドのどれかが入り口となります。

<table>
  <tr>
    <th rowspan=2>Cmdlets</th>
    <th rowspan=2>Input</th>
    <th rowspan=2>Output</th>
    <th colspan=4>Parameters</th>
  </tr>
  <tr>
    <th>Delimiter</th>
    <th>NoTypeInformation</th>
    <th>Header</th>
  </tr>
  <tr>
    <td><strong>ConvertFrom-Csv<strong></td>
    <td>文字列</td>
    <td rowspan=2>PsCustomObject</td>
    <td rowspan=4>各値を区切るための区切り文字</td>
    <td rowspan=2>-</td>
    <td rowspan=2>ヘッダー文字列を配列で指定</td>
  </tr>
  <tr>
    <td><strong>Import-Csv<strong></td>
    <td>ファイル</td>
  </tr>
  <tr>
    <td><strong>ConvertTo-Csv<strong></td>
    <td rowspan=2>PSCustomObject</td>
    <td>文字列</td>
    <td rowspan=2>型情報を付加しない場合に指定</td>
    <td rowspan=2>-</td>
  </tr>
  <tr>
    <td><strong>Export-Csv<strong></td>
    <td>ファイル</td>
  </tr>
</table>

簡潔にいうと、ファイル操作するときは、 `Import-Csv` と `Export-Csv` を使い、
csv 形式の文字列として扱いたいときは `ConvertFrom-Csv` と `ConvertTo-Csv` を使います。

# 各コマンドレットの使用例

## `Import-Csv` と `Export-Csv`

以下、 `Import-Csv` で csv ファイルから PSCustomObject に変換するサンプルです。
`# TypeInfomation` （後述）が記載されていない csv ファイルは標準で PSCustomObject に変換されます。
　

```powershell
$products = Import-Csv .\products.csv -Encoding UTF8
$products | Format-Table

#=>product_code product_name price
#=>------------ ------------ -----
#=>POSH00001    鉛筆           100
#=>POSH00002    消しゴム         50
#=>POSH00003    ノート          110
#=>POSH00004    定規           90
#=>POSH00005    蛍光ペン         70

$products[1].product_name
#=>消しゴム
```

今度はその逆で、 PSCustomObject から csv ファイルを出力するサンプルです。

```powershell
$products = @(
  [pscustomobject]@{
    product_code = 'TEST00001'
    product_name = 'ボールペン'
    price = 100
  },
  [pscustomobject]@{
    product_code = 'TEST00002'
    product_name = '消しゴム'
    price = 50
  }
)

$products | Export-Csv -NoTypeInformation products.csv -Encoding UTF8

cat -Encoding UTF8 .\products.csv
#=>"product_code","product_name ","price "
#=>"TEST00001","ボールペン","100"
#=>"TEST00002","消しゴム","50"
```

ちなみにこのやり方だと PowerShell 独自の書き方になるため、
JSON みたいに共通のフォーマットから PSCustomObject を生成できた方が便利じゃね、という場合には以下のようなやり方もあります。

```powershell
$products = @'
[
  {
    "product_code": "TEST00001",
    "product_name ": "ボールペン",
    "price ": 100
  },
  {
    "product_code": "TEST00002",
    "product_name ": "消しゴム",
    "price ": 50
  }
]
'@ | ConvertFrom-Json

$products | Export-Csv -NoTypeInformation products.csv -Encoding UTF8

cat -Encoding UTF8 .\products.csv
#=>"product_code","product_name ","price "
#=>"TEST00001","ボールペン","100"
#=>"TEST00002","消しゴム","50"
```

## `ConvertFrom-Csv` と `ConvertTo-Csv`

これまでファイルへの入出力を伴った csv 操作でしたが、
単純に文字列と PowerShell オブジェクトへの相互変換も同じような書き方で実現できます。

```powershell
# csv 文字列から PowerShell オブジェクトへ変換
$products = @'
"product_code","product_name ","price "
"TEST00001","ボールペン","100"
"TEST00002","消しゴム","50"
'@ | ConvertFrom-Csv

$products
#=>product_code product_name price
#=>------------ ------------ -----
#=>POSH00001    鉛筆           100
#=>POSH00002    消しゴム         50

# PowerShell オブジェクトから csv 文字列に変換
$products | ConvertTo-Csv -NoTypeInformation
#=>"product_code","product_name ","price "
#=>"TEST00001","ボールペン","100"
#=>"TEST00002","消しゴム","50"
```

# 各オプションについて

## `NoTypeInformation` オプション

`ConvertTo-Csv` や `Export-Csv` で PowerShell オブジェクトから csv 形式の文字列に変換する場合、
デフォルトでは先頭行に csv データの元となったクラス情報が `#TYPE` にコメントアウトされる形で記載されるようになります。

ただ、PowerShell 6 以降は指定してもしなくても `#TYPE` が付かないようになったみたいです。
（もう `NoTypeInformation` オプションの意味がないような。。）

そのため、 PowerShell 6 以降では気にする必要はないですが、 PowerShell 5 だと先頭行に付加された `#TYPE` によって、
PowerShell 以外からパースして csv ファイルを読み込むときに無効なフォーマットとしてエラーになるケースがあるため、
常に指定使った方が無難です。[^1]
[^1]: むしろ、デフォルトでは `WithTypeInformation` オプションとかにしないと型情報記載されないようにしてほしかったり。

この `#TYPE` という記載ルール、てっきり型情報を書いておけば、再度 `Import-Csv` などで読み込んだ際の
デシリアライズ用にあるものだと思ってたのですが、ちょっと違うみたいです。

```powershell
cd ~\AppData
ls | Export-Csv appdata.csv -Encoding UTF8

cat .\appdata.csv
#=>#TYPE System.IO.DirectoryInfo
#=>"PSPath","PSParentPath","PSChildName","PSDrive","PSProvider",...
#=>...

$appdata = Import-Csv .\appdata.csv -Encoding UTF8
$appdata | Get-Member
#=>TypeName: CSV:System.IO.DirectoryInfo
#=>Name              MemberType   Definition
#=>----              ----------   ----------
#=>CreationTime      NoteProperty string CreationTime=2017/10/30 8:01:14
#=>Exists            string Exists=True
```

このように、確かに `Get-Member` すると `CSV:System.IO.DirectoryInfo` となりますが、
フィールドは全て string 型となってしまっています。

ネット上にも有用に使っているサンプルコード見当たらないですし、
シンプルに PowerShell 5 では `-NoTypeInformation` を付けて使用し、
PowerShell 6 以降では無視して使っていけば特に支障はなさそうです。

## `Delimiter` オプション

区切り文字を変更したい場合は、 `-Delimiter <char>` オプションで変更できます。
デフォルトはもちろん、 カンマ（`,`）になっていますが、例えばタブ区切りにしたい場合は以下のように使います。

```powershell
$products = Import-Csv .\products.csv -Encoding UTF8
$products | ConvertTo-Csv -Delimiter "`t"

#=>"product_code"  "product_name"  "price"
#=>"POSH00001"     "鉛筆"  "100"
#=>"POSH00002"     "消しゴム
#=>...
```

注意点：
　・ PowerShell のエスケープ文字は バッククォート（\`）で、バックスラッシュ（\）ではない。
　・ エスケープシーケンスを区切り文字にしたい場合は ダブルクォート（`""`）で囲むこと。

## `Header` オプション

読み込み対象となる csv 形式のデータにヘッダー行がない場合は PowerShell 側で指定する必要があります。
やり方は単純で、カラム順に合わせて配列を `-Header <string[]>` に渡すだけです。

```powershell
$products_csv = @'
"POSH00001","鉛筆","100"
"POSH00002","消しゴム","50"
"POSH00003","ノート","110"
"POSH00004","定規","90"
"POSH00005","蛍光ペン","70"
'@

$products_csv | ConvertFrom-Csv -Header @('product_code', 'product_name', 'price')
```

ちなみに単純に 1, 2, 3, ... のような列番号を連番で付けたのでよい場合は以下のように書けます。

```powershell
$products_csv | ConvertFrom-Csv -Header @(1..3)
```

# 読み込んだ csv データの確認方法

csv ファイルから生成した PowerShell オブジェクトの値を確認したい場合、
だいたいは表形式「`Format-Table`（Alias: `ft`）」かリスト形式「`Format-List`（Alias: `fl`）」、
または、GUI 画面「`Out-GridView`（Alias: `ogv`）」を使うと便利です。

## 表形式「`Format-Table`（Alias: `ft`）」：csv 全体のデータを確認したい場合

カラム数がそこまで多すぎない場合は有用です。
カラムが多すぎると省略されてしまうため、全カラム確認したい場合は不向きかと思います。

```powerhsell
$products | ft
```

## リスト形式「`Format-List`（Alias: `fl`）」：1 レコードの詳細を確認したい場合

カラム数が多いときに便利です。
表示対象となるレコード数が多いと見づらくなります。

```powerhsell
$products | fl
```

## GridView(GUI) 表示「`Out-GridView`（Alias: `ogv`）」：レコード数、カラム数が多い場合

※ GridView は PowerShell 5 、または、 PowerShell 7 以降で使用可能です。

読み込んだ csv ファイルの内容を GUI で表示させることができます。
ただ、横スクロールができないなど、そこまで作りこまれた Viewer ではないので常にこれ使うべきという品物でもないです。

```powershell
$products | ogv
```

ちなみに、GridView を使うと GUI 操作による行の絞り込みも可能です。

```powershell
$low_price_products = $products | ogv -OutputMode Multiple
```

`Out-GridView` コマンドレットに `-OutputMode Multiple` を指定することで、
表示された GridView のレコードを選択し、「OK」ボタンを押すことで行を抽出することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114728/8d23893c-6942-b927-daa1-9c98b81d4e37.png)

```powershell
$low_price_products | ft

#=>product_code product_name price
#=>------------ ------------ -----
#=>POSH00002    消しゴム         50
#=>POSH00004    定規           90
#=>POSH00005    蛍光ペン         70
```

あまり使ったことないですが、一応紹介まで。

# メタ情報を得る

## カラムごとの型情報を取得する

ファイル・文字列から読み込んだ csv データのカラムを確認したい場合は `Get-Member` コマンドレットで取得可能です。
PowerShell オブジェクトに変換されたそれぞれのカラムは全て string 型で保持されます。

```powershell
$products | Get-Member -MemberType NoteProperty | select Name, Definition

#=>Name         Definition
#=>----         ----------
#=>price        string price=100
#=>product_code string product_code=POSH00001
#=>product_name string product_name=鉛筆
```

## 型変換

全てのカラムが string のままだと不便なので、適切な型へ変換したいケースもあります。
その場合は以下のように書くと変換可能です。

```powershell
$products = $products | % {$_.price = [int]($_.price); $_}
$products | Get-Member -MemberType NoteProperty | select Name, Definition

#=>Name         Definition
#=>----         ----------
#=>price        System.Int32 price=100
#=>product_code string product_code=POSH00001
#=>product_name string product_name=鉛筆
```

# よく遭遇するトラブルへの対処法

## 文字化けする

文字コードについて、ここまで共通して `-Encoding UTF8` をつけてサンプルコード書いてきましたが、
実はこの文字コードの挙動について PowerShell 5 とそれ以降のバージョンで差異が生じています。

・ PowerShell 5 の場合
　→ 指定しない場合は（たぶん）ASCII で出力される。 SJIS を使いたい場合は Default を指定する。（日本語OSのみらしい）
・ PowerShell 6 の場合
　→ 未指定の場合は UTF-8 となる。 SJIS を使いたい場合は oem を指定する。（日本語OSのみらしい）

以下、 products.csv ファイルが SJIS で保存されていた場合のファイルの読み込み方法です。

```powershell
Import-Csv .\products.csv

#=>product_code product_name price
#=>------------ ------------ -----
#=>POSH00001    ���M         100
#=>POSH00002    �����S��     50
#=>POSH00003    �m�[�g       110
#=>POSH00004    ��K          90
#=>POSH00005    �u���y��     70

# PowerShell 5 だと Default で読み込めます。
Import-Csv -Encoding Default .\products.csv

#=>product_code product_name price
#=>------------ ------------ -----
#=>POSH00001    鉛筆         100
#=>POSH00002    消しゴム     50
#=>POSH00003    ノート       110
#=>POSH00004    定規         90
#=>POSH00005    蛍光ペン     70

# PowerShell 6 以降だと oem で読み込みます。
Import-Csv -Encoding oem .\products.csv

#=>product_code product_name price
#=>------------ ------------ -----
#=>POSH00001    鉛筆         100
#=>POSH00002    消しゴム     50
#=>POSH00003    ノート       110
#=>POSH00004    定規         90
#=>POSH00005    蛍光ペン     70
```

## The member "xxxx" is already present.

読み込んだ csv ファイルのヘッダー行に同じカラム名を持つものが存在していた場合に生じるエラーです。
カラム名を直すか、 `-Header` オプションで無理やり読み込むかで解消できます。

```powershell
ipcsv : The member "xxxx" is already present.
At line:1 char:1
+ ipcsv '.\duplicated_headers.csv'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [Import-Csv], ExtendedTypeSystemException
+ FullyQualifiedErrorId : AlreadyPresentPSMemberInfoInternalCollectionAdd,Microsoft.PowerShell.Commands.ImportCsvCommand
```