【RaspberryPi】Node-REDでTwitterを使用する

なかなかやることが見つけられないということもあり、久しぶりにNode-REDを触ろうかなと思いました。 そこで、まだやっていないことをリストアップしてみたところTwitter関連はまだやっていなかったので、 いいタイミングなのでチャレンジします!

また、Node-REDでツイート収集ができればBotやその他のWeb APIとの連携も容易になると思います。

念の為…

念のためバージョンアップしておきます。Ver1.0.4からVer1.0.6 たまたま運良く作業当日にVer1.0.6にバージョンアップしたようです。

【参考】

uepon.hatenadiary.com

$ sudo npm install -g --unsafe-perm node-red
/usr/bin/node-red -> /usr/lib/node_modules/node-red/red.js
/usr/bin/node-red-pi -> /usr/lib/node_modules/node-red/bin/node-red-pi
+ node-red@1.0.6
removed 4 packages and updated 23 packages in 254.657s

【実行画面】 f:id:ueponx:20200424222151p:plain

Node-RED実行します。(サービスからでもOKです。)

$ node-red

【実行画面】 f:id:ueponx:20200424222417p:plain

しっかりとバージョンが1.0.6が表示されています。

Webブラウザからフローエディタへアクセスし、バージョンを確認してみます。

f:id:ueponx:20200425105230p:plain

これで準備ができました。

ノードの追加

Twitterの処理を行うノードの追加を行います。

画面右側にあるハンバーガーメニュー【三】から【パレットの管理】を選択すると、

f:id:ueponx:20200425104339p:plain

ユーザ設定のダイアログが表示されます。つづいて【ノードの追加】タブをクリックします。

f:id:ueponx:20200425105614p:plain

すると、追加するノードを検索する画面に表示が変わります。

f:id:ueponx:20200425105839p:plain

追加するのはnode-red-node-twitterとなるので、入力ボックスにtwitterと入力します。すると一番上あたりに該当のノードが出現するので、右側にある【ノード】追加ボタンをクリックしてインストールを行います。

f:id:ueponx:20200425111811p:plain

インストール後はNode-REDを再起動を行います。

都度起動している場合にはプロセスを終了して再度以下のコマンドを実行します。

$ node-red

サービスで起動している場合には以下で再起動させます。

$ sudo systemctl restart nodered.service

サービスで再起動した場合にはコンソール上には表示がされないので以下のコマンドで実行状態を確認します。

$ sudo systemctl status nodered.service

f:id:ueponx:20200425113629p:plain

active(running)となっていれば無事に起動しています。

再起動したら、先程インストールされたノードがパレットに追加されているか確認します。

f:id:ueponx:20200425114315p:plain

無事にソーシャルというグループにtwitter intwitter outのノードが追加されています。今回はtwitter inを使って作っていこうと思います。

twitter inノードでツイートを取得する

まずは、twitter inノードdebugノードを取り出し、以下のように端子をつなげます。

f:id:ueponx:20200425115840p:plain

とりだしたtwitter inノードをダブルクリックして、【twitter in ノードを編集】ダイアログを開きます。 この画面ではTwitterのツイート取得に関する情報を設定することになります。Twitter IDの入力ボックスの右側にある【ペン】ボタンをクリックします。

f:id:ueponx:20200425121659p:plain

twitter-credentialsの画面に変わります。ここではTwitter アプリに関する認証情報を入力することになります。

f:id:ueponx:20200425202550p:plain

ここでは以下の項目を入力していくことになります。

事前にTwitterアプリを作成しておかないといけません。ポリシーの変更により、以前とは大きく異なって英語でどういうアプリなのかなどの申請を行って承認を得なければ作成することはできないようになっています。つまり即作成できるわけではありませんので、できるだけ事前に作成しておきましょう。 作成したアプリページは以下になります。(ちなみに同じページからアプリが作成申請を行えます)

https://developer.twitter.com/en/apps

事前に作成していればリスト形式でアプリが並びます。使用するアプリの【Details】ボタンをクリックし、

f:id:ueponx:20200425125638p:plain

アプリの詳細画面から【Keys and tokens】タブをクリックして、

f:id:ueponx:20200425125647p:plain

KeyとTokenが表示される画面に移ります。ここではKeyは表示されていますが、Tokenに関してはXで伏せられています。 作成時に表示されたTokenをメモしておけば、それを使用することになりますがメモを忘れた場合には【Regenerate】ボタンをクリックして再生成しましょう。

f:id:ueponx:20200425125653p:plain

Twitterのページで以下の情報を得ることができたので

フローエディタの画面に戻って入力していきます。

f:id:ueponx:20200425125942p:plain

入力が完了したら右上にある更新ボタンをクリックします。

f:id:ueponx:20200425130323p:plain

これでTwitterアプリの認証情報が出来上がりました。(twitter inノードを編集画面のTwitter IDにさきほど入力したユーザー名が入っていれば完了しています)

f:id:ueponx:20200425130421p:plain

あとは【検索対象】の入力ボックスにすべての公開ツイートをセットし、

f:id:ueponx:20200425131855p:plain

【検索条件】の入力ボックスにハッシュタグ#nhkを入力します。

f:id:ueponx:20200425130942p:plain

すべての設定が入力し終わったら右上にある【完了】ボタンをクリックします。 これでTwitterアプリの設定は完了しました。

f:id:ueponx:20200425131629p:plain

デプロイして動作を確認する

まずはデプロイを行います。画面右上にある【デプロイ】ボタンをクリックして

f:id:ueponx:20200425132627p:plain

以下のような表示がでれば完了です。

f:id:ueponx:20200425132712p:plain

デプロイ完了後から自動でツイートを収集をはじめます。

デバック画面を表示すると以下のように次々と表示されていきます。

f:id:ueponx:20200425133116p:plain

twitter inノードの解説では以下のように書かれています。

f:id:ueponx:20200425131142p:plain

これを参考にして確認をしてみたいと思います。

Debugノードpayload表示

これツイートの本文が入っています。絵文字や改行コード(”\n”)なども含んだ形で入っているようです。

【msg.payload】の表示

"今週も #NHKジャーナル をお聴き頂きありがとうございました。\n#ステイホーム のお供に聴き逃し配信や #読むらじる。もおすすめです📻\n#ニュースで短歌 も近日UP予定📖\n我慢することが多いと思いますが少しでも息抜きできる工夫を… https://t.co/Q3D79dbj3A"```

Debugノードオブジェクト全体表示

デバックをこちらに変更するとツイート全体のJSONデータが表示されます。ユーザー名やプロフィールリンク、ツイート日時などが必要であればこちらのJSONデータを解析することになります。以下はNHKラジオニュースさん公式のツイートとなります。

【msgオブジェクト全体】の表示

{
    "topic": "tweets/nhk_radio_news",
    "payload": "今週も #NHKジャーナル をお聴き頂きありがとうございました。\n#ステイホーム のお供に聴き逃し配信や #読むらじる。もおすすめです📻\n#ニュースで短歌 も近日UP予定📖\n我慢することが多いと思いますが少しでも息抜きできる工夫を… https://t.co/Q3D79dbj3A",
    "lang": "ja",
    "tweet": {
        "created_at": "Fri Apr 24 14:10:10 +0000 2020",
        "id": 1253687683504697300,
        "id_str": "1253687683504697344",
        "text": "今週も #NHKジャーナル をお聴き頂きありがとうございました。\n#ステイホーム のお供に聴き逃し配信や #読むらじる。もおすすめです📻\n#ニュースで短歌 も近日UP予定📖\n我慢することが多いと思いますが少しでも息抜きできる工夫を… https://t.co/Q3D79dbj3A",
        "source": "<a href=\"http://www.nhk.or.jp/\" rel=\"nofollow\">NHK</a>",
        "truncated": true,
        "in_reply_to_status_id": null,
        "in_reply_to_status_id_str": null,
        "in_reply_to_user_id": null,
        "in_reply_to_user_id_str": null,
        "in_reply_to_screen_name": null,
        "user": {
            "id": 121348372,
            "id_str": "121348372",
            "name": "NHKラジオニュース",
            "screen_name": "nhk_radio_news",
            "location": "NHKラジオセンター",
            "url": "http://nhk.jp/radionews",
            "description": "▽マイあさ https://t.co/oQVD9ltuMO ▽Nらじ https://t.co/blhHtePPMt ▽NHKジャーナル https://t.co/rbLD12NKAZ の情報を発信。皆さんからのツイートは番組などでご紹介させていただくことがあります。各番組名のハッシュタグをつけて下さい。▼利用規約 https://t.co/Q11WanxIUh",
            "translator_type": "none",
            "protected": false,
            "verified": true,
            "followers_count": 27507,
            "friends_count": 62,
            "listed_count": 1455,
            "favourites_count": 47,
            "statuses_count": 136004,
            "created_at": "Tue Mar 09 06:32:44 +0000 2010",
            "utc_offset": null,
            "time_zone": null,
            "geo_enabled": false,
            "lang": null,
            "contributors_enabled": false,
            "is_translator": false,
            "profile_background_color": "DDF0FF",
            "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
            "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
            "profile_background_tile": true,
            "profile_link_color": "0084B4",
            "profile_sidebar_border_color": "C0DEED",
            "profile_sidebar_fill_color": "DDEEF6",
            "profile_text_color": "333333",
            "profile_use_background_image": true,
            "profile_image_url": "http://pbs.twimg.com/profile_images/1111584406517370885/9FX2-djD_normal.jpg",
            "profile_image_url_https": "https://pbs.twimg.com/profile_images/1111584406517370885/9FX2-djD_normal.jpg",
            "profile_banner_url": "https://pbs.twimg.com/profile_banners/121348372/1553857350",
            "default_profile": false,
            "default_profile_image": false,
            "following": null,
            "follow_request_sent": null,
            "notifications": null
        },
        "geo": null,
        "coordinates": null,
        "place": null,
        "contributors": null,
        "is_quote_status": false,
        "extended_tweet": {
            "full_text": "今週も #NHKジャーナル をお聴き頂きありがとうございました。\n#ステイホーム のお供に聴き逃し配信や #読むらじる。もおすすめです📻\n#ニュースで短歌 も近日UP予定📖\n我慢することが多いと思いますが少しでも息抜きできる工夫を見つけながらお過ごしください(菅野)\nhttps://t.co/kKpOyHEBkN",
            "display_text_range": [
                0,
                157
            ],
            "entities": {
                "hashtags": [
                    {
                        "text": "NHKジャーナル",
                        "indices": [
                            4,
                            13
                        ]
                    },
                    {
                        "text": "ステイホーム",
                        "indices": [
                            33,
                            40
                        ]
                    },
                    {
                        "text": "読むらじる",
                        "indices": [
                            53,
                            59
                        ]
                    },
                    {
                        "text": "ニュースで短歌",
                        "indices": [
                            69,
                            77
                        ]
                    }
                ],
                "urls": [
                    {
                        "url": "https://t.co/kKpOyHEBkN",
                        "expanded_url": "https://www4.nhk.or.jp/nhkjournal/",
                        "display_url": "www4.nhk.or.jp/nhkjournal/",
                        "indices": [
                            134,
                            157
                        ]
                    }
                ],
                "user_mentions": [],
                "symbols": []
            }
        },
        "quote_count": 0,
        "reply_count": 0,
        "retweet_count": 0,
        "favorite_count": 0,
        "entities": {
            "hashtags": [
                {
                    "text": "NHKジャーナル",
                    "indices": [
                        4,
                        13
                    ]
                },
                {
                    "text": "ステイホーム",
                    "indices": [
                        33,
                        40
                    ]
                },
                {
                    "text": "読むらじる",
                    "indices": [
                        53,
                        59
                    ]
                },
                {
                    "text": "ニュースで短歌",
                    "indices": [
                        69,
                        77
                    ]
                }
            ],
            "urls": [
                {
                    "url": "https://t.co/Q3D79dbj3A",
                    "expanded_url": "https://twitter.com/i/web/status/1253687683504697344",
                    "display_url": "twitter.com/i/web/status/1…",
                    "indices": [
                        117,
                        140
                    ]
                }
            ],
            "user_mentions": [],
            "symbols": []
        },
        "favorited": false,
        "retweeted": false,
        "possibly_sensitive": false,
        "filter_level": "low",
        "lang": "ja",
        "timestamp_ms": "1587737410795"
    },
    "location": {
        "place": "NHKラジオセンター"
    },
    "_msgid": "801cfa15.d25888"
}

これでTwitterAPIを使用してツイートデータを取得することができるようになりました。RaspberryPiでは非力なので、ある程度検索ワードで絞ったほうがいいのかなという印象です。

取得したツイートをデータベースに格納する

ここまでではツイートを取得するだけで、保存はできません。 せっかくなので今回取得したツイートをSQLiteのデータベースに格納しようと思います。

SQLiteに関しては基本的には以前やった内容の復習となります。

【参考】 uepon.hatenadiary.com

ではSQLiteの格納に関するフローを作成します。復習になるので、全体像をはじめにみせます。

f:id:ueponx:20200425145650p:plain

クエリ文字列をinjectノードから出力し、そのクエリを実行、debugノードで表示するという形式になります。

では、クエリ文字列をinjectノードから出力する部分を見てみます。この部分が一番重要ともともいえます。

f:id:ueponx:20200425145710p:plain

4つのノードを作成してみました。

  • テーブル生成
  • テストデータの登録(デバック用)
  • テーブル内のデータの表示
  • テーブルの削除

テーブル生成

テーブル作成のノードを以下のようにします。

f:id:ueponx:20200425145653p:plain

【トピック】の入力ボックスへは以下のクエリを入力して、【完了】ボタンをクリックします。テーブルが存在しなければテーブルを作成するようにしています。また、テーブルに格納する値は以下としました。

  • プライマリーキーとなるID(自動カウントアップ)
  • ツイートの日時
  • スクリーンネーム
  • ネーム
  • ツイート
  • フラグ(特に必要はありませんが…)
  • ユーザーのアイコン画像URL

入力するクエリ文字列は以下の様にしています。

CREATE TABLE IF NOT EXISTS TW (ID INTEGER PRIMARY KEY AUTOINCREMENT, DATE TEXT, SCREENNAME TEXT, NAME TEXT, TW TEXT, OAFLAG TEXT, ICON_URI TEXT)

テストデータの登録

次はデバック様に作ったテストデータの登録になります。特に大きな意味はありませんが、記述などで詰まったところでデータを入れてテストするといいと思います。

f:id:ueponx:20200425145657p:plain

【トピック】の入力ボックスへは以下のクエリを入力して、【完了】ボタンをクリックします。ツイートされた日時のタイムゾーンはローカルタイムゾーンになっていないので使用する場合には、+0900などに調整する必要があります。

INSERT INTO TW VALUES(NULL, 'Sun Nov 10 12:21:46 +0000 2019', 'スクリーンネーム', 'なまえ', 'ついーと' , '0', 'アイコン.jpg');

テーブル内のデータの表示

これは作成したテーブルの中身を表示させるノードです。【トピック】の入力ボックスへは以下のクエリを入力して、【完了】ボタンをクリックします。

f:id:ueponx:20200425145701p:plain

select * from TW;

テーブルの削除

これはテーブル削除のノードです。【トピック】の入力ボックスへは以下のクエリを入力して、【完了】ボタンをクリックします。

f:id:ueponx:20200425145707p:plain

drop table TW;

sqliteノードの追加

sqliteノードを追加します。

f:id:ueponx:20200425145714p:plain

データベースファイルは/home/pi/tweet.dbを指定しておきます。

f:id:ueponx:20200425202410p:plain

debugノードの追加

最後は処理結果を表示するdebugノードを追加しておきます。

f:id:ueponx:20200425145717p:plain


Twitter アプリのデータをSQLiteに格納する

いよいよ、ツイートデータを格納します。ツイートデータもすべてが必要ではないので、検索されたツイートのJSONデータの一部のものがだけが必要なので、そのデータをクエリに埋め込みます。クエリの作成はtemplateノードを追加して行います。

f:id:ueponx:20200425145720p:plain

場所としてはtwitter inノードdebugノードの間に格納します。各パラメータはJSONデータを参照することで取り出すことができます。今回はJSONデータのtweetの中にあります。

  • プライマリーキーID … NULL
  • ツイートの日時 … tweet.created_at
  • スクリーンネーム … tweet.user.screen_name
  • ネーム … tweet.user.name
  • ツイート … tweet.text
  • フラグ(特に必要はありませんが…) … 0
  • ユーザーのアイコン画像URL … tweet.user.profile_image_url_https

となります。テンプレートの中に埋め込む場合には{{ }}が必要になります。例えば、tweet.created_atを埋め込む場合には{{tweet.created_at}}となります。

f:id:ueponx:20200425145736p:plain

テンプレートには以下のテキストを入力して、【完了】ボタンをクリックします。

INSERT INTO TW VALUES(NULL, '{{tweet.created_at}}', '{{tweet.user.screen_name}}', '{{tweet.user.name}}', '{{tweet.text}}', '0', '{{tweet.user.profile_image_url_https}}');

ここまでできたら、テンプレートの出力端子をクエリの実行につなぎ、右上の【デプロイ】をクリックします。

f:id:ueponx:20200425145723p:plain

すると、自動的にツイートを取得し、データベースに格納されていきます。(ただし、SQLiteのテーブルを作成していない場合にはエラーとなります)

f:id:ueponx:20200425145740p:plain

画面をぼかしていますが、フローエディタの右側のデバックタブにツイートが表示されています。


twitter in】ノードを停止する場合

デプロイするとtwitter inノードは自動的に収集動作を開始してしまいます。debugノードを接続した状態だと常に表示更新をし続けてしまうので、これでは面倒です。そういう場合には、twitter inノードをダブルクリックし、【twitter in ノードを編集】画面を開き、一番下の【有効】ボタンをクリックして、

f:id:ueponx:20200425202248p:plain

【無効】に変更します。

f:id:ueponx:20200425145730p:plain

するとtwitter inノードが破線の状態になり、動作が無効になります。

f:id:ueponx:20200425145733p:plain

便利なのでメモとしておきます。

おわりに

Node-REDのTwitterノードをしようすることで簡単にツイートデータを収集することができるようになりました。慣れればすぐにできるようになると思います。ただ、DBに格納するとなるとクエリを書く必要があるので、その部分ではコーディングに近い作業が必要かなと思います。収集した後はデータを分析に使うなりなんなりすることができそうです。

他にもWebsocketのサーバーにできればクライアント側との通信を行ってWEB上に表示するといったことも可能かなと思います。Javascriptの知識は必要そうですけどね。

以下に本日作成したフローをエクスポートしました。ちなみにTwitterノードはエクスポートした際にはCredential情報は削除されます。これは情報を公開する側からするといい方法だなと思いました。

JSONファイル抜粋】

f:id:ueponx:20200425234218p:plain

[
    {
        "id": "c1a07194.6d43b",
        "type": "tab",
        "label": "フロー 1",
        "disabled": false,
        "info": ""
    },
    {
        "id": "8c0b920e.bad9a",
        "type": "debug",
        "z": "c1a07194.6d43b",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "x": 850,
        "y": 400,
        "wires": []
    },
    {
        "id": "71c929fa.e1a548",
        "type": "inject",
        "z": "c1a07194.6d43b",
        "name": "テストデータの登録",
        "topic": "INSERT INTO TW VALUES(NULL, 'Sun Nov 10 12:21:46 +0000 2019', 'スクリーンネーム', 'なまえ', 'ついーと' , '0', 'アイコン.jpg');",
        "payload": "",
        "payloadType": "date",
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "x": 170,
        "y": 440,
        "wires": [
            [
                "e1784590.017c28"
            ]
        ]
    },
    {
        "id": "168be7d7.7bb9c8",
        "type": "inject",
        "z": "c1a07194.6d43b",
        "name": "テーブル内のデータの表示",
        "topic": "select * from TW;",
        "payload": "",
        "payloadType": "date",
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "x": 190,
        "y": 480,
        "wires": [
            [
                "e1784590.017c28"
            ]
        ]
    },
    {
        "id": "2e977694.7c7eba",
        "type": "inject",
        "z": "c1a07194.6d43b",
        "name": "テーブルの削除",
        "topic": "drop table TW;",
        "payload": "",
        "payloadType": "date",
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "x": 160,
        "y": 520,
        "wires": [
            [
                "e1784590.017c28"
            ]
        ]
    },
    {
        "id": "81e5a60.fd8fe58",
        "type": "inject",
        "z": "c1a07194.6d43b",
        "name": "テーブル生成",
        "topic": "CREATE TABLE IF NOT EXISTS TW (ID INTEGER PRIMARY KEY AUTOINCREMENT, DATE TEXT, SCREENNAME TEXT, NAME TEXT, TW TEXT, OAFLAG TEXT, ICON_URI TEXT)",
        "payload": "",
        "payloadType": "date",
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "x": 150,
        "y": 400,
        "wires": [
            [
                "e1784590.017c28"
            ]
        ]
    },
    {
        "id": "e1784590.017c28",
        "type": "sqlite",
        "z": "c1a07194.6d43b",
        "mydb": "56879991.d56508",
        "sqlquery": "msg.topic",
        "sql": "",
        "name": "クエリの実行",
        "x": 640,
        "y": 400,
        "wires": [
            [
                "8c0b920e.bad9a"
            ]
        ]
    },
    {
        "id": "1f44d7ed.263438",
        "type": "comment",
        "z": "c1a07194.6d43b",
        "name": "データベース処理",
        "info": "",
        "x": 150,
        "y": 360,
        "wires": []
    },
    {
        "id": "544f2b7c.eed8f4",
        "type": "twitter in",
        "z": "c1a07194.6d43b",
        "d": true,
        "twitter": "",
        "tags": "#nhk",
        "user": "false",
        "name": "read Tweet",
        "inputs": 0,
        "x": 140,
        "y": 597,
        "wires": [
            [
                "71ae5ab9.f11f64"
            ]
        ]
    },
    {
        "id": "795001b7.1378",
        "type": "debug",
        "z": "c1a07194.6d43b",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "topic",
        "targetType": "msg",
        "x": 640,
        "y": 597,
        "wires": []
    },
    {
        "id": "71ae5ab9.f11f64",
        "type": "template",
        "z": "c1a07194.6d43b",
        "name": "",
        "field": "topic",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "INSERT INTO TW VALUES(NULL, '{{tweet.created_at}}', '{{tweet.user.screen_name}}', '{{tweet.user.name}}', '{{tweet.text}}', '0', '{{tweet.user.profile_image_url_https}}');",
        "output": "str",
        "x": 420,
        "y": 597,
        "wires": [
            [
                "795001b7.1378",
                "e1784590.017c28"
            ]
        ]
    },
    {
        "id": "f0d739b1.3ac638",
        "type": "comment",
        "z": "c1a07194.6d43b",
        "name": "※Twitter-inノードのユーザー情報はexportすると消える",
        "info": "",
        "x": 280,
        "y": 657,
        "wires": []
    },
    {
        "id": "56879991.d56508",
        "type": "sqlitedb",
        "z": "",
        "db": "/home/pi/tweet.db",
        "mode": "RWC"
    }
]

【参考】

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com