kaggleやってみた
背景
データ分析を学んでみようということで社内エンジニアチームでkaggleをやってみようという話になったので軽く触ってみたことを書きます。
準備
始めるに当たってまず準備したのはkaggleのサイトへのユーザー登録と、Pythonとその周りの機械学習ライブラリのセットアップです。
$ brew install python3 pip3 $ pip3 install pandas numpy sklearn
ユーザー登録は以下のサイトからそれっぽいところをクリックしていけばできます。英語のサイトですがFacebook連携もできるので意外と楽。
Kaggle: Your Home for Data Science
とりあえずやってみる
あまり書くことがないのでやったことを一通り書いてみたいと思います。やったのはタイタニックと呼ばれる一番有名っぽいデータセット。
Titanic: Machine Learning from Disaster | Kaggle
Dataと書いてあるタブから、トレーニングデータとテストデータをダウンロードします。
決定木を使う
まずは決定木を使ってやってみます。
データはcsv形式で降ってきているはずなのでpandasでよしなに処理して作成した決定木モデルに流し込みます。
import pandas as pd import numpy as np from sklearn import tree train = pd.read_csv("train.csv") test = pd.read_csv("test.csv") # 欠損しているデータを埋める train["Age"] = train["Age"].fillna(train["Age"].median()) train["Embarked"] = train["Embarked"].fillna("S") train["Sex"][train["Sex"] == "male"] = 0 train["Sex"][train["Sex"] == "female"] = 1 train["Embarked"][train["Embarked"] == "S" ] = 0 train["Embarked"][train["Embarked"] == "C" ] = 1 train["Embarked"][train["Embarked"] == "Q"] = 2 test["Age"] = test["Age"].fillna(test["Age"].median()) test["Sex"][test["Sex"] == "male"] = 0 test["Sex"][test["Sex"] == "female"] = 1 test["Embarked"][test["Embarked"] == "S"] = 0 test["Embarked"][test["Embarked"] == "C"] = 1 test["Embarked"][test["Embarked"] == "Q"] = 2 test.Fare[152] = test.Fare.median() target = train["Survived"].values # 追加となった項目も含めて予測モデルで使う値を取り出す features_two = train[["Pclass","Age","Sex","Fare", "SibSp", "Parch", "Embarked"]].values # 決定木の作成とアーギュメントの設定 max_depth = 10 min_samples_split = 5 my_tree_two = tree.DecisionTreeClassifier(max_depth = max_depth, min_samples_split = min_samples_split, random_state = 1) my_tree_two = my_tree_two.fit(features_two, target) # tsetから使う項目の値を取り出す test_features_2 = test[["Pclass","Age", "Sex", "Fare", "SibSp", "Parch", "Embarked"]].values # 決定木を使って予測をしてCSVへ書き出す my_prediction_tree_two = my_tree_two.predict(test_features_2) PassengerId = np.array(test["PassengerId"]).astype(int) my_solution_tree_two = pd.DataFrame(my_prediction_tree_two, PassengerId, columns = ["Survived"]) my_solution_tree_two.to_csv("my_tree.csv", index_label = ["PassengerId"])
Submit Predictionを押して解答のCSVをサブミットしてみたところ、正答率は75%程度でした。
トレーニングデータの中身を見てみたところ、年齢欄の欠損が多かったので、年齢を説明変数から削除して再度予測してみた結果、正答率が76%と微増しました。
ランダムフォレストを使う
ソースコードを軽くいじって、予測モデルにランダムフォレストを使ってみます。
import pandas as pd import numpy as np from sklearn.ensemble import RandomForestClassifier train = pd.read_csv("train.csv") test = pd.read_csv("test.csv") train["Age"] = train["Age"].fillna(train["Age"].median()) train["Embarked"] = train["Embarked"].fillna("S") train["Sex"][train["Sex"] == "male"] = 0 train["Sex"][train["Sex"] == "female"] = 1 train["Embarked"][train["Embarked"] == "S" ] = 0 train["Embarked"][train["Embarked"] == "C" ] = 1 train["Embarked"][train["Embarked"] == "Q"] = 2 test["Age"] = test["Age"].fillna(test["Age"].median()) test["Sex"][test["Sex"] == "male"] = 0 test["Sex"][test["Sex"] == "female"] = 1 test["Embarked"][test["Embarked"] == "S"] = 0 test["Embarked"][test["Embarked"] == "C"] = 1 test["Embarked"][test["Embarked"] == "Q"] = 2 test.Fare[152] = test.Fare.median() target = train["Survived"].values features = train[["Pclass","Sex","Fare", "SibSp", "Parch", "Embarked"]].values clf = RandomForestClassifier() clf.fit(features, target) test_features = test[["Pclass", "Sex", "Fare", "SibSp", "Parch", "Embarked"]].values test_pred = clf.predict(test_features) PassengerId = np.array(test["PassengerId"]).astype(int) my_solution = pd.DataFrame(test_pred, PassengerId, columns = ["Survived"]) my_solution.to_csv("rf.csv", index_label = ["PassengerId"])
解答を提出しましたが、正答率は75%程度でした。
SVMを使う
今度は予測モデルにサポートベクターマシンを利用してみます。
import pandas as pd import numpy as np from sklearn import svm train = pd.read_csv("train.csv") test = pd.read_csv("test.csv") train["Age"] = train["Age"].fillna(train["Age"].median()) train["Embarked"] = train["Embarked"].fillna("S") train["Sex"][train["Sex"] == "male"] = 0 train["Sex"][train["Sex"] == "female"] = 1 train["Embarked"][train["Embarked"] == "S" ] = 0 train["Embarked"][train["Embarked"] == "C" ] = 1 train["Embarked"][train["Embarked"] == "Q"] = 2 test["Age"] = test["Age"].fillna(test["Age"].median()) test["Sex"][test["Sex"] == "male"] = 0 test["Sex"][test["Sex"] == "female"] = 1 test["Embarked"][test["Embarked"] == "S"] = 0 test["Embarked"][test["Embarked"] == "C"] = 1 test["Embarked"][test["Embarked"] == "Q"] = 2 test.Fare[152] = test.Fare.median() target = train["Survived"].values features = train[["Pclass","Age","Sex","Fare", "SibSp", "Parch", "Embarked"]].values clf = svm.SVC() clf.fit(features, target) test_features = test[["Pclass","Age", "Sex", "Fare", "SibSp", "Parch", "Embarked"]].values test_pred = clf.predict(test_features) PassengerId = np.array(test["PassengerId"]).astype(int) my_solution = pd.DataFrame(test_pred, PassengerId, columns = ["Survived"]) my_solution.to_csv("svm.csv", index_label = ["PassengerId"])
こちらの正答率はなんと60%台でした。。。
まとめ
kaggleのタイタニックデータセットを軽くいじってみましたが、なかなか正答率が上がらず大変です。このデータセットに限って言えば、モデルよりはデータの前処理が大変に感じました。少し触れましたが、年齢欄や乗船地の欠損が多かったですし、それらをどのように扱えば高い正答率が得られるかというのは、経験に基づくところがあるのではないかと思います。
CentOS7 で Bitzeny のマイニングをする
背景
会社で余らせているサーバーがいくつかあったので仮想通貨をマイニングしようということになりました。費用がペイするかどうかも不明ですが、やってみようと思います。
マイニング
マイニングについては説明が大変なのと、何よりすでにわかりやすく説明してある記事が無数に存在しているので個々では特に解説することはしませんな
さて、マイニング対象の仮想通貨ですがBitzenyを選択しています。今回は、余っているサーバーをつかってCPUマイニングを行うということで、GPUやASICへの耐性が高くCPUでも十分に稼げそうな仮想通貨を選択しました。GPUやASIC耐性があるゆえに価値が上がらないというのは十分にありそうですね・・・・
環境構築
アカウント作成
CPUで十分に稼げるとはいえ、一人で掘るには力不足ですから、プールマイニングをします。プールマイニングとは、みんなで協力して正解ハッシュを探索し、正解が見つかった際には正解者にかかわらずハッシュの計算力に応じて報酬を分配するマイニング団体のことです。LAPoolというマイニングプールを使用します。
上記のサイトにアクセスしてアカウントを作成しましょう。
ログインできたらワーカーを作成します。ワーカー名やパスワードは適当で構いませんが、コマンドラインでヒストリに残る使い方をするのでバレてもかまわないものにするのがいいでしょう(普段使ってるものは絶対につかわないように)
マイナーのインストール
マイニングツールのインストールをします。今回使用するのはcpuminerというもののをフォークしてBitzeny (yescriptという方式) に対応したものです。
パッと見たところ、Linux向けのバイナリがみつからなかったので、ソースからビルドすることにしました。
まずは必要なパッケージをインストールします。
$ yum -y install git autoconf libcurl-devel jansson-devel openssl openssl-devel gcc gawk screen
つぎに、ソースをダウンロードします。
$ git clone https://github.com/bitzeny/cpuminer.git $ cd cpuminer
最後にバイナリをビルドします。
$ ./configure CFLAGS="-O3 -march=native -funroll-loops -fomit-frame-pointer" $ make
ほってみる
実際に掘ってみましょう
$ ./minerd -a yescrypt -o stratum+tcp://jp.lapool.me:3014 -u {ユーザー名}.{ワーカー名} -p {ワーカーのパスワード}
以下のような表示が出ればオッケーです
[2018-03-22 15:54:52] accepted: 1/1 (100.00%), 2.06 khash/s (yay!!!)
裏で動かすためにscreenでセッションを作成しましょう。
$ screen -AmdS znyminer ./minerd -a yescrypt -o stratum+tcp://jp.lapool.me:3014 -u {ユーザー名}.{ワーカー名} -p {ワーカーのパスワード}
まとめ
余っているサーバーでとりあえずBitzenyをほってみました。収益などは十分に時間が経ってからこの記事に追記しようとおもっています。
便利なSlackの使い方 (基本編)
背景
前回の記事の冒頭でも触れたとおり、社内にSlackが導入されたのでちょっとした便利な使い方を紹介していきたいと思います。
Slack
Slackはビジネス向けチャットアプリという位置づけでリリースされたアプリケーションでいまやITスタートアップ界隈では必須となりつつあるツールとなっています。*1
最近になって日本語版がリリースされ、テレビコマーシャルやタクシー・電車向け広告などにもなっているのでさらに利用者は広がることになるでしょう。
プロフィールの設定
具体的なチャットの使い方の前にまずはプロフィールの設定方法から紹介していきたいと思います。
画像はPC英語版のものですがスマートフォン向けアプリでもUIの配置はだいたい変わらないので随時置き換えて見てください。
まずは下記の写真の赤枠で囲われた部分を順にクリックしていきます。
下の画像がプロフィールの編集画面になります。
それぞれ、Full nameには自分の名前、Display nameはSlackに実際に表示される名前を設定します。What I doでは自分がそのチームで何をしているか(ここでは社内でどんな仕事をしているか)を入力します。アイコンはProfile photoをクリックすることで変えられるようになるので変更しておきましょう。
チャットの使い方
基本的な機能はLINEなどのチャットツールと同じように利用できますがSlackでは更にいろいろな機能が用意されているので紹介します。
いろいろなチャットコマンド
以下のように記述することでいろいろなチャットメッセージを送信することができます。コードブロックはバッククオート(JIS配列であればShift+@
)で入力できます。
画像は実際の表示例です。
例 | 説明 | 表示 |
---|---|---|
*hoge* | 太字 | hoge |
~hoge~ | 取り消し線 | |
_hoge_ | イタリック | hoge |
`hoge` | コードブロック | hoge |
```hoge``` | 複数行コードブロック | |
> hoge | 引用 | |
>>> hoge | 複数行引用 |
投稿にリアクションをつける
何か連絡に対していちいち返信を行うのは面倒ですよね。そんなときに使える方法です。
赤枠で囲ったボタンを押すとリアクションをつけることが可能です。(絵文字のパレットがでてきます)
Slackの絵文字にはすべて文字列が設定されていて:[絵文字の名前]:
とすることで設定されている絵文字を投稿できます。例えば:+1:
とすると👍の絵文字が表示されます。また、+:[絵文字の名前]:
とすると直前の投稿にリアクションを行うことが可能です。
メールやソースコードを貼り付ける
メールやソースコードをチャットに貼り付けたい場面があるかもしれませんが、そのまま貼り付けてしまうといろいろなチャットが流れてしまったり、その後のチャットで流されてしまったりといろいろ面倒なことが起こります。
そんなときにはsnippetを利用しましょう。
上の画像のようにチャットのプラスボタンからCode or text snippetを選択します。
左上にテキストの名前を入力、右上ではテキストのタイプを指定します。メールであればPlain Textを選択、ソースコードであればそのプログラミング言語を選択すると良いでしょう。
Create Snippetをクリックするとチャットに貼り付けられます。貼り付けたSnippetはShared Fileから閲覧できるため、チャットで流されてもスムーズに閲覧できます。
スレッドを作成する
あるチャンネルで1つのことに議論をすすめると、それまで何を話していたかわからなくなってしまうことがあります。そんなときに有効なのがスレッドです。
Slackでいうスレッドは2ちゃんねるなどの掲示板でいうスレッドとは違い、Twitterのリプライ機能に近いです。
上の画像の吹き出しボタンをクリックするとスレッドタブが表示されます。
コメント欄に文字を入力することでスレッドを作成することができます。
チャンネルを作成する
Slackでは話題をチャンネルという単位で分割します。議論がごちゃまぜになったりチャットが流れたりしないための手段となっています。
チャンネルを作成する目安としては、今後もその話題についての議論が続く可能性があるとき、くらいが良いと思います。
チャンネル一覧の右にあるプラスボタンをクリックします。
Nameにはチャンネル名を入力します。簡潔になにを話すチャンネルかがわかるように名前を設定しましょう。
Purposeには実際に話す内容を1センテンス程度で入力します。(任意)
Send invites toには招待したいメンバーを入力します。こちらも任意で、あとから招待することも可能です。また、Publicチャンネルであれば他のメンバーが自分から参加することも可能です。
一番上の項目をクリックすることでチャンネルをPrivateに設定することも可能で、Privateにすると招待した人しか参加できなくなります。*2
まとめ
今回はSlackに用意されている基本的な機能を紹介しました。次回はSlackで利用できる色々な拡張機能について紹介をしたいと思っています。
是非実践してみてください!!
出退勤管理システムをSlackに移植した
背景
社内Slackが導入されたので下記記事で紹介した出退勤管理システムをSlackに移植することにしました。
実装
従来のLINEの機能は残しつつ機能追加という形で実装を行うことにします。
Slackの設定
incoming webhookとoutgoing webhookを設定する。それぞれカードがタッチされた時と確認を行うときのための機能です。
incoming webhook
上記のWebhook URLに以下の形式のJSONをPOSTします。
{ "text": "message" }
outgoing webhook
上記のURL(s)の欄にエンドポイントのURLを記入する。複数行に記述することで複数の宛先に送ることが可能です。
Trigger Word(s)には検出したい文字列を設定します。こちらにも複数の条件を設定することが可能で、カンマ区切りで行います。
受け取ったリクエストに対してResponse bodyに以下のJSONを設定することで投稿できます。
{ "text": "message" }
ソースの追記
以下のようなコードを追記しました。
main.cr nfcpush()の末尾に追記
HTTP::Client.post(Params::SLACK_WEBHOOK_URL, headers: HTTP::Headers{ "Content-Type" => "application/json", }
ICカードのタッチに発火してSlackのincoming webhookに投げます。
def whoishere(request : HTTP::Request, list : Hash) list = list.select {|k, v| v == true} text = list.size != 0 ? "#{list.size}人がオフィスにいます\n" : "オフィスには誰もいません" cardNames = Response::CardNames.from_json(File.read(Params::PATH_TO_CARDNAMES_JSON)).cardNames list.map {|k, v| cardNames.select {|c| c.idm == k}.size != 0 ? cardNames.select {|c| c.idm == k}[0].name : "未登録のユーザー"}.each {|e| text += "#{e} "} return {200, "{\"text\": \"#{text}\"}"} end
Slackのoutgoing webhookに発火してResponseを返します。
まとめ
無事Slackと連携することができました。
出退勤管理システムのAPI endpointをCrystalに置き換える
背景
前回の記事でDjangoとNFCリーダーを用いた簡単な出退勤管理システムを紹介しました。
しばらく動作させていたところ、Raspberry piのCPU使用率が天井に張り付いてしまっていました。(動作がめちゃくちゃ重くなった)
CPU使用率は常時50%となっていましたが、熱対策のためのスロットリングが働いて50%が天井に設定されていると考えられます。実際本体をさわってみると長時間は触っていられないくらいの温度になっていました・・・
対策
各プロセスのCPU使用率を見てみるとDjangoが30%、NFCリーダーのプログラムが15~20%、ngrokとmysqlが数%、といった感じになっていました。*1
DjangoとNFCリーダーのプログラムを両方移行したかったのですが、NFCリーダーについてはnfcpy以外にまともに使えるライブラリがないのでひとまずDjangoをCrystalに置き換えてみることにします。さらにmysqlを利用するほどのことでもないと思い、これもjsonファイルに置き換えました。
CrystalにはARMアーキテクチャ向けコンパイラは用意されていませんが、x86_64環境でクロスコンパイルをすることにしました。
環境構築
まず、手元のMacbookにCrystalのコンパイラをインストールします。
$ brew install crystal
また、Raspberry piにはLLVM3.8がインストールしてあり、この環境でCrystalの実行形式ファイルをリンクするには1.0系のopensslが必要だったため、これもインストールしました。
$ sudo apt install libssl1.0.2
プログラム
製作したプログラムはGithubに公開しています。
Crystalというプログラミング言語はマイナーではありますが最近とある界隈で話題になっていて*2日本語の記事も少しずつ増えているので探してみてください。
Crystalは静的型付き言語なので、JSONのような型が不明なデータをあつかうのが意外と大変です。CrsytalではJSON#parse
によってJSON::Any
というクラスに変換してからif文とis_a?
メソッドをつかって型を決定していく方法と、JSON#mapping
によって予めJSONの型を指定しておく方法があります。今回はLINEのAPIドキュメントによってあらかじめJSONの型がわかるようになっているのでJSON#mapping
を書いておきます。
json_map.cr
require "json" module Response class Result JSON.mapping({ events: Array(Event), }) end class Event JSON.mapping({ type: String, timestamp: Int64, source: Source, replyToken: { type: String, nilable: true }, message: { type: Message, nilable: true }, }) end class Source JSON.mapping({ type: String, userId: { type: String, nilable: true }, groupId: { type: String, nilable: true }, roomId: { type: String, nilable: true }, }) end class Message JSON.mapping({ id: String, type: String, text: { type: String, nilable: true }, fileName: { type: String, nilable: true }, fileSize: { type: String, nilable: true }, title: { type: String, nilable: true }, address: { type: String, nilable: true }, latitude: { type: Float64, nilable: true }, longitude: { type: Float64, nilable: true }, packageId: { type: String, nilable: true }, stickerId: { type: String, nilable: true }, }) end class Reply JSON.mapping({ replyToken: String, messages: Array(ReplyMessage), }) def initialize(@replyToken : String, @messages : Array(ReplyMessage)) end end class Push JSON.mapping({ to: String, messages: Array(ReplyMessage), }) def initialize(@to : String, @messages : Array(ReplyMessage)) end end class ReplyMessage JSON.mapping({ type: String, text: { type: String, nilable: true }, fileName: { type: String, nilable: true }, fileSize: { type: String, nilable: true }, title: { type: String, nilable: true }, address: { type: String, nilable: true }, latitude: { type: Float64, nilable: true }, longitude: { type: Float64, nilable: true }, packageId: { type: String, nilable: true }, stickerId: { type: String, nilable: true }, }) def initialize(@type : String) end def add_text(@text : String) self end end class Cardid JSON.mapping({ cardid: String, }) end class CardNames JSON.mapping({ cardNames: Array(CardName), }) end class CardName JSON.mapping({ idm: String, name: String, }) end end
上記のソースがJSONのパースの仕方を記述したものです。含まれていないかもしれない値にはnilable
というクラス変数にtrue
を代入して置かなければなりません。
さらに、nilである可能性がある値を利用する場合にはnilでないことが保証されなければなりません。
以下が利用例になります。
event = Response::Result.from_json(body.gets_to_end).events[0] message = event.message if message.is_a?(Response::Message) puts "message event!" groupId = event.source.groupId roomId = event.source.roomId if groupId.is_a?(String) puts "Group ID:#{groupId}" text = message.text replyToken = event.replyToken if text.is_a?(String) && replyToken.is_a?(String) replyMessage(replyToken, text, list) end elsif source.roomId.is_a?(String) puts "Room ID:#{roomId}" else puts "User ID:#{event.source.userId}" end else puts "other event!" end
結果
DjangoとMySQLを停止した結果、CPU使用率が17,8%程度まで抑えられました。Herokuにデプロイするという選択肢もありましたが、最近Crystalを主に書いていたのと、Raspberry piでCrystalのプログラムを動かしてみたかったのでこの選択肢を選んでいます。
NFCリーダーについても、未だ高めのCPU使用率になっているので、他の選択肢があるなら随時置き換えていきたいと考えています。
Raspberry piとNFCリーダーで出退勤管理システムをつくった
背景
先日下記の記事を投稿しましたが、その後ICカードを使った出退勤管理システムをつくったので色々と書き留めておきたいと思います。
NFCリーダーのセットアップは下記で紹介いたしました。
環境構築
以下、環境構築です。
LINE Messaging API
出退勤管理を通知するにあたってLINEのMessaging APIを使用することにしました。基本的にこちら(Messaging API)から指示通りに進んで行けば登録が可能ですが、今回はPUSH MESSAGEを使用したいため、Developer Trialのアカウントを取得しました。下記のブログを参考にさせていただいております。
Django
LINE Messaging APIのWebhookリクエストの受け取りやPUSH MESSAGEの送信のためにPythonのWebフレームワークであるDjangoを利用しました。nfcpyがPython2系のみの対応だったためDjangoもPython2系の古いバージョンを使用しています。JSONをさばいたりHTTPリクエストを吐いたりするためにjsonとrequestsパッケージもインストールします。
$ sudo pip install django==1.11 json requests
導入から動作テストまではだいたい以下の記事を参考にさせていただいております。
Herokuにデプロイせずに以下で紹介するngrokでHTTPSにフォワーディングにして稼働させることにしました。*1
上記は、今後構成を変更するかもしれない。
ngrok
LINE Messaging APIはHTTPSのみのサポートのため、HTTPからHTTPSにトンネリングができるアプリケーションを導入します。Djangoのデフォルトのポート番号は8000ですからコマンドライン引数に8000を与えて起動しておきます。
$ wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip $ unzip ngrok-stable-linux-arm.zip $ ./ngrok http 8000
これでlocalhost:8000が表示されたURLにフォワーディングされています。
mysql
ICカードのIDmと利用者の名前を紐付けるためにmysqlを導入しました。もっと楽にできる方法はありそう。*2
アプリケーション作成
シュッとコードを書いて動くようにします。
linebot/urls.py
"""linebot URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.11/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url, include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^bot/', include('bot.urls')), ]
bot/urls.py
from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index, name="index"), url(r'^callback', views.callback), url(r'^nfcpush', views.nfcpush), ]
bot/views.py
# -*- encoding: utf-8 -*- from django.shortcuts import render from django.http import HttpResponse import json import requests import mysql.connector REPLY_ENDPOINT = 'https://api.line.me/v2/bot/message/reply' PUSH_ENDPOINT = 'https://api.line.me/v2/bot/message/push' ACCESS_TOKEN = '[ACCESS_TOKEN]' HEADER = { "Content-Type": "application/json", "Authorization": "Bearer " + ACCESS_TOKEN } PUSH_GROUP = "[GROUP_ID]" user_states = {} def index(request): return HttpResponse("This is bot api.") def callback(request): reply = "" request_json = json.loads(request.body.decode('utf-8')) print(json.dumps(request_json)) for e in request_json['events']: reply_token = e['replyToken'] message_type = e['message']['type'] if message_type == 'text': text = e['message']['text'] reply += reply_text(reply_token, text) return HttpResponse(reply) def reply_text(reply_token, text): if text.find(u"[検出したい文字列]") != -1: count = 0 users = [] for k, v in user_states.items(): if v == True: count += 1 users.append(k) reply_text = str(count) + u"人がオフィスにいます\n" dbcon = mysql.connector.connect( database="pi", user="root", password="[password]", host="localhost" ) for u in users: dbcur = dbcon.cursor() sql = "select name from cardinfo where id='" + u + "'" dbcur.execute(sql) row = dbcur.fetchone() if row == None: reply_text += u"未登録のユーザー " else: reply_text += row[0] + " " payload = { "replyToken":reply_token, "messages":[ { "type":"text", "text": reply_text } ] } requests.post(REPLY_ENDPOINT, headers=HEADER, data=json.dumps(payload)) return reply_text def nfcpush(request): request_json = json.loads(request.body.decode('utf-8')) dbcon = mysql.connector.connect( database="pi", user="root", password="[password]", host="localhost" ) dbcur = dbcon.cursor() sql = "select name from cardinfo where id='" + request_json['cardid'] + "'" dbcur.execute(sql) row = dbcur.fetchone() text = u"さんが" print(request_json['cardid'] in user_states) if not request_json['cardid'] in user_states: user_states[request_json['cardid']] = True text += u"出勤しました。" else: if user_states[request_json['cardid']]: text += u"退勤しました。" user_states[request_json['cardid']] = False else: text += u"出勤しました。" user_states[request_json['cardid']] = True name = "" if row == None: name = u"未登録のユーザー" else: name = row[0] payload = { "to":PUSH_GROUP, "messages":[ { "type":"text", "text": name + text } ] } requests.post(PUSH_ENDPOINT, headers=HEADER, data=json.dumps(payload)) return HttpResponse("OK")
すべての処理は bot/views.py
に記述してあります。
callback(request)
ではLINE Messaging APIのWebhookリクエストを捌きます。送られてきたテキストの先頭が任意の文字列であった場合に現在オフィスにいる人数と人の名前を返信します。
nfcpush(request)
ではNFCリーダーで読んだIDm入りのJSONを捌きます。DBでIDmから名前を確認し、未出勤であるなら"出勤しました"、出勤済であるなら"退勤しました"というPUSH MESSAGEを設定してあるグループに投稿するようにしてあります。
NFCリーダーからICカードを読み取ってリクエストを投げるスクリプトは以下です。
nfcpush.py
import binascii import nfc import json import requests HEADER = { "Content-Type": "application/json", "Authorization": "Bearer" } class MyCardReader(object): def on_connect(self, tag): print("touched") self.idm = binascii.hexlify(tag.idm) payload = { "cardid":self.idm } requests.post("http://localhost:8000/bot/nfcpush", headers=HEADER, data=json.dumps(payload)) return True def read_id(self): clf = nfc.ContactlessFrontend('usb') try: clf.connect(rdwr={'on-connect': self.on_connect}) finally: clf.close() if __name__ == '__main__': cr = MyCardReader() while True: print("touch card:") cr.read_id() idm = cr.idm print("released") print(cr.idm)
また、DBへの名前の登録は以下のスクリプトで行っています。
register.py
import binascii import nfc import json import mysql.connector class MyCardReader(object): def on_connect(self, tag): print("touched") self.idm = binascii.hexlify(tag.idm) return True def read_id(self): clf = nfc.ContactlessFrontend('usb') try: clf.connect(rdwr={'on-connect': self.on_connect}) finally: clf.close() if __name__ == '__main__': print("enter your name:") input_line1 = raw_input() cr = MyCardReader() print("touch card:") cr.read_id() idm = cr.idm dbcon = mysql.connector.connect( database="pi", user="root", password="[password]", host="localhost" ) dbcur = dbcon.cursor() sql = "INSERT INTO cardinfo (id, name) values ('" + idm + "', '" + input_line1 + "')" dbcur.execute(sql) dbcon.commit() print("released") print(cr.idm)
実行
うまく動作しました!!!
まとめ
名前の登録をするときは、 nfcpush.py
をいちいち止めないと行けないのが面倒。登録はいまはCUIのみなのでグラフィカルにいい感じにやりたい。
そもそも、ICカードをタッチするのが面倒なのでカメラで顔認識とかしてうまいことやりたい。
Raspberry PiでNFCタグを読み込む
背景
出退勤システムが作りたかったのでNFCタグリーダをもってきた。
いろいろインストール
必要なものはPython2系とそのpipくらい。
$ sudo apt install python python-pip
nfcpyをpipで入れる
$ sudo pip install -U nfcpy
サンプルのリポジトリをクローンしてみる
$ git clone https://github.com/nfcpy/nfcpy.git $ cd nfcpy
実行してみる
$ sudo python examples/tagtool.py show No handlers could be found for logger "nfc.llcp.sec" [nfc.clf] searching for reader on path usb [nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:001:005 ** waiting for a tag ** Type3Tag 'FeliCa Standard (RC-S???)' ID=**************** PMM=**************** SYS=0003
IDmだけ取れればいいので、サンプルを参考に何度も取れるように改良する。
# read_idm.py import binascii import nfc class MyCardReader(object): def on_connect(self, tag): print "touched" self.idm = binascii.hexlify(tag.idm) return True def read_id(self): clf = nfc.ContactlessFrontend('usb') try: clf.connect(rdwr={'on-connect': self.on_connect}) finally: clf.close() if __name__ == '__main__': cr = MyCardReader() while True: print "touch card:" cr.read_id() print "released" print cr.idm
実行する
$ sudo read_idm.py No handlers could be found for logger "nfc.llcp.sec" touch card: touched released 0114************ touch card: touched released 0114************ touch card:
まとめ
なんとかWebアプリかなんかと連携していい感じに表示できるようにしたいところ。