ファイルの読込

概要

PowerShellにはファイル読込だけでもいろいろあります。

  • Get-Content ファイル名
  • type ファイル名
  • cat ファイル名
  • Import-Csv -Path ファイル -Delimiter “,”
  • New-Object System.IO.StreamReaderによる読込

色々やってみましたが、データが少ない場合はGet-Contentが使い勝手がよいです。
typeとcatはそれぞれGet-ContentのAliasなので効率は同じです。StreamReaderは.NET Frameworkのオブジェクトを生成して処理しているのでソースの記述量が増えます。たいていの場合はGet-Contentで事足ります。

CSVファイルなどちゃんと整形されているファイルでかつデータが少ない場合はImport-Csvのほうが項目単位で値が取得できるので使い勝手がよいです。

大容量(100MByteを超えるあたり)のファイルは上記2つは使わないほうがよいです。
StreamReader一択です。
なぜかというと、PowerShellはパイプ処理が遅い、そしてメモリを大量に使うからです。
Get-Contentでファイルから1行読み取り、読み取ったデータをパイプで次の処理に渡す場合、PowerShellはいったん読み取ったデータをオブジェクトにして次のパイプの処理に渡します。
このオブジェクトに変換する作業がとてつもなく遅く、かつメモリを食いつぶしていきます。
メモリに関してはパイプ処理が全量終わるまで(レコードをすべて読み取るまで)解放されません。

なので、大容量のファイルの場合は、StreamReaderを使ってください。

Get-Content

よく使うのが、

Get-Content ファイル名 | ForEach-Object {
  $data = $_
  ・・・
}

のようにGet-Contentでファイルを読み、ファイルのレコード1行($_に格納される)毎にForEach-Objectで繰り返し処理を行う書き方です。

単純に全行取得する場合は

$data = Get-Content ファイル名

とすればよいですが、ファイルに複数行のデータが格納されている場合は、$dataは配列で返却されます。1行しかデータが格納されていない場合は$dataは通常の変数で返却されます。

レコードの件数によっていちいち型が変わるのはうっとうしいので、以下のように変数の型を事前に定義しておけば返却される型も固定されます。

[array]$data1 = @()
[string]$data2 = ""

$data1 = Get-Content ファイル名  #常に配列で返却される
$data2 = Get-Content ファイル名  #常に文字列で返却される

Get-Contentを使ってはいけないケース

バイナリデータを1バイトずつ処理するためにGet-Contentを使ってはいけません。使ってもよいのだけれども遅いです。Get-Contentでバイナリデータを読み込み、ForEach-Objectで回すと、どうやら内部で1バイトずつReadしているようで非常に遅くなります。バイナリデータを1バイトずつ処理したい場合は、New-Object Stream.IO.FileStreamを使用しましょう。

Import-Csv

Import-Csvはタブやカンマやスペースなどの区切り文字付のデータを読み込むのに適しています。特に、CSV形式で作成された設定ファイルを読み込むのに向いていると思います。

C:\abc\Test.txt
ID,NAME
001,太郎
002,次太郎
003,三太郎
[array]$data_list = @()
$data_list = Import-Csv -Path C:\abc\Test.txt -Delimiter "," -Header @("HITONO_ID","HITONO_NAME")
$data_list = $data_list[1..($data_list.length-1)]
foreach($data in $data_list) {
   $id = $data.HITONO_ID
   $name = $data.HITONO_NAME
}

-Headerでヘッダの項目を指定しなければ、ファイルの先頭行がヘッダとして使われ、$data.ID,$data.NAMEなどのように値を参照できます。-Headerを指定した場合は$data.HITONO_IDなどのように指定した値で参照できますが、先頭行もデータとして扱われます。

普通であれば-Headerを指定しないで使えばよいのですが、ヘッダの文字が変わったらプログラムまで変えなければならないのは暗黙のルールすぎて不具合のもとになりそう(ありそうなのがヘッダに位置調整のためにスペースを含めたらプログラムが動かなくなったなど)。またプログラムでいきなり$data.IDとか登場してもなんのことかわからないので、明示的に-Headerを指定したほうがよいと思います。ただし、-Headerを指定すると先頭行もデータとして扱われてしまうので、$data_list = $data_list[1..($data_list.length-1)]で1行目を除外します。

StreamReader

説明はサンプルのコメントを参照のこと。

# 入力のファイルを読み取り、そのまま出力のファイルに書き込む
# なんだか穴を掘って埋めるようなサンプル

$ErrorActionPreference = "Stop"

$Error.Clear()

# 読み取りファイルの文字コード  932はSJIS
$enc_from = [System.Text.Encoding]::GetEncoding(932)

# 書き込みファイルの文字コード
$enc_to = [System.Text.Encoding]::GetEncoding(932)

try {

    # 読み取りファイルオープン
    # なお、StreamReaderは文字コードを指定してテキストファイルを読む(TextReaderの派生クラス)
    # 引数は、 ファイル名、エンコード、バイト順マーク検出、バッファ
    # バイト順マーク検出は、いわゆるBOMを読み取って文字コードを検出するオプション
    # バッファは比較的大きくして実際に処理速度を計測して調整していく 
    $reader = New-Object System.IO.StreamReader("c:\temp\test_input.txt", $enc_from, $false, 20480000)

    # 書き込みファイルオープン
    # 引数は、ファイル名、上書き・追加($falseは上書き)、エンコード、バッファ
    $writer = New-Object System.IO.StreamWriter("c:\temp\test_output.txt", $false, $enc_to, 20480000)

    # レコード数分繰り返す
    while(!($reader.EndOfStream)) {
        # レコードを1件読み取り
        $read_data = $reader.ReadLine()

        # レコードを1件書き込み
        $writer.WriteLine($read_data)
    }
    $reader.Close()
    $reader = $null
    $writer.Close()
    $writer = $null
}
catch {
    $Error
    if ($reader -ne $null) {
        $reader.Close()
    }
    if ($writer -ne $null) {
        $writer.Close()
    }
}