MATLAB Answers の新着質問をお知らせする Twitter bot (Powered by ThingSpeak)

# やったこと

[MATLAB Answers](https://jp.mathworks.com/matlabcentral/answers/?language=ja&s_eid=PSM_29435) に投稿された日本語の質問を自動でつぶやく bot を作ってみました。
MATLAB 経験のある方はフォローして、MATLAB に関する悩み解決を一緒にやってくれると嬉しいです。
アカウントはこちら -> [@JPMATLABAnswers](https://twitter.com/JPMATLABAnswers)

:::note warn
2023/08/15 追記
ThingSpeak からの Tweet が現在機能していないので今は GitHub Actions と Twitter API v2 を使っています。[GitHub:minoue-xx/Tweet-New-MATLAB-Answers-Japanese](https://github.com/minoue-xx/Tweet-New-MATLAB-Answers-Japanese)
:::

![Capture.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/eb29a83d-9295-d417-b165-1fe9ae5656a7.png)


**MATLAB Answers？**: MathWorks が運営する MATLAB/Simulink に関する Q&A サイトです。スタッフ（社員）も回答していますがユーザー同士のやりとりも多数。

**動機**: MATLAB Answers では英語のやりとりがほとんどなので、日本語の質問も目立たせたい。


わざわざ Bot 自作しなくても同様のサービスなんていくらでもあるとは思います。
ただ、自分でやってみるのは自由に色々いじれますし面白いですよね?
ということで、この記事では 

- MATLAB Answers の新着質問情報の抽出方法（XML, RSS）
- ThingSpeak から Twitter への自動投稿 (ThingTweet)

を解説します。参考にして頂けると幸いです。





# 環境

MATLAB R2019a (Toolbox 不要）
[ThingSpeak](https://thingspeak.com?s_eid=PSM_29435) (MATLAB Analysis, ThingTweet)

### なぜ ThingSpeak で？

ThingSpeak には MATLAB Analysis と名のついた機能があり、MATLAB のコードが実行できるので、MATLAB に慣れていると大変馴染みやすい。しかも TimeControl を使って定期的に自動実行（例えば15分毎）も設定可能。そう、サーバーいらず。

### そもそも ThingSpeak って？

いわゆる IoT プラットフォームですが、以下の２資料が分かりやすいと思います。

- [iwathiの/var/log: データを簡単に保存&グラフ化できるThingSpeakが便利！](http://iwathi3.hatenablog.com/entry/Data-to-Graph-ThingSpeak)
- [Qiita: ThingSpeakのMATLAB VisualizationsでOpenWeatherMapのAPIを叩いてグラフ表示](https://qiita.com/iton/items/3cf4f14f92b4d809b555)

あくまで IoT プラットフォームなので、本来は Raspberry Pi などのハードウェアからの情報を吸い上げていろいろ楽しいことをするのが正しい使い方なんですが、今回はウェブ上で完結しちゃいます。


# 各種設定

## ThingTweet の設定

Twitterアカウントをリンクして、API Key の取得が必要です。

![Capture.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/124e141e-88d1-9efc-749b-86ae101da787.png)

## Channel作成

実際の Channel は[こちら](https://thingspeak.com/channels/854373)で確認できますが、
以下の4つの情報をトラックしています。

- LatestID_JP: 日本語質問の最新ID番号
- FlagLatest: 新着があればあがるフラグ
- FailTwitterPost: Twitter 投稿に何か問題があればあがるフラグ
- FailAnswersRead: 日本語投稿のチェックに何か問題があればあがるフラグ

それぞれの推移はこんな感じ。

![Capture.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/5a3814a8-30ee-c99e-855f-4b6dee78fae6.png)

とくに問題はなさそうですね。




# さて本題の MATLAB Analysis

ここで紹介するコードが ThingSpeak 上で15分に一回実行され、

- 新着チェック
- 新着情報を Tweet

を実行する仕組みとなってます。コード全文は最後に纏めて再掲しますので、ここでは要所だけ解説します。

![Capture.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149511/b3891c5c-8e67-56d6-8b90-36307540e7b8.png)

## MATLAB Answers 投稿チェック

MATLAB Answers トップページの RSS（日本語、投稿の新しい順表示）を使います。
トップページだけだと 50 投稿分しかチェックできませんが、今のところ15分の間に50個以上の日本語で質問が投稿されたことはないので、ひとまずOKとします。

XML フォーマットを解析するために [xmlread 関数](https://jp.mathworks.com/help/matlab/ref/xmlread.html?s_eid=PSM_29435) を使いました。href 値の取得に手間取りましたが

> [Qiita: RSSのlinkタグのhrefの値の取得の仕方](https://qiita.com/fantm21/items/2dcb5ff204a2b99cc182)

に助けられました。ありがとうございます！


```matlab:analyzeRSSfeed.m

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番号と比較します。

```m:isUpdated.m
% 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 もあるので、投稿文は区別しておきます。


```m:post2twitter.m

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 の情報を更新しておきます。

```m:updateID.m
%% Write Data %%
analyzedData = {LatestID_JP, FlagLatest, FailTwitterPost, FailAnswersRead};
thingSpeakWrite(channelID, analyzedData , 'WriteKey', writeAPIKey);
```


# まとめ

ThingSpeak を使って MATLAB Answers bot を作りました。
XMLフォーマットから情報を吸い上げる部分が一番苦労したところでした。



## MATLAB コード全文（再掲）

各 APIKey だけ設定すればそのまま機能するはず。

```m:ScriptOnThingSpeak.m
% 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);
```