ナッキーの「Delphiでビジネスアプリ奮闘記」 - 第1回 CSVファイルをデータベースへ移行する その1

By: Hitoshi Fujii

Abstract: Delphiプログラミングの基礎を習得したナッキーが、いよいよ実用的なソフトウェア開発に着手。高橋先生の助けを借りながらも、データベースを使ったビジネスアプリ開発に挑戦します。

Hide image
nacky_adv_75x83

ナッキー

皆さん、お久しぶり、ナッキーです。今回から、友達の会社のシステム作りをお手伝いすることになったんです。高橋先生の教室が終わってから、ひとりでDelphiの修行をしていたんだけど、少しは腕が上がったかしら。


Hide image
nacky_phone

Hide image
nacky_talk

Hide image
takahashi_phone

Hide image
takahashi_talk

ナッキー:実は私、友達の会社のシステム作りのお手伝いをしてるんですよ。まだ、少しずつなんですけど、ちょっとお願いが…

高橋先生:なんだい?「代わりに作って」っていうのは反則だよ。

ナッキー:私だってそれぐらいはわきまえるようになったわ。そんな無理なお願いはしないけど、ちょっとだけ分からないことがあって。今、顧客情報をEXCELからデータベースに移行しようとしてるんですけど、その移行プログラムで、どうやってEXCELデータを読み込んだらいいかな、と思って。

高橋先生:それならEXCELのCSV出力を使えば?CSVならテキストデータだから簡単だと思うよ。

ナッキー:そうか、私EXCELのファイルを解析しなきゃいけないのかと、悩んじゃってました。また、分からないことがあったら質問していいですか?

高橋先生:いいよ。電話でもメールでも。でも、作るのは自分だからね。

ナッキー:分かってますって。じゃあ、ありがとうございました。


    システム構成

さあて、今取り組んでいるシステムの構成を説明しましょう。

友達の会社の顧客データが、現在Excelに保管されています。これまで、Excelで管理していたんだけど、だんだん数が増えてきて大変になってきたので、データベース化しようというわけです。

最初の目標は、データを移行して、表示、編集などができるようにすること。つまり、

  1. ExcelのデータをInterBaseに移す(高橋先生のアドバイスでCSVファイルを使うことにする)
  2. Delphiで開発したアプリケーションで追加・更新・削除ができるようにする
  3. 表示したデータを印刷できるようにする

です。現在は、1番のデータの移行に取り組んでいます。高橋先生のアドバイスに従って、Excelファイルから、CSVファイルに変換しなくっちゃ。

    CSVファイルとは

Excelの「CSV出力」の操作は、名前を付けて保存するときに「ファイルの種類(T):」で「CSV(カンマ区切り(*.csv))」を選択するだけでいいんです。ExcelファイルからCSVファイルに変換するのは簡単ですね。

CSVファイルには、データ以外の情報は入っていないので、プログラムで扱うのも簡単。MS-Excelがインストールされていないパソコンでも動くから、作ったプログラムを幅広く使ってもらえるわ。

CSVファイルをメモ帳で開くと、内容がどんなふうにできているかがわかります。

Hide image
01CSVファイル

図01 CSVファイルをメモ帳で開く

このように、メモ帳やワードパッドなどの「テキストエディタ」と呼ばれるツールで開いても、ちゃんとデータが読めるものを「テキストファイル」、Excelファイルのようなデータが読めないファイルのことを「バイナリファイル」っていいます。

CSVファイルはテキストファイルの一種で、データが「,」(カンマ)で区切られているという特徴があるからカンマ区切りファイルとも呼ばれています。

    テキストファイルを読み込む

前にファイルの読み込み処理を書いたときには、Memoコンポーネントのメソッドを使って読み込むことができました。今回はコンポーネントを使わずに読み込む方法を考えてみます。CSVファイルを読み込むっていうルーチンはなさそうなので、テキストファイルの読み込み方法を探します。

RAD Studioドキュメント(ヘルプ)には「標準ルーチンと入出力」というタイトルで掲載されていました。「AssignFile」ルーチンは、実際のファイルを「TextFile」型のファイル変数に割り当てるんですって。

書き方はこんな感じ。

var
  tFile : TextFile
begin
  AssignFile(tFile,'ファイル名')
end;

「ファイル名」には「c:¥~」のようにフルパスで書くか、プロジェクトのある、カレントフォルダにファイルを保存してファイル名だけを書きます。ファイル変数を経由して参照や操作ができるんですね。

開いたり、読み取ったりするのは別のルーチンを使います。ファイルを開くのが「Reset」ルーチン。1行分読み込むのが「Readln」(リードエルエヌ)ルーチンです。

Reset(tFile);
Readln(tFile,str);

ResetルーチンやReadlnルーチンでは、割り当て済みのTextFile型のファイル変数が必要です。読み込みでも、書き込みでもファイル変数を使って処理します。

1行分を読み込むReadlnルーチンで使われている文字「l」は、大文字の「I」(アイ)や数字の「1」と似ているけど、アルファベットのL(エル)なんです。間違えないようにしなくっちゃね。Readlnルーチンのパラメーターにはファイル変数のほかに、読み込んだデータを保存するためのstring型の変数を用意します。1行分が読み込まれるので、CSVファイルの場合は「aaa,bbb,ccc」のようなデータが保存されます。

    Eofルーチン

データを読み込むときには、カレント行が大切だったわね。カレント行とは現在注目しているレコードのこと。ファイルを開いたときは、1行目にカレント行があります。そして、Readlnルーチンで読み込みの処理が終わると、次の行にカレント行が移動します。データの最後の行を読み込んだ後で次の行に移動するとき、カレント行はEOFという特殊な場所に移動するんですって。EOFは「End Of File」の頭文字をとったもの。

カレント行がEOFまで移動したことは、Eofルーチンで調べることができます。パラメーターはファイル変数。最後の行を超えると、EofルーチンはTrueを返します。ループ文でデータを読み込むとき、終了条件に「Eof(tFile)=True」を使えば、最後までデータを読み込むことができるわね。もしもファイルが空だった場合にEofルーチンを使うと、はじめからTrueを返します。

while not Eof(csvFile) do
  begin
    Readln(csvFile, strData);
      //読み込んだデータを処理する
  end;

ここまでのコードをまとめてみましょう。まずはCSVのデータをみてみます。いただいたデータはExcelでしたが、保存するときにCSVに変換しました。項目は、社名、住所1、住所2、電話、HPの5つです。

社名,住所1,住所2,電話,HP
株式会社ぽんぽこ,銀座区,銀座4丁目1-78,03-123-4567,http://www.pokopon.com
渋谷セイヨ株式会社,青山区,南青山3丁目2-4,03-234-5678,http://seiyo.jp
有限会社メカミライ,丸の内区,丸の内1-2-3,03-345-6789,http://www.mekamirai.com
キャッシー薬局,銀座区,銀座7丁目3ー89,03-456-7890,http://www.knk.ne.jp/kyacy
下中山商事株式会社,池袋区,池袋2-4,03-567-8901,http://www.snny.co.jp
コヤブ洋菓子店,丸の内区,丸の内2-7,03-678-9012,http://www.nifchy.com/koyabu1
山高販売株式会社,池袋区,池袋3-14,03-890-1234,http://www.yamataka_mm.jp
メラ動物病院,六本木区,六本木2丁目3-4,03-901-2345,http://www.smallloge.ne.jp/mera23
リバーオスト株式会社,青山区,青山2丁目2-2,03-012-3456,http://www.co-net.ne.jp/river_o
杉山スターライツ株式会社,銀座区,銀座6丁目3-56,03-123-4444,http://ssl.co.jp

皆さんが試してみる場合には、テキストエディタを起動してから、データをコピー&ペーストします。保存するときはファイルの種類を「すべてのファイル」に変更してから、ファイル名を「ファイル名.CSV」にすればCSVファイルとして保存できます。データは架空のもので実在する会社や、HPアドレスとは関係ありません。

データが用意できたら、Delphiを起動してフォームにボタンをひとつ配置します。Captionプロパティや、Nameプロパティは適当につけています。

Hide image
03フォーム

図02 フォーム

ボタンのClickイベントハンドラに、これまで調べたことを基にして、CSVファイルを読み込むコードを記述します。TextFile 変数と読み込んだデータを一時的に保存する、string型の変数を1つずつ用意します。1行読み込むコードまではできたわね。コンパイルをかけて、エラーが出ないかチェックします。

procedure TfrmTransCSV.btnStartClick(Sender: TObject);
var
  csvFile : TextFile;
  strBuf : string;
begin
  AssignFile(csvFile, 'C:¥Delphi_App¥顧客.csv');
  Reset(csvFile);

  while not Eof(csvFile) do
    Readln(csvFile, strBuf);
end;

    CSVをフィールドごとに区切る

次は、読み込んだ文字列をカンマごとに区切る作業です。データベースに格納するためレコードをフィールドごとに渡さなくちゃいけません。だからReadlnルーチンで取得した文字列をカンマのたびに取り出してフィールドにあった型で渡したいんだけど、カンマ毎に区切るってどうやるんだろう?

ちょっと高橋先生に相談してみようっと。

FROM: ナッキー

TO: 高橋先生

SUBJECT: CSVファイルの1行をうまく切り出すには


高橋先生、

こんにちは、佐竹です。その後、CSVファイルを1行ずつ読み込むプログラムを作成することができました。でも、肝心のCSVファイルの1行を細かく分割する良い方法が思いつかず試行錯誤しています。

なにか、よいヒントをいただけると助かります。

佐竹


すると、次のような返事が。

FROM: 高橋先生

TO: ナッキー

SUBJECT: RE: CSVファイルの1行をうまく切り出すには


ナッキー、

こんにちは、高橋です。がんばっているね。CSVファイルといっても、ただコンマで区切られているだけだから、根気よく切り出していけばいいんだよ。でも、切り出した文字列をどこに格納するかを考えておかなくちゃね。

ちょっとヒントになるコードを送ってあげるね。

Regards,

Takahashi

--

Hide image
text_icon SAMPLE01.PAS


高橋先生から送られてきたのは、こんなコードの断片。

// uses節にStrUtilsを追加してください

for i := 0 to 3 do
  begin
    FieldPos := AnsiPos(',', strBuf);
    FieldData[i] := LeftBStr(strBuf, (FieldPos - 1));
    strBuf := RightBStr(strBuf, (Length(strBuf)- FieldPos));
  end;
FieldData[4] := strBuf;

これを、さっきのプロジェクトにコピー&ペーストします。変数も用意してステップ実行してみたら、確かに文字列が切り出されてる。よし、ここのコード分析してみよう。

まずはAnsiPos関数。ヘルプには1つ目のパラメーターにセットした文字列が、2つ目のパラメーターの中に見つかったら、それが何バイト目にあったかを返すと説明してあります。

FieldPos := AnsiPos(',', strBuf);

このコードで、何バイト目にカンマが出てくるかをFieldPos変数に代入しています。

次はLeftBStr関数。文字列の左端から一部を切り出してくれる関数です。uses節にStrUtilsユニットを追加するのは、このLeftBStr関数と次のRightBStr関数を使用できるようにするためなのね。パラメーターについてヘルプを見てみると、1つ目のパラメーターに切り出す元の文字列、2つ目のパラメーターに先頭から何バイト目まで取り出すかをセットします。ここではカンマの直前、つまりFieldPos変数の1バイト手前までを取り出したいので、「FieldPos - 1」となりました。

    FieldData[i] := LeftBStr(strBuf, (FieldPos - 1));

これで、FieldData[i]変数には、1つの項目の値が入るのね。FieldData変数の個数は項目数に合わせてありました。

Hide image
02csvデータ

図03 CSVデータ

次のコードでは、strBuf変数に改めて文字列が代入されています。使っているRightBStr関数は文字列の最後から、指定したバイト数までの文字列を切り取ってくれる関数。パラメーターについては、1つ目のパラメーターに切り出す元の文字列、2つ目のパラメーターに末尾から何バイト目まで取り出すかをセットします。Length関数は文字列の長さをバイトで返します。

    strBuf := RightBStr(strBuf, (Length(strBuf)- FieldPos));

この1文で、全体の長さからカンマまでを差し引いた長さをstrBuf変数に代入しています。ループ文で次の項目を切り出すために準備しているのね。このコードがなければ、FieldData[i]変数に1つ目の項目を繰り返し代入するところでした。

ここまでの処理をループ文で4回繰り返します。5つある項目のうち4つが埋まったわ。

最後のコードではstrBuf変数の内容をそのままFieldData[4]変数に代入しています。

FieldData[4] := strBuf;

CSVのデータには項目と項目の間にカンマがありますが、最後の項目の後ろにはカンマがないので、strBuf変数の値をそのまま代入しているのね。これで5つの項目すべてがそろいました。

イベントハンドラ内にブレークポイントを置いてステップ実行でテストすると、1行目の項目名まで格納しているのがわかります。Excelデータでは1行目が項目名になっていることが多いのよね。データベースにはデータだけ格納したいので、1行目のデータは読み飛ばしたいんです。そこで、処理する前にReadlnルーチンを呼び出して、1行分次へ送ります。

    終了処理ブロック

テキストファイルを開いたあとは、必ずファイルを閉じなくっちゃね。開いたままだと、ほかのプログラムで開けなくなることもあるんですって。なにがなんでも閉じてほしいんですが、もしも閉じる前に例外が発生したら、以降の処理が中止されてしまいます。そこで、例外が発生しても必ず処理する、終了処理ブロックというのがあります。形式は例外処理ブロックに似ています。

try
  //例外が発生しそうな処理;
finally 
  //例外が発生しても必ず実行したい処理;
end;

例外処理ブロックと組み合わせて使うこともできますよ。

try
  try
    //例外が発生しそうな処理;
  except
    //例外が発生したときする処理;
    raise;
  end;
finally
  //必ず実行したい処理;
end;

finallyとend;の間にテキストファイルを閉じる処理を入れれば、必ず閉じる処理をするっていうこと。

ファイルを閉じるコードはfinallyとend;の間に記述します。CloseFileルーチンを使ってね。パラメーターは、割り当てたファイル変数です。

try
  while not Eof(csvFile) do
    begin
      Readln(csvFile, strData);
      //読み込んだデータを処理する
    end;
finally
  CloseFile(csvFile);
end;

ファイルを閉じる処理はこれでOKね。まだ、何も出力しないので処理が終了したかどうかわからないわね。エラーのときと、うまく終了したとき、それぞれにメッセージボックスを出します。エラーが起こったかどうかは例外処理ブロックで知ることができます。ここまでのコードをまとめます。

uses節には、StrUtilsを忘れずに追加。

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, StrUtils;

データ変換の内容はこちらです。

procedure TfrmTransCSV.btnStartClick(Sender: TObject);
var
  csvFile : TextFile;
  strBuf : string;
  i : Integer;
  FieldPos : Integer;
  FieldData : array [0..4] of string;
begin
  AssignFile(csvFile, ' C:Delphi_App顧客.csv');
  Reset(csvFile);
  Readln(csvFile, strBuf);

  try
    try
      while not Eof(csvFile) do
        begin
          Readln(csvFile, strBuf);
          for i := 0 to 3 do
            begin
              FieldPos := AnsiPos(',', strBuf);
              FieldData[i] := LeftBStr(strBuf, (FieldPos - 1));
              strBuf := RightBStr(strBuf, (Length(strBuf) - FieldPos));
            end;
          FieldData[4] := strBuf;
        end;
    except
      raise Exception.Create('データを読み込めません');
    end;
  finally
    CloseFile(csvFile);
  end;
  MessageDlg('データ変換終了', mtConfirmation, [mbOK], 0);
end;

出力結果がないので、ステップ実行で変数などの値を確認します。

Hide image
04ステップ実行

図04 ステップ実行

ちゃんとデータが切り分けられているみたい。次回はデータベースにデータを移す作業をしなくっちゃね。がんばります。

Server Response from: ETNASC04