固定長ファイルのテキストデータ呼び込み時処理
今回の内容は、固定長ファイルのデータとして「全角文字が含まれるデータ」をVBA側が取り込むと、末尾がNull文字になるため処理が必要 というものです。前報「固定長ファイルのテキストデータ呼び込み時処理」も同じ内容でしたが、説明の一部に誤りがありましたので改訂し、併せて全体の見直しを行いました。 具体的な誤り内容は「Excel側の固定長文字列のサイズ」を「 但し、固定長ファイルを扱う範囲に限ってみれば、半角文字数単位と考えてもそれほど間違いでは無さそうな感じです。 更に「Nullが付いてしまう原因」について、もう少し分かり易い説明方法を思いつきましたし、また新たな現象も発見?しましたので盛り込んでいます。 |
1.固定長ファイル
1-1.固定長ファイルの特徴
固定長ファイルをデータベースとしたシステムでは、データ読み書き用として「テキストファイル」を使用します。イメージとしては図01のように、1つのデータの長さ(=レコード長:バイト単位)が一定で、データの数(図01では13個)の分だけ繋がっているファイルです。(ハードディスクの円盤を思い浮かべると、近いかもしれません)1データの長さは、あらかじめ決まっていますので、「何番目のデータ(レコード番号)か」が分かれば、データの取り出し・書き込みが素早くできるという特長があります。
図01
1データの長さは、レコードを構成している要素(データベースで言う「カラム(列)」)の合計値で決まります。例えばInteger型は2バイト、Date型は8バイトなどと決まっています。一方、文字列データは「固定長文字列」にする必要があり、「Dim 文字列変数 As String * 6」のように、As Stringの後ろに「*(アスタリスク)」に続けて「文字数(半角文字でも全角文字でも1文字単位)」を指定します。
1-2.固定長文字列
固定長文字列を使用する場面は、今回説明している「固定長ファイル」以外にもあります。例えば以下は、固定長文字列に文字列を代入後、イミディエイトWindowに出力する例です。- '========== ⇩(1) 固定長文字列へのデータ格納 ============
- Sub its005_01()
- Dim Str As String * 6 '←6文字の固定長文字列の変数を宣言
- Str = "あいうえおかきくけこ"
- Debug.Print Str
- Str = "abcdefghijklmn"
- Debug.Print Str
- End Sub
02行目「Dim Str As String * 6」では、6文字の固定長文字列の変数Strを宣言しています。
04行目「Str = "あいうえおかきくけこ"」では、固定長文字列変数に「6文字超の全角文字」を代入しています。変数Strは6文字で設定してあるため、変数に格納されるのは先頭側の6文字「"あいうえおか"」となります。
06行目「Str = "abcdefghijklmn"」では、固定長文字列変数に「6文字超の半角文字」を代入しています。こちらも先頭側の6文字「"abcdef"」が変数に格納されます。
格納された文字数は、05行目・07行目「Debug.Print Str」により、下記のように6文字ずつ出力されます。「6文字で固定長文字列を宣言」したのですから当然です。
図03
1-3.固定長ファイルの読み書き
一方、固定長ファイルとして使用する場合は、以下のように宣言部で「ユーザー定義のデータ型(≒1レコードの内容)」を定義してから使用するのが通常の手法です。- '========== ⇩(2) 固定長のデータ型を定義 ============
- Type Rec
- S As String * 6 '←6文字の固定長文字列
- End Type
- '========== ⇩(3) 固定長ファイルのデータの出力+入力 ============
- Sub its005_02()
- Dim str(1 To 4) As Rec '←固定長データ型で変数Str1を宣言
- Open "its-005.txt" For Random As #1 Len = Len(str(1))
- str(1).S = "あいうえおかきくけこ"
- Put #1, 1, str(1)
- str(2).S = "abcdefghijklmn"
- Put #1, , str(2)
- Close #1
- Open "its-005.txt" For Random As #1 Len = Len(str(3))
- Get #1, 1, str(3)
- Debug.Print str(3).S
- Get #1, , str(4)
- Debug.Print str(4).S
- Close #1
- End Sub
21~23行目のType~End Typeでは、ユーザー定義型を定義しています。22行目「S As String * 6」で、固定長文字列要素「S」を6文字で定義しています。この構造体定義が1レコード当たりの要素の内容となります。データ型は、この場合「Rec」となります。
尚このTypeステートメントは、モジュール先頭部の「宣言部」で定義する必要があります。
26~43行目のプロシージャ内では、固定長ファイルのデータを読み書きしています。
まず27行目「Dim str(1 To 4) As Rec」では、上で定義した「Rec」型で4要素の配列を宣言しています。特に配列にする理由は無く、GetとPutの実行数分(4つ)の変数があればOKです。
(Getで取得した値は、変数の全てを置き換えない) 4つの変数を作ったのは、このプロシージャ内で「固定長ファイルへのレコード書き込みを2つ」+「固定長ファイルからのレコード読み取りを2つ」の合計4つのデータを扱うからです。 変数に新たな値を代入すれば全てが置き換わるはずなので、複数の変数を作る意味が分からないかもしれません。 Getステートメントで、例えば6バイトの文字列を取得した時、その文字列はGetの第3項目の変数に代入されます。 しかしその変数に、既に12バイトの文字列が入っていたとしたら、前半の6バイトを「取得した文字列」で置き換え、後半の6バイトは「そのまま」残る事になります。 図04では変数Strに4回も値を入れ直しているので、前の代入の影響を受けないように別個の変数(≒4要素の配列変数)としています。 |
29~34行目では、2つのレコードを固定長ファイルに書き込んでいます。
29行目「Open "its-005.txt" For Random As #1 Len = Len(str(1))」ではテキストファイルをRandomモードで開いています。その際レコード長は「Len(str(1))」つまりユーザー定義型Recの長さとしています。
Recは21~23行目で定義していますが、その中身は22行目「S As String * 6」の1つだけです。この要素Sは「6文字の固定長文字列」ですのでLen(str(1))は「6」という事になります。Len句に指定する値の単位は「バイト」ですので、結果としてレコード長は6バイトという事になります。
図05
図02では全角6文字が固定長文字列に入ってくれるのに、上記の説明だと「S As String * 6」は半角に限定されてしまう事になります。この矛盾を解決するために、30~31行目の説明をします。
30行目「str(1).S = "あいうえおかきくけこ"」では、固定長文字列に全角文字を格納しています。この格納状況を確認したのが下図です。固定長文字列のサイズは6文字なので、全角でも6文字が格納されています。これは図03の結果と同じです。
図06
31行目「Put #1, 1, str(1)」では、その固定長文字列をファイルに「レコードデータ」として書き込んでいます。2つ目の項目「1」が書き込むレコード位置です。
その書き込み結果をファイル上で見たのが以下です。ファイルに書き込まれたのは「全角3文字(=ファイル上で6バイト)」です。
図07
図07で全角3文字になってしまった原因は「レコード長が6バイトになっているからでは?」と思われるかもしれませんので、レコード長をLenBを使って2倍(=12バイト)にしたのが以下です。ファイルに書き込まれるのは「6バイト(=全角3文字)」と同じです。
図08
つまり固定長ファイルとして書き込まれる文字列は、固定長文字列の「バイト単位のサイズ」という事になります。
このように、1レコード長よりも長いデータを入れようとすれば下記の中央のように溢れ、逆にレコード長より短い場合はその隙間を半角スペースで埋めて、レコード長を一定にします。
図09
一方32行目「str(2).S = "abcdefghijklmn"」では全て半角文字なので、6文字の固定長文字列に「"abcdef"」までが格納されます。
33行目「Put #1, , str(2)」では、2つ目の項目を省略していますが、これは「現在のレコード位置」に書き込むという意味になります。31行目で1レコード目を書き込んだのでカーソルポインタとしては次の2レコード目にいますので、「2レコード目のデータとしてstr(2)の内容を書き込む」ことになります。書き込むデータは「固定長文字列 "abcdef"」と全て半角文字ですので、そのままファイルに書き込まれます。
29~34行目のコードで書き込まれたファイル内容が以下です。
図10
36~41行目では、29~34行目で書き込んだ固定長ファイルの内容を読み込んでいます。
36行目「Open "its-005.txt" For Random As #1 Len = Len(str(3))」では、Randomモードで固定長ファイルを呼び出しています。その際レコード長としては「Len(str(3))」で、21~23行目で定義したユーザー定義型のサイズです。29行目の時と表現は異なるものの、同サイズです。
37行目「Get #1, 1, str(3)」では、2つ目の項目(読み出しレコード位置)を「1」としているため、1番目のレコードを読み込み、そのデータを変数str(3)に格納しています。1レコード目には「全角3文字(=6バイト)」が入っていますので、下図のように変数str(3)にデータが入ります。
図11
上図右側の値欄には「あいう・・・」と表示されています。変数strは「6文字の固定長文字列」ですので、変数strの前半に「固定長ファイルから取得した3文字」が入ります。後半は「・・・」と表現されていますが、文字コードを調べてみるとChr(0)です。つまり「Null文字」で、固定長文字列を宣言した時に重点された文字の状態です。
また39行目「Get #1, , str(4)」は、2つ目の項目を省略しています。37行目で1番目のレコードを読み出し、現在のポインタ位置は2番目のレコードにありますので、2番目のレコードを読み出す事になります。
2番目のレコードのデータは「半角6文字」ですので、「6文字の固定長文字列」の変数strにピッタリと入り隙間は無く、Null文字はありません。
1-4.Null文字が発生する原因
図11のように「全角文字を読み込んだ時には、Null文字が付いてくる」ことが分かりました。この状況をまとめたのが以下になります。Nullが付く原因は、以下の3つに整理できると思います。
・固定長ファイルを開く時、レコード長とデータ取得時変数は同じサイズ(値)を指定しなければならない。
・しかしVBAと固定長ファイルとでは、文字を扱う単位が異なる
(VBAは文字数単位、固定長ファイルはバイト数単位(半角=1バイト、全角=2バイト))
・固定長文字列の既定状態は、Null文字で埋められている
そのため固定長ファイルから文字列データを読み込んだ際、データに全角文字が含まれていると、以下のようにズレが発生しながらデータが格納されます。
図12
固定長ファイルのデータは全て読み取って固定長文字列変数に格納されるのですが、ズレた分(全角の文字数分)だけ「上書きされない残り部分」が生じます。そして元々の固定長文字列変数は、既定状態はNull文字(Chr(0))で埋められていますので、その上書きされない部分がNull文字となる という事のようです。
なお前報「固定長ファイルのテキストデータ呼び込み時処理」では、以下のように説明しました。理論的には間違っていない気がしますが、今読み返すと分かり難いことは確かなので、上記のような説明に置き換えました。御了承下さい。
(元々の説明内容) ExcelのVBAを使って固定長ファイルからデータを取り出した後は、取り出した先(Excel側)のルールで文字列を取り扱う必要があります。固定長ファイル内では「全角文字=2バイト」「半角文字=1バイト」というルールでしたが、Excel内では文字列を「2バイト文字セット(DBCS:Double-byte Character Sets)」という扱い方をしています。これは「半角でも全角でも2バイトを使用」するというルールです。 例えば、固定長文字列の文字数を「20文字」に設定した時を考えます。その文字列全てが半角であれば、固定長ファイル内には20バイト単位でデータが存在している事になります。この中に「ABCDEFGHIJ」という「半角で10文字」のデータがあり、それを取り出したとします。半角10文字は10バイトですから、残り10バイトは「スペース(半角)」で埋められていることになります。 そのデータを受け取るExcel側では「半角20文字は40バイト」というサイズが必要です。 ここで、受け取るバイト数をあらかじめ決めている「Excel側の変数」に違和感を持つかもしれません。データを受け取ったら、受け取ったサイズに合わせて形を変えてくれるのがExcelの良いところです。 しかし、今回受け取るのは「1データ」というまとまりで、その中には例えば3つの項目が入っている場合もあります。個別項目(Excelで言えば3列分)も、サイズはそれぞれ固定ですので、全体として「あらかじめ決めたサイズで受け取る」必要があるのです。 図13は、固定長ファイルの文字列をExcelの文字列に変換した時の様子を示したもので、上段が固定長ファイル(20バイト)、下段がExcel側(40バイト)です。半角の単位が1バイトから2バイトに単純に変換するだけですので、「ABCDEFGHIJ」という「半角で10文字」のデータは、スペースも含めてExcel側で受け取れます。 図13 図13は半角のみの場合でした。では次に「全角が含まれる場合」を考えます。例えば「ひらがな5文字(あ~お)+半角英数字5文字(A~E)」です。 ひらがなは「全角」ですから、固定長ファイル内でも2バイトずつ使いますので、合計15バイトを使い、残り5バイトがスペースで埋められています。このデータをExcel側に持ってきた時の様子が図14になります。 図14 Excel側の受け口は「半角20文字 = 40バイト」と変わりませんが、全角文字の部分は2バイト→2バイトと「バイト数の変化が無い」ので、1文字1文字変換していくと「5文字分(Excel側で10バイト分)余ってしまう」ことになります。 文字を受取る側のExcelでは「文字数を決める変数を宣言(例:Str As String * 20 )」した場合「20文字分=40バイトが全てNull」という態勢で待っているのに、30バイト分しか文字が来ないので、来なかった5文字分は「Nullのまま残ってしまう」ことになります。 |
1-5.ユーザー定義型と裸の固定長文字列型との違い
図04では、ユーザー定義型を用いて固定長のデータ型を定義していました。しかし今回は、1つのレコード=1つの固定長文字列(6文字)ですので、単に固定長文字列を宣言しても良いのでは?との考えが浮かびます。以下ではプロシージャ内で固定長文字列を宣言しています。なお、モジュールの宣言部でPublic宣言しても結果は同じでした。
- '========== ⇩(4) 単独の固定長文字列を使用しての固定長ファイル読み書き ============
- Sub its005_03()
- Dim str1 As String * 6
- Dim str2 As String * 6
- Dim str3 As String * 6
- Dim str4 As String * 6
- Open "its-005.txt" For Random As #1 Len = Len(str2)
- str1 = "あいうえおかきくけこ"
- Put #1, 1, str1
- str2 = "abcdefghijklmn"
- Put #1, , str2
- Close #1
- Open "its-005.txt" For Random As #1 Len = Len(str4)
- Get #1, 1, str3
- Debug.Print str3
- Get #1, , str4
- Debug.Print str4
- Close #1
- End Sub
52~55行目では、以下で使用する固定長ファイル読み書き用の変数を宣言しています。4つの変数とも6文字の固定長文字列です。図04の27行目では配列形式で変数宣言をしましたが、訳あってここでは個別の宣言としています。
これ以降は変数の形が異なるだけで、内容は図04と全く一緒です。
58行目「str1 = "あいうえおかきくけこ"」で固定長文字列の変数に全角文字のデータを代入し、59行目「Put #1, 1, str1」で第1レコードとしてデータをファイルに書き込んでいます。しかしここでエラーが発生します(ここが新発見?です)。
エラー内容は「レコード長が一致せず」で、OpenステートメントのLen句で指定したレコード長とPutの第3項目(変数str1)の長さが合わないようです。変数str1には「全角6文字(12バイト相当)」が入っているためのようですが、図04のように「固定長文字列が、ユーザー定義型の要素」になっている時にはエラーは出なかったのに不思議です。
対策としては、52行目を「Dim str1 As String * 3」(書き込む文字列の総バイト数をレコード長さに合わせる)とすれば良いのですが、これは格納する文字列の「全角の個数によって固定長文字列のサイズを変える」ことを意味します。しかし52行目の文字数「6」の部分は変数にはできませんので、結構やっかいな問題です。
なお変数str1に格納する全角文字列が「"あ"」のように1文字でも、固定長文字列サイズは6文字なので、「"あ" + 半角スペース5個」の合計7バイトとなり、やはりエラーとなります。
一方で半角文字の場合は、60行目「str2 = "abcdefghijklmn"」で変数str2に6文字分を格納し、61行目「Put #1, , str2」で2番目のレコードデータとして書き込みます。こちらは正常に通ります。
また読み取り時の方は、65行目「Get #1, 1, str3」で固定長ファイルから全角文字を読み込み、変数str3に格納する際に「ファイルから読み取った文字数が3文字(6バイト)」なのに「格納する変数が6文字のサイズ」となり、やはり「レコード長が一致せず」のエラーとなります。
一方、半角文字だけの67行目「Get #1, , str4」は問題ありません。
以上の事から、図15の52~55行目は以下のように宣言すればエラーは出ませんが、あまりにも非現実的です。
- Dim str1 As String * 3 '←全角文字用
- Dim str2 As String * 6 '←半角文字用
- Dim str3 As String * 3 '←全角文字用
- Dim str4 As String * 6 '←半角文字用
上記の問題を解決するには、以下の2点だと考えます。
・固定長ファイルに読み書きするデータは、必ずTypeを使ってユーザー定義型にする。
・データに全角文字が含まれる場合は、固定長文字列を余裕を持った文字数にする。
2.Null文字の削除法
Excel側としては、受け取った文字列の後ろに「スペース」や「Null」がくっついているのは都合が悪いため、取り除く必要があります。しかし「Trim関数では、Null文字は取り除けない」ため、別な処理が必要です。固定長ファイルからデータを取り出し、Nullの置換・スペースの削除処理までの流れが図17のコードになります。
- '========== ⇩(5) 読み込んだ文字データのNull文字削除 ============
- Sub its005_04()
- Dim str(1 To 2) As Rec
- Dim str3 As String
- Open "its-005.txt" For Random As #1 Len = Len(str(1))
- str(1).S = "あab"
- Put #1, 1, str(1)
- Close #1
- Open "its-005.txt" For Random As #1 Len = Len(str(2))
- Get #1, 1, str(2)
- str3 = Replace(str(2).S, Chr(0), "")
- str3 = Trim(str3)
- Debug.Print str3
- Close #1
- End Sub
固定長ファイルのデータ型は図04の21~23で定義したRec型を流用しています。Recの中身は、「6文字の固定長文字列」です。
82行目「Dim str(1 To 2) As Rec」では、Rec型の変数を2つ宣言します。配列型にしていますが、書き込み用と読み込み用の2つの変数であればOKです。
83行目「Dim str3 As String」では、可変型文字列の変数を宣言しています。これは固定長ファイルから読み込んだ文字列から、不要なスペースやNull文字を削除したものを入れる変数です。
85行目「Open "its-005.txt" For Random As #1 Len = Len(str(1))」では、ファイルをRandomモードで開いています。レコード長は6バイト(Rec型のバイトサイズ)になります。
86行目「str(1).S = "あab"」では、Rec型の変数に「"あab"」を代入しています。全角1文字+半角2文字です。
87行目「Put #1, 1, str(1)」で、ファイルに書き込んでいます。
90行目「Open "its-005.txt" For Random As #1 Len = Len(str(2))」で再び固定長ファイルをRandomモードで開きます。
91行目「Get #1, 1, str(2)」で、1番目のレコード内容を読み込み、固定長文字列変数str(2)に格納しています。この状態は図18の②で、全角文字が1つ含まれているために固定長文字列の最後に1つNull文字が付いています。
図18
通常不要なスペースを削除するにはTrim関数を使うのですが、Null文字よりも内側は「Trim関数では処理できない」ので、92行目「str3 = Replace(str(2).S, Chr(0), "")」では、Null文字(文字コード&H0 : Chr(0))を「""(長さゼロの文字列)」に置換しています。この処理で、図18の③の状態にすることが出来ます。
なお、「""」では無く「半角スペース」に置換してもOKです。
92行目の処理でNull文字が無くなったので、93行目「str3 = Trim(str3)」でTrim関数を使ってスペースを削除します。これで図18の④の状態となり、「文字列のみ」のデータとなります。
なお、92~93行目を1つの連なった式「str3 = Trim(Replace(str(2).S, Chr(0), ""))」としても、もちろんOKです。
なお、データとして半角文字しか扱わないシステムでしたら、このような処理は不要になります。ただし、入力制限(日本語入力不可など)をシステムに確実に盛り込む必要があります。
アプリ実例・関連する項目
「共有コメント付きカレンダー」「会社番号検索システム」
「文字列のスペースを削除・集結するTrim関数」
サンプルファイル
今回、説明の中で紹介したコードは、以下のサンプルファイルの標準モジュール(Module1)に記載しています。実行するにはVBEから直接実行してください。またOpenステートメントの指定固定長ファイルにはPathを指定していませんので、「C:¥Users
固定長ファイルのテキストデータ呼び込み時処理(its-005.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始) 解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。 |