Powershell入門 - 12.ファイルの参照・更新

#### 注意
2022/10/22以前に参考用のファイルをダウンロードした方は、[こちら](https://github.com/daifukusan/powershell)からファイルを再度ダウンロードしてください。

---

これまでの内容は、主にフォルダ階層に注目した操作を中心に扱ってきました。
つまり、エクスプローラを使ってファイルを確認したときに変化が分かる部分（ファイルの場所やファイルの名称）の変更が中心です。

今回は、ファイルの中身を確認する・更新するという内容がメインになります。  
新しいコマンドレットもいくつか登場するので、しっかりと覚えていきましょう。

# 1. ファイルの参照・更新
まずは、一般的なファイル（テキスト形式のファイル)の参照・更新についてみていきます。

## 1.1. ファイルの参照(Get-Content)
まず、何度か使ったことのある`Get-Content`を確認しましょう。
こちらの使い方は以前説明した通りですが、引数としてファイルのパスを指定することで、ファイルの中身を確認できます。
今回は、日本語のファイルを使っているので、文字コードも指定します。

```powershell
Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8
```
まずはこのコマンドレットをもう少し詳しく解説するところから始めます。

## 1.1.1. ファイルの中身を一行ずつ処理する
以下の処理を見てみましょう。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8
Write-Host $lines[0]
```

Get-Contentの結果は、**テキストファイルの中身を1行ずつ格納した配列**となります。
そのため、上記の例を実行すると、先頭行のみ出力されることが分かります。

この行の特定の1文字や部分文字列を取得したい場合は、さらに添え字を指定します。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8
Write-Host $lines[0][5]      # 先頭行の5文字目だけ表示
Write-Host $lines[0][0..2]   # 先頭行の0~2文字目の文字列を表示
```

例えば、すべての行の0~2文字目だけ抽出したい場合、for文と組み合わせて以下のように書けます。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8
#for文を使った場合
for($i=0; $i -lt $lines.Count; $i++){
    Write-Host $lines[$i][0..2]
}

# foreach文を使った場合
foreach($line in $lines){
    Write-Host $line[0..2]
}
```

では、パイプラインとForEach-Objectコマンドレットを使った場合はどう書けるでしょうか？  
パイプラインにも慣れてほしいので、是非挑戦してみましょう。

---
#### 例題1.
上記の例(すべての行の0~2文字目だけ抽出)をパイプラインとForEach-Objectコマンドレットを使って実装しなさい。

---

## 1.1.2. ファイルの中身を一括処理する。
`Get-Content`コマンドレットは、`-RAW`オプションをつけることで全ての行を1つの変数に丸ごと格納することが出来ます。
以下の例を確認しましょう。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8 -RAW
Write-Host $lines[0]
```
一度確認した処理によく似ていますが、`-RAW`オプションがついています。  
`-RAW`オプションなしの場合、`$lines[0]`の出力結果は「0行目の文字列すべて」でした。  
今回のケースでは、すべての行を一括で取得しているため、$lines[0]の出力結果は「0文字目」となります。  

ファイルを一括で取得するメリットとしては「ファイルの走査をまとめて行える」点に尽きると思います。  
例えば、以下のような用途が考えられます。
- 住所録のファイルから、郵便番号のみを取得したい
- 小説の中から、特定の文字列(ある登場人物の名前)などがどこに出てくるか調べたい。
- ソースコードの中から変数`$hoge`をすべて`$huga`に置き換えたい

これらは1行ずつ処理するより、ファイル全体を読み込んで一括処理した方が短く書くことが出来ます。
実際に例題で試してみましょう。

---
#### 例題2.
以下の例は、sample11.txtに含まれるカンマ(,)をすべて半角スペース4つ分に置き換える処理を1行ずつ実行している。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8
foreach($line in $lines){
    $line.Replace(",","    ")
}
```

Get-Contentに`-RAW`オプションをつけて、foreach文を使わずに処理しなさい。

---

# 1.2. ファイルの更新
続いて、ファイルの更新(新規作成も含む)を見ていきましょう。  

## 1.2.1. 上書き更新(Set-Content)
ファイルの作成には、`Set-Content`を使います。  

```powershell
Set-Location D:\powershell\data\sampleFolder3                 # コマンドが長くなるので、カレントディレクトリを変更

Set-Content -Path set_test1.txt -Value "test" -Encoding UTF8 # -Valueで書き込む文字を指定
$line2 | Set-Content -Path  set_test2.txt -Encoding UTF8     # パイプラインで書き込む文字列を指定(推奨)
```

Set-Contentの2つの例を紹介しましたが、下のパイプラインの方が良く見る気がします。
こちらで慣れておくとよいでしょう。
また、文字コードの指定を忘れると文字化けの原因になりますので、忘れずに行ってください。

Set-Contentの使い方のサンプルを2つほど紹介するので、こちらでイメージをつかんでください。

#### サンプル1. あるファイルの小文字を大文字に変更して、別ファイルに保存
```powershell
Set-Location D:\powershell\data\sampleFolder3 
$lines = Get-Content sample12.txt -Encoding UTF8 -RAW
$lines.ToUpper() | Set-Content -Path sample12_2.txt -Encoding UTF8
```

#### サンプル2. あるコマンドレットの処理結果をファイルに保存
```
Set-Location D:\powershell\data\sampleFolder3 
Get-Date | Set-Content -Path date.txt -Encoding UTF8 # Get-Dateは日時を取得するコマンドレット
```

----
#### 例題3.
例題2を修正し、結果をsampleFolder3内のsample11_2.txtに保存しなさい。

## 1.2.2. ファイルの追記更新(Add-Content)
例題2に書かれている1行ずつ処理するタイプの例を見てみます。
```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8
foreach($line in $lines){
    $line.Replace(",","    ")
}
```
上記の例と`Set-Content`を組み合わせた処理を考えます。
以下のようになるはずです。
```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8
foreach($line in $lines){
    $line.Replace(",","    ") | Set-Content -Path sample11_3.txt -Encoding UTF8
}
```
実行して結果を確認すると、最後の1行分しかファイルに保存されていません。  
これは、Set-Contentが**上書きモード**で実行されるため、1行ずつ書き込まれるたびに前の出力結果を上書きして消してしまうせいです。
1行ずつ書き込みたい場合には、`Add-Content`を使います。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8
foreach($line in $lines){
    $line.Replace(",","    ") | Add-Content -Path sample11_4.txt -Encoding UTF8
}
```
`Add-Content`は**追記モード**で書き込みを行います。  
つまり、新しい書き込みは既存のファイルの中身の下に書かれます。  

`Add-Content`はログの書き込みなどでも重宝しますので、覚えておきましょう。

---
#### 例題4.
Add-Contentを使って、以下の処理結果を画面出力ではなくファイルに書き込むように修正しなさい。  
書き込むファイルは、sampleFolder3の中のadd_test.txtとする。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample11.txt -Encoding UTF8

# foreach文を使った場合
foreach($line in $lines){
    Write-Host ($line[0..2] -join "")
}
```
---

# 2. CSVファイルの参照・更新
続いてCSVファイルの中身を確認する方法を学びます。  
## 2.1. CSVファイルの参照
まずはGet-Contentを使って、CSVファイルを開いていきます。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample10.csv -Encoding UTF8

# foreach文を使った場合
foreach($line in $lines){
    Write-Host $line
}
```

出力結果を見ると分かりますが、1行目がヘッダ、2行目以降がデータになります。  
カンマ区切りのデータのままでは扱いづらいので、さらにデータを分割してみます。

```powershell
$lines = Get-Content D:\powershell\data\sampleFolder3\sample10.csv -Encoding UTF8
# foreach文を使った場合
foreach($line in $lines){
    Write-Host $line.Split(",")
}
```

この時点である問題が起こっています。
例えば沖縄の総面積は、もともと"228,114"でした。
このカンマ区切りは桁を表すためのもので、データの区切りを示してはいません。
ですが、`Split`メソッドはそんなことはお構いなしにデータを分割してしまいました。

このようにCSVファイルを普通のテキストファイルと同様に扱おうとすると、少し面倒な手間が発生します。
そのため、CSVファイルの読み込み専用のコマンドレットが用意されています。
それが`Import-CSV`です。

早速使ってみましょう。
コマンドレットに渡す引数はGet-Contentと同じで大丈夫です。

```powershell
$lines = Import-CSV -Path D:\powershell\data\sampleFolder3\sample10.csv -Encoding UTF8

# foreach文を使った場合
foreach($line in $lines){
    Write-Host $line
}
```

実行すると、`@{・・・}`のような形式でデータが出力されます。  
これは、**連想配列**と呼ばれるデータ構造で、PHPの連想配列、Pythonの辞書、JavaのMapと同じようなものです。  
`Import-CSV`を使うと、ヘッダ行の各列を自動で解釈し、各行のデータの値を列名とセットで保存してくれます。
このようにデータを構造化して格納してくれるため、各データにアクセスするのが容易になります。  
例えば、データの2行目(ヘッダ行を除いて2行目)の"地域"列の値は以下のように調べられます。

```powershell
$lines = Import-CSV -Path D:\powershell\data\sampleFolder3\sample10.csv -Encoding UTF8
$lines[2].地域
```

Get-Contentでは、さきほど説明した通りの問題も発生しやすく、何よりCSVの操作に最適化されていません。
CSVファイルを扱う場合は、こちらを使うようにしましょう。

---
#### 例題5.
和歌山県の最高気温を出力する処理を書きなさい。

※余裕のある人向け: パイプラインを使って書きなさい。

---

## 2.2. CSVファイルの更新
CSVファイルの更新は`Export-CSV`で行います。
これは、Powershellの処理結果を、CSV形式で保存したい場合などによく使います。  

Export-CSVは以下のように使います。

```powershell
Get-Command -CommandType Cmdlet |
    Export-Csv -Path export_test1.csv -Encoding UTF8
```

これは、Get-Commandを実行した結果を、CSV形式で保存する処理です。
しかし、このままだと可読性に問題があります。
CSVをExcelなどで開いてみましょう。

コマンドレットの結果をExport-CSVに直接渡すと、その実行結果のオブジェクトがもつ全パラメータが列として保存されます。  
その結果、本来確認したい項目以外も出力されてしまうため、可読性が悪化してしまいます。

そこで、通常は`Select-Object`パイプラインを間に挟むことが多いです。

```powershell
Get-Command -CommandType Cmdlet |
    Select-Object -Property Name, CommandType, Version, Verb, Noun |
        Export-Csv -Path export_test1.csv -Encoding UTF8 
```

また、Powershellのバージョンが6.0より前の場合、出力されたファイルの1行目に`#TYPE`から始まるコメント行が挿入されるようです。  
この場合、`-NoTypeInformation`オプションで回避できます。

```powershell
Get-Command -CommandType Cmdlet |
    Select-Object -Property Name, CommandType, Version, Verb, Noun |
        Export-Csv -Path export_test1.csv -Encoding UTF8 -NoTypeInformation
```

また、Export-CSVの書き込みは通常**上書きモード**ですが、`-Append`オプションをつけることで、書き込みモードを**追記モード**に変更可能です。

```powershell
# コマンドレットのみを出力
Get-Command -CommandType Cmdlet |
    Select-Object -Property Name, CommandType, Version, Verb, Noun |
        Export-Csv -Path export_test2.csv -Encoding UTF8 -NoTypeInformation -Append

# ファンクションのみを出力(追記)
Get-Command -CommandType Function |
    Select-Object -Property Name, CommandType, Version, Verb, Noun |
        Export-Csv -Path export_test2.csv -Encoding UTF8 -NoTypeInformation -Append
```

---
#### 例題6.
以下のコマンドレットの出力結果をCSV形式で保存しなさい。  
出力先はsampleFolder3で、export_test3.csvという名前とすること。

```powershell
Get-ChildItem -Path D:\powershell\data -Recurse
```

CSVに出力する列は、以下の5項目とする。
- Name
- FullName
- Length
- LastWriteTime
- Mode

---

Prev : [11.プロセスの操作](./73421db2e8f2244bbc75)
Next : [13.正規表現①](./6953cbe20fff49d83fc4)

[目次へ戻る](./926b8053a031539437a3)