Jsonデータを扱うRubyプログラムを作ってみた
前回の「Jsonデータを扱うRubyとJavaScriptのプログラムを作ってみた」ブログの続きです。
Jsonのデータは、Rubyのハッシュと考えれば割とすんなり作成する事が出来ました。
ここでは、作成済みのExcel表を読んで、Json形式のテキスト・ファイルを作成するプログラムを記録したいと思います。
なお、Excelへのアクセスについては、以下のページで書いたwin32oleを使用しています。
プログラムの構造
先ず、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プログラムを作成する時には、気を付けたいと思います。