QA@IT
この質問・回答は、@ITの旧掲示板からインポートされたものです。

VBでCSV読み込み & 書き出しの処理速度向上について

いつも拝見させて頂いております。
今回初めて投稿させて頂きました。

下記環境

Windows XP(CPU:2GHz メモリ:512MB)
Visual Basic 2005 Express Edition

において、CSVファイルからデータを読み込み、読み込み行を更に分解して再度CSV
ファイルに書き出すというプログラムを作成しております。
事情があって、不本意ながら上記仕様のプログラムを作成せざるを得ない状況です。

本題ですが、下記プログラムを実行すると、確かに上記仕様の処理を行ってくれる
のですが、5Mバイト程度のCSVファイルを処理しようとすると、処理速度が極端に
落ち、処理が終了するまで30分程の時間を要してしまいます。プログラマとしては
初歩的な質問でお恥ずかしいのですが、処理速度UPの方法をどなたかご教授して頂
けないでしょうか?

*一部抜粋*

Private Structure CsvDat
    Dim year As String          '年
    Dim month As String         '月
    Dim day As String           '日
    Dim time As String          '時
    Dim value1 As String        '値1
    Dim value2 As String        '値2
End Structure
Private sd() As CsvDat
Private spt() As String

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

Dim n As Integer
Dim line As String
Dim temp() As String

Dim sr1 As New System.IO.StreamReader("読み込みファイル名", System.Text.Encoding.Default)
Dim sw1 As New System.IO.StreamWriter("書き出しファイル名", False)
'ファイルの最後までループ
n = 0
Do Until sr1.Peek() = -1
line = ""
temp = Split(sr1.ReadLine(), ",")
ReDim Preserve sd(n)

  spt = Split(temp(0), "/")                '年月日分解
  sd(n).year = spt(2)                 '年 
  sd(n).day = spt(1)                  '月
  sd(n).month = spt(0)                '日
  sd(n).time = temp(1)
  sd(n).value1 = temp(2)
  sd(n).value2 = temp(3)
 '取得結果を書き出し
  line = sd(n).year & "," & sd(n).month & "," & sd(n).day & "," & sd(n).time & "," & sd(n).value1 & "," & sd(n).value2
  sw1.WriteLine(line)

  n += 1
  Loop
  sr1.Close()     'ファイルを閉じる
  sw1.Close()

end Sub

質問者:七誌

回答



ReDim Preserve sd(n)

sdをループの外で使うんですか?
使わないなら不要な変数だし、使うならコレクションを使ったほうがいいです。
[ メッセージ編集済み 編集者: burton999 編集日時 2007-05-15 13:03 ]

投稿者:burton999

編集 履歴 (0)

諸農です。

「sd」が何をする機能を持っているのか判りませんが、最初にReadToEnd()で読み込みテキストの行数を取得してからsdの配列要素数を決めれば良いんじゃないでしょうか。
ただ「sd」を一時的にワークとして使用しているのなら、わざわざ配列にする必要もないような気がしますが。。

_________________諸農和岳
Powered by Turbo Delphi & Microsoft Visual Studio 2005

十兵衛@わんくま同盟
http://blogs.wankuma.com/jubei/

投稿者:Jubei

編集 履歴 (0)

最初に調べないにしても、せめて倍倍くらいには、しましょう。
たとえば初期値100で、101行目が来たら200に、201行目が来たら400に、と。
…という処理を、コレクションなら勝手にやってくれるわけですが。

投稿者:mio

編集 履歴 (0)

他の方も指摘されていますが、ループごとに ReDim Preserve するのは
ヒープメモリの確保が頻繁に発生しますので、非常に効率が悪いです。
後続処理でこのデータを使用しないのならば、配列にいれない方がよいと思います。
後続で使用するため、どうしても保持する必要があるのならコレクションを遣いましょう。

sd(n).year & "," & sd(n).month & "," & sd(n).day & "," & sd(n).time & "," & sd(n).value1 & "," & sd(n).value2

sw1.WriteLine(line)

この文字列の結合も、String.Format を使ったほうが速いし、見やすくなると思いますよ。

投稿者:KI

編集 履歴 (0)

皆さん指摘されていますが、変数 sd について何も言及されていないので、
後で利用するために配列を確保しているのかそれとも単にワークとして使
用しているのかが不明ですね。
言及されていないこともあって、私にはワークとして使用しているように見
えました。

ワークとして使用しているのであれば、burton999 さんが書かれている
ように不要な変数ですし、使わずに済むのであればそちらの方がいいでしょ
うね。
時間がかかるのは、ループの中で Redim しているためだと思います。

とりあえずループの中をこんな感じにしてみればいいんじゃないでしょうか。




    temp = Split(sr1.ReadLine(), ",")
    Dim params As New List(Of String)
    params.AddRange(Split(temp(0), "/"))    ' 年月日分解
    params.Add(temp(1))
    params.Add(temp(2))
    params.Add(temp(3))

    ' 取得結果を書き出し
    sw1.WriteLine(String.Join(",", params.ToArray()))

もし後で使うために取っているというのであれば、それも皆さんがすでに書
かれているようにコレクションを使うと良いと思います。
_________________ぽぴ王子@わんくま同盟
ぽぴ王子の人生プログラミング中 / ぽぴンち。

投稿者:ぽぴ王子

編集 履歴 (0)

七誌さんの書き込み (2007-05-15 12:38) より:

本題ですが、下記プログラムを実行すると、確かに上記仕様の処理を行ってくれる

のですが、5Mバイト程度のCSVファイルを処理しようとすると、処理速度が極端に

落ち、処理が終了するまで30分程の時間を要してしまいます。

タスクマネージャーでその間のCPU使用率はどうなっているでしょうか。100%(CPUの数によっては50%や25%)ならば、配列処理が遅いのでしょう。
もし100%よりずっと低ければディスクの読み書きのどちらかまたは両方が遅いのでしょう。APIに対してバッファーサイズの指定ができるはずなので、多めにしてはどうでしょうか。また、読み書きも、読み、と書き、に分けて計測してみることをお勧めします。

--
unibon {B73D0144-CD2A-11DA-8E06-0050DA15BC86}

投稿者:unibon

編集 履歴 (0)

ちょっと勘違いしていたかもしれません。
最初は単に日付が一項目でスラッシュ区切りになっているのを、年月日で
分けて CSV ファイルにしたいのかと思っていましたが、どうも違うみたい
ですね。

5/15/2007,10,1,2

2007,5,15,10,1,2

のように変換したい、という話でしょうか。
だとすると、こんな感じで。
# なんかコメントも間違ってたのでえらく解析に苦労した気が。




    temp = Split(sr1.ReadLine(), ",")
    Dim params As New List(Of String)
    spt = Split(temp(0), "/")   ' 年月日分解
    params.Add(spt(2))          ' 年
    params.Add(spt(0))          ' 月
    params.Add(spt(1))          ' 日
    params.Add(temp(1))
    params.Add(temp(2))
    params.Add(temp(3))

    ' 取得結果を書き出し
    sw1.WriteLine(String.Join(",", params.ToArray()))

_________________ぽぴ王子@わんくま同盟
ぽぴ王子の人生プログラミング中 / ぽぴンち。

投稿者:ぽぴ王子

編集 履歴 (0)

変数sdを使用しないという前提で作成してみました。
ほぼ同スペックのパソコンでテストしてみましたが、2〜3秒程度で終了しました。



Dim temp() As String
Dim year As String
Dim month As String
Dim day As String
Dim reg As Regex = New Regex("/|,", RegexOptions.Compiled)

Dim sr1 As New System.IO.StreamReader("読み込みファイル名", System.Text.Encoding.Default)
Dim sw1 As New System.IO.StreamWriter("書き出しファイル名", False)
'ファイルの最後までループ 
Do Until sr1.Peek() = -1
    temp = reg.Split(sr1.ReadLine())

    year = temp(2)
    month = temp(1)
    day = temp(0)
    temp(0) = year '年 
    temp(1) = month '月 
    temp(2) = day '日 
    '取得結果を書き出し 
    sw1.WriteLine(String.Join(",", temp))
Loop
sr1.Close() 'ファイルを閉じる 
sw1.Close()

投稿者:うにくま

編集 履歴 (0)

皆様ご指摘ありがとうございます。

ぽぴ王子さんの第1回答を参考にし、以下のようにプログラムを書き直した所、
2,3秒程で希望通りのデータを得る事ができるようになりました。ありがとうございます。ちなみに皆様のご指摘通り変数sdはワークとして用いておりました。配列として
いたのは、完全に私自身のデータ型選択誤りです。

*変更点*

       'ファイルの最後までループ
        n = 0
        Do Until sr1.Peek() = -1
            temp = Split(sr1.ReadLine(), ",")
            Dim params As New List(Of String)
            params.AddRange(Split(temp(0), "/"))    ' 年月日分解
            params.Add(temp(1))
            params.Add(temp(2))
            params.Add(temp(3))
            '取得結果を確認の為に表示
            sw1.WriteLine(String.Join(",", params.ToArray()))

            n += 1
        Loop

VBはまだかじった程度ですが、適切なデータ型を選択する事でここまで結果に
変化がでるものなのかと驚いています。本当にありがとうございました。

投稿者:七誌

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。