2011年3月24日木曜日

VBA テキストファイルの最も高速な読み込み方法

Excel、AccessのVBAでのファイル読み込み方法はたくさん有ります。

でも結局どのやり方が一番早いんだ!ってことでいろいろコード書いて計測してみました。

条件

・約3MBの可変長CSVファイルの全読み込みと一部のコードでは行数取得。
・処理時刻の計算はVBA 超高精度タイマークラスを使っています
・メモリ処理のRedim preserveは重たい処理ですが、可変長ファイルなのでメモリの一括確保はしませんでした。
 ファイルサイズからメモリ容量を計算してメモリを確保するともっと早くなります。メモリマッピングのような処理です。
・For文で10回同じコードを走らせてます。ですので初回以降はキャッシュが効いて高速になります。
・時刻の単位は秒です。
・最終的な文字コードはUnicodeです

・計測用コードは以下の通り
Option Explicit

Public Function ReadFileTest_LineInput(ByVal File_Target As String)
'===========================================================================
'OpenステートメントのLine Inputでの順次読み込みを行います
'===========================================================================

    Dim intFF As Long                           'ファイル番号
    Dim str_buf As String                       'ただの汎用変数
    Dim str_Strings() As String                 'ファイルの中身全部の文字列型配列
    Dim Row As Long                             '行数カウント
    Dim cls_Timer As New Cl_QueryPerformance    '高精度タイマークラス
    
    Call cls_Timer.GetQueryPerformanceTime 'タイマー初期化
    
    intFF = FreeFile
    Open File_Target For Input As #intFF
        Do While Not EOF(intFF)           'ファイルの終端かどうかを確認します
        
            Line Input #intFF, str_buf  'データ行を読み込みます
            
            '==============================================================
            '配列化有効時
            'ReDim Preserve str_Strings(Row) As String
            'str_Strings(Row) = str_buf
            'Row = Row + 1
            '==============================================================
            
        Loop
    Close #intFF
    
    Debug.Print cls_Timer.GetQueryPerformanceTime   '前回タイマー初期化時からの時間

End Function

Public Function ReadFileTest_InputB(ByVal File_Target As String)
'===========================================================================
'OpenステートメントのInputBでの順次読み込みを行います
'===========================================================================

    Dim intFF As Long                           'ファイル番号
    Dim var_buf                                 'ただの汎用変数
    Dim str_Strings() As String                 'ファイルの中身全部の文字列型配列
    Dim Row As Long                             '行数カウント
    Dim cls_Timer As New Cl_QueryPerformance    '高精度タイマークラス
    
    Dim bytSjis As String
    Dim str_Uni As String
    
    Call cls_Timer.GetQueryPerformanceTime 'タイマー初期化
    
    intFF = FreeFile
    Open File_Target For Input As #intFF
        bytSjis = InputB(FileLen(File_Target), #intFF)      ' SJISのテキスト読み込み
    Close #intFF
    
    str_Uni = StrConv(bytSjis, vbUnicode)    ' SJISからUNICODEへ
                                    
'    '==============================================================
'    '配列化有効時
'    var_buf = Split(str_Uni, vbCrLf) '改行コードごとに区切って配列化
'    Row = UBound(var_buf)            '行数取得
'    '==============================================================
    
    Debug.Print cls_Timer.GetQueryPerformanceTime   '前回タイマー初期化時からの時間

End Function

Public Function ReadFileTest_Open_Binary(ByVal File_Target As String)
'===========================================================================
'OpenステートメントのBinaryでの順次読み込みを行います
'===========================================================================

    Dim intFF As Long                           'ファイル番号
    Dim byt_buf() As Byte
    Dim var_buf
    Dim str_buf  As String                      'ただの汎用変数
    Dim str_Strings() As String                 'ファイルの中身全部の文字列型配列
    Dim Row As Long                             '行数カウント
    Dim cls_Timer As New Cl_QueryPerformance    '高精度タイマークラス
    Dim i As Long
    
    Dim bytSjis As String
    Dim str_Uni As String
    
    Call cls_Timer.GetQueryPerformanceTime 'タイマー初期化
    
    intFF = FreeFile
    Open File_Target For Binary As #intFF
        ReDim byt_buf(LOF(intFF))
        Get #intFF, , byt_buf
    Close #intFF
    
    str_Uni = StrConv(byt_buf(), vbUnicode) 'Unicodeに変換
    
    '==============================================================
    '配列化有効時
    var_buf = Split(str_Uni, vbCrLf) '改行コードごとに区切って配列化
    Row = UBound(var_buf)            '行数取得
    '==============================================================
    
    Debug.Print cls_Timer.GetQueryPerformanceTime

End Function

Public Function ReadFileTest_FSO_ReadAll(ByVal File_Target As String)
'===========================================================================
'FileSystemObjectのReadAllでの一括読み込みを行います
'===========================================================================

    Dim var_buf
    Dim str_buf As String
    Dim Row As Long                             '行数カウント
    Dim cls_Timer As New Cl_QueryPerformance    '高精度タイマークラス
    Dim FSO As New FileSystemObject             'FileSystemObject
    
    Call cls_Timer.GetQueryPerformanceTime 'タイマー初期化
    
    With FSO.OpenTextFile(File_Target, ForReading)
        str_buf = .ReadAll  '一括読み込み
        .Close
    End With
    
'    '==============================================================
'    '配列化有効時
'    var_buf = Split(str_buf, vbCrLf) '改行コードごとに区切って配列化
'    Row = UBound(var_buf)            '行数取得
'    '==============================================================
    
    Set FSO = Nothing
    Debug.Print cls_Timer.GetQueryPerformanceTime
    
End Function

Public Function ReadFileTest_FSO_ReadLine(ByVal File_Target As String)
'===========================================================================
'FileSystemObjectとTextStreamでのReadLineを行います
'===========================================================================

    Dim str_buf As String                       'ただの汎用変数
    Dim str_Strings() As String                 'ファイルの中身全部の文字列型配列
    Dim Row As Long                             '行数カウント
    Dim cls_Timer As New Cl_QueryPerformance    '高精度タイマークラス
    
    Dim FSO As New FileSystemObject
    Dim FSO_TS As TextStream            ' TextStream
    
    Call cls_Timer.GetQueryPerformanceTime 'タイマー初期化
    
    Set FSO_TS = FSO.OpenTextFile(File_Target, ForReading)
    With FSO_TS
        Do Until .AtEndOfStream
            str_buf = .ReadLine
'            '==============================================================
'            '配列化有効時
'            ReDim Preserve str_Strings(Row) As String
'            str_Strings(Row) = str_buf
'            Row = Row + 1
'            '==============================================================
        Loop
        .Close
    End With
    Set FSO_TS = Nothing
    Set FSO = Nothing
    
    Debug.Print cls_Timer.GetQueryPerformanceTime
    
End Function



・結果はこのようになりました

順次読み込み 一括読み込み 一括読み込み
Line Input
(配列化有)
Line Input
(配列化無)
InputB
(配列化有)
InputB
(配列化無)
Binary
(配列化有)
Binary
(配列化無)
1回目 0.4208 0.3645 0.1739 0.1394 0.0998 0.1167
2回目 0.2878 0.2487 0.1413 0.1072 0.0928 0.0602
3回目 0.2821 0.2459 0.1178 0.0917 0.0979 0.0549
4回目 0.2817 0.2498 0.1081 0.0909 0.0992 0.0569
5回目 0.2808 0.248 0.1083 0.0893 0.09 0.0591
6回目 0.2881 0.2452 0.1181 0.0899 0.0926 0.0568
7回目 0.283 0.2457 0.1129 0.0906 0.0922 0.0573
8回目 0.2801 0.252 0.1095 0.0908 0.0925 0.0588
9回目 0.28 0.2461 0.1085 0.0917 0.092 0.059
10回目 0.2916 0.2455 0.109 0.0917 0.0945 0.0572
平均 0.2976 0.25914 0.12074 0.09732 0.09435 0.06369
一括読み込み 順次読み込み
FileSystemObject
ReadAll
(配列化有)
FileSystemObject
ReadAll
(配列化無)
FileSystemObject
ReadLine
(配列化有)
FileSystemObject
ReadLine
(配列化無)
1回目 0.4591 0.4369 0.4437 0.4139
2回目 0.2952 0.2922 0.3263 0.2748
3回目 0.2969 0.2787 0.3184 0.274
4回目 0.2973 0.2789 0.3188 0.2757
5回目 0.3001 0.2786 0.322 0.2763
6回目 0.2964 0.292 0.319 0.2768
7回目 0.295 0.2786 0.3186 0.2743
8回目 0.2988 0.2787 0.3248 0.2739
9回目 0.2992 0.282 0.3203 0.2754
10回目 0.2942 0.2962 0.3199 0.2772
平均 0.31322 0.29928 0.33318 0.28923


結果

・OpenステートメントのBinaryモードにより一括バイナリ読み込み、Unicode変換が最も高速という結果になりました。
 まぁ当然ですね。配列化も簡単なので今後はこれをメインに使っていこう。

 おそらくこれがVBAでAPIを使わない場合でもっとも高速です。
 さらにテキストファイルの最終行の取得なんてのはさらに早くできます。
 これはログを監視するようなマクロではかなり重要です。
 まぁファイル開きっぱなしの方が高速ですが。

・数百MBものファイルなら一気に配列(メモリ)に読み込むなんてマネは恐ろしくてできません。
 FileSystemObjectとTextStreamでの順次読み込み。同時にデータに対する処理を行うっていのがいいと思います。
 これだとメモリをほとんど使わないで済みます。その変わりファイルIO中に他の作業をするので堅実なコードが必要です。

用途によって使い分けるといいものが作れそうです。

0 件のコメント:

コメントを投稿