12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

定時退社力を高めるための方策、そのほんのさわり

Posted at

これはマイネット Advent Calendar 2016 18日目の記事です。
本日は最近コードを書いている時間よりGoogle検索している時間の方が長いのに悩んでいる@aonoがお送りします。

##エンジニアは定時に帰りたい
家族との時間を増やしたい、ペットをモフりたい、酒飲みたい、録画したアニメを見たい、積んである技術書を読みたい、イベントを走りたい、ただ寝たいなどなど、理由はいろいろですがエンジニアは定時に帰りたいのです。
しかし定時に帰るのは、「今日中の仕事」によって妨害されます。つまり今日中の仕事をなくしてしまえば定時に帰ることが出来るわけです。
弊社はソーシャルゲームの運用を行っている企業なので、今日中の仕事の主なものは定常系の更新、特にガチャやイベント、キャンペーンの仕込みとなります。
そういった作業を分解すると

1.プランナが施策を決め、ExcelやSpreadsheetでマスタを作成する
2.エンジニアがマスタをアプリに取り込める形に整形し、リポジトリに上げる
3.デザイナが必要な画像を作成し、リポジトリに上げる
4.(必要に応じて)エンジニアが画面を作成し、リポジトリに上げる
5.(必要に応じて)エンジニアがアプリを修正し、リポジトリに上げる
6.リリース

こんな感じかと思います。
エンジニアの手が入るのは、マスタの取り込み、画面の作成、アプリの修正、リリース。まんべんなく関わっています。
このうち、イベント開催ごとのアプリ修正は主に設定値が実装に入り込んでいる事によるものが多かったりするので、早急にマスタに追い出してしまうとして、マスタの取り込み、画面の作成、リリースが残ります。こいつらが定時退社の敵となります。
ここで「定常作業なんて、手を動かすだけの作業なんて誰でも出来るもん! なんにもない、エンジニアが作業する必要なんてなんにも…」と言いきって他職種に渡してしまいたいところなのですが、ただ手を動かすだけの作業なのにもかかわらず、マスタの取り込みやリリースがいわゆる黒い画面に白い文字での作業だったり、画面を作成するにはHTMLをいじらないといけなかったりで、ハードルが高いことが多いのがネックです。15日目の記事のようにディレクターからの提案ならまだ良いのですが、手数そのままで他の職種に渡すというのは当たり前ですが嫌がられますし、それでエンジニアだけさっさと帰ってしまってはみんなの恨みを買って大ダメージをもらってしまうので、出来る限り簡単に作業出来るようにして、みんなで早く帰れるようにしたいところです。
さて、マスタや画面仕様は(なぜか)ExcelやGoogle Spreadsheetといった表計算アプリでやってくることが多いです。なので、それからそのままマスタのCSVやJSON、それと画面を作成出来るシステムを作れば一番手数が少なくなるはずです。
作業の流れとしては「入力→ボタンポチー→出来たファイルをダウンロード→リポジトリへ」ですね。
さて、実装例…という流れですが、弊社ではGoogle Driveを活用しているのでとりあえずGoogle Spreadsheetを対象として作成したいと思います。
そうなれば王道はGASかと思いますが、後々の拡張を見越してRubyを使用して作成していこうと思います。
関係ないですがECMAScriptはあまり好みではないです。ええ関係ないですとも。

##Google Spreadsheetへの接続
ググるとたくさん情報が出てくるのでそれに従いましょう。Google Developer Consoleの仕様変更やGoogle謹製のgoogle_drive gemの仕様変更に追従出来ていない古い情報が大半のため、ドハマりを経験出来ます。

Google Developer Consoleは日本語化され、UIが多少変わった程度なので

  • APIの有効/無効はメニューの「ライブラリ」内、「Google Apps API」の「Drive API」
  • 認証情報はウィザードが出来たので使用するAPIに「Google Drive API」、API を呼び出す場所に「その他の非UI」、アクセスするデータの種類に「アプリケーション データ」

の2点を抑えておけばハマることはないはずです。
DLされるJSONは重要なので削除しないようにいて下さい。

問題は、google_drive gemです。
出てくる情報ではgoogle/api_clientをrequireと言うことになってることが多いのですが、後方互換性を破棄したアップデートが行われる前の情報なので、現在のgoogle_drive gemではエラーになってしまいます。
また、基本的にgoogle/api_clientをrequireする情報では、API keyを取得して接続する方式をとっているのですが、現在Google DriveではAPI keyを使用したログインは非推奨となっていて、googleauthの使用が推奨されています。
一応現状のgoogle_drive gemでもAPI keyのためにGoogle::APIClient::KeyUtilsというクラスが用意されてはいるのですが、こいつをrubydoc.infoで調べると

 Deprecated. Use google-auth-library-ruby instead

つまり、「非推奨 変わりにgoogle-auth-library-rubyを使え」となっています。
API key方式にこだわる理由もないので、ここは素直にgoogle-auth-library-rubyを使いましょう。
ちなみに、Spreadsheetの操作にはgoogle-drive-ruby gemを使用します。

require 'googleauth'
require 'google_drive'

GOOGLE_CRED_JSON_PATH = 'path/to/json'
scope = 'https://www.googleapis.com/auth/drive'
credential = Google::Auth::ServiceAccountCredentials.make_creds(
  json_key_io: File.open(GOOGLE_CRED_JSON_PATH),
  scope: [scope]
)
session = GoogleDrive::Session.from_credentials(credential)

これでSpreadsheetへの接続は完了です。最新の情報さえあれば簡単ですね。

##Spreadsheetからのデータ抽出
素直に表のデータが入っていればそのままCSVでもいいのですが、例えばこんな形式のシートからマスタを作らなければいけないとします(一応、ガチャを想定しています)。また、後でやりたいことがあるのでデータは全てテキストとして取得します。
コードは先ほどのものの続きと考えて下さい。

ws = session.spreadsheet_by_key(
  'spreadsheet_key').worksheet_by_title('worksheet_title')

data = ws.rows(skip=1).inject(Hash.new{|h, k| h[k] = ''}) do |h, row|
  case row[0]
  when 'ボックス'
    h['box'] << "id: #{row[1]}, probability: #{row[3]}\n"
  when 'カード'
    h['card'] << "box_id: #{row[1]}, card_id: #{row[3]}\n"
  end
  h
end

#=> {"box"=>"id: 1, probability: 1\nid: 2, probability: 10\nid: 3, probability: 20\nid: 4, probability: 69\n", "card"=>"box_id: 1, card_id: 1001\nbox_id: 1, card_id: 1002\nbox_id: 2, card_id: 2001\nbox_id: 2, card_id: 2002\nbox_id: 2, card_id: 2003\nbox_id: 2, card_id: 2004\nbox_id: 3, card_id: 3001\nbox_id: 3, card_id: 3002\nbox_id: 3, card_id: 3003\nbox_id: 3, card_id: 3004\nbox_id: 3, card_id: 3005\nbox_id: 3, card_id: 3006\nbox_id: 4, card_id: 4001\nbox_id: 4, card_id: 4001\nbox_id: 4, card_id: 4001\nbox_id: 4, card_id: 4001\nbox_id: 4, card_id: 4001\nbox_id: 4, card_id: 4001\nbox_id: 4, card_id: 4001\nbox_id: 4, card_id: 4001\n"}

という感じで、1枚のシートから2つ分のマスタの元データが取得出来ました。

##テキストのパース
さて、取得したのはテキストなのでパースしてやらなければいけません。
パースの方法として真っ先に思いつくのは正規表現を駆使して頑張ることですが、正規表現は可読性が低いのであまり長々とは書きたくないものです。
そんなときのためにあるのが構文解析器、パーサですね。
Rubyには標準ライブラリとしてYACC形式のパーサジェネレータ、RACCが存在しています。
が、調べてみたところYACC形式はすでに時代遅れだからPEG(Parsing Expression Grammar)形式のパーサジェネレータを使いましょうという記事が出てきました。
PEGってなんやねん、というと、

Parsing Expression Grammar (PEG, Parsing Expression Grammar) は、分析的形式文法の一種であり、形式言語をその言語に含まれる文字列を認識するための一連の規則を使って表したものである。PEGは再帰下降構文解析を文法を示すためだけに純粋に図式的に表現したものと見ることもでき、具体的な構文解析器の実装やその用途とは独立している。

(by Wikipediaさん)だそうです。なるほどわからん。
まあ、実用上大事なのはこの部分ではなく、それよりちょっと下にある

このため、文脈自由文法とは異なり、PEGには曖昧さは存在しない。文字列を構文解析する場合、正しい構文木は常に1つしかない。このためPEGはコンピュータ言語の構文解析に向いており、一方、自然言語の多義性を、そのまま複数の構文木が可能である、という形で形式化するのには向かない。

こっちの方ですね。曖昧さを持たないというのはコンピュータ向きの性質です。
Rubyには標準ではPEG形式のパーサジェネレータライブラリは付属していませんが、gemとしてはTreetopParsletなどいくつか存在しているようです。
Treetopはエラー表示や文法がわかりにくく、Parsletではそれが改善されているとのことなので、今回はParsletを使用します。

require 'parslet'

class BoxParser < Parslet::Parser
  root(:lines)
  rule(:lines){ line.repeat }
  rule(:line) { (spaces >> expression.repeat >> newline).as(:line) }
  rule(:newline) { str("\n") >> str("\r").maybe }

  rule(:spaces) { space.repeat }
  rule(:space) { str(' ') }
  rule(:comma) { str(',') >> spaces.maybe }

  rule(:numbers){ match['0-9'].repeat }

  rule(:id) { str('id:') >> spaces.maybe >> numbers.as(:box_id) }
  rule(:probability) { str('probability:') >> spaces.maybe >> numbers.as(:probability) }

  rule(:expression){
    (id | probability) >>
    comma.maybe
  }
end
box = BoxParser.new.parse(data['box'])
#=> [{:line=>[{:box_id=>"1"@4}, {:probability=>"1"@20}]}, {:line=>[{:box_id=>"2"@26}, {:probability=>"10"@42}]}, {:line=>[{:box_id=>"3"@49}, {:probability=>"20"@65}]}, {:line=>[{:box_id=>"4"@72}, {:probability=>"69"@88}]}]

class CardParser < Parslet::Parser
  root(:lines)
  rule(:lines){ line.repeat }
  rule(:line) { (spaces >> expression.repeat >> newline).as(:line) }
  rule(:newline) { str("\n") >> str("\r").maybe }

  rule(:spaces) { space.repeat }
  rule(:space) { str(' ') }
  rule(:comma) { str(',') >> spaces.maybe }

  rule(:numbers){ match['0-9'].repeat }

  rule(:box_id) { str('box_id:') >> spaces.maybe >> numbers.as(:box_id) }
  rule(:card_id) { str('card_id:') >> spaces.maybe >> numbers.as(:card_id) }

  rule(:expression){
    (box_id | card_id) >>
    comma.maybe
  }
end
card = CardParser.new.parse(data['card'])
#=> [{:line=>[{:box_id=>"1"@8}, {:card_id=>"1001"@20}]}, {:line=>[{:box_id=>"1"@33}, {:card_id=>"1002"@45}]}, {:line=>[{:box_id=>"2"@58}, {:card_id=>"2001"@70}]}, {:line=>[{:box_id=>"2"@83}, {:card_id=>"2002"@95}]}, {:line=>[{:box_id=>"2"@108}, {:card_id=>"2003"@120}]}, {:line=>[{:box_id=>"2"@133}, {:card_id=>"2004"@145}]}, {:line=>[{:box_id=>"3"@158}, {:card_id=>"3001"@170}]}, {:line=>[{:box_id=>"3"@183}, {:card_id=>"3002"@195}]}, {:line=>[{:box_id=>"3"@208}, {:card_id=>"3003"@220}]}, {:line=>[{:box_id=>"3"@233}, {:card_id=>"3004"@245}]}, {:line=>[{:box_id=>"3"@258}, {:card_id=>"3005"@270}]}, {:line=>[{:box_id=>"3"@283}, {:card_id=>"3006"@295}]}, {:line=>[{:box_id=>"4"@308}, {:card_id=>"4001"@320}]}, {:line=>[{:box_id=>"4"@333}, {:card_id=>"4001"@345}]}, {:line=>[{:box_id=>"4"@358}, {:card_id=>"4001"@370}]}, {:line=>[{:box_id=>"4"@383}, {:card_id=>"4001"@395}]}, {:line=>[{:box_id=>"4"@408}, {:card_id=>"4001"@420}]}, {:line=>[{:box_id=>"4"@433}, {:card_id=>"4001"@445}]}, {:line=>[{:box_id=>"4"@458}, {:card_id=>"4001"@470}]}, {:line=>[{:box_id=>"4"@483}, {:card_id=>"4001"@495}]}]

Parsletのパーサはこんな感じで記述します。
エントリポイントとしてrootを作成し、そこから各ruleを呼びだしていく感じですね。
ruleの中で使用されるメソッドは文字列を示すstrと、正規表現を示すmatchがあります。match内では+や?等は使用出来ず、n回以上の繰り返しを示すrepeatと0または1回を示すmaybeを使用することになります。
>>は「aの次にb」を、|は「集合を前から順に試行し、マッチしたものを返す」を示します。「集合のうちのどれか」のような曖昧さはPEGでは表現出来ないようになっています。この動作のため、場合によっては|の前後を入れ替えると挙動が変わることがあるので注意が必要です。
で、パースが完了すると木構造データが戻ってきます。基本的には配列とハッシュの集合体なので、手で処理をすることも出来ますが、Parsletには木から別形式のデータを作成するTransformerが用意されているので、それを利用してYAML形式に変換してやろうと思います。

##Transformer
まずはコードを

require 'yaml'
class LineData < Struct.new(:line)
  def eval
    line.inject(Hash.new){|h, l| h[l.keys[0]] = h[l.values[0]].to_i;h}
  end
end

class LineTransformer < Parslet::Transform
  rule(line: subtree(:l)){
    LineData.new(l).eval
  }
end

box_yaml = YAML.dump(LineTransformer.new.apply(box))
card_yaml = YAML.dump(LineTransformer.new.apply(card))

Parslet::Transformを継承したクラスがTransformerになります。
Parserと同じく、ruleを定義してその中で処理を行う感じです。木構造内の木構造を示すsubtree、即値のsimple、即値の配列であるsequenceを組み合わせて表現していきます。
しかし、それより気になるのはStructのインスタンスを継承しているクラスかと思います。
Parsletのお約束としてTransformer内の処理をこのように記載するんですが、微妙な気持ち悪さがあります。きれいには書けるんですけどね。
とりあえずここまででGoogle Spreadsheetのデータをマスタ化することが出来ました。
ポチッとするボタンやそことの連携など作り込むものはまだいろいろとありますが、長くなりましたので今回はここまで。

##終わりに
ParsletはExampleが豊富に用意されているので、一通り目を通すと使い方がわかるかと思います。
Google APIにしてもParsletにしてもこの記事で紹介したのはほんのさわりの部分なので、是非いろいろと試してエンジニアの定時退社力を高めていきましょう。
奇跡も魔法も(作れば)あるんだよ。

では、明日は@maeda1991さんの記事となります。
残り一週間、マイネット Advent Calendar 2016 をお楽しみ下さい。

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?