既存システムのリプレイス対応にて、元々機能として提供されていたDBFファイルの入出力処理をRailsで実装することになったので、ノウハウの書き起こしです。
(ファイルフォーマットとしてはレガシーな部類なのかと思いますので、誰得な内容です… 😅)
はじめに説明しておきますが、Ruby単体でDBFファイルの取り扱いは完全にサポートされていないので、処理を部分的にGoへ委譲しました。ご参考になれば幸いです。
使用するGem
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側で呼び出してあげれば処理は完了です。