Wakotech Blog

ポエム

rubyの簡単なスクリプトでファイル・文字列操作 〜MH4Gのお守りファイル処理を例に〜

ハンター的なお話(前置き)

私はモンスターハンター4Gにはまっています。スキルシミュレータ(泣)さんで装備を組ませていただいています。
そこで大事になるお守りなのですが、お守りの管理ってしっくりくるものがなく困っていたところ、(泣)さんでお守りのインポート/エクスポートができるようになりました。
ので、rubyで簡単なスクリプトを書いてソートしたのですが、思ったより色々やったのでファイル操作について書いておきます。

お守りデータの解釈

エクスポートされたお守りはこのような形です

,3,乗り,8
,2,溜め短縮,6
,1,納刀,6,属性解放,6

一番前にあるコロンはおそらく区切りでしょう。最後ではなく最前に持ってくる記法ですね。
ちなみに構成は、[,スロット,第1スキル名,第1スキルポイント(,第2スキル名,第2スキルポイント)]という形です。
このようなエクスポートされたお守りファイルをamulets.txtとして保存してあるとして、まずはこれを開いて解釈するスクリプトを書きます。

path = 'amulets.txt'
file = File.open(path)
amulets = Array.new

file.each_line do |line|
  arr = line.chomp.split(',')
  amulets << arr
end

File.openでファイルを開き、each_lineで各行ごとに、改行文字を取り除いた上でカンマで区切り1つのお守りの情報を配列とし、それをさらに配列amuletsに格納します。

次に見やすくすることが目的なので、スキル1とスキル2のスキル値の大きい方を前にするように入れ替えます。

amulets.each do |amu|
  if amu[4] && amu[5].to_i > amu[3].to_i
    amu[2..5] = amu[4], amu[5], amu[2], amu[3]
  end
end

まず最初にamu[4](スキル2)が存在するかを確認した上で、スキル1とスキル2のスキル値を比較します。存在を確認しないで比較すると、存在しない場合にエラーになります。
比較する際に、元の要素は文字列であるため、整数型にキャストしてから比較します。(文字列の場合'10'<'2'となる。これは後述のソートの際も同様の理由でキャストが必要。)
スキル2の方が大きい場合は入れ替えるのですが2要素がセットなので代入で入れ替えます。配列に複数代入するのにrubyではこの書き方ができます。

ソートの順序を定義する

このままでもソート自体はできるのですが、文字順になってしまい使い勝手が悪くなってしまいます。
そのためここでは、スキルシミュレータのスキル順序でソートのルールを定義します。
まず定義するためにシミュレータページからセレクトボックスの中身をいただいてきます。(個人利用なのでここには内容は載せません。)
セレクトボックスなので、以下の形式になっているはずです。

<select>
  <option value="スキル名">スキル名</option>
  <option value="スキル名">スキル名</option>
  <option value="スキル名">スキル名</option>
  <option value="スキル名">スキル名</option>
</select>

ここから正規表現でスキル名をこの順序で取り出します。最終的にはスキル名をキー、優先度を表す整数をバリューとしたハッシュを吐き出します。
まずは上記のソースからoption部分だけど切り出し、これをskills.txtとします。取り方にもよるのですが、私の場合インスペクタからだと改行されていなかったので、その前提で処理します。

require 'yaml'

path = 'skills.txt'
file = File.open(path)
hash = Hash.new

# 正規表現でスキル名を切り出し
skill_regexp = /<option\svalue=".*?">(.*?)<\/option>/
keys = file.scan(skill_regexp).map{|key| key[0]}
size = keys.size

# Hashの要素を追加
keys.each.with_index do |key, i|
  hash[key] = size-i
end

#YAMLファイルに出力
File.open('skills.yml', 'w') do |f|
  YAML.dump(hash, f)
end

正規表現は、.*では最長で検索され全体がマッチしてしまうため、最短検索の?をつけ.*?とします。括弧でくくった部分がマッチ結果として配列で返され(この場合は一箇所のみのため要素1つの配列)、scan関数で結果を配列に格納します。
そのため結果が2次元配列となってしまうが、keysを2次元配列にするべきではないためこの時点でmapで対処。(flattenでも同様になるが意味を明確にするためにmapを使用。)
ソートを降順で行うため、降順でハッシュにし、YAMLファイルへ出力。100要素をハッシュのためYAMLで外部に出力するのが適切かと思います。

実際にソートを実行する

require 'yaml'

path = 'amulets.txt'
file = File.open(path)
amulets = Array.new
file.each_line do |line|
  arr = line.chomp.split(',')
  amulets << arr
end

amulets.each do |amu|
  if amu[4] && amu[5].to_i > amu[3].to_i
    amu[2..5] = amu[4], amu[5], amu[2], amu[3]
  end
end

# 出力したYAMLを読み込む。Hash。
skills = YAML.load_file('skills.yml')

amulets.sort_by! do |amu|
  [skills[amu[2]], amu[3].to_i, amu[1].to_i]
end
amulets.reverse!

out_amulets = amulets.map{|amu|amu.join(',')}

File.open('out_amulets.txt', 'w') do |f|
  out_amulets.each{|amu| f.write("#{amu}\n")}
end

sort_by!はブロック引数でソートする破壊的メソッドです。配列で渡された場合は前からの優先度でソートします。
ここではamu[2]をキーとしたバリュー(つまりスキル名に対応した通し番号)、第1スキルポイント、スロット数の優先度でソートします。数値比較のため文字列は整数型にキャストしています。
このソートは降順で、昇順にするためには本来はreverseすれば良いのですが、複数項目を指定する場合は降順昇順が混在することはできません。そのため、降順に揃えます。
最後は元と同じようにカンマで繋げ、各行ファイル出力することで完了となります。


このような簡単なスクリプトでソートツールを自作できるわけです。が、今回は最適っぽいのを調べていたら時間がかかってしまいました。Rubyなので簡単な処理ほど別の記述の仕方がありそうという気がしてしまいます。
あまりファイル操作っぽい話ではなくなってしまいましたが、スクリプトっぽいのは書けたと思いますので今回はこれにて。明日はRailsについて書いて今年の締めとします。