はじめに
これは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 |
[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桁の固定長データのファイルです。
101 あああ そうめん
101 000100010000000100
102 いい ひやむぎ
102 000500020000001000
103 ううううう北海道味噌ラー
103 001000045000004500
104 え 焼きソバ
104 002000035000007000
105 おおお 讃岐うどん
105 010000010000010000
出力結果
DataSet型で2つのテーブル(セクションがテーブル名)が格納されます。
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型の格納内容
2行目のDataTable型の格納内容
ソースコード
TextFileStreamクラスを作成しています。
この中で、「Utility.ReadIni」は無いですが、次サイトのReadIniを参考にしています。
INI形式のファイルを読む - smdn
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に公開します。