MAGAZINE

ルーターマガジン

Ruby

Ruby でハッシュを使ってCSVに書き込めるクラスを作ってみた

2024.03.01
Pocket

皆様、こんにちは。エンジニアの Hodoshima です。 今回は、業務でCSVファイルへ書き出す際に不便に感じた出来事があったので、その不便を解消するようなクラスを自作してみました。

ページ下部にコードを載せたので、是非皆さんも使ってみてください。

導入

私が携わってきた業務で、 csv ファイルでデータを納品することになっている業務がありました。 そして、その csv ファイルへのデータの書き込みの依頼として、以下のものがありました (実際に受けた依頼から本質部分を抜き出して、改変して記載します)。

  • 2 つのサイト A, B にて出品されている商品のデータを記録した csv を作成してほしい
  • カラムは以下の通り
    • id
    • 商品番号
    • 商品が売られているサイト (ここでは A と B とします)
    • 名前
    • 価格
    • (サイト が A である時のみ記載) 評価値、評価数、割引価格
    • (サイト が B である時のみ記載) 出品者、残り個数、発売開始日

このように、商品が売っているサイトによって記載するカラムが別々になっているという内容でした。

Ruby による csv ファイル書き込みの課題点

クロールしたデータを Ruby で csv 形式として保存しようとした場合、次のように記述することになります (サンプル説明の都合上、商品の情報はハードコーディングで記述します)。

ここでは、出力先の csv ファイルのパスを products.csv とします。

require 'csv'

csv_file = CSV.open('products.csv', 'w', force_quotes: true)

headers = %w[id product_number site name price evaluate_value evaluate_count discount seller remain begin]
csv_file << headers

csv_file << [1, 'A-123', 'A', '人参', 500, 30, 12, 100, nil, nil, nil]
csv_file << [2, '98765', 'B', '鮪', 780, nil, nil, nil, '魚市場 根', 25, '2024/02/15']

このプログラムによって出力される csv ファイルは以下のようになります。

"id","product_number","site","name","price","evaluate_value","evaluate_count","discount","seller","remain","begin"
"1","A-123","A","人参","500","30","12","100","","",""
"2","98765","B","鮪","780","","","","魚市場 根","25","2024/02/15"

しかし、従来の ruby による csv への書き込みは以下の点で不便です。

  • 配列で記述しているので、どの値が何を意味しているのかがわかりづらい。
  • 途中でカラムを追加したくなった場合、いちいち該当の場所を探すのが面倒くさい。

ruby では csv を読み込む際に、headers オプションを true にことによって、各レコードをハッシュにように、キーで取得する箇所を指定できます (リンク)。しかし、書き込みの際にハッシュを使うことはできません。

別の言語に視線を向けると、Python では、DictWriter というクラスが用意されており、このクラスでは 1 レコードにつき 1 つの辞書 (ruby でいうところのハッシュ) を用いて csv に書き込むことが可能です (リンク)。

Python にこのような便利なクラスが用意されているので、 Ruby でも自分で作れば良いという発想になり、今回、自作で CSVHashWriter というクラスを作成しました。

使い方

このクラスの使い方を説明します。

(1) CSVHashWriter クラスのインスタンスを生成します。この際に出力先 csv のパス、書き込みモード (wa など)、ヘッダーのリスト、(必要ならば) オプションを設定します。

ヘッダーのリストは長くなることがほとんどだと思われるので、先に変数にまとめておくのが良いと思われます。

headers = %w[id product_number site name price evaluate_value evaluate_count discount seller remain begin]
csv_file = CSVHashWriter.new('products.csv', 'w', headers, force_quotes: true)

(2) 追加したい 1 レコード分の内容からなるハッシュを CSVHashWriter クラスのインスタンスに対して、 << メソッドを使って、書き込みます。

csv_file << { id: 1, product_number: 'A-123', site: 'A', name: '人参', price: 500, evaluate_value: 30, evaluate_count: 12, discount: 100 }
csv_file << { id: 2, product_number: '98765', site: 'B', name: '鮪', price: 780, seller: '魚市場 根', remain: 25, begin: '2024/02/15' }

(3) 全てのレコードの記載が完了したら、 インスタンスを close して、ファイルを閉じます。

csv_file.close

この例を上から順に実行すると、以下のような csv ファイルを得ることができます。

"id","product_number","site","name","price","evaluate_value","evaluate_count","discount","seller","remain","begin"
"1","A-123","A","人参","500","30","12","100","","",""
"2","98765","B","鮪","780","","","","魚市場 根","25","2024/02/15"

従来の方法と比較して、レコードの持ち方が配列からハッシュに変わったことで、キーバリューの対応が人間にも一目で理解で切るようになりました。そして、欠損値にも対応できているということがわかります。

これは実装の途中で列を追加したくなったとき、従来の 1 レコードが配列となっている場合で記載する方法だと位置に注意して、改修を行う必要があります。しかし、CSVHashWriter では headers を適切な位置で追加した後はハッシュにカラムの順番を気にすることなく書き込める点でも優れていると考えられます。

最後に

今回の記事で書いたように、標準で用意されていたり、配布されていたりしている機能は必ずしも自分自身の要求を完全に満たしてくれているとは限りません。 その際には諦めたり強行突破するだけではなく、自分で要求を完全に満たすような機能を自作してみるのも一つの手かもしれません。

CSVHashWriter クラスのプログラム

require 'csv'

class CSVHashWriter
  def initialize(file_path, mode, headers, **options)
    @csv = CSV.open(file_path, mode, **options)
    @headers = headers.map(&:to_sym)
    @csv << headers
  end

  def <<(record_hash)
    @csv << record_hash.values_at(*@headers)
  end

  def close
    @csv.close
  end
end
Pocket

CONTACT

お問い合わせ・ご依頼はこちらから