ユーザによって登録されるデータの保存について

解決


Kob  2003-04-17 03:44:32  No: 3340

作成しているアプリ内で、ユーザーが設定(登録)したデータを
保存する必要があるのですが、その保存(と読み込み及び例外処理)の仕方について
ご意見頂けたら幸いです。

簡単なデータ(例えば、チェックボックスのオン・オフなど)の場合には、
INI ファイルに、そのデータを保存することで解決しています。

より複雑なデータの場合には、適当な外部ファイルを作成して、
現在は保存を行っています。

それで、その複雑なデータを外部ファイルに保存する仕方についてなのですが、
例えば、ユーザーが電話帳を登録したい場合を考えて見ますと、
電話帳に記載される「名前」「電話番号」「その他」などの項目を
設定し、それを保存する場合には、どのように外部ファイルに書き込みを
行ったら、より良い方法となるのでしょうか?

といいますのも、例えば、TStringList を用いて、

procedure SaveData(Name, Number: string);
const
  Delimiter = '#';
var
  sl: TStringList;
begin
  sl := TStringList.Create;
  sl.LoadFromFile('データを保存してあるファイル');
  sl.Add(Name + Delimiter + Number); // デリミタで、名前と電話番号の境目の目印をつける
  sl.SaveToFile('データを保存してあるファイル');
  sl.Free;
end;

とすれば、用意に保存でき、また、データを読み込む場合でも
上記のサンプルと同じような様式で、簡単にデータを取り込むことが
できますが、以下の点において、このようなデータの保存は
汎用性に欠けると考えています。

・ユーザーによって、故意にデータの書き換えが行われた場合の
対処の仕方を一意に決めることができない。
・電話帳に、付加情報(例えば、特定の人物だけ、「その他」の項目を付け足したい等)がある場合、つまり、データ項目の増減などがある場合には、保存形式を分岐して
処理を行う必要がある。

などがあると思います。それで、現在検討しているのは、
TFileStream 等を用いて、

+---------------------+--------------------+
|データ情報(ヘッダ) | 具体的なデータ情報 |
+---------------------+--------------------+

のように、ヘッダにデータの情報(上記の例でいけば、登録されている人数は
何人いるか、及びデータのサイズ等)を記述して、その直下に具体的なデータ情報
(名前、電話番号等)を記述して、ファイルに保存するようにできないかを
考えています。(さらに、ファイルをテキスト形式で保存するのではなく、
バイナリ形式(?)での、保存を考えています。つまり、秀丸などのテキストエディタで
ファイルを開いても、中身に何が記述されているのかを分からないようにしたい)

ただ、上記のような方法でも、ユーザーが設定を保存したファイルを開いてデータの
書き換えを行った場合には、その対処をする必要があるのですが…。

何か、ユーザーによって動的に設定されるデータの読み書きを行うに当たって、
どのような点を考慮すればよいかなどのご意見頂けたら幸いです。

よろしくお願いいたします。


にしの  2003-04-17 06:21:44  No: 3341

問題は2つですね。
1.安易に読まれないファイル形式
2.ユーザが、他アプリケーションなどでファイルに変更をかけたときの対処

1は、古典的な手法ですが、固定長フィールドのランダムアクセスファイルで、よろしいかと思います。

単純に、データ保存用のRecord型を用意して、あらかじめバイナリになるように設定しておき、そのままread/writeすればOKだと思います。
もし、名前の欄などで、固定バイトにしたくない場合は、インデックスとデータ本体を分けて書き込めばよいと思います。その場合、固定長と違ってn件目のデータを1回で取得できないので、1から順にインデックスを探していき、n件目のインデックスからデータ本体の位置を取得していきます。
効率を考えたら、インデックスだけ固定長にするか、インデックスとデータを別々にするのもよいかもしれません。

2は、結構難しい問題です。ユーザの変更はどこまで許されるか、が問題となります。
一番簡単なのは、CRCなりMD5なり、現在のデータの整合性を確認するためのキーをファイルの一部に書き込んでおき、読み込むときに整合性チェックを行うというものです。
この場合、「正常」「異常」の区別はできても、修復はできません。
修復も含めたい場合は、修復用データも持たねばなりません。
簡単なのは、ZIPなどで圧縮したデータをファイルに埋め込むことです。この部分が壊れたらおしまいですが、本体部分が壊れても圧縮部分が生きていれば、修復できます。


Kob  2003-04-18 02:39:15  No: 3342

にしのさん、どうもありがとうございます。

今回、私が考慮せねばならない点が、にしのさんのご指摘により
 2 つあることが分かりました。

それで、まず、データの読み書きについて取り掛かろうと考えています。

適当なレコードを作成し、TFileStream を用いてのデータの
読み書きするコードを作成してみました。

type
  TMyRec = record
    Name: array[0..9] of Char; // string[10] との違いは?
    Number: array[0..9] of Char;
  end;

const
  MyFileName = 'test.dat';

// データの書き込みを行う
procedure TForm1.Button1Click(Sender: TObject);
var
  fs: TFileStream;
  MyRecArr: array[0..3] of TMyRec;
  i: Integer;
begin
  // データのセット
  for i := 0 to 3 do begin
    StrPCopy(MyRecArr[i].Name, 'Name'+IntToStr(i));
    StrPCopy(MyRecArr[i].Number, 'Number'+IntToStr(i));
  end;

  fs := TFileStream.Create(MyFileName, fmCreate);
  try
    for i := 0 to 3 do begin
      fs.Write(MyRecArr[i], SizeOf(TMyRec));
    end;
  finally
    fs.Free;
  end;
end;

// データの読み込みを行う
procedure TForm1.Button2Click(Sender: TObject);
var
  fs: TFileStream;
  MyRec: TMyRec;
  sz: Integer;
begin
  sz := SizeOf(TMyRec);
  fs := TFileStream.Create(MyFileName, fmOpenRead);
  try
    while sz = fs.Read(MyRec, sz) do begin
      ListBox1.Items.Add(MyRec.Name + ' : ' + MyRec.Number);
    end;
  finally
    fs.Free;
  end;
end;

上記のようなレコード型である TMyRec に Name, Number を持たせています。
コードを実行すると、正常に動作するのですが、宜しければ
ご意見いただけると助かります。

1. TMyRec にある Number: array[0..9] of Char; は、
 Number: string[10] と等価と考えて宜しいのでしょうか?

上記の Number を string[10] と変更した場合、StrPCopy の箇所を
適切に変更すれば、正常に動作させる事が出来ました。ということは、
Char の配列(サイズ 10) と string[10] は完全な等価と
考えて宜しいのでしょうか?(何を気にしているのかといいますと、
C 言語などは、末尾がヌルで終わるようになっていると
記憶していますので、その辺の事を考慮する必要があるのかが
分からないでいます)

2. TMyRec が保持できるデータは、固定長です。もし、Number を
 可変長にしたい場合には、何を考慮すればよいのでしょうか?

 例えば、Number: string; とした場合、Number は AnsiString 型に
なり、すなわち Number はポインタを意味しているので、
上記のサンプルコードのような手法では、データを保存することは
できませんよね。私が考えているのは、恐らく、可変長データを
扱う場合には、その(可変長)データのサイズを別の値として保持しておき、
そのサイズ分のデータを書き込む(或いは読み込む)のだと思うのですが、
その場合、可変長データとして扱いたい Number は、どのような型として
宣言すればよいのでしょうか?

3. 上記のような形式のデータ書き込みは、にしのさんのおっしゃっている

> 単純に、データ保存用のRecord型を用意して、あらかじめバイナリに
> なるように設定しておき、そのままread/writeすればOKだと思います。

となっているのでしょうか?
「予めバイナリになるように設定」というのは、何か特別な処置を
行う必要があるのでしょうか?

長くなってしまいましたが、ご意見頂けたら幸いです。

※ユーザーによって、ファイル変更された場合の対処の件は、

> 一番簡単なのは、CRCなりMD5なり、現在のデータの整合性を確認するためのキーを
> ファイルの一部に書き込んでおき、読み込むときに整合性チェックを行うというも
> のです。

という対処ができれば、私としてはとても満足です。ただ、CRC, MD5 などは、
初めて聞くものですものですので、少し自分で調べてから、報告させて下さい。

よろしくお願いいたします。


にしの  2003-04-18 04:57:40  No: 3343

> 1. TMyRec にある Number: array[0..9] of Char; は、
>  Number: string[10] と等価と考えて宜しいのでしょうか?
まず、
TTest1=record
  str: array[0..9] of Char;
end;
と、
TTest2=record
  str: string[10];
end;
の長さ(SizeOf)を確認してください。そして、ヘルプで「短い文字列」について調べてみてください。なぜそうなるのかわかります。

> 2. TMyRec が保持できるデータは、固定長です。もし、Number を
>  可変長にしたい場合には、何を考慮すればよいのでしょうか?

例を出します。
"A","AB","ABC","ABCD"
というデータを保存するとき、
"A",$0,"A","B",$0,"A","B","C",$0,"A","B","C","D",$0
となっていれば、$0で区切りながら読み込むことができますが、4つ目のデータをとるのに最初から順に読み込まなければなりません。

他の方法として、
0,2,5,9
というインデックスデータがあり、
"A",$0,"A","B",$0,"A","B","C",$0,"A","B","C","D",$0
という本体があれば、4つ目のデータは9番目から読み込めばいいということを、すぐに判別できます。データ量が多くなればなるほど、上の処理と比べるとその速度に差が出ます。

> 3. 「予めバイナリになるように設定」というのは、何か特別な処置を
> 行う必要があるのでしょうか?

データは読まれたくないのですよね?
たとえバイナリデータで書き込んだつもりでも、文字列をそのまま出力すれば(前後の文字が化けていたとしても)名前などの項目は読めてしまいます。
これを読めない形式に変換する必要があります。バイナリである必要はありません。読めないようにするだけです。


Kob  2003-04-20 04:11:36  No: 3344

にしのさん、どうもありがとうございます。

> (略)
> の長さ(SizeOf)を確認してください。そして、ヘルプで「短い文字列」について調べて> みてください。なぜそうなるのかわかります。

確認してみました。短い文字列の場合は、「文字の長さ」を保持しているのですね。

> 0,2,5,9
> というインデックスデータがあり、
> "A",$0,"A","B",$0,"A","B","C",$0,"A","B","C","D",$0
> という本体があれば、4つ目のデータは9番目から読み込めばいいということを、すぐに判別できます。データ量が多くなればなるほど、上の処理と比べるとその速度に差が出ます。

すいません。これなのですが、にしのさんに解説して頂きました内容は、
理解できました。ですが、これをどのようにプログラムするのでしょうか?
厚かましくて、大変申し訳ないのですが、サンプルなどいただけると助かります。

よろしくお願いいたします。

> データは読まれたくないのですよね?

いえ、読まれても構わないんです。他のアプリからのデータ修正を
して欲しくないのが、僕が望んでいることです。
ですから、データが他アプリから編集されたことが分かるだけで十分なんです。
誤解させてしまいまして、すいませんでした。これは、前回教えていただきました
「データの整合性」を行い、解決させようと考えています。


にしの  2003-04-20 07:41:37  No: 3345

> 厚かましくて、大変申し訳ないのですが、サンプルなどいただけると助かります。
あまり難しくないですよ。
以下にサンプルを示しますが、エラー処理など抜けている部分がたくさんあります。適宜直してください。
途中まで作って、今回はインデックスと本体を分ける必要がないことに気づきました。分ける必要があるのは、ファイルの一部を読み出したいときです。一度すべてを読み込んでから使うのであれば、本体にインデックスを追加し、
[次のINDEX][本体][次のINDEX][本体]...
でOKです。
今回のサンプルは、インデックスと本体を分けています。
TMyRec=record
  Name: String;
  Number: String;
end;
TMyRecArray = array of TMyRec;

・・・

procedure TForm1.SaveToFile(const FileName: String; rec: TMyRecArray; iLength: Integer);
var
  idxStrm: TFileStream;
  datStrm: TFileStream;

  i: integer;
  iBufLen, iValue: integer;
  buf: Pointer;
begin
//ファイル構造
//[IDX]
//+0000(4) Length
//+0004(4) rec[0].Nameのインデックス
//+0008(4) rec[0].Numberのインデックス
//+000C(4) rec[1].Nameのインデックス
//+0010(4) rec[1].Numberのインデックス
//+n*8+4+0(4) rec[n].Nameのインデックス
//+n*8+4+4(4) rec[n].Numberのインデックス

//[DAT]
//+0000(?) rec[0].Nameの本体
//+????(1) $0
//+????(?) rec[0].Numberの本体
//+????(1) $0

  idxStrm := nil;
  datStrm := nil;
  try
    idxStrm := TFileStream.Create(FileName + '.idx', fmCreate  or fmOpenWrite);
    datStrm := TFileStream.Create(FileName + '.dat', fmCreate  or fmOpenWrite);

    idxStrm.Seek(0, soFromBeginning);
    datStrm.Seek(0, soFromBeginning);

    idxStrm.Write(iLength, 4);
    for i := 0 to iLength - 1 do
    begin
      iValue := datStrm.Position;
      idxStrm.Write(iValue, 4);
      iBufLen := Length(rec[i].Name) + 1;
      buf := GetMemory(iBufLen);
      CopyMemory(buf, PChar(rec[i].Name), iBufLen);
      datStrm.Write(buf^, iBufLen);
      FreeMemory(buf);
      
      iValue := datStrm.Position;
      idxStrm.Write(iValue, 4);
      iBufLen := Length(rec[i].Number) + 1;
      buf := GetMemory(iBufLen);
      CopyMemory(buf, PChar(rec[i].Number), iBufLen);
      datStrm.Write(buf^, iBufLen);
      FreeMemory(buf);
    end;
    iValue := datStrm.Position;
    idxStrm.Write(iValue, 4);

  finally
    if Assigned(idxStrm) then idxStrm.Free;
    if Assigned(datStrm) then datStrm.Free;
  end;

end;
procedure TForm1.LoadFromFile(const FileName: String; var rec: TMyRecArray; var iLength: Integer);
var
  idxStrm: TFileStream;
  datStrm: TFileStream;

  i: integer;
  iBufLen: integer;
  buf: Pointer;
  iPos, iNextPos: integer;
begin
  idxStrm := nil;
  datStrm := nil;
  try

    idxStrm := TFileStream.Create(FileName + '.idx', fmOpenRead);
    datStrm := TFileStream.Create(FileName + '.dat', fmOpenRead);

    idxStrm.Seek(0, soFromBeginning);
    datStrm.Seek(0, soFromBeginning);

    idxStrm.Read(iLength, 4);
    SetLength(rec, iLength);
    idxStrm.Read(iPos, 4);
    for i := 0 to iLength - 1 do
    begin
      idxStrm.Read(iNextPos, 4);
      iBufLen := iNextPos - iPos;
      buf := GetMemory(iBufLen);
      datStrm.Read(buf^, iBufLen);
      rec[i].Name := Copy(String(buf), 1, iBufLen - 1);
      FreeMemory(buf);
      iPos := iNextPos;

      idxStrm.Read(iNextPos, 4);
      iBufLen := iNextPos - iPos;
      buf := GetMemory(iBufLen);
      datStrm.Read(buf^, iBufLen);
      rec[i].Number := Copy(String(buf), 1, iBufLen - 1);
      FreeMemory(buf);
      iPos := iNextPos;
    end;
  finally
    if Assigned(idxStrm) then idxStrm.Free;
    if Assigned(datStrm) then datStrm.Free;
  end;

end;


Kob  2003-04-21 04:16:49  No: 3346

にしのさん、サンプルまで作成していただき、どうもありがとう御座います。

早速、示していただいたサンプルを実行してみました。
インデックスデータと、本体を別ファイルにしてある方が、
プログラムとして、分かりやすく感じましたので、
基本構造は、にしのさんに示していただいたコード通りの
骨格で、プログラムを作成していこうと思います。どうもありがとうございました。

それで、ファイルの整合性チェックを調べる前に、

> エラー処理など抜けている部分がたくさんあります。適宜直してください。

と、言われている部分の対応を行おうと考えいるのですが、
示していただいたプログラムで、エラー処理を行う必要がある部分は、

・ファイルが存在するかどうかのチェック

ぐらいしか思いつかないのですが、他には何かありますでしょうか?
といいますのも、「データの整合性チェック」は、

http://www.efg2.com/Lab/Mathematics/CRC.htm

にありますサンプルを参考にして作成していこうと考えていますが、
このチェックを行えば、ファイルに書き込まれているデータは、
チェックした結果が OK であれば、「(内容、構造、共に)ただしく記述されているデータ」となりますので、
にしのさんに示していただいたコードで、チェックを行う必要がある部分は、
ファイルが存在するかどうかぐらいしか思いつかないのですが…。
(※CRC に関しては、殆ど無知ですので、何か勘違いしているかもしれません。
 そうでしたら、すいません)

 よろしくお願いいたします。


にしの  2003-04-21 04:36:52  No: 3347

> エラー処理を行う必要がある部分は、
> ・ファイルが存在するかどうかのチェック
> ぐらいしか思いつかないのですが、他には何かありますでしょうか?

ファイルの書き込みであれば
・ファイルが存在しないときに作成できない
・ファイルが存在するときにファイルが開けない
・ファイルに書き込めない
くらいですかね。
いろいろあっても、結局これらは「ファイルが作成できない」ということになりますので、try...exceptで例外が発生したらエラーを表示すればOKでしょう。

読み込み時には、
・ファイルが存在しない
・ファイルが開けない
・ファイルが読めない
は、書き込みと同じように「読み込めない」と処理できます。
整合性チェックを加えるとき、上のような例外が発生しなくても、整合性があわなかったときに「読み込めない」と処理すべきです。
整合性があわなかった場合と、エラーで読み込めない場合を分けたい場合は、整合性チェックは例外処理と同じにせずに、if文で処理させてやればよいかと思います。

> このチェックを行えば、ファイルに書き込まれているデータは、
> チェックした結果が OK であれば、「(内容、構造、共に)ただしく記述されているデータ」となりますので、
> にしのさんに示していただいたコードで、チェックを行う必要がある部分は、
> ファイルが存在するかどうかぐらいしか思いつかないのですが…。

CRCが正しければ、ファイルは前と同じ、とは限りません。同じCRCを持つデータがあるかもしれませんから。
そういう意味でも、たとえば、次のデータ位置の、1バイト前のデータが$0でなければ、エラーとする、などの処理が必要になるかと思います。

C言語が読めるのであれば、「C言語による最新アルゴリズム事典」という本(奥村晴彦著・技術評論社・ISBN4-87408-414-1)に、CRC16,CRC32の解説とサンプルが出ているので参考になるかと思います。題名通り、CRCだけでなく様々なアルゴリズムが出ています。
以前には、「Pascalによる〜」が出ていたそうですが、その改訂版として出ました。
# といっても、かなり古いので今はもっと新しいアルゴリズム本が出ているかもしれません


Kob  2003-04-23 07:40:24  No: 3348

にしのさん、いつもどうもありがとうございます。

>ファイルの書き込みであれば
>・ファイルが存在しないときに作成できない
>・ファイルが存在するときにファイルが開けない
>・ファイルに書き込めない
>いろいろあっても、結局これらは「ファイルが作成できない」ということになりますので、try...exceptで例外が発生したらエラーを表示すればOKでしょう。

ファイルが開けない場合も考慮する必要があるのでしたね。
ありがとうございます。try..except で囲ってエラー処理を行っていこうと思います。

>CRCが正しければ、ファイルは前と同じ、とは限りません。同じCRCを持つデータがあるかもしれませんから。
>そういう意味でも、たとえば、次のデータ位置の、1バイト前のデータが$0でなければ、>エラーとする、などの処理が必要になるかと思います。

確かに、万が一ということもあるかもしれませんね。CRC 以外にも
簡単なチェックを行うようにしてみます。

>C言語が読めるのであれば、「C言語による最新アルゴリズム事典」という本(奥村晴彦著・技術評論社・ISBN4-87408-414-1)に、CRC16,CRC32の解説とサンプルが出ているので参考になるかと思います。題名通り、CRCだけでなく様々なアルゴリズムが出ています。

さっそくみてみました。

http://www.matsusaka-u.ac.jp/~okumura/publications.html

で、ソースをダウンロードすることができましたので、早速 Delphi に
書き直したところ、簡単な実験では、(C での実行結果と Delphi での実行結果が)
同じ CRC 値(?) を返してくれています。
それで、C から Delphi への変換の際に、私が行ったものが正しい変換かどうか
よろしければ、チェックをお願いできないでしょうか?よろしくお願いします。

以下のように変換しました。(crc32.c の ungisned long crc2(int n, byte c[]))

C → Delphi

r は ungisned long として宣言 → r は Cardinal として宣言
ungisned long → Cardinal
r ^= c[i] → r := r xor c[i]
CHAR_BIT → 8
if (r & 1) → if Boolean(r and 1) then
r = (r >> 1) ^ 定数 → r := (r shr 1) xor 定数
r >>= 1 → r := r shr 1
return r ^ 0xFFFFFFFFUL → Result := r xor $FFFFFFFF

以上です。よろしくお願いいたします。


にしの  2003-04-23 07:45:14  No: 3349

おおむねよろしいかと思います。
ただ、Boolean(r and 1) は強引な気がします。
(r and 1)=1のほうが文字数も少ないですし(笑)。


Kob  2003-04-23 07:45:49  No: 3350

すいません、訂正です。

> r は ungisned long として宣言 → r は Cardinal として宣言
> ungisned long → Cardinal

誤り→ungisned
正解→unsigned

失礼しました。


Kob  2003-04-24 07:42:33  No: 3351

にしのさん、どうもありがとうございます。

> おおむねよろしいかと思います。
> ただ、Boolean(r and 1) は強引な気がします。
> (r and 1)=1のほうが文字数も少ないですし(笑)。

そうですね。教えていただいた方でコードを作成していこうと思います。

にしのさん、どうもありがとうございました。大変お世話になりました。
後は、なんとか自分で作成できそうです。

また、質問する際はよろしくお願いいたします。
ありがとうございました。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加