プログラマ・アゲイン blog

還暦を過ぎたけどプログラマ復帰を目指してブログ始めました

Jsonデータを扱うRubyプログラムを作ってみた

前回の「Jsonデータを扱うRubyJavaScriptのプログラムを作ってみた」ブログの続きです。

Jsonのデータは、Rubyのハッシュと考えれば割とすんなり作成する事が出来ました。

ここでは、作成済みのExcel表を読んで、Json形式のテキスト・ファイルを作成するプログラムを記録したいと思います。

 

pagain.hatenablog.com

なお、Excelへのアクセスについては、以下のページで書いたwin32oleを使用しています。

 

pagain.hatenablog.com

 

 

プログラムの構造

先ず、Rubyプログラムの構造を図に整理してみます。

 

メインで処理の流れを制御し、主に実行時の引数(Excelファイル名)のチェックと、Jsonファイルへの書き出しを行っています。

作成したEXCELSHEETクラスでは、Excelを起動してwin32oleでシートからデータの読み込みを行い、インスタンス変数に格納しています。

グループ数の取得や、Json形式のテキストデータを作成して返すメソッドを持っています。

作成したLogFileクラスでは、あらかじめ決めているログファイルに、渡されたメッセージを書き出すとともに、コンソールにも出力するようにしています。

 

メイン処理

メインの処理は、Excel2Json.rb というプログラムで行っています。

先ずは、メインの処理から記述します。

メインのソース

メインのソースの全体は以下のようになりました。

#! ruby -Ku
# write json format file from excel format version 1
# 使用方法 : ruby Excel2Json.rb xxxxxxxx yyyyyyyy
# (xxxxxxxx: Excel file name without File identifier ,
#  yyyyyyyy: Json file name without File identifier(<-default: Excel file name))

# ライブラリやファイルを読み込む
require 'win32ole'
require 'logger'
require 'json'
require 'date'
Dir[File.expand_path('..', __FILE__) << './lib/*.rb'].each do |library|
  require library
end

# Excel VBA定数のロード
module ExlConst; end

# Excelファイル名の入力と書き出すJsonファイル名の取得
# Jsonファイル名未入力の場合は、Excelファイル名をセット
# Excelファイル名未入力、または余分なパラメータが入力された場合は終了
if ARGV.length == 2
  xlsx_name = ARGV[0]
  json_name = ARGV[1]
else
  if ARGV.length == 1
    xlsx_name = ARGV[0]
    json_name = ARGV[0]
  else
    raise ArgumentError, '拡張子を除いたExcelファイル名を入力してください'
    exit
  end
end

# Excelファイルの存在確認
xlsx_filename = getAbsolutePath("../#{xlsx_name}.xlsx")
if File.exist?(xlsx_filename)
  json_filename = getAbsolutePath("../#{json_name}.json")
else
  raise ArgumentError, '存在する拡張子を除いたExcelファイル名を入力してください'
  exit
end

# Main Routine
log = LogFile.new
log.log_info("Excel_to_Json has started")
begin
  # Excelファイルの読み込み、グループ・オブジェクトの作成およびグループ数の取得
  group_object = EXCELSHEET.new(xlsx_filename)
  if group_object.getGroupCount == 0
    raise ArgumentError, '正しい拡張子を除いたExcelファイル名を入力してください'
    exit
  end

  # Jsonファイルのオープン
  json_io = File.open(json_filename, "wt")
  # Main Logic:グループ・オブジェクトからデータを抽出し、Jsonファイルに書き出す
  json_io.puts(group_object.getJson)

rescue => exception
  # 障害時処理
  log.log_error(exception.full_message)
  log.log_error(exception.backtrace)
ensure
  # Jsonファイルのクローズ
  json_io.close
  log.log_info("json file #{json_filename} was written and closed")
end
log.log_info("Excel_to_Json has ended")
log.log_close

主にJsonに関わる部分について、どのようにコーディングしたかをメモします。

メイン・ルーチン

42行目からメインの処理が始まっています。

begin ~ rescue ~ ensure ~ end で、処理中にエラーが発生しても対応できるようにしています。

ファイルを扱う時には、中途半端にオープンしたままになるというのはまずいですから。

Excelファイルの読み込み
44   group_object = EXCELSHEET.new(xlsx_filename)
45   if group_object.getGroupCount == 0

 

44行目では、EXCELSHEETクラスにExcelファイル名を渡して、インスタンス・オブジェクトを作成し、group_objectという変数にセットしています。

EXCELSHEETクラスのインスタンス作成時に、自動的に実行されるinitializeメソッドについては、後述します。

45行目では、読み込んだExcelデータのグループ数(sampleの場合は地域の数)を、getGroupCountメソッドで取得し、0件かどうか判定しています。

0件であれば、無効なデータと判断し、エラー・メッセージを出力して終了します。

Jsonファイルの書き出し
49   # Jsonファイルのオープン
50   json_io = File.open(json_filename, "wt")
51   # Main Logic:グループ・オブジェクトからデータを抽出し、Jsonファイルに書き出す
52   json_io.puts(group_object.getJson)

 

50行目では、Fileクラスのopenメソッドを使って、引数(Jsonファイル名)のファイルをオープンしています。

モード "wt" は、w:書き出しで、wt:LFはプラットフォーム依存を意図してます。

この辺りは、ファイルの識別子に「.json」が付くという事以外には、通常のテキスト・ファイルI/Oと変わらないと思います。

52行目では、オープンしたファイルのインスタンス・オブジェクトのjson_ioに、putsメソッドで書き出しています。

その際の書き出すデータは、読み込んだExcelデータが、getJsonメソッドでJson形式の文字列に変換されたデータです。

 

EXCELSHEETクラス

今回のメインであるEXCELSHEETクラスは、次のような構造になっています。

インスタンス変数を5個用意しているのですが、まだそのあたりのスキルが低く、取り敢えず感は否めません。

また、Excelの表と変数の対応は、以下のようになっています。

EXCELSHEETクラスのソース

EXCELSHEETクラスのソースの全体は、以下のようになりました。

# Provides excel to json Class
class EXCELSHEET
  def initialize(xlsx_filename)
    excel = WIN32OLE.new('Excel.Application')
    WIN32OLE.const_load(excel, ExlConst)
    excel.Visible = false
    @column = Array.new
    @group_count = 0
    @column_count = 0
    @group_hash = Hash.new
    @row_hash = Hash.new

    # Main Logic:Excelデータを読み込んで、グループ・オブジェクトを作成し、グループ数を返す
    begin
      # Excelファイルのオープンと最初のワークシートの取得
      book = excel.Workbooks.Open(xlsx_filename, true)
      sheet = book.Worksheets[1]
      # 列タイトル(キー)の取得
      9.times do |i|
        item = sheet.Cells.Item(1, i+1).Value
        if item != nil
          @column[i+1] = item
          @column_count += 1
        end
      end
      # 列毎のデータ(値)の取得
      row = 2
      group_array = "["
      group_name = ' '
      until group_name == nil || row > 100 do
        group_next = sheet.Cells.Item(row, 'A').Value
        # グループの値が変更されたら、グループ名をキーとする値が配列のハッシュを書き出す
        if group_next != group_name
          # 但し、書き出すのは3行目以降のデータを読み込んだ時
          if group_name != ' ' && row > 2
            group_array = group_array.chop.chop + "]"
            @group_hash.store(group_name, group_array)
            group_array = "["
          end
          @group_count += 1
          group_name = group_next
        end
        # 1行のデータをタイトルをキーとする値がセル値のハッシュを書き出す
        for i in 2..@column_count do
          data = sheet.Cells.Item(row, i).Value
          data = data.to_i if data.to_s =~ /^[0-9]*\.[0-9]+$/
          @row_hash.store(@column[i], data.to_s)
        end
        # ハッシュ・オブジェクトを一行の JSON 形式の文字列に変換してグループに追加する
        json_str = JSON.generate(@row_hash)
        group_array = group_array + json_str + ",\n"
        # 次の行へ
        @row_hash.clear
        row += 1
      end

    rescue => exception
      # 障害時処理
      print exception.full_message
      print exception.backtrace

    ensure
      # Excelファイルのクローズ
      excel.DisplayAlerts = true
      excel.Workbooks.Close
      excel.Quit
    end
  end

  def getGroupCount
    return @group_count
  end

  def getGroupObject
    return @group_hash
  end

  def getJson
    json_str = "{\n"
    @group_hash.each do |key, object|
      json_str = json_str + '"' + key + '"' + ": \n" + object + ",\n"
    end
    json_str = json_str.chop.chop + "\n}"
    return json_str
  end
end

Jsonに関わるところについて、以下にメモします。

initializeメソッド

前述したとおり、initializeメソッドはインスタンス・オブジェクトが作成される時に自動的に実行されます。

プログラムの作りとしては別の形態も当然あると思いますが、作成したEXCELSHEETクラスでは、このinitializeメソッドの中でほとんどの処理を実行しています。

このinitializeメソッドでは、Excelの表から以下の構造の@group_hashを作り上げることを目的としています。

      {"グループ1": [{"キー":値, "キー":値,・・・},{・・・},・・・],

       "グループ2":[・・・],・・・}

なお、ハッシュ・オブジェクトの@group_hashを作るので、変数の@group_countを作る必要が無かったことに後で気づきました。

列タイトルの取得
18       # 列タイトル(キー)の取得
19       9.times do |i|
20         item = sheet.Cells.Item(1, i+1).Value
21         if item != nil
22           @column[i+1] = item
23           @column_count += 1
24         end
25       end

 

19行目から25行目までで、Excel表のそれぞれの列のタイトル・データを、@column配列にセットしています。

後にこのデータは、配列の中のハッシュのキーになります。

因みに、配列のインデックスは「0」から始まるけれど、Excelのインデックスは「1」から始まるので、配列も「1」からデータを挿入するようにしています。

列毎のデータの取得

Excel表のデータ部分を、1行づつ処理していきます。

26       row = 2
27       group_array = "["
28       group_name = ' '
29       until group_name == nil || row > 100 do
・・・
54       end

 

26行目は、Excel表の2行目から処理するように、行番号をセットしています。

27行目は、"グループ"をkeyとするハッシュの、value部分が配列の形式なので、配列を示す先頭の"["をgroup_arrayにセットしています。

理由としては、group_arrayの配列の形式が、配列オブジェクトではなく、ストリング・オブジェクトとしてプログラムを作成してしまったからです。

でも後から考えれば、ストリング・オブジェクトではなく、素直に配列オブジェクトにしておいた方が良かったのではないかと思います。

28行目は、最初の"グループ"を空白にしています。

29行目から54行目までを、until文で繰り返しています。

繰り返し条件は、A列(グループ名)のデータがnil(値が無い)になるまでですが、変に永久ループになることがあると嫌なので、取り敢えず100回の制限も付けてます。

グループ名をキーとする値が配列のハッシュをセット
30        group_next = sheet.Cells.Item(row, 'A').Value
31        # グループの値が変更されたら、グループ名をキーとする値が配列のハッシュを書き出す
32        if group_next != group_name
33          # 但し、書き出すのは3行目以降のデータを読み込んだ時
34          if group_name != ' ' && row > 2
35            group_array = group_array.chop.chop + "]"
36            @group_hash.store(group_name, group_array)
37            group_array = "["
38          end
39          @group_count += 1
40          group_name = group_next
41        end

 

30行目で、Excel表のA列の該当行のデータを、比較のための変数group_nextにセットします。

32行目で、グループが変わったかどうかを判定しています。

34行目で、今までのグループ名が空白でなく、且つ処理対象がExcel表の3行目以降かを判定しています。

正しくグループが変わったのであれば、今まで処理してきたデータに対して35行目から37行目までの処理を実行します。

35行目では、ストリング・オブジェクトのgroup_arrayに配列の最後を示す"]"を加えています。

chopメソッドは、文字列の最後の文字を取り除いた新しい文字列を生成して返してくれるので、2回実行して","、"\n"の2文字を削除しています。

36行目は、ハッシュ@group_hashに、"キー:値"を登録しています。

37行目では、ストリング・オブジェクトのgroup_arrayを初期化するために、配列の最初を示す"["をセットしています。

1行のデータからハッシュを作成
43        for i in 2..@column_count do
44          data = sheet.Cells.Item(row, i).Value
45          data = data.to_i if data.to_s =~ /^[0-9]*\.[0-9]+$/
46          @row_hash.store(@column[i], data.to_s)
47        end

 

43行目から47行目までは、該当のExcel行のデータを処理して、ハッシュ・オブジェクトの@row_hashを作成しています。

44行目から45行目は、取得したExcelのデータがFloatオブジェクトであれば、文字にした時に9999.0のように小数点以下が付いてしまうので、一度Floatオブジェクトから小数点以下を切り捨てたIntegerオブジェクトに変換しています。

46行目で、ハッシュの@row_hashに、各列の"キー:値"を登録しています。

1行のデータをJson形式の文字列に変換
49        json_str = JSON.generate(@row_hash)
50        group_array = group_array + json_str + ",\n"

 

49行目では、@row_hashはハッシュ・オブジェクトなので、これをJson形式の文字列に変換します。

JSONモジュールのgenerate functionでは、与えられたオブジェクトを一行の Json 形式の文字列に変換して返してくれます。

50行目では、group_arrayのストリング・オブジェクトに、作成したJson形式の文字列と、配列要素の区切りの","と改行"\n"を追加しています。

Json形式の文字列の取得
72   def getJson
73     json_str = "{\n"
74     @group_hash.each do |key, object|
75       json_str = json_str + '"' + key + '"' + ": \n" + object + ",\n"
76     end
77     json_str = json_str.chop.chop + "\n}"
78     return json_str
79   end

 

72行目から79行目は、ExcelのデータをJson形式の文字列で返してくれるメソッドです。

素直に配列オブジェクトを使用していれば、こんな面倒なコーディングをしなくて良かったのでしょうが、配列形式のストリング・オブジェクトにしたため、自分で文字列を組み立てています。

ハッシュの@group_hashには、以下の形でデータが入っています。

      {"キー": [配列形式の文字列],"キー":[文字列],・・・}

74行目から76行目で、ハッシュから要素を1つづつ取り出して、ストリング・オブジェクトのjson_strに追加しています。

その際、キーを""で囲ったり、適当に"\n"(改行)を追加しています。

77行目は、文字列の最後なので、不要な文字",\n"を削除しています。

 

Jsonの勉強の為にRubyのプログラムを作成してみましたが、Rubyのハッシュと表記が似ており、JSONライブラリーもあるので、扱うのが簡単と感じました。

ただ、ブログに記録するために改めてソースを見てみると、勉強不足は否めず、無駄なコーディングをあちこちしていることに気が付きました。

次にJsonを扱うRubyプログラムを作成する時には、気を付けたいと思います。