Pythonでword2vecを自在に操って高次元ベクトルを可視化

標準

word2vecで色々な言葉をベクトルに出来たのは良いものの、それを一切活用できない宝の持ち腐れ状態だったのでpythonで色々といじくれるように頑張ってみました。

word2vecをpythonでいじれる環境を作る

依存するパッケージをpipでインストールします。

sudo -H pip install -I numpy scipy gensim matplotlib sklearn

python仕様に辞書を作り直す

以前作ったモデルファイルはバイナリになっていてpythonで使うことが出来ないので(多分)、それも含めてもう一度学習をしないといけなくなりました。ので、今回はpythonでコーパスから学習するpythonのコードを。

# -*- coding:utf-8 -*-
from gensim.models import word2vec

files = word2vec.Text8Corpus('tweets.txt')

#size 次元数 コーパスに収録されている語数に応じて増やすと良いらしい
#min_count この回数未満登場する単語は無視される
#window 文脈判断 この前後の語数だけ一つの文脈だと判断します
#sample 単語の無視 この頻度だけ単語を無視します

model = word2vec.Word2Vec(files, size=200, min_count=10, window=10, sample=1e-3)
model.save("tweets-train-200.model")

これでモデルファイルが生成されます。binaryオプションはつけちゃだめです。コーパスの量によって処理の時間はかかりますが、私の場合は4,000万以上のツイート集を利用したので処理に4時間ほどかかりました。(Bash on Windowsを利用しました)

(ここまでの環境はUbuntuでしたが、ここからの環境はWindowsです。)

実際にベクトルを見てみる

無事に学習が終わったので、実際に単語のベクトルを見てみます。

# -*- coding:utf-8 -*-
from gensim.models import word2vec
import numpy as np
import sys

target = sys.argv[1]

model = word2vec.Word2Vec.load("tweets-train-200.model")
try:
    target_model = model[target]
except:
    #辞書に収録されてないときはゼロベクトルを代入
    target_model = [0]
    raise

#配列形式で出力
print(np.array2string(target_model, separator=', ', formatter={'float_kind': lambda x: '{: .4f}'.format(x)}))

#普通に標準出力(指数表示)
print(target_model)

これでコマンドラインから引数に調べたい単語を添えると結果が標準出力になります。

> python view_vec.py アニメ

[-3.0598, -1.0856,  0.5501,  0.3603,  6.1139, -0.8803,  1.1255, -1.6247,
  2.4462,  0.8340, -1.7479, -0.8233,  4.1820, -2.8125,  1.5961,  0.0054,
  2.7226,  0.2497,  1.0567, -1.6948, -2.7111, -2.0759,  1.5670,  1.9177,
 -2.1502,  4.3175,  0.1491,  2.5644,  2.2666, -1.4657,  2.9952,  0.9764,
 -1.1360,  0.1766,  1.9482,  2.1181, -1.3740, -7.5991,  2.0472, -1.2783,
 -0.7213,  3.8815, -1.4831, -0.9652,  2.2482,  2.2893,  3.3173,  4.9056,
 -0.4750,  0.4818, -0.4340, -0.0209, -1.6915, -1.1480, -1.7376,  3.0522,
 -1.2222, -1.2791, -1.5357,  3.8215, -0.9456, -0.2643, -0.0183, -2.4773,
  1.4308, -0.8442, -0.7839,  2.2442,  2.0562,  3.6583,  0.7669, -0.1983,
  1.9040, -0.4223, -0.3367, -3.7683,  0.9073, -2.0037,  7.4025,  0.7309,
  1.6203, -0.3519, -0.3557,  3.9803,  2.0642,  0.8938, -2.8267, -2.5608,
 -3.1776,  1.1625, -0.7042, -0.9658,  4.8135,  0.9713,  4.2898,  3.2817,
 -3.8686,  1.8089,  0.8076, -1.7774,  2.5647,  0.4590,  0.0024,  1.5371,
 -2.8700,  0.3400, -0.7222,  2.9524,  0.9778,  0.3452,  5.9796,  3.4122,
  3.2135,  1.0425, -3.4586, -1.3652, -2.2248,  2.8334, -0.2026,  0.4311,
 -4.9417,  1.5280, -0.5980,  0.2509, -0.2174, -0.0101,  2.7904,  3.4795,
 -1.0065, -1.2637,  3.2353,  2.8361,  1.3290, -0.6255,  4.8891, -1.0721,
  3.9757,  1.3872,  2.8494, -2.4399, -2.2528,  3.7583, -2.9619,  0.1122,
  2.6769,  1.6758,  3.1519, -2.9497, -1.6210,  4.9706,  1.6269,  0.8466,
  1.3961,  0.1470, -0.6976, -0.1300, -1.1262,  1.3919,  0.9148, -1.0835,
 -5.0799,  1.2272,  0.2235,  1.0859, -2.9946,  3.3173, -3.7431, -0.4212,
 -4.8388, -1.1162,  0.7835, -0.2319, -3.7270, -1.8361, -0.6881, -1.1337,
  0.5921, -3.0593, -0.5235,  0.1439,  1.5064, -0.3918, -1.0260, -3.3447,
  2.0733, -1.2811, -0.0204,  3.2155,  1.7667, -1.5619,  0.7945,  4.1375,
 -1.8093, -2.2561, -3.6757,  0.9597, -1.2544, -3.7155,  3.7733,  0.7932]

これが成功なのかよく分かりませんが、一応ベクトルの生成には成功しているのがわかります。

次元圧縮をしてみる

200次元ベクトルを人間が理解するのは到底不可能なので、ここでは誰でも簡単に理解できる2次元まで次元を圧縮します。
主成分分析(PCA;Principal Component Analysis)と呼ばれる手法を使うのですが、こういう話は全然分からないのでソフト任せで適当にやってもらいます。

# -*- coding:utf-8 -*-

from gensim.models import word2vec
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

model = word2vec.Word2Vec.load("tweets-train-200.model")

words = []
words.append("りんご")
words.append("バナナ")

length = len(words)
data = []

j = 0
while j < length:
    data.append(model[words[j]])
    j += 1

pca = PCA(n_components=2)
pca.fit(data)
data_pca= pca.transform(data)

length_data = len(data_pca)

i = 0
while i < length_data:
    #配列形式に整形
    print(np.array2string(data_pca[i], separator=', ', formatter={'float_kind': lambda x: '{: .4f}'.format(x)}))
    i += 1

これでコマンドから実行すると

> python pca_sample.py

[-12.5588, -0.0000]
[ 12.5588,  0.0000]

いい感じに2次元に圧縮できています。では次はもうちょっと応用して座標平面にプロットしてみます。

座標平面にプロットして相関を調べる

MatPlotLibを使って次は座標平面にプロットしてみます。

# -*- coding:utf-8 -*-

from gensim.models import word2vec
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

model = word2vec.Word2Vec.load("tweets-train-200.model")

#調べる情報を配列に収納
capital_name = []
capital_name.append(["前橋市","群馬県","Maebashi","Gumma"])
capital_name.append(["横浜市","神奈川県","Yokohama","Kanagawa"])
capital_name.append(["甲府市","山梨県","Kohu","Yamanashi"])
capital_name.append(["金沢市","石川県","Kanazawa","Ishikawa"])
capital_name.append(["名古屋市","愛知県","Nagoya","Aichi"])
capital_name.append(["津市","三重県","Tsu","Mie"])
capital_name.append(["大津市","滋賀県","Ohtsu","Shiga"])
capital_name.append(["神戸市","兵庫県","Kobe","Hyogo"])
capital_name.append(["松江市","島根県","Matsue","Shimane"])
capital_name.append(["高松市","香川県","Takamatsu","Kagawa"])
capital_name.append(["松山市","愛媛県","Matsuyama","Ehime"])
capital_name.append(["那覇市","沖縄県","Naha","Okinawa"])

length = len(capital_name)
data = []

j = 0
while j < length:
    print(capital_name[j][1])
    data.append(model[capital_name[j][0]])
    data.append(model[capital_name[j][1]])
    j += 1

pca = PCA(n_components=2)
pca.fit(data)
data_pca= pca.transform(data)

length_data = len(data_pca)

i = 0
j = 0
while i < length_data:
    #点プロット
    plt.plot(data_pca[i][0], data_pca[i][1], ms=5.0, zorder=2 ,marker="x")
    plt.plot(data_pca[i+1][0], data_pca[i+1][1],ms=5.0, zorder=2 ,marker="x")

    #線プロット
    plt.plot((data_pca[i][0], data_pca[i+1][0]),(data_pca[i][1],data_pca[i+1][1]),c="b",linewidth=0.5,zorder=1,linestyle="--")

    #文字プロット
    plt.annotate(capital_name[j][2],(data_pca[i][0], data_pca[i][1]),size=7)
    plt.annotate(capital_name[j][3],(data_pca[i+1][0], data_pca[i+1][1]),size=7)

    j += 1
    i += 2

plt.show()

これで
県名(x)——-県庁所在地名(x)
と言った感じに線で結んだ図を出力できて、容易にその関係を把握することが出来ます。
(上記コードはmatplotlibに2バイト文字入力すると文字化けするから適当に対処したコードです。後にこの文字化けは回避できると学習しました。)

実際に実行すると以下のような図が出力されます。

集中してて文字は読みにくいところありますが、見事県庁所在地名と県名の間のベクトルの相関を観測することが出来ました。

おまけ程度に国名と首都名の間での相関も調べてみました。

県名の場合とは違ってちょっとバラつきました。サンプル数が少なかったんでしょうか。今回はこの相関が偶然じゃないことを示すために、「豚肉」-「鶏肉」の関係も調べて図に組み込まれています。

座標平面にプロットして分類してみる

前項で一定の関係にある単語間には一定の相関がある(つまり、引き算すると一定のベクトルが抽出できる)ことがわかったので、次は色々な単語のベクトルを調べてジャンルごとに分類できているのかを調べてみます。

# -*- coding:utf-8 -*-

from gensim.models import word2vec
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

model = word2vec.Word2Vec.load("tweets-train-200.model")

words = []
words.append(["生物","r"])
words.append(["数学","r"])
words.append(["現代文","r"])
words.append(["世界史","r"])
words.append(["物理","r"])
words.append(["豚肉","c"])
words.append(["鶏肉","c"])
words.append(["キャベツ","c"])
words.append(["チーズ","c"])
words.append(["牛乳","c"])
words.append(["卵","c"])
words.append(["白菜","c"])
words.append(["じゃがいも","c"])
words.append(["ニンジン","c"])
words.append(["国土交通省","y"])
words.append(["厚生労働省","y"])
words.append(["外務省","y"])
words.append(["総務省","y"])
words.append(["バス停","m"])
words.append(["電車","m"])
words.append(["新幹線","m"])
words.append(["バス","m"])
words.append(["タクシー","m"])
words.append(["車","m"])
words.append(["自転車","m"])

length = len(words)
data = []

j = 0
while j < length:
    data.append(model[words[j][0]])
    j += 1

pca = PCA(n_components=2)
pca.fit(data)
data_pca= pca.transform(data)

length_data = len(data_pca)

i = 0
while i < length_data:
    #点プロット
    plt.plot(data_pca[i][0], data_pca[i][1], ms=5.0, zorder=2, marker="x", color=words[i][1])

    #文字プロット
    plt.annotate(words[i][0], (data_pca[i][0], data_pca[i][1]), size=7)

    i += 1

plt.show()

今回は適当に思いついた単語をジャンルごとに人間の観点で分類し(r,c,y,m…はカラーコードです)、それを機械も正しく分類できるのかを試します。

実際に実行すると以下のような図が出力されます。

いい感じに色ごとに単語が集まってくれました。完璧ですね!

おしまい

今回が初めてのpythonコーディングだったんですけど、意外と書きやすくて面白いなと思いました。今回学んだ仕組みを応用すれば、受け取った単語がどのような分類に所属するのかを機械が判定する、なんてこともできるんじゃないかなと思いました。いつかやってみたい。

ffmpeg使い方のまとめ

標準

最近フリーの動画変換ソフトが減ってきて不便だなーと感じたので、今日はそんなGUIに依存せずコマンドラインで動画を変換できるffmpegの使い方をまとめます。 ダウンロードはこちらのサイトから。開いたときに何かポップアップ出るかもしれませんが、ここが公式なので無視してください。

使い方

コマンドラインに変換を命じるコマンドを入力するのみです。単純に変換するだけなら起動して10秒以内で動画のエンコード始められます。

ダウンロードしたzipを解凍した後、中にあるbinフォルダーに実行ファイルが入ってます。そこに上記の画像のように作業フォルダーをbinフォルダーに変更したコマンドプロンプトのショートカットを作ってあげると超簡単にそれ以降使うことが出来ます。

変換に必要な(主な)オプション

ffmpegは賢いので、入出力ファイルの形式は拡張子で判断してくれます。

  • 指定例「-」はそれ単独で使えるって意味です。
  • 説明(カッコ内)はデフォルト値です。
  • 説明[角カッコ内]は覚え方です。
オプション名 説明 指定例
-i 入力ファイル指定 -i input.mp4
-y 上書き許可
-o 上書き禁止
-stats エンコード中に進捗報告する
-vol 音量変更(-vol 256) -vol 128
-t 切り取る間の時間[time] / 秒 -t 30 (30秒切り取り)
-fs 出力のサイズ制限[file size] -fs 1024 (1024バイトまで)
-ss 時間指定のオフセット[set start] -ss 20 (20秒のところからカウント)
-sseof 最後から数えてオフセット[-end of file] -sseof 30 (最後から30秒のところから)
-vframes 指定したフレーム数だけ出力 -vframe 100 (100フレームだけ)
-frames xxフレーム目の画像を出力 -frames 50 output.jpg
-r フレームレート[rate] -r 30 (30Hzで出力)
-s フレームサイズ[size] -s 1024×720 (幅x高さ)
-aspect アスペクト比 -aspect 4:3
-aspect 1.333
-vn ビデオ無効化[video none]
-vcodec ビデオコーデック指定 -vcodec libx264 (H.264を指定)
-vcodec copy (入力のまま引き継ぐ)
-pass パス -pass 1 (シングルパス)
-pass 2 (2パス/最大)
-b:a 音声のビットレート(bitrate:audio) -b:a 192k (192kbps)
-b:v ビデオのビットレート(-:video) -b:v 1000k (1000kbps)
-ar サンプリングレート[audio sampling rate] -ar 48000 (48,000Hz)
-ac チャンネル数[- channel] -ac 1 (モノラル)
-ac 2 (ステレオ)
-an 音声無効化[audio none]
-acodec 音声コーデック指定 -acodec libfdk_aac (AAC)
-acodec copy (入力のまま引き継ぐ)

利用例

# input.aviをH.264でモノラルにして480x360にサイズ変更してoutput.mp4として出力
> ffmpeg -i input.avi -vcodec libx264 -s 480x360 output.mp4
# input.aviをH.265で15fpsにしてオーディオコーデックはmp3にしてoutput.mp4として出力
> ffmpeg -i input.avi -vcodec hevc -r 15 -acodec libmp3lame output.mp4
# input.aviを音無しにしてoutput.mp4として出力
> ffmpeg -i input.avi -an output.mp4
# input.aviをモノラルにしてoutput.mp4として出力
> ffmpeg -i input.avi -ac 1 output.mp4

コマンドオプションはソフト側が分からなくならない程度ならどんな順番でも構いません。

普段使いするオプションのみのまとめです。本格的なオプションを使うと、トリミングが出来たり、文字を書き込めたりするらしいですけど、それは大人しくaviutlとか使ってください、というか私は使います。

Mastodonアップデート用シェルスクリプト

標準

アクティブユーザー数が増え、極力ダウンタイムを減らしたいので自動化するために作成したシェルスクリプトを備忘で公開します。
システムによってパスとか全然違うので、ご利用の際はご自身の環境に合わせてからお使いください。
何か不備などありましたら、コメントお願いします。

#!/bin/sh

echo "########## Removing old backup data ##########"
rm -rf /mstdn_backup/old/postgresql
rm -rf /mstdn_backup/old/redis
mv -v /mstdn_backup/postgresql /mstdn_backup/old/postgresql
mv -v /mstdn_backup/redis /mstdn_backup/old/redis
rm -rf /mstdn_backup/postgresql
rm -rf /mstdn_backup/redis

echo "########## Refreshing the git data of mastodon ##########"
git fetch
git status
git stash save
git checkout v2.0.0
git stash pop

echo "########## Saving new backup ##########"
docker cp mastodon_db_1:/var/lib/postgresql /mstdn_backup/postgresql
docker cp mastodon_redis_1:/data /mstdn_backup/redis

echo "########## Building ##########"
docker-compose pull
docker-compose build
docker-compose run --rm web rails db:migrate
docker-compose run --rm web rails assets:precompile

echo "########## Stopping Mastodon ##########"
touch /var/tmp/503
docker-compose stop

echo "########## Starting Mastodon ##########"
docker-compose up -d

echo -n "########## If you are in ready, press ANY KEY! ##########"
read okpress

echo "########## Deleting tempolary files ##########"
rm -f /var/tmp/503
docker system prune -f

echo "########## Finish! Check yourself whether updating was succeed. ##########"

ファイル作成後は

chmod +x update.sh

すると、それからは

./update.sh

だけで起動できるようになります。

ラブライブ!サンシャイン!!楽曲一覧

標準

なかなかネットで調べていて最新の情報が見つからないので、自分で作りました。間違えなどありましたら、コメントでお知らせください。
最新情報が発表され次第、更新していきます。情報量が多いので、横にスクロールできるようになっています。

→→→→ スクロール →→→→

曲名 発売日 備考 収録アルバム キャッチコピー センター
君のこころは輝いてるかい? 2015/10/07 1stシングル 君のこころは輝いてるかい? 高海千歌、桜内梨子 Aqours
Step! ZERO to ONE
Aqours☆HEROES
恋になりたいAQUARIUM 2016/04/27 2ndシングル 恋になりたいAQUARIUM 渡辺曜 Aqours
待ってて愛のうた
届かない星だとしても
元気全開DAY!DAY!DAY! 2016/05/11 1stミニユニットシングル 元気全開DAY!DAY!DAY! CYaRon!
夜空はなんでも知ってるの?
トリコリコPLEASE!! 2016/05/25 1stミニユニットシングル トリコリコPLEASE!! AZALEA
ときめき分類学
Strawberry Trapper 2016/06/08 1stミニユニットシングル Strawberry Trapper Guilty Kiss
Guilty Night, Guilty Kiss!
青空Jumping Heart 2016/07/20 1期アニメOP 青空Jumping Heart 高海千歌 Aqours
ハミングフレンド
決めたよHand in Hand 2016/08/03 1期1話挿入歌 決めたよHand in Hand
/ダイスキだったらダイジョウブ!
──手に手をとって行こう! 2年生 2年生
ダイスキだったらダイジョウブ! 1期3話挿入歌
ユメ語るよりユメ歌おう 2016/08/24 1期アニメED ユメ語るよりユメ歌おう 進む時だよ、あたらしい場所へ! Aqours
サンシャインぴっかぴか音頭
夢で夜空を照らしたい 2016/09/14 1期6話挿入歌 夢で夜空を照らしたい
/未熟DREAMER
歌ってみよういっしょにね! 1、2年生
未熟DREAMER 1期9話挿入歌 松浦果南 Aqours
Pops heartで踊るんだもん! 2016/09/27 Blu-ray第1期第1巻 Aqours オリジナルソングCD (1) Aqours
空も心も晴れるから 2016/10/26 Blu-ray第1期第2巻 Aqours オリジナルソングCD (2) 2年生
想いよひとつになれ 2016/11/09 1期11話挿入歌 想いよひとつになれ/MIRAI TICKET あこがれ抱きしめて次へ進むんだ! 高海千歌、渡辺曜 桜内梨子以外の8人
MIRAI TICKET 2016/11/09 1期13話挿入歌 Aqours
ジングルベルがとまらない 2016/11/23 スクフェスオリジナル ジングルベルがとまらない Aqoursのシャンシャン♪
Christmas♪
Aqours
聖なる日の祈り
Waku-Waku-Week! 2016/11/25 Blu-ray第1期第3巻 Aqours オリジナルソングCD (3) 1年生
2016/11/30 1期アニメOST Sailing to the Sunshine 輝きの欠片を集めて…
Daydream Warrior 2016/12/22 Blu-ray第1期第4巻 Aqours オリジナルソングCD (4) Aqours
G線上のシンデレラ 2017/01/27 Blu-ray第1期第5巻 Aqours オリジナルソングCD (5) 3年生
P.S.の向こう側 2017/02/16 Blu-ray第1期ゲーマーズ
全巻購入特典(日付は初出)
P.S.の向こう側 CYaRon!
LONELY TUNING Blu-ray第1期ソフマップ
全巻購入特典(日付は初出)
LONELY TUNING AZALEA
Guilty Eyes Fever Blu-ray第1期アニメイト
全巻購入特典(日付は初出)
Guilty Eyes Fever Guilty Kiss
スリリング・ワンウェイ 2017/02/24 Blu-ray第1期第6巻 Aqours オリジナルソングCD (6) Aqours
太陽を追いかけろ! 2017/03/24 Blu-ray第1期第7巻 Aqours オリジナルソングCD (7) Aqours
HAPPY PARTY TRAIN 2017/04/05 3rdシングル HAPPY PARTY TRAIN 松浦果南 Aqours
SKY JOURNEY
少女以上の恋がしたい
近未来ハッピーエンド 2017/05/10 2ndミニユニットシングル 近未来ハッピーエンド 近未来を選べ! CYaRon!
海岸通りで待ってるよ
GALAXY HidE and SeeK 2017/05/31 2ndミニユニットシングル GALAXY HidE and SeeK やけどするほど、危険な3人! AZALEA
INNOCENT BIRD
コワレヤスキ 2017/06/21 2ndミニユニットシングル コワレヤスキ 愛にとどめをさす! Guilty Kiss
Shadow gate to love
Landing action Yeah!! 2017/06/30 「Aqours NEXT Step! Project」
テーマ曲
Aqours CLUB CD SET Aqours
夏への扉 Never end ver. 2017/08/02 「ハリケーン・ブロッサム」 ラブライブ!サンシャイン!!
デュオトリオコレクションCD
VOL.1
桜内梨子、国木田花丸、小原鞠莉
真夏は誰のモノ? 「インフェルノ・フェニックス」 黒澤ダイヤ、黒澤ルビィ
地元愛♡満タン☆サマーライフ 「ユニコーン・ブリザード」 渡辺曜、津島善子
夏の終わりの雨音が 「トワイライト・タイガー」 高海千歌、松浦果南
未来の僕らは知ってるよ 2017/10/25 2期アニメOP 未来の僕らは知ってるよ 私たち、輝きたい! 高海千歌 Aqours
君の瞳を巡る冒険
勇気はどこに?君の胸に! 2017/11/15 2期アニメED 勇気はどこに?君の胸に! 夢は、消えない Aqours
“MY LIST” to you!
MY舞☆TONIGHT 2017/11/29 2期3話挿入歌 MY舞☆TONIGHT
/MIRACLE WAVE
Aqours
MIRACLE WAVE 2期6話挿入歌 高海千歌

PHPを使った位置情報収集(ラブライバーはどこから来るの?)

標準

開発に至った経緯

9月29日、30日に「Aqours 2nd ラブライブ! HAPPY PARTY TRAIN TOUR 埼玉公園」(公式サイト)が開催されまして、1日目に私はライブ・ビューイングに参戦しました。(2日目は残念ながらTwitterを眺めるだけでしたが😥)(感想は後述)
そこで、ライブのために埼玉まで来た方はどこから来ているのかと言うのを簡易的に調査するために、Twitterを使った位置情報収集アプリを作ることにしました。

調査方式

  1. ある任意の座標(今回は西武ドームの 35.768653,139.419132 / 国土地理院の地図)をプログラムに与えると、その地点の半径1km以内で投稿されたツイートを集めます。
  2. そのツイート主を全員それぞれ200ツイートずつ遡ってツイートを取得します。
  3. その中でジオタグ(位置情報)があれば集めます。
  4. Excelで収集したデータセットを整理します。
  5. 最後にそれを地図に出力します。

といった感じで、本当に簡易的です。

コード

<!DOCTYPE html>
<html lang="ja">
<head>
    <title>Twitter trace</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="index.css" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
    <?php
    ini_set('display_errors', 1);
    require_once(__DIR__ . "/twitteroauth/autoload.php");
    use Abraham\TwitterOAuth\TwitterOAuth;
    $consumer_key = "***************";
    $consumer_secret = "****************************************************";
    $access_token = "***************************************************";
    $access_token_secret = "*******************************************";

    $connection = new TwitterOAuth($consumer_key, $consumer_secret, $access_token, $access_token_secret);

    ////////////////////////////////////////
    $count_limit = (int)"100"; //0-100
    $get_users_status = (int)"200"; //0-200
    $default_lat = "35.768653";
    $default_long = "139.419132";
    ////////////////////////////////////////

    if(isset($_GET["lat"]) && isset($_GET["long"]) && $_GET["lat"] != "" && $_GET["long"] != "") {
        $set_lat = $_GET["lat"];
        $set_long = $_GET["long"];
    } else {
        $set_lat = $default_lat;
        $set_long = $default_long;
    }

    if(isset($_GET["max_id"]) && $_GET['max_id'] != "") {
        $max_id = $_GET["max_id"];
    } else {
        $max_id = "";
    }

    $geo_params = Array(
        "geocode" => "{$set_lat},{$set_long},1.0km",
        "count" => $count_limit,
        "result_type" => "mixed",
        "max_id" => $max_id,
        "include_entities" => false

    );

    $result_tweets = $connection->get("search/tweets", $geo_params);

    $result_tweets_array = json_decode(json_encode($result_tweets),true);

    $result_coordinate = Array();
    $oldest_id = "";
    $debug_info = "";

    $i = (int)"0";
    while (true) {
        $debug_info .= "{$i}\r\n";
        $status = $result_tweets_array["statuses"][$i];
        if($created_at = $status["created_at"] == "") {
            break;
        }
        $screen_name = $status["user"]["screen_name"];
        $lat = $status["geo"]["coordinates"][0];
        $long = $status["geo"]["coordinates"][1];

        if($screen_name != "mgn_tokorozawa") {
            if($lat != "" && $long != "") {
                array_push($result_coordinate,Array("lat" => $lat,"long" => $long));
            }
        }

        $users_info = Array(
            "screen_name" => $screen_name,
            "count" => $get_users_status,
            "exclude_replies" => true,
            "include_rts" => false,
            "trim_user" => true,
            "exclude_replies" => true
        );

        $users_data = $connection->get("statuses/user_timeline",$users_info);

        $users_data_array = json_decode(json_encode($users_data),true);

        $i2 = (int)"0";
        while(true) {
            $status_data = $users_data_array[$i2];
            if($status_data["geo"] != "" && $lat != $status_lat && $long != $status_long) {
                $status_lat = $status_data["geo"]["coordinates"][0];
                $status_long = $status_data["geo"]["coordinates"][1];
                $debug_info .= "status_lat:{$status_lat}\r\n";
                $debug_info .= "status_long:{$status_long}\r\n";
                $debug_info .= "http://maps.gsi.go.jp/#16/{$status_lat}/{$status_long}/&base=std\r\n";
                if ($screen_name != "mgn_tokorozawa") {
                    if($status_lat != "" && $status_long != "") {
                        array_push($result_coordinate,Array("lat" => $status_lat,"long" => $status_long));
                    }
                }
            }

            $i2++;
            if($i2 >= $get_users_status) {
                break;
            }
        }

        if ($status['id'] != "") {
            $oldest_id = $status['id'];
        }

        $debug_info .= "screen_name:{$screen_name}\r\n";
        $debug_info .= "id:{$status['id']}\r\n";
        $debug_info .= "text:{$status['text']}\r\n";
        $debug_info .= "lat:{$lat}\r\n";
        $debug_info .= "long:{$long}\r\n";
        $debug_info .= "http://maps.gsi.go.jp/#16/{$lat}/{$long}/&base=std\r\n";
        $debug_info .= "https://twitter.com/{$status['user']['screen_name']}/status/{$status['id']}\r\n=================\r\n";
        $i++;
        if($i >= $count_limit) {
            break;
        }
    }

    $api_limit_info = $connection->get("application/rate_limit_status");
    $api_limit_info_array = json_decode(json_encode($api_limit_info),true);
    ?>
    <div class="container" id="main">
        <?php
        echo "<h2>API LIMIT INFO</h2>";
        echo "<ul class='list-group'><li>/search/tweets:{$api_limit_info_array['resources']['search']['/search/tweets']['remaining']}/{$api_limit_info_array['resources']['search']['/search/tweets']['limit']}</li>";
        echo "<li>/statuses/user_timeline:{$api_limit_info_array['resources']['statuses']['/statuses/user_timeline']['remaining']}/{$api_limit_info_array['resources']['statuses']['/statuses/user_timeline']['limit']}</li></ul>";

        $result = (int)"0";
        $result_count = count($result_coordinate);
        $output = (string)"";
        while(true) {
            $output .= "{lat: {$result_coordinate[$result]['lat']},lng: {$result_coordinate[$result]['long']}},";
            $result++;
            if($result >= $result_count) {
                break;
            }
        }
        ?>

        <h2>Custom search</h2>
        <form method="get" action="#" class="form-horizontal">
            <div class="form-group">
                <label for="lat" class="control-label" >latitude</label>
                <input type="text" id="lat" name="lat" class="form-control input-lg" value="<?php echo $set_lat; ?>" />
            </div>
            <div class="form-group">
                <label for="long" class="control-label" >longitude</label>
                <input type="text" id="long" name="long" class="form-control input-lg" value="<?php echo $set_long; ?>" />
            </div>
            <div class="form-group">
                <label for="submit" class="control-label">search</label>
                <input type="submit" class="form-control input-lg" id="submit" value="Search" />
            </div>
        </form>

        <h2>Result</h2>
        <form action="#" class="form-horizontal">
            <div class="form-group">
                <label for="coordinates">Result coordinates dataset</label>
                <textarea rows="40" cols="100" class="form-control input-lg" name="coordinates"><?php echo $output; ?></textarea>
            </div>
            <div class="form-group">
                <label for="debug_info">Debug information</label>
                <textarea rows="40" cols="100" class="form-control input-lg" name="debug_info"><?php echo $debug_info; ?></textarea>
            </div>
        </form>

        <h2>Re-search</h2>
        <form action="#" method="get" class="form-horizontal">
            <div class="form-group">
            <label for="max_id" class="control-label">max_id</label>
            <input type="text" class="form-control input-lg" name="max_id" id="max_id" value="<?php echo $oldest_id; ?>" />
        </div>
        <div class="form-group">
            <label for="submit" class="control-label">search</label>
            <input type="submit" class="form-control input-lg" value="Search" />
        </div>
        </form>
        <p><a href="direct_input.php">input page</a></p>
    </div>
</body>
</html>

人に見せることを目的としていないコードなので本当に適当ですが、一応自分の中では使いやすいように色々とカスタムしてあります。このプログラムを順次走らせていくことで、ライブ期間中の全ツイートを調べることが出来ます。

データを整理する

上記のプログラムで収集した座標のデータセットは大量の重複を含んでいて、そのままのデータを地図にプロットすると閲覧するのにちょっと不便です。なので、重複データを排除するためにExcelで整理します。
適当に集めたJSON形式のデータをExcelに打ち込んでいきます。(改行挟んだりの整形はPHP使うと便利です。)

ワンクリックで重複削除できるんですね。便利!整理したデータを再びJSONにして、次はいよいよ地図にプロットしていきます。

地図にプロット

今回はGoogleが公開している Maps JavaScript API を使っていきたいと思います。APIの使い方のページに記載されている方法で自分用のAPIキーを取得します。そして、以下のようなページを作成して、いざプロットしていきます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <title>Twitter trace</title>
    <meta charset="UTF-8" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Cache-Control" content="no-cache" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="index.css" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
        <script>
        var map;
        var marker = [];
        var infoWindow = [];
        var markerData = [
            {lat:35.768575,lng:139.420501},
            {lat:35.768493,lng:139.420522},
            {lat:35.77059347,lng:139.4194273},
            {lat:35.7707,lng:139.4198},
            {lat:35.76880072,lng:139.4209541},
            {lat:35.76879838,lng:139.4209604},
            {lat:35.7112046,lng:139.4640989},
            {lat:35.77136271,lng:139.4202191},
            {lat:35.8353742,lng:139.6503192},
            {lat:35.83783604,lng:139.6495825},
            {lat:35.76880259,lng:139.4209549},
            {lat:35.76946619,lng:139.4382386}
        ];
        function initMap() {
            var mapLatLng = new google.maps.LatLng({lat: markerData[0]['lat'], lng: markerData[0]['lng']});
            map = new google.maps.Map(document.getElementById('result_map'), {
                center: mapLatLng,
                zoom: 15
            });
            for (var i = 0; i < markerData.length; i++) {
                markerLatLng = new google.maps.LatLng({lat: markerData[i]['lat'], lng: markerData[i]['lng']});
                marker[i] = new google.maps.Marker({
                    position: markerLatLng,
                    map: map
                });
            }
        }
        </script>

        <div id="result_map"></div>

        <script async defer
        src="https://maps.googleapis.com/maps/api/js?key=*************************************&callback=initMap">
        </script>
    </div>
</body>
</html>

これだけで好きなだけ地図に地点をプロットできるなんて凄いですね。

まとめ

今回は最終的に8,000件くらいのツイートを収集して2,405件のユニークなデータを取得しました。このデータに関しては、誰でも簡単に閲覧することができるように公開してありますので、参考までに見てみてください。
データ閲覧

日本国内でも北は北海道、南は鹿児島県(確認できた中で最南端)から、世界にまで目を向けると上海やシンガポールなどからも来ていることが分かりました。こんなにも津々浦々から人々を呼び寄せることができるなんてラブライブ凄いなぁ、と言った感じです。

(ライブの)感想

初披露の曲や衣装やダンスを拝めて本当に素晴らしかった!初めてライブを見たんですけど、楽しすぎる…!というかキャスト同士にイチャつきが尊すぎる!!!!!もっと見たかった、というか2日目も参戦したかった!
次はファンミーティングや3rdツアーなどが予定されていますので、こちらの方はぜひ現地で参戦したいですね。

Mastodonのカスタム絵文字追加のお知らせ

標準

Mastodonでカスタム絵文字が追加できるようになったらしいとの話を聞いて、早速最新のコミットまでアップデートしてみました。正式なバージョンではないので、若干バグは目立ちます。カスタム絵文字が追加できるようになったおかげで、個々のインスタンスの特色を色濃く反映することが出来るようになりました。私のインスタンスでも早速アイデンティティーを確立(?)するために、幾つかのカスタム絵文字を追加してみました。以下がその一覧です。

絵文字 ショートコード 備考
:haruhi:
:yui:
:azusa:
:chika:
:konata:
:kagami:
:yuki:
:chan_yuki: 『「涼宮ハルヒちゃんの憂鬱」の有希』の略
:50: 各文字の間に1スペース必要
:00:
:cho:
:yen:
:ho:
:shi:
:i_:


取り敢えず、素材を持っていたキャラクターだけ試しに追加してみました。何か他に追加してほしい要望等有りましたら、教えてください。

Mastodon軽量化に係る犠牲一覧

標準

Mastodonが栄えるにつれて、サーバーのスペックがMastodon側の要求に応えることが出来ていない、という状況が頻繁に見られるようになりました。Mastodonの長所の一つに、好みのインスタンスに次々と繋がることが出来ると言うものが挙げられますが、今はこれが裏目に出てしまっているようです。だからといって利用していただいているユーザーさんには極力迷惑を及ぼすのは避けたいですし、かと言って繋がりも絶ちたくない。うーんどうすれば良いんだろうか…悩みに悩んだ挙句に私は「ユーザーさんの居心地」を選びました。一身上の都合により、サーバーのスケールアップはまだ半年はできそうにありません。なので、適宜ドメインブロックを行って部分的な鎖国をすることにしたのです。日々、なるべく軽く動くように働きかけていたのですが、今回はドメインブロックが大規模に行われることを避けることは出来ませんでした。申し訳ないです。ご要望等ありましたらお気軽にご相談ください。

(ロースペックサーバーでも長期的に動くように何かPuSHの輪を広げる規則の制限がつけられれば苦労しないんですが…例えばRTが連合TLに流れただけならばPuSH購読しない、みたいな。)

更新(2017/08/13):最新の情報に差し替えました。
更新(2017/09/03):最新の情報に差し替えました。

ドメイン
7144.party mayodon.club
a.weirder.earth md.ggtea.org
admins.mstdn.social md.regastream.com
animalliberation.social mdn.hinaloe.net
anitwitter.moe mellified.men
anti.energy memetastic.space
august-don.site metal.odon.space
babymetal.party mevo.xyz
bcn-users.degica.com minami.shinshyu.life
betterletter.io msdn.yourrhythm.jp
blackice.online msdnaart.net
blessedgeeks.jp mstd.tokyo
bonn.social mstdn-d.info
bookdon.jp mstdn-scc.jp
bookwitty.social mstdn-tech.jp
boys.computer mstdn-workers.com
catdon.jp mstdn.791127.net
catdon.life mstdn.b-shock.org
chitose.moe mstdn.binfish.jp
claristdon.net mstdn.boxertwin.info
cmpwn.com mstdn.co33k.org
co-mastdn.ga mstdn.creatorsnight.com
cybre.space mstdn.dasoran.net
dhtls.net mstdn.ernix.jp
don.archae.me mstdn.fetus.jp
ecodigital.social mstdn.gifu.jp
ekimemo.info mstdn.haoyayoi.net
elephant.bluecore.net mstdn.harusamex.com
emojidon.global mstdn.ho-chi-minh.info
equestria.social mstdn.ht164.jp
f-rabbit.com mstdn.i-red.info
fern.surgeplay.com mstdn.it-infra.jp
ffxiv-mastodon.com mstdn.itmedia.co.jp
fnya.ggtea.org mstdn.klamath.jp
foodon.jp mstdn.kwmr.info
forumanalogue.fr mstdn.kwmr.me
framapiaf.org mstdn.mini4wd-engineer.com
friends.nico mstdn.mk39.xyz
from.komic.eu mstdn.mochiwasa.xyz
gam.m.to mstdn.morendo.daemon.asia
gbtdn.tokyo mstdn.niigata.jp
gl.phil-ia.net mstdn.plus
gnusocial.no mstdn.poyo.me
golfdn.com mstdn.ropo.jp
grimoire.kanzakiranko.jp mstdn.serv-ops.com
gundam.masto.host mstdn.soysoftware.net
gunmastodon.com mstdn.techdrive.top
gyudn.com mstdnsrv.moe.hm
hackers.town mustodon.xyz
hatabowyou.club nfg.zone
herds.eu niigata.minnna.xyz
hfukuchi.masto.host nikki.m.to
hige.alterna-cloud.com nomlishdon.racing-lagoon.info
home.aqraf.tokyo o.kagucho.net
hydroxyquinol.net obitsudon.midyuki.net
ika.moe ofuton.io
ika.queloud.net omochi.xyz
inari.opencocon.org open2.ch
infosec.exchange ostatus.ikeji.ma
iomstdn.tokyo ostatus.shnoulle.net
k0ta.net ostatus.taiyolab.com
kaisendon.asmodeus.red otajodon.com
kakudon.com p2px.me
kirapower.ichigo-hoshimiya.com pao.moe
kiwaitsu.hostdon.jp photog.social
koyuston.tk pleasehug.me
kurage.cc pouet.april.org
kurosawa-ruby.xyz pouet.couchet.org
linuxinthenight.com psodon.com
linuxjobs.social queer.party
linuxrocks.online queer.town
lou.lt quitter.cat
m.loovto.net quitter.es
m.moriya.faith quitter.im
m.pref.yokohama rainyman.jp
m.sl-network.fr rich.gop
m.uncate.org ro-mastodon.puyo.jp
m6n.jp sandbox.skoji.jp
malfunctioning.technology scuba.masdon.life
mammouth.inframed.net sdfn-01.ninjawedding.org
mammut.fsck.jp sealion.club
mamot.fr shelter.moe
manx.social shimaidon.net
mastd.racing shkval.net
mastdn.lovesaemi.daemon.asia sitedethib.com
mastdon.amazedkoumei.com sns.gdgd.jp.net
mastdon.jp social.48bin.net
masto.cloud social.atypique.net
masto.raildecake.fr social.ballpointcarrot.net
masto.themimitoof.fr social.cloudfrancois.fr
mastodon-omoshiro.com social.devloprog.org
mastodon.ar.al social.diskseven.com
mastodon.burnworks.com social.freedombone.net
mastodon.cgx.me social.guimik.fr
mastodon.cloud social.heldscal.la
mastodon.crazynewworld.net social.hyuki.net
mastodon.eliotberriot.com social.imirhil.fr
mastodon.gracie-tech.tokyo social.logilab.org
mastodon.hekki.info social.mecanis.me
mastodon.huma-num.fr social.nah.re
mastodon.indie.host social.nasqueron.org
mastodon.infra.de social.newspeak.house
mastodon.jtwp470.net social.sakamoto.gq
mastodon.kirimi.net social.taker.fr
mastodon.lertsenem.com status.fsf.org
mastodon.matcha-soft.com status.pointless.one
mastodon.matrix.org sunshinegardens.org
mastodon.mit.edu taruntarun.net
mastodon.ohgro.net tgp.jp
mastodon.org.uk the.resize.club
mastodon.p2pquake.net tk2-234-26965.vs.sakura.ne.jp
mastodon.papey.fr toot-lab.reclaim.technology
mastodon.partecipa.digital toot.matereal.eu
mastodon.partipirate.org toot.mst-dn.me
mastodon.potager.org toot.oekaki.st
mastodon.potproject.net toot.place
mastodon.robotstart.info toot.redmine.jp
mastodon.sdf.org toot.yukimochi.jp
mastodon.snowandtweet.jp toukae.com
mastodon.social tulip.blue
mastodon.sportsfans.social tusk.social
mastodon.survival-machines.fr vapedon.club
mastodon.swordlogic.com vastodon.com
mastodon.wakin.site vocalodon.net
mastodon.xyz waraiotoko.net
mastodon.ynu.zone witches.town
mastodon.zombocloud.com www.blueblueblue.blue
mastodon.zunda.ninja www.mofgao.space
mastodonar.club www.techdon.info
mastodonte.me xn--zck4azd638n.com
mastodonti.co xserver.mstdn.hosting
mastodonturkiye.club xserver.work
mastodos.com yaskey.tokyo
mastonon.net youthdon.com
mastoton.fi yso.pet
masutabedon.com 以上 271ドメイン

WordPressの重大な脆弱性の検証

標準

2017年2月17日、Wordpressに重大なREST APIに関する脆弱が存在することが公表されました。

WordPress の脆弱性対策について – IPA

あまりにも重大すぎて、パッチが配布されるまで脆弱の存在の公表を遅らせるほどでした。この脆弱はREST APIの不具合を利用することで、ユーザー認証を必要とせずにWordpressに投稿されている記事を簡単に書き換えることが出来てしまいます。今回はこの脆弱をWindowsで実際にWordpressを仮想環境で構築して検証していきます。また、この脆弱を利用して他人のサイトを不正に書き換えることは国内法に抵触しますので、真似は自分の中だけで終わらせてください。

WindowsにDocker環境を整備する

簡単にWordpressの仮想環境を構築するために今回はDockerを使っていきます。(WordPressの仮想環境を作る方法は色々とありますので、ここらへんは個人の好みで。) また、この方法はWindows10 proでないと出来ない工程もありますので、ご注意ください。

まずはDockerのインストーラーを公式サイトからダウンロードします。適当にstable版などをご利用ください。

インストールが完了すると、Dockerが起動します。しっかりと起動するまでに少し時間がかかるので、気長にお待ち下さい。

しっかりと起動しましたら、DockerのServerが稼働していることをPowerShellで確認します。

> docker version
Client:
 Version:      17.06.0-ce
 API version:  1.30
 Go version:   go1.8.3
 Git commit:   02c1d87
 Built:        Fri Jun 23 21:30:30 2017
 OS/Arch:      windows/amd64

Server:
 Version:      17.06.0-ce
 API version:  1.30 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   02c1d87
 Built:        Fri Jun 23 21:51:55 2017
 OS/Arch:      linux/amd64
 Experimental: true

Serverの部分が確認されずエラーが発生している場合は、Dockerが起動しきっていないかHyper-Vを有効にする必要があります。 コントロールパネル→プログラムと機能→Windowsの機能の有効下または無効化 からHyper-Vを有効にしてからもう一度試してみてください。

次にDockerの構成情報をまとめていきます。適当なディレクトリを作成し、その中に docker-compose.yml を作成し、以下のようにします。

version: '2'
services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: wordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

  wordpress:
    depends_on:
      - db
    image: wordpress:4.7.0
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_PASSWORD: wordpress

volumes:
  db_data:

ファイルを作成し終えたらPowerShellのカレントディレクトリをdocker-compose.ymlが存在するディレクトリに変更して

> docker-compose pull
> docker-compose build
> docker-compose up -d

これでイメージをビルドして起動するまでがおしまいです。しっかりと起動しているか確認するために

> docker ps

でしっかりとwordpressがport:8000で起動しているかを見ます。

正常な起動を確認できましたら、ブラウザで http://127.0.0.1:8000/ にアクセスします。もし正常なレスポンスがなければまだWordpressが起動しきってないかもしれないので、1分くらい待ってみてください。
この辺りの作業はMastodonで何回もやったので慣れたもんですね。

WordPressの初期設定を一通り終えると以下のようにサイトが表示されます。

これでWordpressの仮想環境の構築はおしまいです。こんな簡単にWindowsで出来るとは。

早速攻撃してみる

今回はChromeアプリのAdvanced REST clientを使っていきます。指定したURLにリクエスト出来るのであれば利用するツールの種類は問いません。

一番最初の投稿の記事IDが1なので、以下のURLにtitleとcontentをPOSTしてみます。

http://127.0.0.1:8000/wp-json/wp/v2/posts/1?id=1

このように、普通のURLでは 401 – Unauthorized が返ってきて、記事を書き換えることなんて出来ません。しかし、

http://127.0.0.1:8000/wp-json/wp/v2/posts/1?id=1abc

上記のURLに対して同様にtitleとcontentをPOSTしてみます。すると、

なんと、処理が完了して 200 – OK が返ってきてしまいました。実際に投稿の方を見てみますと、

確かに、記事が書き換えられていることが確認できます。

原因は何か

この脆弱はPHPの厄介な仕様で起こってしまう脆弱です。詳しくは Content Injection Vulnerability in WordPress を見ていただければ全てが分かると思いますが、端折って説明しますと、

$request = array("id"=>"1abc");
$id = (int)$request['id'];
var_dump($id);

// int(1)

このようにして不正にIDがキャストされてしまい、しっかりとした判断ができなくなってしまうのです。困った!

まとめ

こうしてWordpressの脆弱を検証することが出来ました。自分の手で作業をしてみると、まさかこんなに簡単な手順でページが改ざん出来るとは… 私自身もWordpressやMastodonを運営していますので、こういったセキュリティホールなどに関する情報には常に敏感になってないといけないですね。

Mastodonの色々なインスタンス情報の統計

標準

暇つぶしに色々なインスタンスのユーザー数だとかトゥート数だとかを統計するっていうことをしてみました。一応記録として書き記しておきます。

インスタンス情報をcsvでかき集める

TacosTea / versionbattle
上記のリポジトリを活用させていただきました。これを使うことでシェルを叩くだけでinstances.social (jsonとしてはこちら) に登録されている全てのインスタンスの遅延(ms)、バージョン、ユーザー数、トゥート数、接続数をcsvとして出力させることが出来ます。凄い!

jsonを取得し直す場合、jsonからキーを抜き出すjqコマンドっていうのがあるんですけど、一部環境(私の場合はそうでした)ではインストールされていないこともあるので yum install -y jq などでさっとインストールしてしまってください。

最終的に2つのファイルに分けてcsvが出力されるので、これをExcelとかに専用の機能を使って貼り付けて色々とノイズを除去したりソートしたりすると以下のようになります。最終的に調査数は1,336になりました。

散布図にまとめてみる

今回のデータは少人数のような1桁の数からmstdn.jpやpawoo.netのような8桁の数までを扱わなければならないので、いずれの軸も対数目盛にしてみました。しかし、これに伴ってお一人様インスタンスなどのデータを示す際、大変なことになってしまうので、これまたいずれの軸も原点を10に設定しました。

というわけで散布図にまとめてみると以下のようになりました。

なんとなくないい感じに比例になりました。良かったです。(これ以上の成果は無い)
今回は以上です。これからもちょくちょくどうでもいい小ネタを投稿していくので、よろしくお願いいたします。

Mastodonクライアントのまとめ(iOS版)

標準

Mastodonが日本で流行り始めてから早いもので3ヶ月が経とうとしています。この3ヶ月弱でスマートフォン向けのクライアントが多いに増えました。特にiOS版の最初はAmaroqの一つだけでしたね。しかし、今は十数のクライアントがAppStoreに登録されています。ので、それに伴って「どれが便利なんだろう」「わざわざインストールして試すのが面倒」といった声が散見されるようになったので、それぞれのアプリの長所や短所、特徴などを簡単にまとめていきます。クライアント乗り換えの際の一助となれば幸いです。ちなみに、私の中では内蔵ブラウザで表示させているだけのアプリはクライアントとは認めていませんのでご承知おきください。紹介順序は使いやすさの順序ではないですよ。あと、私のiOSは英語に設定してあるので、スクショでは英語でも実際は日本語で使えるものもあります。つまり、スクショは参考までに

最終更新:2017/07/04

評価基準

  • LTL … ローカルタイムラインを見れるか
  • FTL … 連合タイムラインを見れるか
  • 通知 … 通知がちゃんとリアルタイムで通知されるか
  • 安定動作 … いきなりクラッシュしたりしないか、良くわからないエラーを吐かないか
  • デザイン … 見やすさ
  • 複数アカウント … 複数のアカウントでログインできるか
  • iPad … iPad版がAppStoreで配信されているか

Amaroq

ダウンロード
Mastodonクライアントとしては老舗っていう感じなくらい、初期から存在するアプリです。安定動作はしますが、機能の豊富さとしては少し劣る点を感じます。是もなく非もなくと言った感じですが、ローカルタイムラインが見れないっていうのは個人的には大きいですねローカルタイムライン見れました。しかしながら、はじめの一歩としてのアプリならとても使い勝手が良いと思います。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad
×


Tootdon

ダウンロード
個人的に一番気に入っているアプリです。デザイン、動作が共に良くて通知もリアルタイムで届いてくれるので便利です。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad


Mustor

ダウンロード
長らくお世話になったアプリです。機能やデザインに特に不満はありませんが、通知が時々届かなかったり、いきなりアプリが終了してしまったり、内部的なバグがまだまだあるのかなと思う感じです。リリース当初から見守っているので、今後の展望も楽しみです。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad


Pawoo

ダウンロード
Pixivが運営しているアプリです。大企業が運営、開発しているため、安定性、使いやすさはピカイチだと思います。pawoo.net以外のインスタンスで利用すると、不便はしない程度の機能制限がかかるらしいです。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad
△?


GON

ダウンロード
触った感じ他のアプリに比べて軽いような気がしますが、安定性に欠ける点があります。リロードする度にフォントサイズが変わったり。ただ、最低限までUIを絞っているわりには他のアプリと同程度の機能を有しているので、その点では捨てられません。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad


Friends.nico

ダウンロード
ニコニコ動画を運営しているドワンゴが開発しているクライアントです。Pawooに比べると若干マイナーな感じはしますが、それでも使用感はPawooに劣らない感じです。さすが大企業の力…けど若干カクつく感じはします。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad
△? ×


Tootle

ダウンロード
最近リリースされた遅生まれのクライアントです。タイムラインにフィルターをかけられたりなどの独自の機能が見られますが、安定性ではまだ欠ける点がありそうです。今後の展望に期待。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad


Mameleon

ダウンロード
遅生まれのアプリです。しかしながらサッパリとしたUIを基調としており、とても使いやすくなるように考えられています。しかし、時々謎のエラーを吐いたりと不安定な部分も見られます。開発者さんには今後も頑張っていただきたい。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad
△? ×


11t

ダウンロード
こちらも遅生まれのアプリです。他のアプリに比べると機能が少なかったりなど不便な点が多く見受けられます。あとちょっと動作もレスポンスが無かったりと改良すべき点も多いです。

LTL FTL 通知 安定動作 デザイン 使いやすさ 複数アカウント iPad
× × × × ×