もちっとメモ

もちっとメモ

管理人のホームページで書ききれていない調べ物のメモページです

MattermostでTwitterの更新情報を自動で呟いてくれるbot的なのを作った

はじめに

最近、職場でOSSの環境を乱立しまくっているのですが、そのひとつとしてMattermostも立ち上げました。

釈迦に説法かとは思いますが、Mattermostは早い話が、オンプレミスで動くSlackです。うちもセキュリティが厳しいので、流行りのツールを使おうにもオンプレで動くものでないと使えなかったりします。

qiita.com

で、立ち上げたのはいいのですが、早速言われたのが、「これ何に使うの?」。うちは独自のチャットツールがあるので、わざわざMattermostを使わなくてもいいじゃんという考えの人が多いようです。そこで、よくチェックする記事を自動で呟いてくれるbotを作れば需要があるんじゃなかろうかということで作りました。

使っている環境はMattermostとJenkinsとpythonです。構成としてはこんな感じになっています。

f:id:T-N-Clark:20180614000213p:plain

即興で作ったのでガバガバなのはご愛嬌ということで。Jenkinsはただのスケジューラーとして使っているので、こうして見返してみるとJenkinsである必要ないですね。まあ、Mattermostと繋げてビルドに失敗したときに通知が飛ばせるから(震え声)。あったので使った程度です。

今回はこちらのTwitterアカウントの更新情報を取得させていただくことにします。機械学習などの最新の論文情報をまとめてくださっているサイトです。アブストの日本語訳などもあるので、さっとチェックしたいときにとても参考になります。

twitter.com

実装

では、早速実装していきます。まずは、Twitterの更新情報を取得します。色々方法はあると思うのですが、今回はこちらを利用しました。

TwitRSS.me - rss of twitter user feeds by screenscraping with perl

次に、Mattermost側で情報を受け取るURLを用意しておきます。今回は情報を受け取るだけなので、内向きウェブフックを使います。

qiita.com

いよいよ、RSSpythonで取得、データベースに保存、json形式に変換、Mattermostに送信部分を組んでいきます。最後に最終的に実装したコードを載せていますので、説明はいいという方は飛ばしてください。長いので処理内容毎に分けて説明します。といっても浅い理解なので間違っていたらすみません。コードもとりあえず、動くことだけしか考えてないので出来の悪い実装は適宜修正してください。ベースはこちらのサイトを参考にさせていただきました。

qiita.com

requestsで指定したURLからRSSの情報を取得します。requestライブラリについては、こちらをどうぞ。

qiita.com

プロキシの設定が必要な場合はこちらが参考になります。

qiita.com

無事、RSSの情報が取ってこれたら、feedparserでcontentから必要な情報を取ってきます。(これなんでfeedparserだけじゃだめなんだっけ?プロキシのせいだったかな。忘れてしまった。)この中のentryの中に欲しいデータがあるみたいなのでpandas形式に変換しておきます。そこまでできたらこちらを参考にTwitRSSでとってきた中から必要情報を抜き出します。

qiita.com

今回必要なのは、idとtitleとlinkです。idはツイート毎のユニークな値です。これを使って既出か否かの判別をします。titleはツイートの内容にあたるのでこれをMattermostのメッセージにします。linkはそのツイートへのリンクです。今回はMattermostでメッセージをクリックするとTwitterにリンクするようにしておきます。

取ってきたRSSの情報をデータベースに格納します。今回はそこら辺はサボってCSVに保存しています。(ちなみにこの安易な考えで後に少々面倒なことになりました)

qiita.com

全体通して書くとこうなります。



import requests,feedparser

import pandas as pd

import os

import json



def getNewFeed(rss_data,already_printed_feeds,filepath):

    feeds_info = []

    feed = feedparser.parse(rss_data.content)

    entries = pd.DataFrame(feed.entries)

    if already_printed_feeds.empty:

        #全て新着Feed

        new_entries = entries

    else:

        #既出のFeedは除く

        new_entries = entries[~entries['id'].isin(already_printed_feeds['id'])]

    if not new_entries.empty: #新着Feedがあれば

        for key, row in new_entries.iterrows():

            title = row['title'].split('http')[0]

            #Mattermostに投稿されるメッセージ.ここではmarkdown形式でリンクになるように書いている.

            feedinfo = '[**%s**](%s)' % (title, row['link'])

            feeds_info.append(feedinfo)

        #新着データがあれば既存のリストに追加する

        already_printed_feeds = already_printed_feeds.append(new_entries)

        #データベース(csv)に保存

        if os.path.exists(filepath):

            new_entries.to_csv(filepath,encoding='utf-8',mode='a',header=False)

        else:

            new_entries.to_csv(filepath,encoding='utf-8')

    else: #新着Feedが無ければ

        print('not found new entries')

    return feeds_info



#既出のfeed情報の取得

def getAlreadyPrintedFeeds(filepath):

    if os.path.exists(filepath):

        already_printed_feeds = pd.read_csv(filepath)

    else:

        already_printed_feeds = pd.Series()

    return already_printed_feeds



def setPostMessage(feedinfo,username):

    payload = {

        'text':feedinfo,

        'username':username,

    }

    return payload



#データベース(csv)へのパス

filepath = 'entries.csv'

#proxyの設定

proxies = {

    'http':'http://id:passward@proxyadress:port',

    'https':'http://id:passward@proxyadress:port'

}

#RSSFeed取得先のURL

url = 'http://twitrss.me/twitter_user_to_rss/?user=arxivtimes'

rss_data = requests.get(url,proxies=proxies)

already_printed_feeds = getAlreadyPrintedFeeds(filepath)

feedinfo = getNewFeed(rss_data,already_printed_feeds,filepath)



#Mattermostの内向きウェブフック

mattermosturl = 'http://localhost/hooks/***'

#Mattermostのつぶやき時に表示される名前(好きな名前をつける)

username = 'FeedBot'

header = {'content-Type':'application/json'}

#新着Feedを順にMattermostに投げる

for i in range(len(feedinfo)):

    payload = setPostMessage(feedinfo[i])

    resp = request.post(mattermosturl,

                        header=header,

                        data=json.dumps(payload))

最後にこれを定期的に実行するためにJenkinsで実行スケジュールを組みます。と言ってもやることは単純で1時間に1回ジョブを実行させるだけです。確か今回採用しているRSSの取得方法が1時間単位に更新された気がするのでこんなもんでいいでしょう。こちらを参考にさせていただきながら、スケジュールを設定していきます。

qiita.com

ということで最低限飛ばせるようになりました。

ただ、ここで2つ問題が起きました。

  1. 一つはこのままじゃ複数ページから取ってこれないのでもうちょっとだけいじります。といってもurlをリストにして順々に実行するだけです。なので大したことないです。
  2. もう一つは取ってきたRSS情報のcolumn数が揃ってないだと…どういう条件かまでは見てないのですが、ツイートによっては特定のcolumnのデータがあったりなかったりするみたいです。NaNになるならいいのですが、CSVへの書き出し部分がいけないのかcolumnそのものが違って保存されてしまっているようです。そのせいで既出情報を読み出す際にcolumn数が合わないとエラーを食らいました。ダサいですが、`pandas`で読み込む際に`names`を指定して読み込むことにします。

    nekoyukimmm.hatenablog.com

    pandasでカラムサイズが一定でないcsv/tsvを読み込む : mwSoft blog

import requests,feedparser
import pandas as pd
import os

def getNewFeed(rss_data,already_printed_feeds,filepath):
    feeds_info = []
    feed = feedparser.parse(rss_data.content)
    entries = pd.DataFrame(feed.entries)

    if already_printed_feeds.empty:
        #全て新着Feed
        new_entries = entries
    else:
        #既出のFeedは除く
        new_entries = entries[~entries['id'].isin(already_printed_feeds['id'])]
    if not new_entries.empty: #新着Feedがあれば
        for key, row in new_entries.iterrows():
            title = row['title'].split('http')[0]
            #Mattermostに投稿されるメッセージ.ここではmarkdown形式でリンクになるように書いている.
            feedinfo = '[**%s**](%s)' % (title, row['link'])
            feeds_info.append(feedinfo)
        #新着データがあれば既存のリストに追加する
        already_printed_feeds = already_printed_feeds.append(new_entries)
        #データベース(csv)に保存
        if os.path.exists(filepath):
            new_entries.to_csv(filepath,encoding='utf-8',mode='a',header=False)
        else:
            new_entries.to_csv(filepath,encoding='utf-8')
    else: #新着Feedが無ければ
        print('not found new entries')

    return feeds_info

#既出のfeed情報の取得
def getAlreadyPrintedFeeds(filepath):
    if os.path.exists(filepath):
      col_names = ['no',
                    'author',
                    'author_detail',
                    'authors',
                    'guidislink',
                    'id',
                    'link',
                    'links',
                    'published',
                    'published_parsed',
                    'summary',
                    'summary_detail',
                    'title',
                    'title_detail',
                    'twitter_place',
                    'twitter_source']
        already_printed_feeds = pd.read_csv(filepath)

    else:
        already_printed_feeds = pd.Series()

    return already_printed_feeds

import json

def setPostMessage(feedinfo,username):
    payload = {
        'text':feedinfo,
        'username':username,
    }

    return payload



def postForMattermost(feedinfo):
    #Mattermostの内向きウェブフック
    mattermosturl = 'http://localhost/hooks/***'
    #Mattermostのつぶやき時に表示される名前(好きな名前をつける)
    username = 'FeedBot'
    header = {'content-Type':'application/json'}

    #新着Feedを順にMattermostに投げる
    for i in range(len(feedinfo)):
        payload = setPostMessage(feedinfo[i])
        resp = request.post(mattermosturl,
                            header=header,
                            data=json.dumps(payload))


def main():
    #proxyの設定
    proxies = {
        'http':'http://id:passward@proxyadress:port',
        'https':'http://id:passward@proxyadress:port'
    }

    #データベース(csv)へのパス
    filepath = 'entries.csv'
    already_printed_feeds = getAlreadyPrintedFeeds(filepath)

    #RSSFeed取得先のURL
    urls = ['http://twitrss.me/twitter_user_to_rss/?user=arxivtimes',
            'http://twitrss.me/twitter_user_to_rss/?user=a_i_news',
            'http://twitrss.me/twitter_user_to_rss/?user=ai_m_lab'
    ]


    for i in range(len(urls)):
        #RSS情報の取得
        rss_data = requests.get(urls[i],proxies=proxies)
        #データベースへの登録
        feedinfo = getNewFeed(rss_data,already_printed_feeds,filepath)
        #Mattermostへの送信
        postForMattermost(feedinfo)

if __name__ == "__main__":
    main()

多少整理もしたのでさっきよりは見やすくなったのではないでしょうか。Cの頃の癖なのか気づくと思考停止でインデックスでforループ回してますね。これはひどい

長いわりに稚拙な内容でしたが参考になれば幸いです。

後半で追加したサイト

twitter.com

twitter.com

【Unity】シーンの切り替えエフェクト

【2018/7/14追記】作成したスクリプトにFadeCanvasをアタッチする部分が抜けていたので追記しました。

RPGでマップ画面から戦闘画面に切り替わるときとかにあるあれです。既にたくさんの方が書かれているので今さらですが、個人的にはまったので、備忘録として書いておきます。

手っ取り早くやりたいなら

github.com

bibinbaleo.hatenablog.com

複雑なエフェクトをかけたいなら

tsubakit1.hateblo.jp

使い方の例:ボタンクリックでエフェクトをかけながら次のシーンに移る

  1. シーンを2つ用意する
  2. ルール画像を取得する4you.bz

  3. 取得したルール画像をUnityのProjectにインポートする。フォルダー構成は任意。
  4. ルール画像のInspectorを変更する。TextureType=Default,TextureShape=2D,sRGB=on,AlphaSource=FromGrayScale,AlphaIsTransparency=on
    にしてApplyを押す。
  5. シーン切り替えエフェクト用のパッケージを取得する。

    github.com

  6. ダウンロードしたパッケージを[Assets]->[Import Package]->[Custom Package]からインポートする。インポート対象はallで良い。
  7. シーン移動する前のシーンにインポートしたパッケージフォルダ[Fade]->[FadeCanvas]のプレハブをシーンに置く
  8. FadeCanvasにアタッチされている[FadeImage]でエフェクトの様子を調整できる。Materialを変更することで変化の様子を調整する。[UI-Fade-Cutout]にすると円を描くようなエフェクトになる。MaskTextureを変更するとエフェクトの模様を変更できる。ここに3.でインポートしたルール画像をアタッチする。
  9. Canvas下に[UI]->[Button]を置く。
  10. ButtonにFade用のスクリプトをアタッチする。今回は以下のようなスクリプトを自作して与えた。
    using System.Collections;
    
    using System.Collections.Generic;
    
    using UnityEngine;
    
    using UnityEngine.SceneManagement;
    
    
    
    public class FadePractice : MonoBehaviour {
    
    	[SerializeField]
    
    	Fade fade = null;
    
    
    
    	public void Fadeout()
    
    	{
    
    		//fade.FadeIn (1, () => {
    
    		//	fade.FadeOut (1);
    
    		//});
    
    		fade.FadeIn (1, () => {
    
    			Invoke("LoadScene",0.5f);
    
    		});
    
    	}
    
    	public void LoadScene(){
    
    		SceneManager.LoadScene ("NextScene");
    
    	}
    
    }
  11. ButtonのOnClickに先ほどButtonにアタッチした[FadePractice]をアタッチする。この時[FadePractice]はInspector上のものをアタッチすること。アタッチできたら、[Runtime Only],関数名:[FadePractice.Fadeout]を選択する。
  12. FadePracticeにシーン上のFadeCanvasをアタッチする
  13. シーンをSaveしてゲームを実行する。ボタンをクリックして、フェードアウトエフェクトがかかりながら次のステージに遷移できたら完成。

あとはお好みのエフェクトに変更したり、ステージの遷移になるように調整してください。

FadeCanvasのSortingLayerをいじることで描画順序も調整できるようです。Materialによっても見え方がことなるので適宜変更してください。デフォルトだと[UI-Fade-Alpha]になっているので下にobjectがあると透過して下のobject見えるっぽいです。[UI-Fade-Cutout]に変更したら透過しなくなりました。

【GitBucket】社内用HPページを作って情報共有してみる

 以前、Hugoを使って簡単ホームページの作り方をご紹介したかと思います。

t-n-clark.hatenadiary.jp

しかし、作ったのはいいのですが、あんまり外に見せたくない、社内への情報共有程度に使えればいいんだけどな、オンプレミスで使えるGitHub Pases的なのがあればなあと思っていたら便利なものがありました。ちなみにGitHub Pasesとはこんな感じのものです。

www.tam-tam.co.jp

GitHubだと社内の情報セキュリティ的に...という場合に使えるオンプレミスで動くGitBucketというものがあるのですが、これのプラグインGitHub Pages的なものがあったのでこれを使うことにしました。

gitbucket.github.io

github.com

こちらで紹介されている通り、特定の名前のブランチに上げるとホームページとして見ることができます。
GitBucketを補助するプラグイン4選 | 株式会社INDETAIL - インディテール

Hugoを使う場合はpublicフォルダをGitBucketに上げます。うん、これで無事作ったホームページが公開できました。満足満足。

年度が変わったので新しいメンバーに自分のプロジェクトの紹介とかに使ってはいかがでしょうか。

【Unity】画像をグルクル回転させるアニメーションを作りたい

最近行っているゲーム製作の中で、幾何学模様を組み合わせてサイバーっぽいパーツを作りました。で、友人の「これって回ったら格好よさそうじゃない?」という無責任な発言でこれを回すことになりました。

今回はこんな感じのアニメーションを作ります。

f:id:T-N-Clark:20180421151712g:plain

いろいろやりようはあると思いますが、色んなUIパーツに対して使えるよう今回はスクリプトで回すことにします。今回はこちらを参考に組みました(というか、ほぼそのままです)。

tsubakit1.hateblo.jp

docs.unity3d.com

実際のコードはこちら。自作部分はえいやで組んだので適当です。

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

public class RotAmimation : MonoBehaviour {
	
	public float angle = 1;
	public bool rot = true;

	void LateUpdate(){
		if (rot) {
			transform.rotation *= Quaternion.AngleAxis (angle, Vector3.back);
		} else {
			transform.rotation *= Quaternion.AngleAxis (angle, Vector3.forward);
		}
	}
}

公式リファレンスにある通りにangleで回転角をで回転方向を決めています。つまり、angleで回転速度(角速度)を調整します。回転方向については、Vector3とあることから分かるように3次元に回転できて、それぞれ

  • up:(0,1,0)
  • down:(0,-1,0)
  • back:(0,0,-1)
  • forward:(0,0,1)
  • left:(-1,0,0)
  • right:(1,0,0)

のようになっています。今回は2次元平面上での回転なのでz方向の回転、すなわち

  • back
  • forward

を使います。とりあえず回転方向を2種類を切り替えられればいいので、時計回りならtrue、反時計回りならfalseのboolで切り替えています。あとは、これを回転させたい画像にアタッチして、インスペクターでパラメータを調整すれば完成です。

f:id:T-N-Clark:20180421153331p:plain

ちなみに、私の場合は描画順序がうまくいかなかったのでSortingLayerで調整しました。

qiita.com

JupyterLabで開発しながら.pyファイルを作りたい

最近、JupyterLabのβ版が公開されて、ますますJupyterでの開発が捗っている方も多いのではないでしょうか?

github.com

私もそんな一人なのですが、複数人でソースを共有したいときやflaskで作ったapiを叩くとき、Jenkinsからキックするときはやっぱり.py形式の方が使いやすい。ということで、.ipynbを保存すると同時に.pyファイルも保存してくれるやり方を探したら、ありましたのでメモっておきます。今回はこちらを参考にさせていただきました。

adtech.cyberagent.io

まず、ターミナルなどのコンソールを起動して、configを作成します。

jupyter notebook —generate-config

生成時に生成されたパスが表示されるので、そのファイルを開いて中身を書き換えます。

#c.FileContentsManager.post_save_hook = None

これを

import io
import os
from notebook.utils import to_api_path

_script_exporter = None

def script_post_save(model, os_path, contents_manager, **kwargs):
    """convert notebooks to Python script after save with nbconvert
    
    replaces `ipython notebook --script`
    """
    from nbconvert.exporters.script import ScriptExporter
    
    if model['type'] != 'notebook':
        return

    global _script_exporter
    if _script_exporter is None:
        _script_exporter = ScriptExporter(parent=contents_manager)
    log = contents_manager.log

    base, ext = os.path.splitext(os_path)
    py_fname = base + '.py'
    script, resources = _script_exporter.from_filename(os_path)
    script_fname = base + resources.get('output_extension', '.txt')
    log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
    with io.open(script_fname, 'w', encoding='utf-8') as f:
        f.write(script)

c.FileContentsManager.post_save_hook = script_post_save    

のように書き換えます。

これで後は実際にJupyterLabを起動して、ファイル保存をするだけで.ipynbファイルと.pyファイルが同時に生成されているはずです。

ついでに、「個別の環境にJupyterをインストールするのは面倒だよ」という場合は、オンプレOSSのようにサーバー用PCにみんなでアクセスして使うという方法があります。これもブラウザで稼働する点のメリットですね。これでユーザーはブラウザさえあれば即使えますし、バージョンやライブラリの管理も1台のPCで済みます。リモートで使うための方法はこちらが参考になります。

qiita.com