3流プログラマのメモ書き

元開発職→現社内SEの三流プログラマのIT技術メモ書き。 このメモが忘れっぽい自分とググってきた技術者の役に立ってくれれば幸いです。(jehupc.exblog.jpから移転中)

(.Net)固定長のテキストデータを読み込むための汎用的な方法

固定長のテキストデータを読み込んで何か処理をさせたいという場合で、テキストデータのフィールドの桁数がしょっちゅう変わるのでそのたびにソースを直すのは大変という事態が発生しました。

なので、読み込むテキストデータのフィールドを外部ファイルで定義し、フィールドが変わったときはその定義ファイルの中の値を変えればいいだけという風にしたいと考えました。

思いついたのは、フィールドの定義を.Net内ではDataTableで扱い、内容をXMLファイルにシリアライズしておくという方法です。

ということで、固定長ファイルを読み込み、SQLiteのデータベースに保存するコードのサンプルを載せておきます。

まずフィールド定義のためのDataTableのスキーマXMLは以下のようにしました。
【DataSet_schema.xml

<?xml version="1.0" standalone="yes"?>
<xs:schema id="AtomSekiyu_AggregateSales" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="AtomSekiyu_AggregateSales" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        
        <!-- フィールド定義テーブル。CSVより読み込むときのフィールドの定義(名前とどこからどの桁までか)を行う -->
        <xs:element name="field">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="name" type="xs:string" minOccurs="0" /><!--フィールド名-->
              <xs:element name="start_len" type="xs:int" minOccurs="0" /><!--このフィールドの開始位置-->
              <xs:element name="length" type="xs:int" minOccurs="0" /><!--このフィールドの長さ-->
              <xs:element name="type" type="xs:string" minOccurs="0" /> <!--このフィールドの型-->
              <xs:element name="dbtype" type="xs:string" minOccurs="0" /> <!--このフィールドのDBの型-->
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>

ついで、実際にフィールド定義のDataTableを以下のXMLで定義します。フィールドに修正があったときはこのファイルの値を修正します。
【Data.xml

<?xml version="1.0" standalone="yes"?>
<プロジェクト名>
  <!-- フィールド定義テーブルの値。CSVより読み込むときのフィールドの定義(名前とどこからどの桁までか)を行う -->
  <field>
    <!--商品コード 1桁目から5文字分 型はstring-->
    <name>product_code</name>
    <start_len>1</start_len>
    <length>5</length>
    <type>String</type>
    <dbtype>TEXT</dbtype>
  </field>

  <field>
    <!--日付 -->
    <name>sales_date</name> 
    <start_len>7</start_len>
    <length>6</length>
    <type>String</type>
    <dbtype>TEXT</dbtype>
  </field>


  <field>
    <!--顧客コード -->
    <name>customer_code</name>
    <start_len>14</start_len>
    <length>5</length>
    <type>String</type>
    <dbtype>TEXT</dbtype>
  </field>

  <field>
    <!-- 数量 -->
    <name>volume</name>
    <start_len>20</start_len>
    <length>5</length>
    <type>Double</type>
    <dbtype>REAL</dbtype>
  </field>


  <field>
    <!-- 金額 -->
    <name>price</name>
    <start_len>26</start_len>
    <length>8</length>
    <type>Int32</type>
    <dbtype>INTEGER</dbtype>
  </field>

</プロジェクト名>

実際のコード読み込みとSQLiteへの保存部分は以下のようになります。 (ちょいとコピー元からだいぶ改変しているのでそのままでは動かないかもしれませんが。。。)

private void ReadFileToSaveSqlite(string filePath)
{
    //フィールド定義の入ったDataSet
    DataSet mMaster = new DataSet();

    //マスタデータセット作成
    mMaster.ReadXmlSchema(@"DataSet_schema.xml");
    //フィールド定義データ挿入
    mMaster.ReadXml(@"Data.xml");
    //フィールド定義の入ったDataTable
    DataTable tblField = mMaster.Tables["field"];

    SQLiteConnection sqlite_con = new SQLiteConnection();
    //SQLite接続文字列定義
    sqlite_con.ConnectionString ="Version=3;Data Source=db.sqlite;Compress=True;";
    //DB接続
    sqlite_con.Open();

    //SQLiteのデータベース作成
    SQLiteCommand sqlite_cmd = sqlite_con.CreateCommand();

    //テーブル作成
    string cmd = "CREATE TABLE sales ( ";
    foreach (DataRow row in tblField.Rows)
    {
        cmd += row["name"] + " " + row["dbtype"] + " ,";
    }
    cmd = cmd.TrimEnd(',');
    cmd += " )";
    sqlite_cmd.CommandText = cmd;
    //クエリ実行
    var ret = sqlite_cmd.ExecuteNonQuery();
    //Console.WriteLine(ret);  


    CultureInfo culture = new CultureInfo("ja-JP", true);
    culture.DateTimeFormat.Calendar = new JapaneseCalendar();

    //固定長テキストデータ読み込み
    string[] lines = File.ReadAllLines(filePath, System.Text.Encoding.GetEncoding("Shift_JIS"));

    sqlite_cmd = sqlite_con.CreateCommand();

    //読み込んだテキストデータを行単位でループ
    for (int i = 0; i < lines.Length; i++)
    {
        //sqlite
        string strInsert1 = "";
        string strIntert2 = "";

        //フィールド単位でループ
        for (int j = 0; j < tblField.Rows.Count; j++)
        {
            string fieldName = (string)tblField.Rows[j]["name"];
            int start_len = (int)tblField.Rows[j]["start_len"];
            int substring_len = (int)tblField.Rows[j]["length"];
            //.Net内での型
            Type castType = Type.GetType("System." + (string)tblField.Rows[j]["type"]);

            start_len--; //設定ファイルは 1 始まりだけど、C#は0始まりなので。
            if (start_len < 0)
                continue;
            //文字列から該当するフィールドの値を抽出し、空白削除
            string str = lines[i].Substring(start_len, substring_len).Trim();

            //DBにセットするデータ
            object setvalue = new object();
            //フィール単位で何か個別に処理する必要(今回は和暦6桁を西暦の文字列に変換)があればここで定義
            switch (fieldName)
            {
                case "sales_date":
                    //一旦和暦文字列に変換
                    string ggDate = String.Format("平成{0}年{1}月{2}日", str.Substring(0, 2), str.Substring(2, 2), str.Substring(4, 2));
                    //西暦に変換
                    DateTime date = DateTime.ParseExact(ggDate, "ggyy年MM月dd日", culture);
                    setvalue = date.ToString("yyyy-MM-dd");
                    break;
                default:
                    //フィルド定義で指定された型にキャスト
                    try
                    {
                        setvalue = Convert.ChangeType(str, castType);
                    }
                    catch (Exception e)
                    {
                        setvalue = DBNull.Value;
                    }
                    break;
            }
            strInsert1 += fieldName + " ,";
            strIntert2 += "@" + fieldName + " ,";

            //パラメータ定義
            sqlite_cmd.Parameters.Add(fieldName, (DbType)Enum.Parse(typeof(DbType), (string)tblField.Rows[j]["type"]));
            //データをパラメータにセット
            sqlite_cmd.Parameters[fieldName].Value = setvalue;
        }
        strInsert1 = strInsert1.TrimEnd(',');
        strIntert2 = strIntert2.TrimEnd(',');

        sqlite_cmd.CommandText = "INSERT INTO sales ( " + strInsert1 + ") VALUES (" + strIntert2 + ")";
        //戻り値は影響を受けた行数
        int ret = sqlite_cmd.ExecuteNonQuery();
    }

    sqlite_con.Close();
}