DockerとEclipse MosquittoでMQTT
はじめに
MQTTを勉強しないといけないのでDocker上に環境を作って動かすよ。
- ブローカーはMosquittoの公式イメージをそのまま使う。
- Subscriber、Publisherはブローカーとコンテナを分ける。
tcpdump
も使いたいのでUbuntuから作る。
環境構築
Dockerfile
Subscriber / Publisher側。Ubuntuにmosquitto-clients
とtcpdump
を入れる。
FROM ubuntu RUN apt update \ && apt install -y mosquitto-clients tcpdump
mosquitto.conf
Mosquittoの設定ファイル。コンテナ間で通信するための最小限の設定をする。
allow_anonymous true listener 1883
docker-compose.yml
全体の構成を定義。
version: '3' services: mosquitto-publisher: build: . container_name: mosquitto-publisher tty: true mosquitto-subscriber: build: . container_name: mosquitto-subscriber tty: true mosquitto-broker: image: eclipse-mosquitto container_name: mosquitto-broker volumes: - ./mosquitto.conf:/mosquitto/config/mosquitto.conf
動作確認
コンテナを起動する。
$ docker-compose up -d
Subscriberのコンテナに入る。
$ docker exec -it mosquitto-subscriber /bin/bash
トピックをSubscribeする。
$ mosquitto_sub -h mosquitto-broker -t mytopic -d
Publisherのコンテナに入る。
$ docker exec -it mosquitto-publisher /bin/bash
トピックにメッセージをPublishする。
$ mosquitto_pub -h mosquitto-broker -t mytopic -m "This is a test." -d
ユーザID、パスワード認証を有効にしよう
dockerコンテナに入る。
$ docker exec -it mosquitto-broker /bin/sh
パスワードファイルを新規に作成し、user1
を追加。
$ mosquitto_passwd -c pwdfile user1 Password: Reenter password:
パスワードファイルにuser2
を追加。
$ mosquitto_passwd pwdfile user2 Password: Reenter password:
できたファイルを確認する。
$ cat pwdfile user1:$7$101$Lt7Tg/kTMwmZXe6D$mnDP9IpgkkOMQ6EV7E4mytxWvXAMNmPP391/j1YfkrgJENEAJ3W/1jFm20SRijLAYch6fwqnRw8UojVsAFlipw== user2:$7$101$l/J0c5HiY31XtIFY$4rQkP4egix5w9iEESkLLSCjUmveVUHhjqdAuEb2fGL/4r5dW7aT98DM4+ASI/vnXhQfKWw06FZlcPqOoPbdL3w==
一旦コンテナから抜ける。
$ exit
mosquitto.conf
を編集。
allow_anonymous false listener 8883 password_file /pwdfile cafile /mosquitto/certs/ca.crt keyfile /mosquitto/certs/server.key certfile /mosquitto/certs/server.crt
再起動。
$ docker-compose restart
通信内容を確認しよう
Subscriber / Publisherコンテナ上でtcpdump
を実行する。-w
オプションで内容をファイル出力する。
$ tcpdump -i any -w test.cap port 1883
ひと通り動かしたらファイルをホストにコピーする。
$ docker cp mosquitto-subscriber:/test.cap .
Wiresharkでファイルを開いて、MQTT Specificationを見ながらお勉強。ふーん。なるほどー。
PostgreSQLで5分単位にレコード集計
やりたいこと
timestamp型のカラムを持つテーブルがあって、5分単位、10分単位にレコードを集計したい。みたいなことが2、3年に一度ある。その度に試行錯誤しているので、未来の俺に向けてここにメモを残す。
前提バージョン
9.6.10
使用するテーブル
2020年大阪国際女子マラソンのリザルトを管理するテーブルを用意する。面倒だから準備するレコードは上位20名までに限定する。finished_at
postgres=# \d osaka_marathon_2020_result
Table "public.osaka_marathon_2020_result"
Column | Type | Collation | Nullable | Default
-------------+-----------------------------+-----------+----------+---------
no | integer | | |
name | character varying(32) | | |
finished_at | timestamp without time zone | | |
postgres=# select * from osaka_marathon_2020_result;
no | name | finished_at
----+------------------------------+---------------------
6 | 松田 瑞生 | 2020-01-26 14:31:47
3 | ミミ・ベレテ | 2020-01-26 14:32:40
7 | シンタエフ・レウェテン | 2020-01-26 14:33:03
2 | メスケレム・アセファ | 2020-01-26 14:33:31
10 | リサ・ウェイトマン | 2020-01-26 14:36:02
4 | ボルネス・ジェプキルイ | 2020-01-26 14:36:24
16 | 山口 遥 | 2020-01-26 14:36:35
9 | ファツマ・サド | 2020-01-26 14:37:18
1 | ハフタムネッシュ・テスファイ | 2020-01-26 14:37:50
13 | 田中 華絵 | 2020-01-26 14:37:51
41 | 井上 彩花 | 2020-01-26 14:37:54
19 | ムンフザヤ・バヤルツォグト | 2020-01-26 14:38:03
8 | 小原 怜 | 2020-01-26 14:38:12
14 | カタリナ・スタインラック | 2020-01-26 14:38:48
11 | 谷本 観月 | 2020-01-26 14:38:48
42 | 兼重 志帆 | 2020-01-26 14:38:51
43 | 西田 美咲 | 2020-01-26 14:38:51
44 | 水口 瞳 | 2020-01-26 14:42:33
17 | 下門 美春 | 2020-01-26 14:42:48
12 | 竹地 志帆 | 2020-01-26 14:44:09
(20 rows)
分単位に集計する場合
単純に日単位、時間単位、分単位に集計するのであれば、date_trunc
関数が使える。
postgres=# select date_trunc('minute', finished_at), count(*) from osaka_marathon_2020_result group by 1 order by 1;
date_trunc | count
---------------------+-------
2020-01-26 14:31:00 | 1
2020-01-26 14:32:00 | 1
2020-01-26 14:33:00 | 2
2020-01-26 14:36:00 | 3
2020-01-26 14:37:00 | 4
2020-01-26 14:38:00 | 6
2020-01-26 14:42:00 | 2
2020-01-26 14:44:00 | 1
(8 rows)
5分単位に集計する場合
こんな感じになる。
postgres=# select to_timestamp(trunc(extract('epoch' from finished_at) / 300) * 300), count(*) from osaka_marathon_2020_result group by 1 order by 1;
to_timestamp | count
------------------------+-------
2020-01-26 14:30:00+00 | 4
2020-01-26 14:35:00+00 | 13
2020-01-26 14:40:00+00 | 3
(3 rows)
やっていることは以下の通り。
-
extract
関数でタイムスタンプ型からUNIXエポック時間を取得。 -
UNIXエポック時間を集計単位(例では300秒)で割る。
-
trunc
関数で小数点以下を切り捨てる。 -
集計単位(例では300秒)を掛けて、丸めたUNIXエポック時間を得る。
-
to_timestamp
関数でタイムスタンプ型に変換する。
ちなみにto_timestamp
の戻り値はtimestamp with time zone
型なので、元のカラムのtimestamp without time zone
型と合わせるなら最後にtimezone
関数を使ってあげれば良い。
跋文
Java + Spatial4jでGeofence
やりたいこと
多角形(ポリゴン)または円(サークル)でGeofenceを定義する。それでデバイスの現在位置がGeofenceの内側にいるか外側にいるかを判定する。
こんなイメージ。 © OpenStreetMap contributors
Spatial4jとは
地理空間情報を扱うためのJavaライブラリ。公式サイトはこちら。
試してみる
pom.xml
<dependency> <groupId>org.locationtech.spatial4j</groupId> <artifactId>spatial4j</artifactId> <version>0.8</version> </dependency>
多角形(ポリゴン)型Geofence
import org.locationtech.spatial4j.context.jts.JtsSpatialContext; import org.locationtech.spatial4j.io.WKTReader; import org.locationtech.spatial4j.shape.Shape; public class Spatial4jPolygonTest { public static void main(String[] args) { JtsSpatialContext ctx = JtsSpatialContext.GEO; // (1) try { WKTReader reader = new WKTReader(ctx, null); // (2) // 仙台市営地下鉄南北線、東西線のターミナルを頂点としたPolygonをGeoFenceとする Shape polygon = reader.read("POLYGON((" + "140.880560 38.324137, " // 泉中央駅 + "140.948398 38.244902, " // 荒井駅 + "140.870713 38.214264, " // 富沢駅 + "140.843720 38.243071, " // 八木山動物公園駅 + "140.880560 38.324137" // 泉中央駅 + "))"); // (3) // デバイスが仙台駅にあるケース Shape point0 = reader.read("POINT(140.882448 38.260300)"); // (4) System.out.println(polygon.relate(point0)); // (5) // デバイスが小鶴新田駅にあるケース Shape point1 = reader.read("POINT(140.934837 38.273621)"); System.out.println(polygon.relate(point1)); } catch (Exception e) { e.printStackTrace(); } } }
- 何をやるにしても
JtsSpatialContext
が必要になるので、まずはこれを取得する。JtsSpatialContext.GEO
でシングルトンインスタンスが用意されているので、そのまま使用する。 - 今回はGeofenceと位置情報をWKT(Well-known text)で用意する。そのため、
WKTReader
のインスタンスを作成する。WKTの詳しい説明はウィキペディアが参考になる。 - GeofenceをPolygonで表したWKTを
WKTReader
に読み込ませて、Shape
オブジェクトを作成する。 - 比較対象とするデバイスの位置情報はPointで表す。Geofenceと同様に
Shape
オブジェクトを作成する。 - Geofenceの
Shape.relate()
を呼び出し、2つのオブジェクトの関係を求める。
実行するとこうなる。
CONTAINS DISJOINT
Shape.relate()
の戻り値はSpatialRelation
のEnum。PolygonとPointとの比較だとCONTAINS
(比較対象が含まれている)、 DISJOINT
(比較対象との接点がない)のどちらかしか返さないが、他にINTERSECTS
(比較対象と交差している)、 WITHIN
(比較対象の中にある)がある。
ちなみに、180度経線(下の図の赤い線)を跨いで領域を定義した場合、ライブラリによってはうまく結果を返さないものもある。Spatial4jは問題なく動いた。これならフィジー諸島とかロシアでもバッチリ監視できる。
円(サークル)型Geofence
Geofenceの中心とデバイスの位置情報の2点間の距離を測れば事足りる気がする。だけど、円を表すGeoCircle
クラスがあるので、せっかくなので使ってみる。
import org.locationtech.spatial4j.context.jts.JtsSpatialContext; import org.locationtech.spatial4j.distance.DistanceUtils; import org.locationtech.spatial4j.io.WKTReader; import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Shape; import org.locationtech.spatial4j.shape.impl.GeoCircle; public class Spatial4jCircleTest { public static void main(String[] args) { JtsSpatialContext ctx = JtsSpatialContext.GEO; try { WKTReader reader = new WKTReader(ctx, null); Shape center = reader.read("POINT(140.878100 38.288440)"); // 台原駅 // 半径2kmをdegreeに変換 double radiusDEG = DistanceUtils.dist2Degrees(2.0d, DistanceUtils.EARTH_MEAN_RADIUS_KM); // (1) // 台原駅を中心とした半径2kmをGeofenceとする GeoCircle circle = new GeoCircle((Point) center, radiusDEG, ctx); // (2) // デバイスが旭ヶ丘駅にあるケース Shape point0 = reader.read("POINT(140.885062 38.296344)"); System.out.println(circle.relate(point0)); // デバイスが八乙女駅にあるケース Shape point1 = reader.read("POINT(140.883689 38.312862)"); System.out.println(circle.relate(point1)); } catch (Exception e) { e.printStackTrace(); } } }
GeoCircle
の半径はdegree(度)で指定するので、「中心から半径2km」のように指定する場合は、DistanceUtils.dist2Degrees()
で変換処理を行う必要がある。ちなみにDistanceUtils
はEARTH_MEAN_RADIUS_KM
(平均の半径?: 約6371km)とEARTH_EQUATORIAL_RADIUS_KM
(赤道半径: 約6378km)と2つの半径がある。ここではGoogle Mapの距離測定と結果が近い方のEARTH_MEAN_RADIUS_KM
を採用。- 中心と半径、
JtsSpatialContent
を指定してGeofenceを表すGeoCircle
オブジェクトを作成。
以降は先ほどと同様。実行するとこうなる。
CONTAINS DISJOINT
最後に
Big Brother is watching you.
Python + NLTKで英語を品詞ごとに色分け表示するWebサービスを作ってみる
序文
先日、久しぶりにTOEIC受験を申し込んだので、気合を入れて効率良く英語を身に着ける方法についての勉強を始めた。その中で、英文を品詞ごとに色分けしてくれるWebサービス(https://english.edward.io)を見つけた。
へー、よくできているなー...と感心したので、どうやって実装できるのか調べてみた。すると、自然言語処理(NLP)のライブラリを使えば割とシンプルに実装可能なことが判明。そこで、ちょっと勉強がてら自作してみることに。言語はPython、ライブラリはNLTK(Natural Language TookKit)を使用する。ちなみにPythonは触ったことある程度。
事前準備
作業に使うM1 MacBook Airは買ったばかりで、python周りはまっさらな状態。なので、いくつか事前に準備しておく。
Python 3.xをデフォルトに変更する
とりあえずバージョンを確認。
# macOS $ sw_vers ProductName: macOS ProductVersion: 11.1 BuildVersion: 20C69 # python (default) $ python --version Python 2.7.16 # python 3.x $ python3 --version Python 3.8.2 # python 2.x $ python2 --version Python 2.7.16
デフォルトが2系になっているので、これを3系に変更する。vi ~/.zshrc
で以下を追加する。
alias python="python3"
ファイルを保存したらsource
で反映する。
source ~/.zshrc
pipをインストールする
$ curl https://bootstrap.pypa.io/get-pip.py -s -o get-pip.py $ python get-pip.py
pipを入れたディレクトリをPATHに追加する。vi ~/.zshrc
で以下を追加する。
export PATH=$PATH:/Users/****/Library/Python/3.8/bin
新しいバージョンにアップグレード(19.2.3 -> 20.3.3)。
$ python -m pip install --upgrade pip
NLTK
以下、公式ドキュメント(https://www.nltk.org)に従って作業する。
NLTKをインストールする
$ pip install nltk
NLTKで使用するデータをダウンロードする
NLTKを使用する際にはコーパスやその他のデータが必要になる(とドキュメントに書いてあるが意味を理解していない)。pythonをインタラクティブモードで起動して以下を実行。
>>> import nltk >>> nltk.download('all')
自分の環境ではnltk.download()
と引数なしで実行するとエラーが発生するので、'all'
を付けた。
動作確認してみる
>>> # 赤い色のモビルスーツ... ザクじゃないけど赤い色のモビルスーツ... シャアじゃないのか!? >>> sentence = "A red mobile suit... It's not a Zaku but good red mobile suit mean... Char is in it!?" >>> tokens = nltk.word_tokenize(sentence) >>> tagged = nltk.pos_tag(tokens) >>> tagged [('A', 'DT'), ('red', 'JJ'), ('mobile', 'NN'), ('suit', 'NN'), ('...', ':'), ('It', 'PRP'), ("'s", 'VBZ'), ('not', 'RB'), ('a', 'DT'), ('Zaku', 'NNP'), ('but', 'CC'), ('good', 'JJ'), ('red', 'JJ'), ('mobile', 'JJ'), ('suit', 'NN'), ('mean', 'NN'), ('...', ':'), ('Char', 'NNP'), ('is', 'VBZ'), ('in', 'IN'), ('it', 'PRP'), ('!', '.'), ('?', '.')]
英文を入力として単語とその品詞が取得できた。品詞の詳細については以下で確認できるみたい。
>>> nltk.help.upenn_tagset()
次のステップ
で一通り完成するはずだが、ここまで結構な時間を費やしたのに肝心の英語の勉強が全然進んでない!のでこのままで一旦終わりかな。
Oracleの結合演算子
例えば、A、B二つの表があって、それを外部結合を使って検索する。外部結合演算子(+)を使うとこんな感じで記述できる。
SELECT * FROM A, B WHERE A.col1 = B.col1(+)
この程度は自分も知っていた。で、右側の表Bに条件を付けるとする。こう書けるのか?
SELECT * FROM A, B WHERE A.col1 = B.col1(+) AND B.col2 IN (1, 2)
いや、col2の値が1, 2以外の行は返されない。これでは外部結合にならない。
これも何となく知っていた。問題はここからだ。上記の問題をどう回避するのか。
SELECT * FROM A, B WHERE A.col1 = B.col1 (+) AND (B.col2 IN (1, 2) OR B.col2 IS NULL)
長い間こんな風に書いてきた気がするんだけど、もう少しエレガントな記述ができるようで。
SELECT * FROM A, B WHERE A.col1 = B.col1(+) AND B.col2(+) IN (1, 2)
先月手伝ってた現場でこれを知ったとき、期待通りに動作することをこっそりテストしながら、今まで知らなかった恥ずかしさと感動に浸っていた...が。
改めてOracleのリファレンスを読んでみると、結合演算子は制限があるからOUTER JOIN推奨なのだそうで。なら今後、積極的に使うこともないだろう。あぁ、何だか力抜けるなぁ...
と、仕事を通じて泣いたり笑ったりした出来事をここに残していこうかと思ってます。でも、まずは脱・3日坊主が目標。ネタに窮してyoutubeの動画を埋め込んでるだけの記事でも、コンスタントに(週一くらいで)アップするつもり。
CSSはそのうちカスタマイズしておきます。