やったこと
MATLAB Answers に投稿された日本語の質問を自動でつぶやく bot を作ってみました。
MATLAB 経験のある方はフォローして、MATLAB に関する悩み解決を一緒にやってくれると嬉しいです。
アカウントはこちら -> @JPMATLABAnswers
2023/08/15 追記
ThingSpeak からの Tweet が現在機能していないので今は GitHub Actions と Twitter API v2 を使っています。GitHub:minoue-xx/Tweet-New-MATLAB-Answers-Japanese
MATLAB Answers?: MathWorks が運営する MATLAB/Simulink に関する Q&A サイトです。スタッフ(社員)も回答していますがユーザー同士のやりとりも多数。
動機: MATLAB Answers では英語のやりとりがほとんどなので、日本語の質問も目立たせたい。
わざわざ Bot 自作しなくても同様のサービスなんていくらでもあるとは思います。
ただ、自分でやってみるのは自由に色々いじれますし面白いですよね?
ということで、この記事では
- MATLAB Answers の新着質問情報の抽出方法(XML, RSS)
- ThingSpeak から Twitter への自動投稿 (ThingTweet)
を解説します。参考にして頂けると幸いです。
環境
MATLAB R2019a (Toolbox 不要)
ThingSpeak (MATLAB Analysis, ThingTweet)
なぜ ThingSpeak で?
ThingSpeak には MATLAB Analysis と名のついた機能があり、MATLAB のコードが実行できるので、MATLAB に慣れていると大変馴染みやすい。しかも TimeControl を使って定期的に自動実行(例えば15分毎)も設定可能。そう、サーバーいらず。
そもそも ThingSpeak って?
いわゆる IoT プラットフォームですが、以下の2資料が分かりやすいと思います。
- iwathiの/var/log: データを簡単に保存&グラフ化できるThingSpeakが便利!
- Qiita: ThingSpeakのMATLAB VisualizationsでOpenWeatherMapのAPIを叩いてグラフ表示
あくまで IoT プラットフォームなので、本来は Raspberry Pi などのハードウェアからの情報を吸い上げていろいろ楽しいことをするのが正しい使い方なんですが、今回はウェブ上で完結しちゃいます。
各種設定
ThingTweet の設定
Twitterアカウントをリンクして、API Key の取得が必要です。
Channel作成
実際の Channel はこちらで確認できますが、
以下の4つの情報をトラックしています。
- LatestID_JP: 日本語質問の最新ID番号
- FlagLatest: 新着があればあがるフラグ
- FailTwitterPost: Twitter 投稿に何か問題があればあがるフラグ
- FailAnswersRead: 日本語投稿のチェックに何か問題があればあがるフラグ
それぞれの推移はこんな感じ。
とくに問題はなさそうですね。
さて本題の MATLAB Analysis
ここで紹介するコードが ThingSpeak 上で15分に一回実行され、
- 新着チェック
- 新着情報を Tweet
を実行する仕組みとなってます。コード全文は最後に纏めて再掲しますので、ここでは要所だけ解説します。
MATLAB Answers 投稿チェック
MATLAB Answers トップページの RSS(日本語、投稿の新しい順表示)を使います。
トップページだけだと 50 投稿分しかチェックできませんが、今のところ15分の間に50個以上の日本語で質問が投稿されたことはないので、ひとまずOKとします。
XML フォーマットを解析するために xmlread 関数 を使いました。href 値の取得に手間取りましたが
に助けられました。ありがとうございます!
xDoc = xmlread('https://jp.mathworks.com/matlabcentral/answers/questions?language=ja&format=atom&sort=asked+desc');
% まず各投稿は <entry></entry> に収まっている様
allListitems = xDoc.getElementsByTagName('entry');
% アイテム数だけ配列を確保
title = strings(allListitems.getLength,1);
url = strings(allListitems.getLength,1);
author = strings(allListitems.getLength,1);
% 各アイテムから title, url, author 情報を出します。
for k = 0:allListitems.getLength-1
thisListitem = allListitems.item(k);
% Get the title element
thisList = thisListitem.getElementsByTagName('title');
thisElement = thisList.item(0);
% The text is in the first child node.
title(k+1) = string(thisElement.getFirstChild.getData);
% Get the link element
thisList = thisListitem.getElementsByTagName('link');
thisElement = thisList.item(0);
% The url is one of the attributes
url(k+1) = string(thisElement.getAttributes.item(0));
% Get the author element
thisList = thisListitem.getElementsByTagName('author');
thisElement = thisList.item(0);
childNodes = thisElement.getChildNodes;
author(k+1) = string(childNodes.item(1).getFirstChild.getData);
end
% URL は以下の形
% href="https://www.mathworks.com/matlabcentral/answers/477845-bode-simulink-360"
url = extractBetween(url,"href=""",""""); % URL 部分だけ取得
entryID = double(extractBetween(url,"answers/","-")); % 投稿IDを別途確保
新着かどうかの判断は?
ThingSpeak 上に記録している最も新しいID番号と比較します。
% ThingSpeak 上に記録している最も新しいID番号を取得
% 1: LatestID_JP
latestID = thingSpeakRead(channelID,'Fields', 1,'ReadKey', readAPIKey);
% これまでに検知した最も大きいIDより大きいIDがあれば
% 新規投稿として認識されます。
newID = entryID > latestID; % new
idxNew = find(newID);
Twitter に新着を投稿
上で新着と認識された投稿を Tweet します。
ユーザーからの質問ばかりでなく、MathWorks Support Team 作成の FAQ もあるので、投稿文は区別しておきます。
tturl='https://api.thingspeak.com/apps/thingtweet/1/statuses/update';
api_key = 'XXXXXXXXXXXXXX'; % Twitterをリンクして APIKey を取得してください。
options = weboptions('MediaType','application/x-www-form-urlencoded');
options.Timeout = 10;
for ii=1:sum(newID)
thisAuthor = author(idxNew(ii));
thisTitle = title(idxNew(ii));
thisURL = url(idxNew(ii));
% 投稿文:~さんからの質問「質問タイトル」-> URL
disp([string(ii) + "個目の投稿"]);
if thisAuthor == "MathWorks Support Team"
status = thisAuthor + " からのヒント:「" + thisTitle + "」 -> " + thisURL;
else
status = thisAuthor + " さんからの質問:「" + thisTitle + "」 -> " + thisURL;
end
disp(status);
try
webwrite(tturl, 'api_key', api_key, 'status', status, options);
catch ME
disp(ME)
FailTwitterPost = true;
end
end
最後に新着IDを ThingSpeak に登録
Channel の情報を更新しておきます。
%% Write Data %%
analyzedData = {LatestID_JP, FlagLatest, FailTwitterPost, FailAnswersRead};
thingSpeakWrite(channelID, analyzedData , 'WriteKey', writeAPIKey);
まとめ
ThingSpeak を使って MATLAB Answers bot を作りました。
XMLフォーマットから情報を吸い上げる部分が一番苦労したところでした。
MATLAB コード全文(再掲)
各 APIKey だけ設定すればそのまま機能するはず。
% Enter your MATLAB Code below
channelID = 854373;
writeAPIKey = 'XXXXXXXXXXXXXXXX'; % それぞれの Channel に固有の APIKey
readAPIKey = 'XXXXXXXXXXXXXXXX'; % それぞれの Channel に固有の APIKey
% Fields
% 1: LatestID_JP
% 2: FlagLatest
% 3: FailTwitterPost
% 4: FailAnswersRead
FailTwitterPost = false;
FailAnswersRead = false;
page = 1; % 1ページ(50投稿分)だけチェック
try
% トップページの RSS を読み込み(日本語、投稿の新しい順表示)
xDoc = xmlread('https://jp.mathworks.com/matlabcentral/answers/questions?language=ja&format=atom&sort=asked+desc');
% まず各投稿は <entry></entry>
allListitems = xDoc.getElementsByTagName('entry');
% アイテム数だけ配列を確保
title = strings(allListitems.getLength,1);
url = strings(allListitems.getLength,1);
author = strings(allListitems.getLength,1);
% 各アイテムから title, url, author 情報を出します。
for k = 0:allListitems.getLength-1
thisListitem = allListitems.item(k);
% Get the title element
thisList = thisListitem.getElementsByTagName('title');
thisElement = thisList.item(0);
% The text is in the first child node.
title(k+1) = string(thisElement.getFirstChild.getData);
% Get the link element
thisList = thisListitem.getElementsByTagName('link');
thisElement = thisList.item(0);
% The url is one of the attributes
url(k+1) = string(thisElement.getAttributes.item(0));
% Get the author element
thisList = thisListitem.getElementsByTagName('author');
thisElement = thisList.item(0);
childNodes = thisElement.getChildNodes;
author(k+1) = string(childNodes.item(1).getFirstChild.getData);
end
% URL は以下の形になっているので、
% href="https://www.mathworks.com/matlabcentral/answers/477845-bode-simulink-360"
url = extractBetween(url,"href=""",""""); % URL 部分だけ取得
entryID = double(extractBetween(url,"answers/","-")); % 投稿IDを別途確保
catch ME
disp(ME)
FailAnswersRead = true; % 読み込み失敗
return;
end
% Get the current data
% 1: LatestID_JP
latestID = thingSpeakRead(channelID,'Fields', 1,'ReadKey', readAPIKey);
% これまでに検知した最も大きいIDより大きいIDがあれば
% 新規投稿として Tweet
newID = entryID > latestID; % new
idxNew = find(newID);
% 無ければ古い値を使用
if sum(newID) == 0
LatestID_JP = latestID;
FlagLatest = 0;
disp('no new entry');
else
FlagLatest = true;
[LatestID_JP,idx] = max(entryID); % latest ID
% 新しい投稿を Tweet
% ThingTweet 設定
tturl='https://api.thingspeak.com/apps/thingtweet/1/statuses/update';
api_key = 'XXXXXXXXXXXXXX'; % Twitterをリンクして APIKey を取得してください。
options = weboptions('MediaType','application/x-www-form-urlencoded');
options.Timeout = 10;
for ii=1:sum(newID)
thisAuthor = author(idxNew(ii));
thisTitle = title(idxNew(ii));
thisURL = url(idxNew(ii));
% 投稿文:~さんからの質問「質問タイトル」-> URL
disp([string(ii) + "個目の投稿"]);
if thisAuthor == "MathWorks Support Team"
status = thisAuthor + " からのヒント:「" + thisTitle + "」 -> " + thisURL;
else
status = thisAuthor + " さんからの質問:「" + thisTitle + "」 -> " + thisURL;
end
disp(status);
try
webwrite(tturl, 'api_key', api_key, 'status', status, options);
catch ME
disp(ME)
FailTwitterPost = true;
end
end
end
%% Write Data %%
analyzedData = {LatestID_JP, FlagLatest, FailTwitterPost, FailAnswersRead};
thingSpeakWrite(channelID, analyzedData , 'WriteKey', writeAPIKey);