LoginSignup
1
5

More than 5 years have passed since last update.

【.NET】固定長ファイル(複数行一組対応)の読み込み

Last updated at Posted at 2017-12-23

はじめに

これはVisual Basic Advent Calendar 2017の21日の記事となります。

1年前に固定長ファイルを使用するアプリケーションを作りました。
その固定長ファイルは2行で1組という過去に見たことがないタイプでした。

定義ファイル

過去にADOで固定長テキストを読み込む際にスキーマファイル(schema.ini)を使用したことがあります。
今回は独自に専用のスキーマファイル(schema.ini)もどき作成しました。
※schema.ini をシェーマ.iniって最近まで読んでました(^-^;

サンプルデータを考えるのが面倒だったので、次のサイトのデータを2行1組に分けて利用しました。
固定長形式テキストデータの読み込み - Excelでお仕事!

No 項目名 タイプ 桁数
1 コード 文字 5
2 メーカー 文字 10
3 品名 文字 15
No 項目名 タイプ 桁数
1 コード 文字 5
2 数量 数値 4
3 単価 数値 6
4 金額 数値 8
Sample.ini
[CONFIG]
ColNameHeader=False
Format=FixedLength
FixedFormat=RaggedEdge

[LINE1]
Col1=CODE       Char Width 5    ;コード
Col2=MAKER      Char Width 10   ;メーカー
Col3=HINNAME    Char Width 15   ;品名

[LINE2]
Col1=CODE       Char Width 5    ;コード
Col2=SURYO      Integer Width 4 ;数量
Col3=TANKA      Integer Width 6 ;単価
Col4=KINGAKU    Integer Width 8 ;金額

本物のスキーマファイル(schema.ini)はセクションで複数ファイルを定義できるのですが、独自形式では固定長単位に(定義名).iniとしてセクションは行単位の定義としています。その為、複数の固定長定義があれば、その分定義ファイルが必要となります。

固定長データ

1行目が30桁、2行目は23桁の固定長データのファイルです。

Sample.txt
101  あああ    そうめん       
101  000100010000000100
102  いい      ひやむぎ       
102  000500020000001000
103  ううううう北海道味噌ラー 
103  001000045000004500
104  え        焼きソバ        
104  002000035000007000
105  おおお    讃岐うどん     
105  010000010000010000

出力結果

DataSet型で2つのテーブル(セクションがテーブル名)が格納されます。

sample.vb
Dim ts As TextFileStream = New TextFileStream

Dim dsTable As DataSet

Try
    ' スキーマ定義ファイルセット
    ts.SchemaPath = "Sample.ini"
    dsTable = ts.ImportData("Sample.txt", vbCrLf, True)

    MessageBox.Show("読み込み完了")

Catch ex As Exception
    MessageBox.Show(ex.Message)
End Try

1行目のDataTable型の格納内容

Text_Line1.png

2行目のDataTable型の格納内容

Text_Line2.png

ソースコード

TextFileStreamクラスを作成しています。
この中で、「Utility.ReadIni」は無いですが、次サイトのReadIniを参考にしています。
INI形式のファイルを読む - smdn

TextFileStream
Imports System.Text
Imports System.IO
Imports System.Text.RegularExpressions

Public Class TextFileStream

    Public Property Section As Dictionary(Of String, String)()
    Public Property SchemaPath As String

    Public Const ERROR_INIFILE_NOTFOUND = "Ini File Not Found"         ' 定義ファイルが見つかりません
    Public Const ERROR_NEWLINE_MISMATCH = "Mismatch In New line code"  ' 改行コードが不一致
    Public Const ERROR_LENGTH_MISMATCH = "Mismatch In length"          ' 桁数が不一致

    ' 定義データオブジェクト
    Private _dsSchema As DataSet

    ''' <summary>
    ''' ファイルオープン
    ''' </summary>
    ''' <param name="path">テキストファイル</param>
    ''' <param name="lineSeparator">改行コード</param>
    ''' <param name="isLengthCheck">桁数チェック</param>
    ''' <returns>True : 成功 / False : 失敗</returns>
    Public Function ImportData(path As String, ByVal lineSeparator As String, Optional ByVal isLengthCheck As Boolean = False) As DataSet

        'ファイルを一時的に読み込むバイト型配列を作成する
        Dim bs(1) As Byte
        Dim bytesRead As Integer = 0
        Dim nBytes As Integer = 10
        Dim drRow As DataRow
        Dim dtTable As DataTable = Nothing
        Dim dsTable As DataSet = Nothing
        Dim lengthCheck As Dictionary(Of String, Integer) = New Dictionary(Of String, Integer)

        Try
            ' スキーマ定義用データセット生成
            If File.Exists(SchemaPath) = False Then
                ' 定義ファイルが見つかりません
                Throw New Exception(ERROR_INIFILE_NOTFOUND + "," + SchemaPath)
            Else
                _dsSchema = CreateSchemaDataSet(Utility.ReadIni(SchemaPath))
            End If

            ' 桁数を集計する
            For Each schema As DataTable In _dsSchema.Tables
                lengthCheck.Add(schema.TableName, 0)
                For Each row As DataRow In schema.Rows
                    lengthCheck(schema.TableName) += row("LENGTH")
                Next
            Next

            ' スキーマ定義に従ってテーブル生成
            dsTable = CreateTable(_dsSchema)

            Using fs As New FileStream(path, FileMode.Open, FileAccess.Read)
                Using sr As StreamReader = New StreamReader(fs)
                    While True
                        For Each schema As DataTable In _dsSchema.Tables
                            dtTable = dsTable.Tables(schema.TableName)
                            drRow = dtTable.NewRow()
                            Dim col As Integer = 0
                            Dim line As String = ""
                            For Each row As DataRow In schema.Rows
                                If row("LENGTH").ToString().IndexOf(",") > 0 Then
                                    ' 小数点の場合、データには小数点は含まない  123.4567→7,4(精度,位取り)と指定
                                    ' 先頭に符号がある場合、精度に符号を含めた桁を指定 +1234567→+123.4567→8,4
                                    ' 読む桁数は精度の値で指定すれば良い
                                    nBytes = Math.Truncate(Decimal.Parse(row("LENGTH").ToString.Replace(",", ".")))
                                Else
                                    nBytes = Integer.Parse(row("LENGTH").ToString())
                                End If
                                If nBytes = 0 Then Exit For

                                ReDim bs(nBytes - 1)
                                bytesRead = fs.Read(bs, 0, nBytes)
                                ' 先頭列が改行コードなら終了する
                                If col = 0 AndAlso bytesRead = 0 Then Exit While
                                If bytesRead = 0 Then Exit For

                                Dim str As String = Encoding.GetEncoding(932).GetString(bs)
                                drRow(row("NAME")) = str
                                line += str

                                col += 1
                            Next
                            dtTable.Rows.Add(drRow)

                            '改行コード分読み飛ばす
                            nBytes = lineSeparator.Length
                            ReDim bs(nBytes - 1)
                            bytesRead = fs.Read(bs, 0, nBytes)
                            If bytesRead = 0 Then Exit For
                            Dim newline As String = Encoding.GetEncoding(932).GetString(bs)
                            If newline <> lineSeparator Then
                                If newline = vbCr OrElse newline = vbCrLf OrElse newline = vbLf Then
                                    ' 改行コード不一致
                                    Throw New Exception(ERROR_NEWLINE_MISMATCH + "," + path)
                                Else
                                    ' 桁数不一致
                                    If isLengthCheck Then
                                        Dim len As Integer = lengthCheck(schema.TableName)
                                        Throw New Exception(ERROR_LENGTH_MISMATCH + "(" + len.ToString() + ")," + path)
                                    End If
                                End If
                            End If
                        Next
                        If bytesRead = 0 Then Exit While
                    End While
                End Using
            End Using

        Catch ex As Exception
            Throw
        End Try

        Return dsTable

    End Function

    ''' <summary>
    ''' スキーマ定義用データセット生成
    ''' </summary>
    ''' <param name="Section"></param>
    ''' <returns></returns>
    Private Function CreateSchemaDataSet(ByVal Section As Dictionary(Of String, Dictionary(Of String, String))) As DataSet

        Dim dsSchema As DataSet = New DataSet("Schemas")
        Dim dtSchema As DataTable = Nothing
        Dim drRow As DataRow
        Dim index As Integer = 0

        ' 定義解析用正規表現
        Dim r As New Regex("Col(\d+)=("""".*?""""|.*?)[\t ]+(.*?)[\t ]Width[\t ](\d+)", RegexOptions.IgnoreCase)

        For Each sec As KeyValuePair(Of String, Dictionary(Of String, String)) In Section
            index = 0
            For Each pair As KeyValuePair(Of String, String) In sec.Value
                Dim mc As MatchCollection = r.Matches(String.Format("{0}={1}", pair.Key, pair.Value))
                If mc.Count > 0 Then
                    For Each m As Match In mc
                        If index = 0 Then
                            '列指定先頭ならテーブル作成
                            dtSchema = dsSchema.Tables.Add(sec.Key)
                            dtSchema.Columns.Add("INDEX", Type.GetType("System.Int32"))
                            dtSchema.Columns.Add("NAME", Type.GetType("System.String"))
                            dtSchema.Columns.Add("TYPE", Type.GetType("System.String"))
                            dtSchema.Columns.Add("LENGTH", Type.GetType("System.String"))
                        End If

                        drRow = dtSchema.NewRow()
                        drRow("INDEX") = Integer.Parse(m.Groups.Item(1).ToString())     '列番
                        drRow("NAME") = m.Groups.Item(2).ToString()                     '名前
                        drRow("TYPE") = m.Groups.Item(3).ToString()                     '型
                        drRow("LENGTH") = m.Groups.Item(4).ToString()                   '長さ
                        dtSchema.Rows.Add(drRow)
                        index += 1
                    Next
                End If
            Next
        Next

        Return dsSchema

    End Function

    ''' <summary>
    ''' スキーマ定義に従ってテーブル生成
    ''' </summary>
    ''' <param name="schema"></param>
    ''' <remarks>引数のschemaからテーブルを作成する</remarks>
    ''' <returns></returns>
    Private Function CreateTable(ByVal schema As DataSet) As DataSet

        Dim dsTable As DataSet = New DataSet
        Dim dtTable As DataTable = Nothing

        For Each dt As DataTable In schema.Tables
            dtTable = dsTable.Tables.Add(dt.TableName)
            For Each row As DataRow In dt.Rows
                Dim tp As String = "System.String"

                Select Case UCase(row("TYPE"))
                    Case "BYTE"
                        tp = "System.Byte"
                    Case "CHAR", "TEXT"
                        tp = "System.String"
                    Case "SHORT"
                        tp = "System.Int16"
                    Case "INTEGER"
                        tp = "System.Int32"
                    Case "LONG"
                        tp = "System.Int64"
                    Case "DOUBLE"
                        tp = "System.Double"
                    Case "DECIMAL"
                        tp = "System.Decimal"
                    Case "FLOAT"
                        tp = "System.Single"
                    Case "DATE", "DATETIME"
                        tp = "System.Date"
                    Case "CURRENCY"
                        tp = "System.Currency"
                    Case "BIT", "Boolean"
                        tp = "System.Boolean"
                End Select

                dtTable.Columns.Add(row("NAME"), Type.GetType(tp))
            Next
        Next

        Return dsTable

    End Function

End Class

ライセンスっぽいこと

コード改変や配布は自由です。
このツールによる義務/責任を何ら負いません。

最後に

え、C#版が欲しいって、これはVisual Basic Advent Calendarなんですよ。
DataSet型やDictionary型を使用しているので、Web系のConvert Code VB to C#は応答がなかったり変換できなかったします。
そのうち、GitHubに公開します。

参照

1
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
5