ホーム ブログ ページ 32

CLIPSTUDIOvsPhotoshopここはこうなる仕様の違い②

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

イラスト作成ツールのCLIPSTUDIOPAINTとPhotoshopの各ツールやコマンドの仕様を比較してみました。

 こんにちはtokinです。
 前回の続きでイラスト制作ソフト「CLIPSTUDIOPAINTPRO」と「PhotoshopCC」の仕様の比較を行いました。大きな違いは勿論ありますが同じツールで異なる仕様も多数存在します。この記事は私がイラスト制作を行っている最中に気づいた微かな違いをご紹介していきます。

前回の記事はこちらCLIPSTUDIOvsPhotoshopここはこうなる仕様の違い①

今回も引き続き比較対象は

  • セルシス CLIP STUDIO PAINT PRO(クリスタ)
  • Adobe PhotoshopCC2017(フォトショップ)

この上記二点のソフトです。
 ちなみに文の中にクリスタEXが登場しますがこちらはCLIP STUDIO PAINT PROをバージョンアップさせたCLIP STUDIO PAINT EX というソフトです。こちらはPROよりも更に漫画作成に特化しているタイプのものでPROの仕様に加え様々なシステムが加わったソフトとなっております。
それでは続いていってみましょう。

キャンバスに右クリック

 クリスタでは右クリックを押している間だけスポイトツールが、フォトショップの右クリックではブラシサイズ設定のタブが開かれます。右クリックの設定については今のところどちらのソフトもカスタマイズは不可能な様子です(クリスタは右クリックの無効化なら設定可能です)。ちなみにフォトショップで一時的にスポイトツールを使用したいときはブラシやバケツツールを選択した状態でAltキーを押すと、クリスタのように押している間だけスポイトツールに代わります。

レイヤーを別キャンバスに貼り付け

 クリスタでは別キャンバスにレイヤーを移すときはコピー&ペーストを行います。逆にフォトショップでは、キャンバスを二面開いた状態で移動させたいレイヤーをそのままドラッグ&ドロップすることで内容を移すことができます。勿論コピー&ペーストで移動することもできるのですがその際はまずコピーしたいデータを選択範囲に入れる必要があります。コマンドだとCtrl+A(全面選択)→Ctrl+C(コピー)→Ctrl+V(ペースト)でできます。ちなみにドラッグやペーストしてデータを移動させる際、shiftキーを押しながらペーストすると移動する前のキャンバスと同じ位置にデータを配置できます。

直線コマンド

 クリスタもフォトショップもShiftキーを押しながら直線を引くことができます。直線の始まりに点を打ち、そこから引きたい線の終点をShiftキーを押しながら打つと直線が描けます。クリスタとフォトショップの違いはクリスタの場合は終点を打つ際にどんな線が引けるかガイドが表示されるところ。これによって描く前の線の角度、長さや太さをよりイメージしやすくしてくれます。
 勿論コマンドではなく直線ツールそのものも搭載されています。クリスタでは図形のサブツール内に、フォトショップではシェイプツールの設定を「シェイプ」→「ピクセル」に変更すれば通常レイヤーに直線が引けます。

色調補正

 クリスタにも色調補正は備わっていますがフォトショップは圧倒的です。写真編集ならではの「自然な彩度」、「露出量」や「カラーフィルタ」など画像データをさらに細かく補正・調整できるシステムが備わっています。
 驚いたのは色調補正レイヤーの存在でした。作画したレイヤー本体の色調は変えることなく、全体の色調の調整のみを色調補正レイヤーで行えるのです。試しに色味を変えてみたり様々な色調補正を重ねたり…これらが全てレイヤーで管理できてしまうのです。補正レイヤーは複製することもクリッピングマスクをかけることもできるので特定のレイヤーのみの調整も可能です。結合すれば補正内容はそのまま適用されます。

フィルタ・アクション機能

 デフォルト状態のクリスタPROにもさほど多くはないのですがぼかしやシャープなどのフィルタが存在しています。更にバージョンアップしたクリスタEXでは有志のユーザーが作り出したフィルタプラグインを導入することができます。一方写真加工に特化したフォトショップに実装されているフィルタはとても多いです。フィルタ一つで一味違った雰囲気の画像にしてしまえるので単体で使ったり重ね合わせたりして使ってみたいもの。デフォルトで備わっているのでCDジャケットのようなスタイリッシュな画像まで簡単に作り出すことが出来ます。
 また、クリスタPRO/EX・フォトショップの両方に備わっているアクション機能もオススメです(クリスタではオートアクション機能と呼ばれています)。これを使いこなせばフィルタを使ったような画像加工も一発でできるのです。というのもこちらはソフトでの一連の作業を記憶し、必要なときに再びそれを利用できるようにする機能なのです。幾つかの工程を記録させればワンクリックでそれらを自動で実行してくれるので短い時間でクオリティの高い加工が行えます。クリスタもフォトショップも、DLできるフィルタプラグインやアクション機能がネット上に多数存在しておりますのでぜひ自分好みの機能を探してみてください。

レイヤー効果

 クリスタのレイヤー効果のうち注目したいのがトーン効果です。レイヤーで描いた部分をそのままトーンに変更できてしまうのでこれ一つで漫画作成で大変だったトーン作業がとても楽になります。色の濃度はそのままトーンの濃さに反映されるので濃さに合わせてドットの大きさも自動で調整してくれます。効果を外せばもとの状態に戻すこともできドット数やドットの形も変更できます。またバージョンアップされたクリスタEXのみの実装なのですが線画抽出のレイヤー効果もそのまま漫画背景にできそうな線を抽出してくれるので話題となっています。enter image description here
 フォトショップのレイヤー効果は「グラデーション」から「ドロップシャドウ」、「べベルとエンボス」など平面を加工するものから影をつけ立体にみせる効果などバラエティに富んでいます。その富み具合たるや直接描き込まずとも効果のみでも数段クオリティをあげたものことが作れるほど。レイヤー効果のすばらしいところは拡大縮小しても効果自体は荒れないところ。UIボタンなどのサイズを変更する可能性の高いデータには効果での演出もオススメです。
enter image description here

素材

 どちらのソフトにもネット上に豊富にありますが、クリスタは漫画作成向けソフトということもあってペンツール・トーン素材が特に多く、写真加工からはじまり色彩に強いフォトショップはブラシ素材が豊富な印象を受けます。クリスタはCLIP STUDIO内のサービスから素材を検索・配布・DLすることができ、DLしたものは全てクリスタペイント内の素材欄に表示されますので後はペンツール内にドラッグして読み込みが完了します。フォトショップは個々のサイトからDLしたabr.データをフォトショップのブラシパレットから読み込み取り入れます。また、サイトの他にもAdobeのもつ「Adobe Stock」というサービスもあり、こちらからも素材の検索やDLが可能です。色を塗ったり、線を描いたりするペン・ブラシツールの中でも煙や泡、花やサラダなどスタンプに近いバラエティ豊かな素材も登場しているのでぜひ探してみてください。

最後に

 いかがでしたでしょうか。最後に前回今回と書き連ねました内容を表にまとめました。
enter image description here
 個人の趣味、業務範囲で使ってみて感じたレベルなので掘り出せばもっとそれぞれの似た点便利な点たくさんあると思います。今回はどちらの設定もデフォルト状態での比較でしたが中にはコマンド設定で差が気にならなくなったりカスタマイズでより自分に合う使い方に仕様変更できたりします。ソフトを移行したときに混乱しない為にも是非自分好みのソフトにカスタマイズしてみてくださいね。それでは。

【Unity】オブジェクトプーリングでオブジェクト節約術

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

オブジェクトを生成・破棄する際にかかるコストを、簡単なオブジェクトプーリングの仕組みを実装して軽減してみます。

まえがき

こんにちは。17新卒エンジニアのinoooooocchiです。
今まではUnity入門的なネットに溢れかえってるような記事をここ(DoRuby)に投稿していましたが、そろそろ多少は実用的な記事を書かなければいけないと気を引き締め、今回はオブジェクトの生成・破棄と”オブジェクトプーリング“について書いていきます。これを皮切りに、これから少しずつ技術的な話を書いていきたいと思います。(願望)

Unityにおけるオブジェクトの生成・破棄

Unityでは、オブジェクトを新たに生成したり、使い終わったオブジェクトを破棄したりする際、それぞれ Object.Instantiate や Object.Destroy を用いて行います。
一般的に、オブジェクトをスクリプトで生成する場合、UnityのPrefab機能を用いてオブジェクトのPrefabを作っておき、それをスクリプトに渡して Instantiate することで生成します。

この Instantiate・Destroy はゲームを動かす上で何百、何千、何万回も呼ばれるであろう関数ですが、処理が重いことで知られています。
(→ Debug.Log() や Instantiate() などの速度を計測してみる (外部サイト))

スマートフォン向けにゲームを作ってみよう!と思い立ち、いざUnityを立ち上げてゲームを作ってみると、意外とこの Instantiate・Destroy の負担は目に見えて現れます。
弊社で運営しているようなRPGベースのゲームでは頻繁に Instantiate と Destroy を繰り返すことは少ないですが、アクションゲームやシューティングゲームなどでは必然的に敵や弾などのオブジェクトの数が多くなるため、Instantiate や Destroy を行う回数は相当なものです。
特に筆者の好きな弾幕系シューティングゲームでは物によっては合計数百〜千程度の弾オブジェクトが生成されては消えを繰り返すため、スペック次第では処理落ちが発生し、プレイヤーの操作感を損なうことに繋がってしまいます。
(コルーチンならフレームを跨いで処理が行えるため、処理落ちを多少は回避できそうですが、今回は置いておきます)

オブジェクトプーリングしよう

そこで登場するのが、 “オブジェクトプーリング” という手法です。
オブジェクトプーリングとは、一定数のオブジェクトを溜めておく(poolしておく)ための貯蔵庫のような物を作り、オブジェクトが必要になったら取り出し、使い終わったら戻すことで、なるべく少ない数のオブジェクトを使い回してオブジェクト数及び生成・破棄のコストを抑えようというものです。
リサイクルみたいなもんです。

今回は、受け取ったPrefabを元にGameObjectを最初に一定数まとめてInstantiateで生成し、それらを使い回すといった簡単なオブジェクトプーリングを実装してみます。
また、最初に生成した数では足りなくなった時は、逐次生成してプールしているリストに加えます。

実装

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ObjectPool : MonoBehaviour {
    private List<GameObject> _poolObjList;
    private GameObject _poolObj;

    // オブジェクトプールを作成
    public void CreatePool(GameObject obj, int maxCount){
        _poolObj = obj;
        _poolObjList = new List<GameObject>();
        for (int i = 0; i < maxCount; i++) {
            var newObj = CreateNewObject();
            newObj.SetActive(false);
            _poolObjList.Add(newObj);
        }
    }

    public GameObject GetObject(){
        // 使用中でないものを探して返す
        foreach (var obj in _poolObjList) {
            if (obj.activeSelf == false) {
                obj.SetActive(true);
                return obj;
            }
        }

        // 全て使用中だったら新しく作って返す
        var newObj = CreateNewObject();
        newObj.SetActive(true);
        _poolObjList.Add(newObj);

        return newObj;
    }

    private GameObject CreateNewObject(){
        var newObj = Instantiate(_poolObj);
        newObj.name = _poolObj.name + (_poolObjList.Count + 1);

        return newObj;
    }

}

オブジェクトを管理するオブジェクト(例: 大砲の弾を管理する大砲オブジェクト)でObjectPoolクラスのCreatePool関数にオブジェクトのPrefabと最初にまとめて生成する個数を渡すことで、オブジェクトプールがリストとして作成されます。
生成されたオブジェクトはSetActive(false)で非アクティブ状態にしておきます。
今回の場合、オブジェクトが使用中かどうかをアクティブかどうかで判断しているためです。
管理オブジェクトからGetObject関数が呼ばれた場合、リストの中から使用されていないものを探して返します。
リスト内のオブジェクトが全て使われていた場合、新たに1個だけオブジェクトを生成し、それを返します。

それでは、実際にプールするオブジェクトと管理オブジェクトを作り、動作を確認してみます。

オブジェクト管理クラス

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[RequireComponent(typeof(ObjectPool))]
public class BulletCreator : MonoBehaviour {

    [SerializeField] private GameObject _bulletPrefab;
    private const int BULLET_MAX = 50;
    private int _state = 0;
    private float _originDirection = 0.0f;
    private bool _isReverse = false;
    private ObjectPool _pool;

    private void Awake () {
        _pool = GetComponent<ObjectPool>();
        _pool.CreatePool(_bulletPrefab, BULLET_MAX);
    }

    // Update is called once per frame
    void Update () {
        if (_state % 30 == 0) {
            var way = 16; // 16方向
            for (int i = 0; i < way; i++) {
                var bullet = _pool.GetObject();
                if (bullet != null) {
                    bullet.GetComponent<Bullet>().Initialize(_originDirection + i * (360.0f / way), _isReverse);
                }
            }
            // 発射のたびに10度ずつ回転し、弾の回転方向を反転
            _originDirection = (_originDirection + 10) % 360;
            _isReverse = !_isReverse;
        }
        _state++;
    }
}

オブジェクト

using UnityEngine;
using System.Collections;

public class Bullet : MonoBehaviour {

    private float _speed = 4.0f;
    private float _direction;
    private bool _isReverse;
    private Rigidbody2D _rigidBody;

    public void Initialize(float direction, bool isReverse){
        _direction = direction;
        _isReverse = isReverse;
        _rigidBody = GetComponent<Rigidbody2D>();

        if (_isReverse) {
            GetComponent<SpriteRenderer>().color = new Color(1.0f, 0.3f, 0.3f); // 赤っぽい色
        } else {
            GetComponent<SpriteRenderer>().color = Color.white;
        }

        SetDirection();
    }

    public void Update(){
        if (_isReverse) {
            _direction = (_direction - 1) % 360;
        } else {
            _direction = (_direction + 1) % 360;
        }
        SetDirection();
    }

    public void OnBecameInvisible(){
        gameObject.SetActive(false);
        ResetPosition();
    }

    private void SetDirection(){
        Vector2 v;
        v.x = Mathf.Cos(Mathf.Deg2Rad * _direction) * _speed;
        v.y = Mathf.Sin(Mathf.Deg2Rad * _direction) * _speed;

        _rigidBody.velocity = v;
    }

    private void ResetPosition(){
        _rigidBody.velocity = Vector2.zero;
        transform.localPosition = Vector3.zero;
    }
}

動作確認

enter image description here

画像サイズ上限の都合上短いですが、上記ソースコードでは最初に生成するオブジェクトプールを50個に設定しており、数が足らなくなったことで追加で2個生成されています。
管理オブジェクトは、30フレームに一度、16個の弾を発射するようになっています。
画像は管理オブジェクトが弾を発射する様子を5秒程度切り取ったものなので、本来なら160個のオブジェクトが必要なところ、オブジェクトプーリングを導入することで52個のオブジェクトで済むようになっています。
これが1分2分と続いていくとしても、弾が定期的かつ規則的に発射される限り、52個前後のオブジェクトで本来なら数千個必要なオブジェクトを賄えるということになります。
すなわち、数千回分のInstantiate・Destroyを削減出来たわけです。

最後に

今回の実装は、オブジェクトプーリングの形だけを作りましたが、実際にゲームで運用するためには幾つか改善すべき点があります。

  1. ObjectPoolがオブジェクト管理オブジェクトと1:1の関係になっているため、同じオブジェクトを生成する管理オブジェクトが複数ある場合、ObjectPoolを共有できず無駄が多い
  2. 最初に生成しておくオブジェクトの数を大きくし過ぎると、最初だけ処理が重くなる  →描画コストなどとの兼ね合いですが、基本的には逐次生成が良さそうです

実際はゲームの種類やシステム、オブジェクトの特徴などによって、どのような形式のオブジェクトプールを使うのか考える必要がありますね。
また、オブジェクトプーリングの考え方はUIにも応用出来ます。
例えば何らかのオブジェクトを一覧表示してスクロールさせるUIでは、最初に幾つかオブジェクトを生成しておき、スクロールに応じて中身を入れ替えることで擬似的にたくさんのオブジェクトが並んでいることを表現できそうです。

以上、「オブジェクトプーリングでオブジェクト節約術」でした。

誰が買うゲーム?~顧客について考える~

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

自分の作りたいゲームを作る!という人は「関係無い」と思うかもしれないが、 整理しておくと困ったときに役立つのでそういう人も一度しておきたいのが顧客についての研究だ。勿論、売り上げを出すには必須となる。

どんな顧客がターゲット?

 売る相手となる顧客から考えるか、作りたいゲームがあってそこから考えるかはそれぞれの製作状況によるが、とにかく「どんな人がターゲットなのか」はなんとなくでも想像はついているはず。
 もし、顧客について考えずに単に「RPGを作る!」とだけ思って作り始めてしまうと「あれもいれたい、これもいれたい・・・」と右往左往するのは目に見えている。RPGと一口に言っても男性女性、子供、成人など色々な層が存在しているのだから、しっかり考えるのは重要だ。
 今回は「小学生の男の子向け冒険活劇RPG」を例にして考えてみる。総務省の統計によれば平成27年(2015年)4月1日時点で日本の小学生(6~11歳)の男の子の人口は330万人となっている。こういった役所から調べられる情報はきちんと収集しておきたい。

参考:http://www.stat.go.jp/data/jinsui/topics/pdf/topics89.pdf(我が国のこどもの数 – 総務省統計局)

ターゲットとなる顧客について考える

 年齢と男女が決まったので、本格的にターゲットとなる顧客について考えていく。その顧客がどんな性質かを見極めるのには様々な観点、手法があるが、今回は「整理したい項目を並べてそれらについて考える」とする。なお、ネットアンケートをとるなどの手法は有効だが、費用がかかるので今回は割愛させていただく。
 今回の例では小学生の男の子が対象なので、例えば「難しい表現や漢字は分からない」といった事を最終的に整理した結果として得ることとなる。以下のように簡単にExcelでまとめてみた。内容や項目は適当なので、詳しくは「マーケティング 基本」などで調べてみるといいだろう。
enter image description here

顧客からゲームを考える

 さて、小学生の男の子の特徴をまとめたところで、どんなゲームにすれば良いかを考えてみる。ここまで特に触れていなかったが、ゲームの形態としては「携帯ゲーム機での買い切りゲーム」としている。
 所得の少なさや相場から考えて価格は5000円程度に抑えることは必須そうだ。趣向を「冒険活劇」としているが、小学生の男の子でも冒険活劇が好きではない子もいるだろう。こういった「携帯ゲーム機を持っている」「冒険活劇が好き」といった項目によって実際の顧客対象となる人口は減っていくこととなる。両方を満たすのは330万人の5割と想定すると165万人となる。(実際には何かしらの手法でどれぐらいの割合となるかは調べる必要がある)
 次に、知識や経験の少なさから、「難しい表現や漢字は使わない」「チュートリアルは丁寧に」「カッコいい演出やストーリーにする」などといった事が決まってくる。プレイ環境は複数人プレイが容易なので、ゲーム構成によっては通信対戦などを取り入れることも出来る。こういった「どんなゲームにするか」の方針も顧客の特徴を掴むことで立てられる。

まとめ

 いかがだっただろうか。きちんと顧客について想定をしていないと、「思ったより全然売れなかった」なんていうことが起こってしまう。仕様に迷ったとき、「どんな顧客か」に立ち返れば多くのことはどうするのが最善か分かるので、しっかりと整理して記録しておきたい。それでは、良い製作ライフを。

Rails時短の巻 -seed編-

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

いちいち本番で1からデータを作る必要なんてありません。既存seedに時間がかかるならまず使うことをやめましょう。DBのダンプと差分管理と並列処理で本番へのデータ投入を爆速にするのです。

はじめに

みなさま、お久しぶりです。新卒エンジニアのくろすです。
素手人間になって久しいですが、たまーにオレンジ折りたくなる衝動に襲われます。

今年こそは一番綺麗な空を飛べると信じて初日参加します。
推しが出るので2日目も参加します。
3日目はチケットがありません。チケットが、ありません。

データ投入用高速seed

localでダンプしたデータを差分管理しつつ直接本番に流し込める高速seedをyaml_dbparallelを用いて作成したのでその紹介です。

差分を管理してデータ更新を高速化する、seed_fu:expressのご紹介
を参考に差分管理を実装し、
RailsのDBの初期データ(rake db:seed用)をyamlで美しく管理する方法
をヒントにデータをyaml_dbで管理することを思いつきました。
実際にはテーブル毎のダンプが出来さえすればいいのでmysqldump等でも問題ないと思いますが、sql管理に比べデータの視認性が良い、手修正がしやすいという2点からyaml_dbを使っています。
まだブロックスタイルでしかダンプできていないのであまり使い勝手はよくありませんが、フロースタイルでダンプできるようにできれば手修正はより容易になると思います。

背景

弊社ではExcelで作成していることが多いゲームのマスターデータですが、これはそのまま使うものもあればdbに投入する際にプログラム側で使いやすくするため加工するものもあります。
このデータを加工する作業というものは、ゲーム運営が長くなりデータが増えていくに従い時間を食う作業になっていきます。
また、このデータの作成に他のモデルを参照しながらデータを作るような書き方をしていると各masterに依存関係が生まれ並列処理できないため、既存seedプログラムにがっつり手を入れなければ高速化のしようがない…なんてこともあります。

新卒としてアピリッツに入社しrubyを書き慣れるにつれ、DRYに精神を乗っ取られ始めました……
スゥパァドゥルァァァァイ
今回の場合Don’t Repeat Ourselfでいきましょう。
なにもデータ作成までDBサーバーで行う必要なんてどこにもありません。
データ作成はlocalのみで行い本番はそのデータだけを流し込んではいけない決まりなんてありません。
すっぱりきっぱりと既存のseedを捨て去る覚悟をしましょう。

投入用データ作成

とりあえずdb:seedの後にダンプを行うrake taskを作ります。

namespace :gundum do
  desc "gundum:seed"
  task :seed => :environment do
    Rake::Task["db:seed"].invoke
    Rake::Task["db:dump_masters"].invoke
  end
end

できました!!!ガンドゥムSEEDです!!!!
この高速seedを作ってる時にseedにinvokeを生やそうと[ seed invoke ]でググってしまったあの日からこれをやろうと心に決めていました。

さて、ここで呼んでいるdb:dump_mastersですが中身はyaml_dbの拡張である以下のプログラムをロードするようになっています。

module YamlDb
  module SerializationHelper
    class Base
      # @Override
      def dump_to_dir(dirname, *dump_table_names)
        Dir.mkdir(dirname) if Dir[dirname].empty?
        tables = if dump_table_names.present?
                   dump_table_names
                 else
                   @dumper.tables
                 end
        tables.each do |table|
          File.open("#{dirname}/#{table}.#{@extension}", "w") do |io|
            @dumper.before_table(io, table)
            @dumper.dump_table io, table
            @dumper.after_table(io, table)
          end
        end
      end
    end
  end
  module RakeTasks
    MASTER_TABLES = ["fizz_master", "buzz_master"].freeze
    def self.dump_masters
      SerializationHelper::Base.new(helper).dump_to_dir("#{Rails.root}/db/masters/yml", *MASTER_TABLES)
    end
  end
end

#--------------------------
YamlDb::RakeTasks.dump_masters

テーブルを指定したダンプが行えるよう拡張して、マスターのみダンプする新しいRakeTaskを追加しています。

データ投入と差分管理

一番最初に紹介した
差分を管理してデータ更新を高速化する、seed_fu:expressのご紹介
を参考に差分管理しつつデータ投入を行えるようyaml_dbを拡張していきます。

require 'parallel'
module YamlDb
  module SerializationHelper
    class Base
      def load_masters_from_dir(dirname, truncate = true)
        updated_masters = []
        Dir.entries(dirname).each do |filename|
          next if filename =~ /^[.]/
          master = MasterVersion.find_or_create_by(master: File.basename(filename, ".yml"))
          checksum = Digest::MD5.file("#{dirname}/#{filename}").to_s
          if master.chesksum == checksum
            p "✔ "
          else
            p "✖"
            updated_masters << master
          end
        end

        if updated_masters.present?
          Parallel.each(updated_masters) do |master|
            filename = "#{master.name}.yml"
            begin
              ActiveRecord::Base.transaction do
                @loader.load(File.new("#{dirname}"/#{filename}", "r"), truncate)
                master.save!
              end
            rescue => e
              p "✖"
              raise e
            end
            p "✔ "
          end
        end
      end 
    end
  end

  module RakeTasks
    def self.load_masters
      SerializationHelper::Base.new(helper).load_masters_from_dir(dump_dir("/masters/yml"))
    end
  end
end

#--------------------------
YamlDb::RakeTasks.load_masters

テーブル情報の管理にMD5を使っています。
MD5の衝突耐性は容易に突破され、ハッシュ値から入力値を求めることができるようになっていますが、テーブル管理に使うくらいなら問題ないでしょう。
手元のファイルからハッシュ値を計算しているため、管理対象のテーブルがタイムスタンプを持っている場合は削除しておきましょう。
seedで更新されるデータのの更新時間なんてそんな使うもんじゃありません。

後はこれを標準のrakeから使えるようにrake taskを作ります。

namespace :gundum do
  desc "gundum:seed_destiny"
  task :seed_destiny => :environment do
    load "gundum/seed_destiny.rb"
  end
end

U・N・M・E・I 感じちゃいますね

まとめ

今までローカル反映にすら15分ほどかかっていましたが、一度誰かが取り込み済みのデータなら全データ入れ替えても1分30秒で済むようになりました。
既存seedが遅い原因は富豪プログラミングによるString.newが走りまくることだったので、それも解決。
結果としてデータ入れ替えながらの作業が捗るようになりました。
gundum:seed_destiny最高ですね。

BOM付きUTF-8で文字化けしないCSV出力

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

Railsで出力したCSVファイルがExcelで文字化けを起こしてしまった場合にBOMを付けて対応する方法をまとめました。

はじめに

 またまた1か月ぶりの投稿となります、むらさきです。探していたSwitchも無事購入できて同期と日々インクを掛け合ってます。
さて、データベースに入っているデータをCSVファイルにUTF-8で出力してExcelで見られるようにしたい!と思い実装してみたところ盛大に文字化けしていた…なんて経験誰もが1度は体験したことがあると思います。
文字化けした文字を解読して読んでやる!…と挑戦した経験も1度ではないと思います。
 ですが、僕に文字化けを解読する能力は備わってなかったようなので、今回はUTF-8の文字コードにBOMを付けて、出力する段階で文字化けしないようにする方法をまとめようと思います。

目次

  1. BOMとは?
  2. CSVをUTF-8で出力する
  3. 終わりに

1.BOMとは?

 BOMとは、バイト・オーダー・マーク(Byte order mark)の略で、Unicodeの符号化形式で符号化したテキストの先頭に着ける数バイトのデータのことです。
 ExcelはCSVファイルを開くときデフォルトでShift-JISで開きに行ってしまいUTF-8では確実に文字化けしてしまいます。ですがUTF-8で出力する際にこのBOMを付けることでExcelにUTF-8で書かれていると認識させることができます。

2.CSVを出力する

 今回は適当な日本語をBOM付きUTF-8とBOM無しUTF-8でCSV出力してExcelで文字化けしているかどうかを確認するところまでやりたいと思います。
まずはBOM無しCSVを出力します。

require 'csv'
def csv_export
  File.open(path + 'test_doruby.csv', "w:UTF-8") do |f|
    csv_data = CSV.generate do |csv|
      csv << csv_text
    end
    f.write(csv_data)
  end
end

def csv_text
  [
    "これは",
    "UTF-8の",
    "BOMなしの",
    "testです"
  ]

 このメソッドを実行すれば、pathに指定した場所に”test_doruby.csv”がUTF-8で作成されるのでそれをExcelで開いてみます。
enter image description here

 案の定日本語の部分が無駄に画数の多い漢字の羅列に置き換わりました。先ほど述べたようにUTF-8をShift-JISで開きに行っているからです。

では次に先ほどのコードを書き換えてBOMを付けます。

require 'csv'
def csv_export
  File.open(path + 'test_doruby_bom.csv', "w:UTF-8") do |f|
    bom = "\uFEFF"
    csv_data = CSV.generate(bom) do |csv|
      csv << csv_text
    end
    f.write(csv_data)
  end
end

def csv_text
  [
    "これは",
    "UTF-8の",
    "BOMありの",
    "testです"
  ]

bom = "\uFEFF"
と宣言し、CSV.generateの引数に渡すことでBOMを付けることができます。

BOMを付けたファイルをExcelで開いてみると
enter image description here
文字化けなく表示することができました。

3.おわりに

 いかがだったでしょうか、今回紹介した方法以外にもWindowsのメモ帳で表示してから保存し直すなど、BOMを付ける方法はありますが何度も出力する必要がある場合はこちらの方が手間が1度でいいので良いと思います。自分の状況にあった方法を選択してください。

姿勢の悪さからも目疲れはくる

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

こんにちは、楽しいことはあっという間に終わってしまうなすちゃです。

はじめに

仕事や趣味などディスプレイを眺める環境が日常化している現代ですが、
長時間パソコン作業やスマホ操作をしているとどうしても目への負担が出てきます。

光による刺激や目の乾燥などありますが、使用中の環境も関係しています。
ブルーライトをカットする眼鏡や保湿の高い目薬など様々なアイテムがありますが
今回は自身で改善できることを調べてみました。

目への負担

なぜ姿勢が悪いと目へ影響がでるのか。
長時間不安定な姿勢でいると両目の視点のバランスが崩れ、片目だけに負担がかかり視力低下にも繋がるそうです。
目疲れが続くと頭痛・肩こりといった眼精疲労の原因にもなります。
眼精疲労が悪化すると頭痛・吐き気など身体にも悪影響を及ぼしてしまいます。

改善

座る際には椅子に深く腰掛け姿勢を正すことを意識します。
前かがみになり画面へ近すぎると目だけでなく首への負担もかかるため目線の移動範囲が広くならない距離に保ち、ディスプレイの高さは目線が真っ直ぐかまたはやや低い位置がいいそうです。

頻繁な視線移動で目の周りの筋肉へ負担がかからないようにしましょう。

最後に

忙しいと目疲れ程度でと思いがちですが、軽いストレッチで体の筋肉を解したり遠くを眺めたりと目を休ませてあげるのが大切だと思います。

コマンドラインから英和/和英辞書を引く

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。


こんにちは。いくたです。

コードを書く上で気をつけるべき最も基礎的なこと…
それは「スペルミス」です。

一文字間違えると今まで動いていたスクリプトがうんともすんとも言わなくなる。
ミスに気付かないと永遠に時間が取られる。
そして何よりスペルミスは、めっちゃしょうもない

スペルミスしちゃうのは英単語のせい…

クラス名やメソッド名に使われている英単語の意味がわからないと、コードはただの英数字の羅列になってしまいます。
コードを書きながら英単語の意味をすぐに調べられたらいいですよね。

そこで、コマンドラインから英和・和英辞書を引けるようにしました。

概要

  1. rubyスクリプトでオンライン英語辞書の weblio http://ejje.weblio.jp から検索結果を取ってきます
  2. 実行するときに検索キーワードを引数に渡すことでコマンドラインから検索できるようにします
  3. bashのエイリアスを設定して、libコマンドを作ります

準備するもの

  • Ruby(ver 2.1.0)
  • gem: open-uri → URL先のデータを普通のファイルと同様に扱えます
  • gem: Nokogiri →  HTMLやXMLの構造を解析して特定の要素を抽出できます

open-uri や Nokogiri の使い方については私が前々回書いた記事でWebスクレイピングについて書いたので割愛します。
【前々回の記事】アニメ名探偵コナンの新一が出てる回だけを見たい

weblioの検索結果を取ってくる

weblioはhttp://ejje.weblio.jp/content/のURLの後ろに検索ワードをくっつけることで検索結果のURLとなります。

(例) rubyの検索結果
  → http://ejje.weblio.jp/content/ruby

このURLのページをnokogiriで分解して「主な意味」の項目をXpathで指定して取り出して表示します。

エラーに対処する

エラーが出ないようにコードを書くのは大切ですが、実行時に実行時に引数を入力しない・検索結果が存在しない等エラーの発生が予想されます。
そうなった時にNoMethodError等を出さないようにあらかじめ逃げ道を作っておきます。
エラーとなる原因をメッセージで表示することで、正しい使い方ができる手助けになります。

例えば、スペルミスをして検索結果が無かった場合、「一致する見出し語は見つかりませんでした。」と表示しておけばスペルミスに気づくことができます。
スペルが不安な単語を検索にかけることで、ミスに気づくことができますね。

実装スクリプト

引数をキーワードにweblioの検索結果を表示する

# weblio.rb
require 'open-uri'
require 'nokogiri'

LIB_URL='http://ejje.weblio.jp/content/'.freeze
XPATH="//*[@id=\"summary\"]/div[2]/table/tbody/tr/td[2]".freeze

# 引数を入れてなかったらメッセージを表示する
if AVGV.empty?
  puts '検索ワードを入れてください。' 
  return
end

# 引数ARGVの検索ワードからURLにする
# `shell script` など複数の単語を組み合わせることができるように'+'で結合しておく
# => 'http://ejje.weblio.jp/content/shell+script'
url = URI.encode(LIB_URL + ARGV.join('+'))

# URLから検索結果ページを取ってくる
html = open(url).read
doc = Nokogiri::HTML.parse(html)
# 検索結果の「主な意味」の項目を抜き出す
result = doc.xpath(XPATH).text

# resultが無かった場合メッセージを表示する
if result.empty?
  puts '一致する見出し語は見つかりませんでした。'
else
  # 結果を表示する
  puts result
end

bashのエイリアス設定

# ~/.bash_profile
alias lib='ruby ~/scripts/library_command/weblio.rb'

使ってみよう!

$ lib ルビー
ruby

$ lib apend
一致する見出し語は見つかりませんでした。

$ lib append
(…に)添える、付加する

$ lib
検索ワードを入れてください。

$ lib お腹すいた
I'm hungry.、hungry

これでスペルミスが少なくなりそうです!
写し間違えなどを気をつけるのが第一ですけどね。

それでは、今日はこの辺で。
読んでいただき、ありがとうございました。

アニメーションキーのスマートな打ち方

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

「とりあえずSキー」に心当たりがある方は今までよりもアニメーション作業が捗るかもしれません。 ※筆者作業環境:Maya2016

はじめに

独学で3DCGを学ぼうとすると、
実際は作業上あまり使われることのない知識が身についてしまうことが多々あります。

今回は独学で身につくであろうキーの打ち方に関するお話です。

『Sでキーを打つ』というやり方

Webでチュートリアルなどを探して何もわからない状態からアニメーションを学ぶときに高い確率で身につくであろう、この「Sでキーを打つ」というアクションは、
アニメーションさせたいオブジェクトをビュー上で動かしてSを押せばアニメーションキーを打ってくれるので、確かに便利ですし非常にシンプルで初心者にもわかりやすいです。
私も以前は、キャラクターを細かく動かしてはSキーを連打していました。

しかし、仕事上でアニメーション作業を教わってからは使うことがなくなりました

なぜ使わなくなったのでしょうか。
そして実際はどのようにキーを打つとスマートなのでしょうか。

使わない理由

無駄な項目にまでたくさんキーを打ってしまっている
これにつきます。

実際にバウンドする球体のアニメーションキーをSを用いて打ってみます。
すると画面右側、チャネルボックスの移動Xから可視性という項目までの数値が赤くなりました。赤くなった部分にキーが打たれたということになります。
Sキーでキーを打つと...

アニメーションの内容としては球体がZ軸方向にバウンドしていくというものなので、
移動Yと移動Zにだけキーが打たれていれば良いのですが、グラフエディタを開いてみるとこのようにたくさんの無駄なキーが打たれてしまっていることがわかります。
実際のアニメーションカーブ

例に挙げたモーションはZ軸移動とY軸移動に限定したものでしたが、
様々な方向、角度に動かすモーションを作っていく中で、いちいちこのやり方でキーを打ってしまうとグラフエディタを開いたときスパゲッティのように複雑なアニメーションカーブを目の当たりにすること間違いなしです。

先ほどのアニメーションの無駄をなくすとここまですっきりします。
これならタイミング調整などもしやすいですよね。
理想のアニメーションカーブ、キーもすっきりしていて編集しやすい

また、アニメーションカーブ自体も先ほどの画像のものと比べ曲線がきれいなことがわかるかと思います。
illustratorやphotoshop等の使用経験がある方ならイメージしやすいと思いますが、アニメーションカーブはベジェ曲線と原理は一緒なので頂点数が少ないほど、ソフト側できれいに補間してくれます。
アニメーション作業ではガタガタのカーブがそのまま見た目に現れてくるので綺麗なアニメーションカーブを作りたいところです。

ゲームなどに使われるモーションはできるだけ少ないキーでアニメーションを作り、一つ一つのデータサイズを小さくしていく必要があるためSキーによるキーの打ち方はゲームには不向きだと言えます。(特にコンシューマーゲームの場合はゲームソフトに入れられるデータ容量が決まっているので少ないキーフレームでモーションを作るスキルが必要と聞きます。)
単にアニメーション作品を作っているのであれば、最終的にはレンダリングで連番画像に書き出すだけなのでそこまでキーの削減に注力する必要はないのかもしれませんが、シーンサイズはできるだけ軽い方がストレスなく作業ができますよね。

ということで、出来るだけ動かしたい項目(移動軸、回転軸)だけにキーを打っていくようにしましょう。

キーの打ち方

私が普段使っている3通りのキーの打ち方を紹介します。

1.チャネルボックス上でのキーの打ち方

チャネルボックス上で特定の項目へキーを打つ場合は「選択項目のキー設定」からキーを打ちます。
方法はチャネルボックス上のキーを打ちたい項目の行にマウスカーソルを合わせ
右クリック→選択項目のキー設定
でキーを打つことが出来ます。
チャネルボックスからキーを打つ

2.グラフエディタ上での基本的なキーの打ち方
グラフエディタ上ではキーを挿入するといった方がわかりやすいです。あらかじめチャネルボックスなどで打ったキー(アニメーションカーブ)を選択して

Iキー+中マウスボタン

で任意のフレームにキーを挿入できます。
グラフエディタ上でキーを挿入する

3.自動キーフレームを用いたキーの打ち方
自動キーフレーム機能はオブジェクトをマニピュレーターで動かしたときに動かした軸にのみ自動でキーを打ってくれるもので、個人的にとても便利だと感じている機能です。
画面右下にあるこのボタンが自動キーフレームのON/OFFボタンです。このボタンが青く表示されていると、自動キーフレームがONの状態です。
自動キーフレーム機能を使ってキーを打つ

グラフエディタと同様、あらかじめキーが打たれていないとどんなに動かしてもキーを打つことができないので一つ目のキーはチャネルボックスで打ちます。
自動キーフレーム機能を使ってキーを打つ2

3通りのキーの打ち方について紹介してみました。
特定の項目に対してのみ任意のフレームにキーを打つという目的は一緒ですが、
場面によって使いやすさが変わってくるのでうまく使い分けていくと作業の単純化、効率化にもつながるかと思います。

終わりに

記事を書きながら、
「やる気があれば独学で3Dコンテンツくらい作れるだろう。」と高を括り、インターネットの力だけを頼って学んでいた頃を思い出して、よくSキーのみでアニメーション作れていたなと思いました。就活でそのアニメーションを見せて回っていたことに若干恥ずかしさを感じています。

最近はスカートや振袖、髪の毛等の「揺れ物」にアニメーションを付ける作業を始めました。体の動きとは違い、しなやかさや遅延表現など様々な要素を考慮して付ける一段難しい作業ですが、キャラクターのモーションがより華やかになる大切な作業なので非常に達成感が強いです。

考え事の場をつくる

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

はじめに

一日中ゲームをしていたら、いつの間にかお盆休みが終わっていました。
休みの間に旅行に行った方、帰省した方、仕事だったという方、私のようにゲーム三昧だった方……
色々な過ごし方をされてきた方がいらっしゃることでしょう。
そんな中でふとした瞬間、考え事をすることはありませんでしたか?
今回はその考え事について、少し書いていこうと思います。

発想の場といえば……

enter image description here

皆さんは日々何気なく考え事をしながら生活していると思いますが、自分にとってどこが最も考え事に適した場所なのか……そんなことを考えたことはありますか?
私の周囲では、「煮詰まったからファミレスに行って作業をする!」という話をよく耳にします。
お洒落で静かなカフェ、薄いノートPCを持ったイケてるサラリーマンたちと女子高生でごったがえすカフェ、近所のファミリーレストラン……
こんなところでキーボードをカタカタさせていたら、格好良いに違いありません。
少し憧れていましたが、残念なことに私はカフェなどで頭脳が活性化するタイプではありませんでした。

自分に合っている場所を見つける

enter image description here

私が集中して考え事をできる場所……それは、「お風呂」でした。
お風呂やトイレなどの、一人でいられる場所が最も集中できるという人はかなり多いと思います。
湯船にゆったりと浸かっている時も良いですが、私の場合は頭からシャワーを浴びている時が絶好調になります。
すぐにでもメモを取って忘れないようにしたいところですが、現実は非情です。
シャンプーを洗い流しているうちに半分くらい忘れています。
お風呂でアイデアが浮かんでも、メモを取れない!とお悩みの方は少なくないと思います。私もその一人です。
なんとかバスタイムを終えるまで、頭の中に残っているよう祈るしかないですね。

考え事をしやすい場所を見つける・気付くことができれば、より良いアイデアが生まれてくるかもしれません。

終わりに

最近では耐水ノートというとても便利なものが登場しています。
これがあれば、お風呂の中でも好きなだけメモを取れますね。
耐水といっても、お風呂に沈めると流石にまずいので、気を付けましょう。

ホワイトボードで代用するのも良いかもしれないです。

select2 で “No Results Found” などのメッセージを変更する

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

jquery を拡張し selectボックス(プルダウンメニュー、ドロップダウンリスト、リストボックス)に対してデザインを整えたりタグやプレースホルダテキストや検索の機能を加える select2 があります。検索が成功していないときに表示されるメッセージを変更したくて時間を費やしたので記載します。

参考

本体
https://select2.github.io/

使用例
https://select2.github.io/examples.html

基本的な使い方

jquery, select2 プラグインと関連のcssを読み込んでおく。
検索機能やスタイルを付与したいセレクトボックスをjqueryのセレクター ‘$()’ で指定して select メソッドを呼ぶだけです。select に引数(連想配列的な何か)を渡すことでカスタマイズできます。検索の時にちらっと表示されるメッセージもそこで上書きできます。

例 (coffeescript)

$(document).ready(->
  $('#select_box').select2()

こんなことをしました。

$(document).ready(->
  $('.select2').each(->
    $select = $(this)
    options = {}
    options.language = {}
    options.language.noResults = ->
      '該当するものがありません。' # 0件ヒットのときのメッセージ : No Results Found
    options.language.searching = ->
      '検索中' # 検索中のメッセージ :  Searching...
    options.language.errorLoading = ->
      '検索中' # 検索が失敗しているときのメッセージ: Error loading results
    $select.select2(options)

※ 文字列を返すのだけれど文字列を入れるのではなく文字列を返す関数を入れるところに気をつけましょう。

1台のWindowsで複数Firefoxを起動させる

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

1台のWindowsで、複数Firefoxを起動させる(いわゆる多重起動)方法。

できると何が嬉しいか

  • cookieや閲覧履歴、保存パスワードなどが独立する
  • 複数のアカウントを分けてログインするというような事を Firefoxだけでできるようになる
  • それぞれ個別に設定する事ができる(設定違いのFirefox)
  • アップデートしたらアドオンや設定がおかしくなるかどうかを事前確認できるようになる

前提

Firefox自体はインストール済であるとする。(これを「メイン」とする。)

手順

  1. Firefox Portable をダウンロードし、任意の場所に展開する(これを「サブ」とする)
  2. 展開されたディレクトリ下の Other\Source\FirefoxPortable.ini を展開されたディレクトリ直下にコピーする
  3. コピーした FirefoxPortable.ini を以下の通り編集する
  • 変更前
AllowMultipleInstances=false
  • 変更後
AllowMultipleInstances=true

これでメインとサブを同時に干渉せず起動する事ができるようになる。

補足

FirefoxPortable は何個あっても問題ないので、この方法を使えば、サブ側はいくつでも起動可能となる。

オプション

任意で以下のような事もできる

FirefoxPortable の起動時にスプラッシュ画面を表示しない

FirefoxPortable.ini を以下の通り編集する

  • 変更前
DisableSplashScreen=false
  • 変更後
DisableSplashScreen=true

サブ側のアイコンを変える

拡張子が .ico である任意の画像ファイルを用意し、 App\Firefox\browser\chrome\icons\default\main-window.ico として上書き保存する

EXCEL(VBA)から、HTTP通信でファイルをアップロードしてみよう。

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

はじめに

EXCEL(VBA)で、JSONデータをHTTP通信する方法に関しては、前回の記事を参考して下さい。
EXCELでファイルリストを管理し、PCに格納しているファイルもアップロード出来れば、作業効率が図れると思ったのがキッカケです。
それを実現する簡単な方法をご紹介します。

※実際は、そんなに簡単ではありませんでした(汗)

参考にさせて頂いたサイト
・WSH + VBScriptでのサンプルを参照して下さい。
・boundaryに関してのこちらの仕様を参照して下さい。

ファイルを送信するには

1.まずHTMLを使ったファイルを送信する処理を確認してみる

ファイルを送信するHTMLは、以下のようになります。
enctype="multipart/form-data" を付ける事でファイルを送信することが可能となるのです。

<form action="http://localhost:8080/" method="POST" enctype="multipart/form-data">
  <input type="text" name="id" /><br />
  <input type="text" name="description" /><br />
  <input type="file" name="file" /><br />
  <input type="submit" name="submit" value="send"/>
</form>

HTMLイメージ

HTMLイメージ

選択しているテキストファイルの内容

選択しているテキストファイルの内容
※使用しているサクラエディタは、こちら

2.HTTP通信の内容はどうなるの?

HTMLのフォームを使ったファイルアップロードの仕様はRFC1867を参照して下さい。
実際に送信し、proxyツールfiddler を使った、HTTP通信内容を確認した様子が以下の図となります。
※HTTPの構文に関しては、こちらのページが参考になります。

リクエスト内容

HTTPヘッダ部分の content-Type が、 multipart/form-data となっていますね。
ここでポイントとなるのは、 boundary= に設定されている”境界線”です。

3.multipart/form-data の構造

詳しくはこちらのサイトを参照して下さい。その中の一部を抜粋して説明します。

■HTTPヘッダ部

Content-Typeの内容

boundaryは、1文字以上70文字以下でなければなりません。
ブラウザによっては、40文字程度の場合もあります。

■ボディ部

ボディ部は、以下の3種類に分類されています。

フォームデータ(赤枠参照)

boundary は、先頭に--HTTPヘッダのboundaryCRLF

テキストのパラメタ

ファイルデータ(バイナリデータも同様)(赤枠参照)

boundary は、先頭に--HTTPヘッダのboundaryCRLF

ファイルを選択時のパラメタ

フッタ(赤枠参照)

boundary は、先頭に--HTTPヘッダのboundary+後尾に--CRLF

フッタ内容

実際にEXCEL(VBA)からHTTP通信にて画像等のファイルを送信

実際簡単ではなかったポイントが、送信するファイルが、テキストではなく画像やEXCELファイル等のバイナリデータの場合です。
送信するmultipart/form-dataパラメタが、テキストであればString型の結合で対応できるのですが、画像やEXCELファイルはByte型になり、容易にはString型Byte型の結合ができませんでした。


解決方法として、上部でも記載さしている参考サイトと同様に、ファイル操作等に使用するADODB.Streamをパラメタ生成用の領域として使用することで、String型Byte型結合を実現しています。


ちなみに、今回もJSONを生成している部分は、こちらを使用させて頂いて居ます。

■サンプルプログラム

①メイン処理

Const adTypeBinary = 1
Const adTypeText = 2

Const adBTypeContent = 1
Const adBTypeBody = 2
Const adBTypeFooter = 3

Public Function UploadFile() As Boolean


    Dim FilePath As String: FilePath = "d:\証明写真サンプル.jpg"

    Dim strMethod As String: strMethod = "POST"
    Dim strUri As String: strUri = "http://localhost"
    Dim strResult As String

    '---------------------------------
    ' リクエストパラメタ用の領域を生成
    '---------------------------------
    Dim tempParamStream As Object
    Set tempParamStream = CreateObject("ADODB.Stream")
    tempParamStream.Open

    '---------------------------------
    ' リクエストパラメタ作成
    '---------------------------------
    Dim FileName As String
    FileName = Dir(FilePath)

    Dim JsonObject As Object
    Set JsonObject = New Dictionary

    JsonObject.Add "name", FileName
    JsonObject.Add "parent", New Dictionary
    JsonObject("parent").Add "id", 0

    If SetNomarlParameter(tempParamStream, "attributes", JsonConverter.ConvertToJson(JsonObject)) Then
    End If

    If SetFileParmater(tempParamStream, "file", FilePath, "application/octet-stream") Then
    End If

    If SetEndParameter(tempParamStream) Then
    End If

    '---------------------------------
    ' リクエストパラメタ取得
    '---------------------------------
    Dim snedParameter As Variant
    GetSendParameter snedParameter, tempParamStream

    '---------------------------------
    ' リクエスト
    '---------------------------------
    Dim objHTTP As Object
    Set objHTTP = CreateObject("msxml2.xmlhttp")
    objHTTP.Open strMethod, strUri, False
    objHTTP.setRequestHeader "Content-Type", "multipart/form-data; boundary=" + getBoundy(adBTypeContent)
    objHTTP.send snedParameter

    statusCode = objHTTP.status

    strResult = StrConv(objHTTP.responsebody, vbUnicode)
    Set objHTTP = Nothing

    UploadFile = True
End Function

②データフォームのパラメタ設定

Private Function SetNomarlParameter( _
                    ByRef tempParamStream As Object, _
                    ByVal fname As String, _
                    ByVal fvalue As String) As Boolean

    If fvalue <> "" Then

        ChangeStreamType tempParamStream, adTypeText

        Dim params As String
        params = ""
        params = params + getBoundy(adBTypeBody)
        params = params + "Content-Disposition: form-data; name=""" + fname + """" + vbCrLf
        params = params + vbCrLf
        params = params + fvalue + vbCrLf

        tempParamStream.WriteText params

    End If

    SetNomarlParameter = True
End Function

③ファイル(バイナリデータ)のパラメタ設定

Private Function SetFileParmater( _
                            ByRef tempParamStream As Object, _
                            ByVal fname As String, _
                            ByVal fvalue As String, _
                            ByVal fct As String) As Boolean

    '-------------------------------------
    ' テキストデータ
    '-------------------------------------
    ChangeStreamType tempParamStream, adTypeText

    Dim params As String
    params = ""
    params = params + getBoundy(adBTypeBody)
    params = params + "Content-Disposition: form-data; name=""" + fname + """; filename=""" + fvalue + """" + vbCrLf
    params = params + "Content-Type: " + fct + vbCrLf
    params = params + vbCrLf

    tempParamStream.WriteText params


    '-------------------------------------
    ' バイナリデータ
    '-------------------------------------
    ChangeStreamType tempParamStream, adTypeBinary

    Dim fileStream As Object
    Set fileStream = CreateObject("ADODB.Stream")
    fileStream.Type = adTypeBinary
    fileStream.Open
    fileStream.LoadFromFile fvalue

    tempParamStream.Write fileStream.Read()

    fileStream.Close
    Set fileStream = Nothing

    SetFileParmater = True
End Function

④フッタのパラメタ設定

Private Function SetEndParameter( _
                    ByRef tempParamStream As Object) As Boolean

    ChangeStreamType tempParamStream, adTypeText
    tempParamStream.WriteText getBoundy(adBTypeFooter)

    SetEndParameter = True
End Function

⑤送信するパラメタを取得

Private Function GetSendParameter( _
                    ByRef parameter As Variant, _
                    ByRef stream As Object) As Boolean

    ChangeStreamType stream, adTypeBinary
    stream.Position = 0
    parameter = stream.Read

    stream.Close
    Set stream = Nothing

    GetSendParameter = True
End Function

⑥パラメタ用の領域の状態を変更する

最初に、p = stream.Positionで現在のポジションを取得しているのは、状態を変更したことでポジションが変わってしまうためです。

Private Function ChangeStreamType( _
                    ByRef stream As Object, _
                    ByVal adType As Integer) As Boolean
    Dim p As Long
    p = stream.Position
    stream.Position = 0
    stream.Type = adType

    If adType = adTypeText Then
        stream.Charset = "UTF-8"
    End If

    stream.Position = p

    ChangeStreamType = True
End Function

⑦Boundy 情報取得

Boundyは、同じ文字列を使用することになるので、変数をstaticにすることで、1度生成した文字列を使って、用途に合わせたBoundyデータを復帰するようにしています。
各種参考させて頂いたサイトでは、Boundyデータは固定の文字列としていますが、折角なのでランダム文字列を生成年月日時分秒までを追加した文字列生成するようにしてみました。

Private Function getBoundy(ByVal adType As Integer) As String

    Static sBoundy As String

    If sBoundy = "" Then

        Dim multipartChars As String: multipartChars = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        Dim boundary As String: boundary = "--------------------"

        Dim i, point As Integer

        For i = 1 To 20
            Randomize
            point = Int(Len(multipartChars) * Rnd + 1)
            boundary = boundary + Mid(multipartChars, point, 1)
        Next

        sBoundy = boundary + Format(Now, "yyyymmddHHMMSS")

    End If

    Select Case adType
    Case adBTypeContent
        getBoundy = sBoundy
    Case adBTypeBody
        getBoundy = "--" + sBoundy + vbCrLf
    Case adBTypeFooter
        getBoundy = vbCrLf + "--" + sBoundy + "--" + vbCrLf
    End Select

End Function

送信パラメタの生成の経過

サンプルプログラムでは、"d:\証明写真サンプル.jpg" と画像ファイルを指定していますが
説明しやすいように、別のバイナリファイル(d:\test.bin)を用意して生成過程を確認してみます。

テキストデータを設定する場合には、tempParamStreamをテキスト状態に変更
ファイル(バイナリデータ)を結合する場合には、tempParamStreamをバイナリ状態に変更することで結合(パラメタを生成)を実現します。

test.bin の内容

テスト用バイナリデータ

テキストパラメタ設定時

    If SetNomarlParameter(tempParamStream, "attributes", JsonConverter.ConvertToJson(JsonObject)) Then
    End If

上記を実行したタイミングでは、tempParamStreamのは以下の通り
※先頭の0xEF 0xBB 0xBF はBOM付き(UTF-8)の場合を表すコードとなります。

テキストパラメタまでの送信パラメタ

ファイルパラメタ設定時

    If SetFileParmater(tempParamStream, "file", FilePath, "application/octet-stream") Then
    End If

上記を実行したタイミングでは、tempParamStreamのは以下の通り

バイナリデータ設定までの送信パラメタ

フッタまで設定時

    If SetEndParameter(tempParamStream) Then
    End If

上記を実行したタイミングでは、tempParamStreamのは以下の通り

フッタを追加した時の送信パラメタ

まとめ

BOX API にて、もしもEXCEL(VBA)でファイルをアップロードさせるには今回の作成した処理を使って送信することが出来るはずです。
という事で、次回は、またBOX API に戻りPOST送信する処理を考えてみようと思っています。

英語弱者が送る翻訳サイトのススメ

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

はじめに

エンジニアに必要なもの。それは英語です。
コードを打つにも英語、リファレンスを読むにも英語、とにかく必要なのです。
いまどきはどんな人でも必要と言われているかもしれませんが特に我々エンジニアと英語は切っても切り離せない存在です。

ですが英語の勉強をしていなくても英語を訳すことができますよね。
そう、翻訳サイトです。わからない単語があったら翻訳サイトですぐに意味を調べることができます。
ある時僕が翻訳サイトを使用していて、イマイチしっくりくる翻訳が出来なかったので試しに他の翻訳サイトで訳してみるとニュアンスがかなり違って理解できた、なんてことがありました。


検証しよう!

そこで英語のことわざをいくつか翻訳サイトで訳してどれくらい違いがあるかを調べてみました。

その過程で「最後のわら一本がラクダの背中を折る」という、少しでも限度を超えると大変なことになる。といった意味の英語のことわざがそれぞれのサイトの特徴が出ていていい感じだったので、それを訳した文と僕が受けたそれぞれのサイトの特徴をまとめてみました。


・翻訳前の文
The last straw breaks the camel’s back.


Bing翻訳

最後のわらはラクダの背中を壊します。

いくつかの言葉を訳していて、一語一語の意味がわかりやすいと感じました。
「You」という単語が文の中に入っていても訳した文には「あなた」とは入れずに文を簡潔にまとめてくることが多かったです。わからない単語が多い時に使うといいかもしれません。


Google翻訳

最後のストローはラクダの背中を壊す。

ことわざをいくつか訳していた限りでは、あまり変な訳はでてきませんでした。さすがGoogle翻訳ですね。
ですが「kettle」をヤカン、釜ではなくケトルとしたり上記の例でも藁をストローと訳しています。
日本で通じる単語は何かに変換せずそのままカタカナにしてくれるのでわかりやすかったりそうじゃなかったりしました。


Weblio翻訳

我慢の限界は、後ろにラクダのものを壊します。

《諺》 ぎりぎりのところまで重荷を負ったラクダはその上わら 1 本でも積ませたら参ってしまう 《たとえわずかでも限度を越せば取り返しのつかない事になる》

日本語的には微妙な感じに翻訳されることが多かったのですが、ことわざが登録されていると直訳とは別にことわざの意味も表記してくれることがありました。
直訳は間違ってなさそうだけど意味はわからない!なんて時にはWeblio翻訳を使うといいのでは。


excyte翻訳

ぎりぎりの重荷を負ったラクダはわら1本でも積ませたら参ってしまう。

ことわざを訳していた限りではexcite翻訳が一番元の意味に近く翻訳してくれました。今回のように話し言葉のような形で翻訳してくれることも多かったので、ことわざや冗談を訳す時はexcyte翻訳に任せたいと思いました。


InfoSeekマルチ翻訳

我慢の限界は、ラクダの背中に怪我をします。

他のサイトとは単語の訳し方が違ったことが多かったです。
上記の例でも「怪我」という訳し方をしたのはここだけでした。
他のサイトで翻訳した文が微妙にしっくりこなかったりした時にはここに尋ねるといいでしょう。


おわりに

いかがでしたでしょうか。思ったより翻訳に差が出て調べている間結構楽しかったです。
よく考えたら翻訳が全く同じ内容なら翻訳サイトが複数あるなんてこと起こりませんよね。

ちなみに翻訳をお金を払ってやってもらうこともできます。
送った文章を機械じゃなく人力で翻訳してもらうことができたり、より精度の高い翻訳が有料だったりします。

……英語勉強してきます。

cURL(カール)ではなく、httpie を使用しBOX APIの確認をしてみる。

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

はじめに

box api referenceでは、cURL(カールと読む)でのリクエスト方法が記載されていますが、
このcURLを使ったの確認の場合、JSONデータが整形されないため、使いにくい。
その事から、別のコマンドラインHTTPクライアントのhttpieを使う事にしてみます。

cURL と、httpie での比較

BOX のフォルダ情報取得処理(Get Folder Info)で比較してみます。

cURL の場合

 > curl https://api.box.com/2.0/folders/0 -H "Authorization: Bearer fAFzgyluzN5JUkjUFIF3jWjGQl48ucvx"
curl の実行イメージ

httpie の場合

 > http -v GET https://api.box.com/2.0/folders/0 "Authorization: Bearer fAFzgyluzN5JUkjUFIF3jWjGQl48ucvx"

シンタックスハイライトも付いているので、見やすい!!

httpie の実行イメージ

httpie をインストールしてみる。

会社からアテンドされているPCもwindow という事で、windows 環境にインストールしてみます。

1.python ダウンロード

httpieを使うには、python が必要です。

こちらのサイトから、python をダウンロードして下さい。

python のダウンロード画面

2.python インストール

インストール画面で、デフォルトのInstall Now を選択してもいいですが、
Customize installation を選択し、インストールする場所を指定する事をオススメしています。
この後説明する、環境設定のパスがOSのエディションなどにもよるが、ほとんどの場合はPATH環境変数の最大長は約2000文字以下に制限されているためです。

インストール時の選択画面1
インストール時の選択画面2

3.環境変数(path)の設定

Windows 10 で環境変数を設定するための流れは
Windowsキー → 設定 → システム → バージョン情報 → システム情報 → システムの詳細設定

仮に、python をインストールしたのが、c:\python の場合
システム境変数 の Path に以下の2つを追加して下さい。

  c:\python
  c:\Python\Scripts
システムプロパティ画面
環境変数設定の画面

4.python の確認

コマンドプロンプト画面を表示し、python とだけ実行すると以下の様に表示されます。

python のバージョン表示

5.httpie をインストール

インストールには、Python のパッケージ管理(pip)を使います。
以下のコマンドを実行して下さい。

 > pip install --upgrade pip setuptools

 > pip install --upgrade httpie

httpie の オプション紹介

一番使いそうな出力オプションのみをご紹介。
詳しくは、httpie使用方法を参照して下さい。

1.出力オプション

-h  応答ヘッダーのみが出力
-b  レスポンスBodyのみが出力
-v  HTTP交換全体(要求と応答)を出力
-p  HTTP交換の一部を選択

 http -v https://api.box.com/2.0/folders/0

1.1.HTTP交換のどの部分を出力するか指定

H  要求Headers
B  要求Body
h  応答Headers
b  応答Body

  http -p=hb https://api.box.com/2.0/folders/0

まとめ

BOXのAPIを簡単に確認出来るようになった気がします。

mapとpluck

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

 サービスの開発でよく使用するデータベース(DB)。DBテーブルの特定のデータを一度に取得する方法として、mapメソッドとpluckメソッドが存在する。どちらともデータを取得するという点では同じですが、いくつか異なる点が存在します。この記事ではこれらの違いとデータを取得する際の使い分けについて説明をします。

mapメソッド
 mapはrubyで使用することができるメソッドです。レシーバの要素の数だけブロックを実行し、ブロック内の結果を配列に格納して配列で返す機能があります。また、&(アンパサンド)を使用することでコードを簡略化することが可能。&には、ブロックを展開する意味がある。

["a", "b", "c"].map { |str| str.upcase } # => ["A", "B", "C"]
["a", "b", "c"].map(&:upcase) # => ["A", "B", "C"]

 mapメソッドは、配列だけでなくハッシュに対しても実行することができます。mapメソッドのレシーバがハッシュでも配列を返します。

{1 => "taro", 2 => "jiro"}.map { |key, value| key.upcase } # => ["TARO", "JIRO"]

 最後にmapメソッドを使用したDBデータ取得についてです。以下のユーザモデルが存在した場合、データの中から取得したいカラムを以下のように指定することで、そのカラムのデータを全て取得することができる。

#<ActiveRecord::Relation [
 #<User id: 1, name: "taro", created_at: "2017-06-01 01:00:00", updated_at: "2017-06-01 01:00:00">,
 #<User id: 2, name: "jiro", created_at: "2017-06-01 01:00:00", updated_at: "2017-06-01 01:00:00">
]>

User.all.map{ |user| user.id } # => [1, 2]
User.all.map{ |user| user.name } # => ["taro", "jiro"]
User.all.map(&:id) # => [1, 2]

pluckメソッド
 pluckメソッドは、引数に指定したカラムの配列を返すメソッドです。このメソッドはRailsで使用できるメソッドなので、Rubyのみでは使用することができません。また、pluckメソッドには、引数に&が必要無く、複数のカラムを指定することもできます。複数のカラムを指定した時は、2次元配列で返ります。

#<ActiveRecord::Relation [
 #<User id: 1, name: "taro", created_at: "2017-06-01 01:00:00", updated_at: "2017-06-01 01:00:00">,
 #<User id: 2, name: "jiro", created_at: "2017-06-01 01:00:00", updated_at: "2017-06-01 01:00:00">
]>

User.pluck(:id) # => [1, 2]
User.pluck(:id, :name) # => [[1, "taro"], [2, "jiro"]]

mapとpluckの違い
 mapメソッドとpluckメソッドの使い分けについて気になったので調べてみました。pluckメソッドは、mapメソッドよりも処理速度が速いと言われていますが、必ずそうとも言えないようです。pluckメソッドは、実行するたびにSQLを発行するので、データ数によっては、mapメソッドより遅くなる場合があります。mapとpluckの使い分けについてですが、参考記事によるとDBからデータを直接取得する場合はpluckメソッドを使用し、インスタンスからデータを取得する場合はmapメソッドを使用するとSQLの回数を抑えることができます。

User.pluck(:id)

user = User.where(“age > 20”)
user.map(&:name)

まとめ
 簡単にmap・pluckメソッドの説明を行いました。私は、データの取得にmapメソッドしか使用せずにパフォーマンスのことも考えてきませんでした。今回の記事で少しでも多くの方がパフォーマンスに気をつけることができるようになれば幸いです。

参考記事
http://qiita.com/metheglin/items/18064851a8f00dab67f8
http://yachibit.hateblo.jp/entry/2014/03/05/002844

beebole/pure.jsで動的にhtmlを表示する方法

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

pure.jsとは

json形式のデータを使って、ページの一部分を自動生成するテンプレートエンジンです。

使用例

たとえば↓のようなチュートリアル通りにHTMLを用意すると、

index.html

<script src="http://pure.github.io/pure/libs/pure.js"></script>

<!-- HTML template -->
<ul>
<li>
  <span></span>
</li>
</ul>

<script>
var data = {
  animals:[
    {name: 'mouse'},
    {name: 'cat'},
    {name: 'bird'},
  ]
};
var directive = {
  'li':{
    'animal<-animals':{
      'span': 'animal.name'
    }
  }
};
$p( 'ul' ).render( data, directive );
</script>

下のような形式で出力されます。

<ul>
  <li><span>mouse</span></li>
  <li><span>cat</span></li>
  <li><span>bird</span></li>
</ul>

※スクリプトの説明
data:表示したいデータ
directive:dataとhtmlの対応
$p( ‘ul’ ).render( data, directive ):表示処理

問題点

htmlを生成するときは
$p( ‘ul’ ).render( data, directive );
を実行するのですが、何回も実行してしまうと表示がおかしなことになってしまいます。

<script src="http://pure.github.io/pure/libs/pure.js"></script>

<ul>
<li>
  <span></span>
</li>
</ul>

<a href="javascript:addFox();">キツネを追加する</a>

<script>
var data = {
  animals:[
    {name: 'mouse'},
    {name: 'cat'},
    {name: 'bird'}
  ]
};
var directive = {
  'li':{
    'animal<-animals':{
      'span': 'animal.name'
    }
  }
};
$p( 'ul' ).render( data, directive );

function addFox(){
  data.animals.push({name: 'fox'});
  $p( 'ul' ).render( data, directive );
}
</script>

初期表示↓

enter image description here

「キツネを追加する」をクリックすると↓

\[押下後.png\]

このように大量に表示されてしまいます。

対処方法

これを防ぐためにはdirectiveに対して以下の1処理(コンパイル)が必要になります。

var compiled = $p( 'ul' ).compile( directive );

このコンパイルされた値を使ってレンダリングすることで大量に表示されてしまうのを防ぎます。
以下が修正後のスクリプトです。

<script>
var data = {
  animals:[
    {name: 'mouse'},
    {name: 'cat'},
    {name: 'bird'}
  ]
};
var directive = {
  'li':{
    'animal<-animals':{
      'span': 'animal.name'
    }
  }
};
var compiled = $p( 'ul' ).compile( directive );
$p( 'ul' ).render( data, compiled );

function addFox(){
  data.animals.push({name: 'fox'});
  $p( 'ul' ).render( data, compiled );
}
</script>

では、「キツネを追加する」をクリックしてみます。

enter image description here

はい、ちゃんとfoxが一つだけ追加されましたね。

感想

pure.jsは完成されたhtmlに埋め込みやすくなっているので割と便利です。
細かい処理にも融通が利くので割と好きです。
pureって名前もいいと思います。

参考

https://beebole.com/pure/

Slack Web API であそぼ

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

Slack Web APIで遊びます。 もともと難しくないと思いますが、rubyだと gemを使えばさらにかんたんです。

とりあえず今回使ってみるgem, slack-apiのリポジトリとドキュメントです。

slack-apiを使ってみよう

トークンの取得

Slack APIを使うために、まずトークンをとってきましょう。

https://api.slack.com/

の “Start Building” から適当な名前 (私は「あああああ」にしました) とAPIを使いたいチームを選ぶとAppができます。
OAuth & Permissionsから 適当な Permission Scopeを1つ以上 (私は emoji:read にしました) 選んで保存、ページ上の方の Install App to Team からアプリをインストール?するとトークンが発行されます。OAuth Access Token というのがそれです。

トークンがあると、API経由でトークンを発行したアカウントとして色々なことができてしまうので、これは基本的に人に見せてはいけないものです。
ソースコードをgitで管理するときは注意しましょう。私はよくうっかりコミットしています。

やってみよう(その1)

トークンを取得したらslack-apiを試してみましょう。
ドキュメントの http://www.rubydoc.info/gems/slack-api のコードの "YOUR_TOKEN" をさっきのトークンに置き換えて実行してみましょう。

require "slack"

Slack.configure do |config|
  config.token = "YOUR_TOKEN"
end

Slack.auth_test

コンソールでこれをそのまま実行すると、認証に成功しようが失敗しようが特に何も表示されません。

p なり何なりで Slack.auth_test の返り値を見てみると、認証に成功した場合はURLなどが、失敗した場合はエラーの内容がhashで返ってきます。
基本的にはSlack Web API https://api.slack.com/web のそのままです。slack-apiはトークンまわりやJSONのparseをしてくれるだけで、このあたりは自力です。DIYです。

# 成功した場合
{"ok"=>true, "url"=>"https://hoge.slack.com/", "team"=>"hoge", "user"=>"fuga", "team_id"=>"TXXXXXXXX", "user_id"=>"UXXXXXXXX"}
# 失敗した場合
{"ok"=>false, "error"=>"invalid_auth"}

やってみよう(その2)

もうちょっと意味のあるAPIを叩いてみたいですね。何かないでしょうか。

https://api.slack.com/methodshttp://www.rubydoc.info/gems/slack-api/Slack/Endpoint

あたりを見比べてやっていきます。
slack-apiのドキュメントにはどういったパラメータを指定すればよいかは書いていないので、そちらはslack apiのメソッド一覧を見てがんばります。

とりあえず役に立ちそうなものとして、ユーザーの一覧 を取得してみます。
https://api.slack.com/methods/users.list を見るに、必須パラメータはトークンだけですが、そのトークンはslack-apiがなんとかしてくれます。

permission がない場合はエラーになります。必要なものが何か返ってくるので、最初にしたように permission を追加してあげましょう。
emoji:read しかpermissionを与えていないと当然ですが駄目です。言われるがままに users:readを追加しましょう。

> Slack.users_list
=> {"ok"=>false, "error"=>"missing_scope", "needed"=>"users:read", "provided"=>"identify,emoji:read"}

permission を 追加すると何かメッセージが上に出てくると思いますが、reinstallする必要があります。
permission を追加すれば無事にできます。せっかくなので実行例を貼りますが、情報の大洪水です。ぜひ読み飛ばしてください。

> Slack.users_list
=> {"ok"=>true,
 "members"=>
  [{"id"=>"UXXXXXXXX",
    "team_id"=>"TXXXXXXXX",
    "name"=>"fugahoge",
    "deleted"=>false,
    "color"=>"4bbe2e",
    "real_name"=>"Hoge Fugefuga",
    "tz"=>"Asia/Tokyo",
    "tz_label"=>"Japan Standard Time",
    "tz_offset"=>32400,
    "profile"=>
     {"first_name"=>"Hoge",
      "last_name"=>"Fugafuge",
      "avatar_hash"=>"db6076802fe8",
      "image_24"=>"https://avatars.slack-edge.com/2000-00-00/173314781776_db6076802fe8e541e650_24.png",
      "image_32"=>"https://avatars.slack-edge.com/2000-00-00/173314781776_db6076802fe8e541e650_32.png",
      "image_48"=>"https://avatars.slack-edge.com/2000-00-00/173314781776_db6076802fe8e541e650_48.png",
      "image_72"=>"https://avatars.slack-edge.com/2000-00-00/173314781776_db6076802fe8e541e650_72.png",
      "image_192"=>"https://avatars.slack-edge.com/2000-00-00/173314781776_db6076802fe8e541e650_192.png",
      "image_512"=>"https://avatars.slack-edge.com/2000-00-00/173314781776_db6076802fe8e541e650_512.png",
      "image_1024"=>"https://avatars.slack-edge.com/2000-00-00/173314781776_db6076802fe8e541e650_1024.png",
      "image_original"=>"https://avatars.slack-edge.com/2000-00-00/173314781776_db6076802fe8e541e650_original.png",
      "status_text"=>"あああああああああ",
      "status_emoji"=>":star:",
      "real_name"=>"Hoge Fugafuga",
      "real_name_normalized"=>"Hoge Fugafuga",
      "team"=>"TXXXXXXXX"},
    "is_admin"=>true,
    "is_owner"=>false,
    "is_primary_owner"=>false,
    "is_restricted"=>false,
    "is_ultra_restricted"=>false,
    "is_bot"=>false,
    "updated"=>1501299574,
    "is_app_user"=>false,
    "has_2fa"=>false},
   {"id"=>"USLACKBOT",
    "team_id"=>"TXXXXXXXX",
    "name"=>"slackbot",
    "deleted"=>false,
    "color"=>"757575",
    "real_name"=>"slackbot",
    "tz"=>nil,
    "tz_label"=>"Pacific Daylight Time",
    "tz_offset"=>-25200,
    "profile"=>
     {"first_name"=>"slackbot",
      "last_name"=>"",
      "image_24"=>"https://a.slack-edge.com/0180/img/slackbot_24.png",
      "image_32"=>"https://a.slack-edge.com/2fac/plugins/slackbot/assets/service_32.png",
      "image_48"=>"https://a.slack-edge.com/2fac/plugins/slackbot/assets/service_48.png",
      "image_72"=>"https://a.slack-edge.com/0180/img/slackbot_72.png",
      "image_192"=>"https://a.slack-edge.com/66f9/img/slackbot_192.png",
      "image_512"=>"https://a.slack-edge.com/1801/img/slackbot_512.png",
      "avatar_hash"=>"sv1444671949",
      "always_active"=>true,
      "real_name"=>"slackbot",
      "real_name_normalized"=>"slackbot",
      "fields"=>nil,
      "team"=>"TXXXXXXXX"},
    "is_admin"=>false,
    "is_owner"=>false,
    "is_primary_owner"=>false,
    "is_restricted"=>false,
    "is_ultra_restricted"=>false,
    "is_bot"=>false,
    "updated"=>0,
    "is_app_user"=>false}],
 "cache_ts"=>1501754086}

いろんな情報が返ってきますね。slackbot の is_bot が false なのが得心いきませんが、こういうもののようです。

idなる文字列 "UXXXXXXXX" がありますが、発言ログなどを取ったときやAPIで投稿するとき、 Slack API ではこれを使います。
チャンネルも同様に “CXXXXXXXX” などになります。idは変わらないので決め打ちでもよいのですが、そうもいかない場合は自力で照合してやりましょう。

permission に users:read.email を追加すると、メールアドレスも返ってくるようになります。

やってみよう(その3)

迷惑ユーザーなのでslackの同じチームのユーザー全員にdirect messegeを送りつけたくなりました(ジョークです)。

direct messageを送りつけるのはかんたんです。
https://api.slack.com/methods/chat.postMessage を見るに、宛先と本文(とトークン)さえあれば送れます。
手始めにslackbotに送ってみます。channelはユーザーid(Uから始まる方)、textは適当な文章でやってみます。
前のusers.listの結果を見るに slackbotのユーザーidは “USLACKBOT” です。

> Slack.chat_postMessage(channel: "USLACKBOT", text: "あああああ")
=> {"ok"=>true,
 "channel"=>"DXXXXXX",
 "ts"=>"1501756000.743447",
 "message"=>{"type"=>"message", "user"=>"UXXXXXXXX", "text"=>"あああああ", "bot_id"=>"BXXXXXXXX", "ts"=>"1501756000.743447"}}

slackbotには何言ってんの?という反応をいただきました。

enter image description here

実は Slack API 的には、UやCから始まる例のidでなくてもメッセージが送れます。

> Slack.chat_postMessage(channel: "@slackbot", text: "あああああ")
=> {"ok"=>true,
 "channel"=>"DXXXXXX",
 "ts"=>"1501756000.743447",
 "message"=>{"type"=>"message", "user"=>"UXXXXXXXX", "text"=>"あああああ", "bot_id"=>"BXXXXXXXX", "ts"=>"1501756000.743447"}}

さて、ここまでやったことを活かせば、無事に1行で迷惑ユーザーになれます。

Slack.users_list["members"].each {|member| Slack.chat_postMessage(channel: member["id"], text: "あああああ")}

間違いなく顰蹙を買うので実行するのはやめたほうがいいと思います。

まとめ

これでslackでなんでもできる気がする!!!!!!!

実際permissonさえ追加すればユーザーとしてできることはたいていできるはずです。

おまけ

派生?gemが存在して、slack-apiよりもこちらの方が若干APIのレスポンスを丁寧に返してくれます。
https://github.com/slack-ruby/slack-ruby-client

また、Real Time Messaging APIもgemから使えます。
Real Time Messaging API は botトークンでないと使えないので注意です。({"ok":false,"error":"not_authed"} ……)

Real Time Messaging API は 先駆者がいるので (名言botをslackに入れて「人生」とは何か考えよう )ここではあまり言及しません。

gemを使う場合もそこまで変わらず、違うのは

  • rtm.start からurlを受け取ったり Faye::WebSocket::Client.new(url) したりするくだりが全部まとめて Slack.realtime でいい
  • JSONもparseしたりしなくていい

くらいです。

Slack API を組み合わせて君だけの最高のbotを作ろう!

RealmでKotlinのモデルクラスを扱うときの注意点

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。


概要

RealmというDBがやたら早いとかでモバイルでは流行りみたいです。
ここ数年はSQLiteからRealmに移行してる人が多いとか。
ということでRealm触ってみました。

サンプルコードの概要

冒頭のgif画像が動いている様子です。
右のボタンを押すと現在時刻をDBに突っ込んで表示します。
左のボタンを押すとDBのレコードを全て削除します。
fab使ってみたかったのでこんな感じにしました。
dataListを直接触らなくてもRealmを使うと簡単にRecyclerViewに反映されます。

以下コード

Activity

class MainActivity : AppCompatActivity() {
    lateinit var mRealm: Realm
    lateinit var mRecyclerView: RecyclerView
    lateinit var mAdapter: RecyclerViewAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_recycler_view)

        // Realmのセットアップ
        val realmConfig = RealmConfiguration.Builder(baseContext)
                .deleteRealmIfMigrationNeeded()
                .build()
        mRealm = Realm.getInstance(realmConfig)

        // Realmを読み込み
        val dateList: RealmResults<CurrentTimeModel> = mRealm.where(CurrentTimeModel::class.java).findAll()

        // RecyclerViewのセットアップ
        mRecyclerView = findViewById(R.id.recycler_view) as RecyclerView
        mAdapter = RecyclerViewAdapter(dateList)
        val layoutManager = LinearLayoutManager(applicationContext)
        mRecyclerView.layoutManager = layoutManager
        mRecyclerView.itemAnimator = DefaultItemAnimator()
        mRecyclerView.adapter = mAdapter
        mRecyclerView.addItemDecoration(DividerItemDecoration(this))

        // ボタンのセットアップ
        val fabAddCurrentDateTime = findViewById(R.id.fab_add_current_date_time)
        fabAddCurrentDateTime.setOnClickListener { addCurrentDateTime() }

        val fabDeleteAllRecords = findViewById(R.id.fab_delete_all_records)
        fabDeleteAllRecords.setOnClickListener { deleteAllRecords() }
    }

    override fun onDestroy() {
        super.onDestroy()

        mRealm.close()
    }

    fun addCurrentDateTime() {
        mRealm.executeTransaction {
            val currentDateTime = mRealm.createObject(CurrentTimeModel::class.java)
            // LocalDateTime.now()がMIN_APIで使えないので、KotlinMomentを使用
            currentDateTime.currentTime = Moment().toString()
            mRealm.copyToRealm(currentDateTime)
        }

        mAdapter.notifyDataSetChanged()
    }

    fun deleteAllRecords() {
        mRealm.executeTransaction {
            mRealm.where(CurrentTimeModel::class.java)
                    .findAll()
                    .deleteAllFromRealm()
        }

        mAdapter.notifyDataSetChanged()
    }
}

Model

open class CurrentTimeModel(
        open var currentTime: String = ""
): RealmObject() {}

エラーが出る

Realmを使っていて、エラーが出て詰まる場面が2つありました。
1. 新しくモデルクラスを追加したときに、class com.list_sample.realmkotlinsample.FooModel is not part of the schema for this Realm. というエラーが出る
2. 既に使っているモデルクラスをリネームしたり、パッケージ移動したりすると `Error:Execution failed for task ‘:app:transformClassesWithRealmTransformerForDebug’.

javassist.NotFoundException: com.list_sample.realmkotlinsample.Model.` というエラーが出る

エラー対策

JavaとKotlinで書いてみて、言語によって起こるエラーとそれ以外を切り分けます。

1. 新しくモデルクラスを作成した場合のエラー

まずはJavaで簡単なサンプルを動かす

#MainActivity
package com.list_sample.realmjavasample;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.RealmResults;

public class MainActivity extends AppCompatActivity {
    private Realm mrealm;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Realmのセットアップ
        RealmConfiguration realmConfig = new RealmConfiguration.Builder(this)
                .deleteRealmIfMigrationNeeded()
                .build();
        mrealm = Realm.getInstance(realmConfig);

        writeRealm();

        RealmResults<HogeModel> mrealmResult = mrealm.where(HogeModel.class).findAll();
        Log.d("Hoge", "Realm result is " + mrealmResult.toString());
    }

    private void writeRealm() {
        mrealm.executeTransaction(new Realm.Transaction() {

            @Override
            public void execute(Realm realm) {
                HogeModel hoge = mrealm.createObject(HogeModel.class);
                hoge.setHoge("hoge");

            }
        });
    }
}
#HogeModel
public class HogeModel extends RealmObject{
    private String hoge;

    public String getHoge() {
        return hoge;
    }

    public void setHoge(String hoge) {
        this.hoge = hoge;
    }
}

当然正常に動きます。
これに新しくモデルクラスとして、FooModelを追加します。

# FooModel
public class FooModel extends RealmObject {
    public String getFoo() {
        return foo;
    }

    public void setFoo(String foo) {
        this.foo = foo;
    }

    private String foo;

}

# MainActivity
public class MainActivity extends AppCompatActivity {
    private Realm mrealm;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Realmのセットアップ
        RealmConfiguration realmConfig = new RealmConfiguration.Builder(this)
                .deleteRealmIfMigrationNeeded()
                .build();
        mrealm = Realm.getInstance(realmConfig);

        writeRealm();

        RealmResults<FooModel> mrealmResult = mrealm.where(FooModel.class).findAll();
        Log.d("Foo", "Realm result is " + mrealmResult.toString());
    }

    private void writeRealm() {
        mrealm.executeTransaction(new Realm.Transaction() {

            @Override
            public void execute(Realm realm) {
                FooModel foo = mrealm.createObject(FooModel.class);
                foo.setFoo("foo");
            }
        });
    }
}

Javaではエラーが出ません。
これをKotlinで書くと、

# FooModel
open class FooModel(
        open var foo: String = ""
): RealmObject(){}
# MainActivity
class MainActivity : AppCompatActivity() {
    lateinit var mrealm: Realm

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Realmのセットアップ
        val realmConfig = RealmConfiguration.Builder(this)
                .deleteRealmIfMigrationNeeded()
                .build()
        mrealm = Realm.getInstance(realmConfig)

        writeRealm()

        val mRealmResults: RealmResults<FooModel> = mrealm.where(FooModel::class.java).findAll()
        Log.d("Foo", "Realm results is $mRealmResults")

    }

    fun writeRealm() {
        mrealm.executeTransaction {
            val foo = mrealm.createObject(FooModel::class.java)
            foo.foo = "foo"
        }
    }
}

落ちます。
class com.list_sample.realmkotlinsample.FooModel is not part of the schema for this Realm.

原因と対策

これの原因はRealmのライブラリのバージョンが古かったことです。
今のRealmライブラリの最新バージョンが3.7.1で使っていたライブラリが1.1.0でした。

2. 既に使っているモデルクラスをリネームしたり、パッケージ移動したりすると `Error:Execution failed for task ‘:app:transformClassesWithRealmTransformerForDebug’.

javassist.NotFoundException: com.list_sample.realmkotlinsample.Model` というエラーが出る
ここでもJavaとKotlinを使って切り分けます。
不要になったHogeModelというクラスを削除してみます。

Javaの場合

削除しても正常に動きます。

Kotlinの場合
Error:Execution failed for task ':app:transformClassesWithRealmTransformerForDebug'.
> javassist.NotFoundException: com.list_sample.realmkotlinsample.HogeModel

落ちます。

原因

Realmはモデルクラスを作ってビルドするときに、Proxyクラスを生成します。
HogeModelの場合はHogeModelRealmProxyというファイルが生成されます。
これはJavaの場合もKotlinの場合も同じなんですが、Kotlinの場合RealmProxy クラスとモデルの名前やディレクトリが異なるとエラーが出ます。

対策

Rebuildすると直ります。一番手っ取り早いです。
あとはRealmProxyクラスを削除しても直ります。 -> android studio でcleanするとbuild以下のファイルは全て消えるため、手動で消すのは無駄だと指摘をうけました。
Javaでモデルクラスを作成した場合はそもそもエラーが出ないです。
Rebuildが一番良さそうな解決方法です。

感想

速度が求められるモバイル開発では今後もRealmを使う場面が増えてくると思います。
今回はざっくりした実装とエラー対策ではありましたが、これからも使い方を勉強してうまく使えるようになりたいですね。
あと、今回の記事が間違ってるよ!って方は教えていただけると助かります。
fabだったりRealmだったり、adb使った動画キャプチャのとり方だったり、画像の圧縮方式だったり、個人的には学びが多かったのでよかったです。
あとkotlinはやっぱりいいです。楽だし書きやすい。

スルメのススメ

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

日々のデスクワークの中、集中して作業するのはとても疲れるもの。
適度な休憩や補給をすることで、かえっていい結果が残せたりするものです。
作業の合間にちょっとしたストレッチ等、やってみてはいかがでしょうか。

さて、今回紹介するのは間食にお薦めの食材、「スルメ」です。
お酒のツマミとかのイメージが多いかもですが、なんと間食に最適な食材だったのです!

ではまず「スルメとは」について紹介します。

「スルメ(鯣)は、イカの内臓を取り除いて素干しや機械乾燥などで乾燥させた加工食品。乾物の一種。古くから日本、朝鮮半島、中国南部および東南アジアにおいて用いられている食品で長期保存に向いている。日本では縁起物とされ結納品などにも用いられ寿留女と表記される。俗語としてアタリメとも言う。」(出典: フリー百科事典『ウィキペディア(Wikipedia)』 (2017/05/17 02:23 UTC 版))

栄養成分(100gあたり):334kcal 
 タンパク質:69.2g
 脂質:4.3g
 炭水化物:0.4g
 ビタミンB群、Eに優れ、ミネラルの含有量も多い。また、疲労回復によいとされるタウリンも多く含まれる。(出典:カロリーslim http://calorie.slism.jp/110353/)

スルメを選ぶメリットとは?

 スルメという食品には以下の健康効果が秘められています。

1. 眠気に効く!

 眠気を感じた時、硬いものを嚙むことは有効です。咀嚼筋が動かされることで脳への刺激が伝わると、脳の機能が活発化し、眠くなりにくくなるのはもちろん、集中力が上がったりします。顎は筋肉の伸縮を感知する「筋紡錘(きんぼうすい)」が多く存在するため、筋肉を動かすと脳に刺激が伝わりやすく、眠気が起きにくくなるといいます。

2. ダイエット効果!

 噛むことは、ダイエットにも効果があります。噛む動作により、脳内にヒスタミンが発生し、満腹中枢が刺激されます。またタンパク質を多く含む食品のため、適度な運動を併せて行うことで、筋肉量を増加し太りにくい体を作ることができます。

3. 疲労回復効果!

 イカはタウリンを多く含みます。タウリンはアミノ酸の一種で、細胞の動きを正常にする作用があるため、肝機能の改善、風邪予防、疲労回復、といった効果があります。余分に取り過ぎた場合は体外に排出されるため、過剰摂取を心配する必要はありません。

スルメを摂取するうえで注意するべきことは?

1. 塩分の摂り過ぎ

 スルメは大体100gあたり3g程度の塩分を含みます。過剰摂取により発生した高い血中塩分濃度を補うため、細胞に水分を保持しようとする(→むくみ)、水分を多くとることで血流量が増大する(→高血圧)、といった現象が発生します。
 そのため、バナナなどに多く含まれるカリウムを摂取して、塩分の排出を促す必要があります。

2. 消化が悪い

 スルメはその硬さ故、消化に時間がかかります。それは反面「腹持ちがいい」ということでもあるのですが、消化が終わるまでの間、胃に負担をかけることになります。胃酸の分泌も促進されるので、注意が必要です。

3. 独特の匂い(スメル)

 イカを含めた海産魚介類には「トリメチルアミンオキシド」というエキス成分が含まれており、これが加熱などにより「トリメチルアミン」に変わります。この成分は量によっては独特な魚の腐敗臭を感じさせることもあり、周囲への配慮が必要です。

まとめ

このように、様々な健康効果を持つ食品であるスルメ。一方、過剰摂取には気を付けないといけない面もあります。摂取量をきをつけつつ、間食の一つとして採用してみてはいかがでしょうか。

名言botをslackに入れて「人生」とは何か考えよう

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

こんにちは、いくたです。

みなさん、人生について考えていますか?
毎日忙しいとなかなか考える暇がありませんよね。

そこで普段仕事中に使っているチャットツールである Slack に名言を教えてくれるbotを作ってみました。忙しい中でもふとした瞬間に人生について考えるきっかけになるかもしれませんね。

準備するもの

  • Ruby(ver 2.1.0)
  • gem: open-uri → URL先のデータを普通のファイルと同様に扱えます
  • gem: Nokogiri →  HTMLやXMLの構造を解析して特定の要素を抽出できます
  • gem: MeCab → 日本語の文章を品詞単位で解析してくれます
  • SlackBot → 導入方法は こちらの記事 がわかりやすかったです
  • 名言 → こちらの 偉人の名言100 から名言をとってきます

MeCab とは日本語の文章を品詞単位で解析してくれるオープンソース形態素解析エンジンです。

試しに二葉亭四迷の名言を解析してみました。
品詞ごとに分けて品詞の種類を教えてくれます。

$ mecab
いや、人生は気合だね。二葉亭四迷
いや  接続詞,*,*,*,*,*,いや,イヤ,イヤ
、 記号,読点,*,*,*,*,、,、,、
人生  名詞,一般,*,*,*,*,人生,ジンセイ,ジンセイ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
気合  名詞,一般,*,*,*,*,気合,キアイ,キアイ
だ 助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
ね 助詞,終助詞,*,*,*,*,ね,ネ,ネ
。 記号,句点,*,*,*,*,。,。,。
二葉亭四迷 名詞,固有名詞,人名,一般,*,*,二葉亭四迷,フタバテイシメイ,フタバテイシメイ
EOS

気合いですか…先生…

実装の概要

今回作るのはユーザーが投稿した内容に反応して名言を返してくれるbotです。
下の画像の saying_bot が今回作ったbotです。
直前の「ただ」という単語に反応してダグ・ハマーショルドさんの名言を投稿しています。

slack_bot.png
図1 投稿した内容の単語に反応して名言を返すsaying_bot

ざっくりとした仕組みとしては以下のような感じです。

  1. 名言データをサイトからとってくる
  2. MeCabで名言データを解析して”名詞”だけを抜き出す(名詞リスト)
  3. 投稿された内容に名詞リストと一致する部分があるか調べる
  4. 一致するものがあれば対応する名言を投稿する

1.については私が前回書いた記事でWebスクレイピングについて書いたので割愛します。open-uri や Nokogiri の使い方と合わせて書いてあるので、よろしければこちらも合わせてご覧ください。
【前回の記事】アニメ名探偵コナンの新一が出てる回だけを見たい

今回の記事では 2〜4 についてどうやって実装したか具体的に見ていきます。
また、裏テーマとして「クラスを役割ごとに分ける」ということも意識しているので、そのあたりも一緒に追っていきます。

MeCabで名詞だけを抽出

Stringに新たにparse_nounメソッドを追加します。
文字列から名詞だけを取り出して、配列に格納してくれます。

こちら↓を参考にして書きました。
Rubyの形態素解析「MeCab」で文章から名詞を抽出してみる

抜き出す名詞の種類を「一般名詞」と「形容動詞語幹」に絞っています。
名詞の全てを抜き出してしまうと「それ」「あれ」といった指示代名詞等も入ってしまうので細分化して絞り込みました。
一般名詞は「本」とか「人間」といった普通の名詞です。
形容動詞語幹とは「親切」とか「変」などのことをいいます。
形容動詞の「親切だ」「変だ」という語から変形して名詞のように使われています。

# mecab_string.rb
require 'MeCab'

# Stringクラスに名詞抽出メソッドを追加
class String
  def parse_noun
    model = MeCab::Model.new(ARGV.join(' '))
    tagger = model.createTagger
    node = tagger.parseToNode(self)

    nouns = []
    while node
      target_node = node.feature.force_encoding('UTF-8')
      # 名詞(一般・形容動詞語幹)を抽出する
      if /^名詞,一般/ =~ target_node || /^名詞,形容動詞語幹/ =~ target_node
        nouns.push(node.surface.force_encoding('UTF-8'))
      end
      node = node.next
    end
    nouns
  end
end

名言を保持するクラス

名言のデータを保持するクラスを考えます。
インスタンスからid(名言のID)、dialog(発言)とgreatman(偉人)というメソッドで名言のデータ取ってこれるように、attr_accessorをつかっています。

# saying.rb
# Sayingクラスは名言データを持つためのクラス
class Saying
  attr_accessor :id, :dialog, :greatman
  def initialize(id, dialog, greatman)
    @id = id
    @dialog = dialog
    @greatman = greatman
  end
end

名言データと名詞リストを扱うクラス

つぎにSayingDaoクラスを作成します。
DaoはData Access Objectの略でデータを扱うオブジェクトです。
今回はサイトからとってきた「名言データ」と名言から名詞を抽出した「名詞リスト」のふたつを同時に扱います。

SayingDaoクラスの役割

initialize

  1. 名言の内容をサイトから取得
  2. 使いやすいようにハッシュ化して「名言データ」をつくる
  3. 名言から名詞を抽出して「名詞リスト」をつくる

find_and_sample_saying メソッド

  1. 任意の文字列(ユーザーの投稿内容)を引数にとる
  2. initialize内で作成した名詞リストと照らし合わせる
  3. 一致したものがあれば名言データをひとつだけ返す
  4. 一致しなければnilを返す

繰り返しになりますが、Open-uriやNokogiriの使い方は前回の記事をどうぞ。

# saying_dao.rb
require 'open-uri'
require 'nokogiri'
require_relative 'saying.rb'
require_relative 'mecab_string.rb'

# 名言リストと名詞テストを作成する
class SayingDao
  attr_accessor :sayings_data, :nouns_list

  # 名言をとってくるURL
  SAYING_URL = 'http://atsume.goo.ne.jp/HxLFhNn4N7Zb'.freeze
  XPATH = "//*[@id=\"atsumeWrapper\"]/div[3]/div".freeze

  def initialize
    # 名言をとってくるURLからNokogiriとXPathでスクレイピング
    html = open(SAYING_URL).read
    doc = Nokogiri::HTML.parse(html)
    div = doc.xpath(XPATH)

    # 名言のデータはsayings_dataに格納する
    @sayings_data = []

    # 名言に含まれる名詞のデータはnouns_listに格納する
    @nouns_list = Hash.new { |hash, key| hash[key] = [] }

    # HTMLの解析結果をdiv要素ごとに処理
    div.each_with_index do |div, saying_id|
      dialog = div.xpath('./p').text
      greatman = div.xpath('./h2').text
      saying = Saying.new(saying_id, dialog, greatman)

      @sayings_data << saying

      # 名言から名詞を抽出
      saying_nouns = saying.dialog.parse_noun

      # 名詞データがないときには新しく作る
      # 既にkeyが一致するデータがあればindexの情報を追加
      saying_nouns.each { |noun| @nouns_list[noun].push(saying_id) }
    end
  end

  # 名詞リストと照合して一致した名詞と名言を返す
  def find_and_sample_saying(text)
    # 名詞リストのうち引数textに含まれるものを取ってくる
    # sayin_idは使わないので引数を捨てるためにアンダースコアをつける
    related_nonus = @nouns_list.select { |noun, _saying_ids| text.include?(noun) }

    # 名詞リストに引っかからなかったらnilを返す
    return if related_nonus.empty?

    # 複数の名詞リストに引っかかることがあるのでsampleで一つだけ取ってくる
    noun = related_nonus.keys.sample

    # 一つの名詞に複数の名言が紐付いていることがあるのでsampleで一つだけ取ってくる
    saying_id = related_nonus[noun].sample
    saying = @sayings_data.find { |saying| saying.id == saying_id }

    { noun: noun, saying: saying }
  end
end

ユーザーの投稿と名詞リストを照らしあわせる

次にslack側から渡されたユーザーの投稿に合わせて名言のデータを返すReplyMessageクラスを作ります。
ここで先ほどSayinDaoで生成した名言データおよび名詞リストをつかいます。
投稿内容に名詞リストと一致するものがあれば、該当するインデックス番号に紐付く名言データをひとつ取ってきます。

SayingReplyクラスの役割

initialize

SayingDaoをnewする

saying_replyメソッド

  1. ユーザーの投稿内容を受け取る
  2. 投稿内容と名言リストを突き合わせる(SayingDaoのfind_and_sample_sayingメソッド)
  3. find_and_sample_sayingの返り値(名言データor nil)に合わせて返信メッセージ or nilを返す
# reply_message.rb
require_relative 'saying_dao.rb'

# メッセージを成形
class ReplyMessage

  def initialize
    @saying_dao = SayingDao.new
  end

  # ユーザーの投稿内容にあった返信内容を返す
  def saying_reply(message)
    match_data = @saying_dao.find_and_sample_saying(message)

    # match_dataがnilだったらnilを返す
    return if match_data.nil?

    # match_dataに値があれば返信内容を返す
    <<~TEXT
    「#{match_data[:noun]}」と言えば、こちらの名言をご覧ください。

    >#{match_data[:saying].dialog}
    >
    >-  #{match_data[:saying].greatman}  -
    TEXT
  end
end

名言を投稿するSlackBot

最後に実際にslackの制御を行うbotを作成します。
基本の挙動は こちらの記事 を参考に書きました。

botの挙動

  1. slack上のアクション(誰がどんな投稿をしたか、誰が記入中ステータスか等)をリアルタイムで補足
  2. ユーザーが何かしら投稿した時にReplyMessageのsaying_replyメソッドに渡す
  3. saying_replyメソッドから返ってきた値をslackに投稿する
# bot.rb
require 'eventmachine'
require 'faye/websocket'
require 'http'
require 'json'
require_relative 'reply_message.rb'

SLACK_API_URL = 'https://slack.com/api/rtm.start'.freeze

response = HTTP.post(SLACK_API_URL, params: { token: ENV['SLACK_API_TOKEN'] })
rc = JSON.parse(response.body)
url = rc['url']

EM.run do
  # Web Socketインスタンスの立ち上げ
  ws = Faye::WebSocket::Client.new(url)

  # ReplyMessageのインスタンスを生成
  rm = ReplyMessage.new 

  #  接続が確立した時の処理
  ws.on :open do
    p [:open]
  end

  # RTM APIから情報を受け取った時の処理
  ws.on :message do |event|
    data = JSON.parse(event.data)
    p [:message, data]
    if data['type'] == 'message' && data['user']

      # ユーザーの投稿data['text']をReplyMessageのsaying_replyメソッドに渡す
      reply_message = rm.saying_reply(data['text'])

      if reply_message
        # 返信内容を投稿する
        ws.send({
          type: 'message',
          text: reply_message,
          channel: data['channel']
        }.to_json)
      end
    end
  end

  # 接続が切断した時の処理
  ws.on :close do
    p [:close]
    ws = nil
    EM.stop
  end
end

まとめ

今回このbotを作る上で一番大変だったのは、クラスを役割ごとに分ける作業でした。
普段だと、書きやすいコードからなんとなく書き始めてしまうことが多いので、作り始めてから「この機能は別のクラスにしよう…」とか「このクラス全く要らないのでは…」となってしまいます。
今度からは必要となるクラスや機能を挙げてから、実際のコードを書いていこうと思いました。

最後にお気に入りの名言をひとつ。

「それも、いいじゃないか」は、おもしろい人生のスローガン。
– メーソン・クーリー –

それでは、今日はこの辺で。
読んでいただき、ありがとうございました。

git log –graphでログを見やすくする

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

git logをコンソール上でグラフィカルに表示してくれるオプションが–graphです。

git log --graph

だけでもマージ状況が視覚的に把握できて便利ですが、
–onelineや、–decorate、 –formatオプションと
組み合わせると、さらに見やすくなります。

*   468ea0a [2017-07-01] Merge branch 'branch3' into 'master' @user1
|\
| * 34ed423 [2017-07-01] コミット5 @user3
* |   1d47e56 [2017-07-01] Merge branch 'branch2' into 'master' @user1
|\ \
| |/
|/|
| * 7ed5440 [2017-07-01] コミット4 @user2
| * 2a32810 [2017-07-01] コミット3 @user2
| * c4a27c5 [2017-06-30] コミット2 @user2
| * a8506c1 [2017-06-30] コミット1 @user2
* |   2925ee7 [2017-07-01] Merge branch 'branch1' into 'master' @user1

 
記事画像のような表示の例だと
こんな感じに指定しています。

git log --graph --oneline --decorate=full -20 --date=short --format="%C(yellow)%h%C(reset) %C(magenta)[%ad]%C(reset)%C(auto)%d%C(reset) %s %C(cyan)@%an%C(reset)"

コミットNo. :黄色
日付     :マゼンダ
ブランチ名  :自動
作業者    :シアン
 

【オプション】
–oneline
 一行表示
–decorate=(short|full|no)
 ブランチ名の表示形式
–date=(relative|local|default|iso|rfc|short|raw)
 日付表示
–pretty[=<format>], –format=
 フォーマット指定。%C()は色指定

 
自分の見やすいフォーマットで設定できたら、
aliasの設定をしておきます。
自分はlog –graphを略して&打ちやすさで
git logaにしています。

$ git config --global alias.loga 'log --graph --oneline --decorate=full -20 --date=short --format="%C(yellow)%h%C(reset) %C(magenta)[%ad]%C(reset)%C(auto)%d%C(reset) %s %C(cyan)@%an%C(reset)"'

※gitのバージョンが古い場合、「%C(auto)」の指定はできないようです。
 1.7だとエラーになりました。

 
詳細は
git log –help
で確認できます。

最近人気な記事