TES Blog

株式会社テクニカルエンジニアリングサポートに所属する社員が、自身が携わるテクノロジーやイベントに関する情報を発信しています。

RubyとGoで、DBFファイルを扱う

既存システムのリプレイス対応にて、元々機能として提供されていたDBFファイルの入出力処理をRailsで実装することになったので、ノウハウの書き起こしです。
(ファイルフォーマットとしてはレガシーな部類なのかと思いますので、誰得な内容です… 😅)

はじめに説明しておきますが、Ruby単体でDBFファイルの取り扱いは完全にサポートされていないので、処理を部分的にGoへ委譲しました。ご参考になれば幸いです。

使用するGem

  • dbf
    • ファイルの読み込み用
  • shp
    • ファイルへの出力用

DBFファイルの読み込み

DBF::Table.new を使用して対象のファイルを読み込みます。
結果は2次元配列に格納されるので、取得したい値のインデックス位置を特定して加工するのがベターかと思います。

DBF_COLUMN_INDEX = {
  id: 0,
  name: 1,
  email: 2
}.freeze
  
records = DBF::Table.new('/path/to/import.dbf', nil, 'Shift_JIS').map do |row|
  {
    id: row[DBF_COLUMN_INDEX[:id]],
    name: row[DBF_COLUMN_INDEX[:name]],
    email: row[DBF_COLUMN_INDEX[:email]]
  }
end

DBFファイルへの書き込み

SHP::DBF.create を使用してファイルを生成します。
生成したファイルを対象に、カラム情報と紐づくデータを追加していきます。 fields 変数の name で指定している値については、読み取りを行う環境に合わせてエンコードが必要です。(今回はWindowが対象なので CP932

dbf_file = SHP::DBF.create(file_path)

fields = [
  { name: 'ID'.encode('CP932'), type: DbfFileMaker::COLUMN_STRING, width: 254, decimals: 0 },
  { name: '名前'.encode('CP932'), type: DbfFileMaker::COLUMN_STRING, width: 254, decimals: 0 },
  { name: '年齢'.encode('CP932'), type: DbfFileMaker::COLUMN_INTEGER, width: 3, decimals: 0 }
]
fields.each do |field|
  dbf_file.add_field(field[:name], field[:type], field[:width], field[:decimals])
end

data = [
  [1, 'test1', 12],
  [2, 'test2', 22],
  [3, 'test3', 32]
]

data.each_with_index do |details, row_index|
  details.each_with_index do |value, column_index|
    if value.nil?
      dbf_file.write_string_attribute(record_no, index, '')
      next
    end

    case fields[index][:type]
    when COLUMN_STRING
      dbf_file.write_string_attribute(row_index, column_index, value)
    when COLUMN_INTEGER
      dbf_file.write_integer_attribute(row_index, column_index, value)
    end
  end
end

dbf_file.close

が、ここで落とし穴なのですが、 shp は日付型のサポートはしていないため、RailsがDBから取得した値をそっくりそのまま出力することができませんでした。 他のDBFファイルを扱うGemを探してみましたが見つからず…。 ここで冒頭に書いたとおり、他言語であるGoを頼りにしました。

処理の流れを整理

すべてをGoに任せると構成がややこしくなるため、以下の通りで役割を整備しました。

  • Ruby on Rails
    • DBからのデータ取得
    • 抽出したレコードをCSVへ出力
    • Goの実行ファイル呼び出し(引数として保存先のパス文字列を渡す)
  • Go
    • CSVファイルのパスを受け取り、DBFファイルを出力

使用ライブラリ

DBFファイルへの書き込み(Go版)

(色々エラーハンドリングが足りていないですが…) godbf.New を使用してファイルの実体を作成し、カラム情報の追加と、CSVから取得したレコードを読み込んだ行を追加しています。 最後に SaveFile を呼び出して指定のパスへ出力します。

package main
import (
    "fmt"
    "flag"
    "github.com/LindsayBradford/go-dbf/godbf"
    "encoding/csv"
    "os"
)

func main(){
        // パスの受け取り
        var (
                csv_path = flag.String("csv_path", "default", "string flag")
                out_path = flag.String("out_path", "default", "string flag")
        )
        flag.Parse()
        fmt.Println(*csv_path)
        fmt.Println(*out_path)

        dbfTable := godbf.New("Shift_JIS")

        dbfTable.AddTextField("ID", 254)
        // 日付が使える!       
        dbfTable.AddDateField("日付")
        dbfTable.AddNumberField("年齢", 3, 0)
        dbfTable.AddTextField("名前", 254)

        file, err := os.Open(*csv_path)
        if err != nil {
                panic(err)
        }
        defer file.Close()

        reader := csv.NewReader(file)
        var line []string
        var currentRow int = 0
        for {
                line, err = reader.Read()
                if err != nil {
                        break
                }

                dbfTable.AddNewRecord()
                dbfTable.SetFieldValue(currentRow, 0, line[0]) // ID
                dbfTable.SetFieldValue(currentRow, 1, line[1]) // 日付
                dbfTable.SetFieldValue(currentRow, 2, line[2]) // 年齢
                dbfTable.SetFieldValue(currentRow, 3, line[3]) // 名前
                currentRow = currentRow + 1
        }

        dbfTable.SaveFile(*out_path)
}

最終的にビルドしたGoファイルをRuby側で呼び出してあげれば処理は完了です。