8
9

More than 3 years have passed since last update.

TLに流れてくるネガティブなツイートを近しい松岡修造語録に上書きするChrome拡張を作ってみた

Last updated at Posted at 2020-03-13

こんにちは!

突然ですがみなさんのTLはポジティブですか?たまにはネガティブなツイートも流れてきますよね。

文章の力は偉大です。
ネガティブな文章は人をネガティブに、ポジティブな文章は人をポジティブに変える力を持っています。
できればポジティブなツイートに囲まれて幸せなツイッターライフを満喫したいところです。

ポジティブといえば誰を思い浮かべますか?
そうですね。松岡修造さんですよね。

ネガティブなTweetは松岡修造さんのお言葉から同等の意味のものを拝借することで伝えたいことを崩さずにTLをポジティブな空間にすることができるかもしれません。

ということでCOTOHA APIを使ってポジティブでないTweetを似た意味の松岡修造語録に書き換えちゃうChrome拡張機能を作ってみたいと思います。

COTOHA API

COTOHA APIはNTTコミュニケーションズさんが提供する自然言語処理・音声処理APIプラットフォームです。

今回はその中でも『感情分析API』を使ってポジティブではないツイートを検出し、『類似度算出API』を使って最も近しい松岡修造語録を見つけたいと思います。

つくってみる。

Chrome拡張機能の作り方のベースは「Chrome 拡張機能でタイムライン上のツイートを「ばぶ」らせてみた - Qiita」を多大に参考にさせていただきました。

とりあえず、適当にワークスペースを作って開発にとりかかります。

マニフェストファイルを作成する

Chrome拡張機能の基本的な設定や情報を記載するmanifest.jsonを作成します。
今回はChrome拡張のアイコンをクリックしたら処理が実行されるようなアプリケーションを検討しているので、以下のようにmanifest.jsonを記述します。

manifest.json
{
  "name": "positter",
  "description": "positter deletes negative tweets from your time line.",
  "manifest_version": 2,
  "version": "1.0",
  "browser_action": {},
  "content_scripts": [
    {
      "matches": [ "https://twitter.com/*" ],
      "js": [ "js/jquery.min.js", "js/content.js" ]
    }
  ],
  "background": {
    "scripts": [ "js/background.js" ],
    "persistent": false
  },
  "permissions": [
    "https://xxxxxxxxxx.com/*" // COTOHA APIのアカウントホームページでBase URLを確認
  ]
}

特に重要な奴らを紹介します。

content_scripts

content_scriptsは読み込むjsファイルとそれを読み込むページを定義しています。
今回はTwitterのページに限り、js/jquery.min.jsjs/content.jsを読み込みます。これらのファイルは後々作成します。

background

backgroundはバックグラウンドで動くjsを定義しています。今回はjs/background.jsを読み込みます。
persistentfalseに指定することで特定のイベント時のみ起動するようになります。(これをしないとずっとバックグランドで処理が動いちゃうので非推奨です:Manage Events with Background Scripts - Google Chrome

permissions

permissionsは利用できる外部APIの穴あけを定義する箇所です。今回はCOTOHA APIのbase urlを設定しておきます。base url

jqueryを使えるようにする

まずはjQueryを使ってささっと開発をしたいので、公式HPからファイルをダウンロードしてjs/jquery.min.jsとして保存します。

backgroundでアイコンクリックを検知する

backgroundでアイコンクリックを検知して、content_scriptsにそれを通知してあげます。

js/background.js
chrome.browserAction.onClicked.addListener(function(tab) {
  chrome.tabs.sendMessage(tab.id, "clicked")
})

chrome.browserAction.onClicked.addListenerでChrome拡張のアイコンがクリックされたイベントを検知して、chrome.tabs.sendMessagecontent_scriptstab.idclickedというメッセージを送信しています。

content_scriptsでこのclickedというメッセージを受け取った場合の処理をコーディングしていきます。

content_scriptsでメッセージを受信する

content_scriptsがメッセージを受診した後にやるべきことは以下です。

  1. 現在開いているページのTLからTweetsの内容を取得(get_tweets
  2. COTOHA APIのアクセストークンを取得する(get_access_token
  3. 取得したTweetsのネガポジ判定をする(get_tweet_sentiment
  4. ポジティブでないTweetsと近しい内容の松岡修造語録を取得する(get_similar_shuzo_quote
  5. ポジティブでないTweetsの内容を松岡修造語録に変換する(change_tweet

ひとまずそれぞれの処理の詳細はおいておいて、backgroundのメッセージを受け取って、それぞれの処理を実行する外形を作ります。

js/content.js
// backgroundからメッセージを受信して処理を実行する
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  if ( request == "clicked" ) {

    // 現在開いているページのTLからTweetsの内容を取得(`get_tweets`)
    tweets = get_tweets()

    // COTOHA APIのアクセストークンを取得する(`get_access_token`)
    get_access_token().then((access_token) => {

      // tweetsから1件ずつtweetに格納して処理する
      for (let tweet of tweets) {

        // 取得したtweetのネガポジ判定をする(`get_tweet_sentiment`)
        get_tweet_sentiment(access_token, tweet).then(sentiment => {

          // tweetがポジティブでないと判断した場合、処理を継続。
          if (sentiment != "Positive") {

            // ポジティブでないtweetと近しい内容の松岡修造語録を取得する(`get_similar_shuzo_quote`)
            get_similar_shuzo_quote(access_token, tweet).then(new_tweet => {

              // ポジティブでないtweetの内容を松岡修造語録に変換する(`change_tweet`)
              change_tweet(tweet, new_tweet)
            })
          }
        })
      }
    })
  }
})

かなり段階的になってしまっていますが、内容自体は単純です。あとは5つの関数を定義していくだけです。
メッセージのやりとりについては「Chrome 拡張機能で background scripts から content scripts にメッセージを送信する - to-me-mo-rrow - 未来の自分に残すメモ -」の記事も参考にさせていただきました。

それでは、ここからは各ファンクションをコーディングしていきます!

get_tweets

get_tweetsはTLからTweetを抜き出してくる機能にします。
jQueryで文字列は要素.text()で抽出することができます。要素の特徴(idとかclassとか)さえわかってしまえば造作もありません。

ということでTwitterのTLの構造を理解するためにbodyタグを抽出してみます!

<body cz-shortcut-listen="true" style="background-color: rgb(255, 255, 255);">
  <!-- 省略 -->
  <div id="react-root" style="height:100%;display:flex;">
    <div class="css-1dbjc4n r-13awgt0 r-12vffkv" data-reactroot="">
      <div class="css-1dbjc4n r-13awgt0 r-12vffkv">
        <!-- 省略 -->
        <div class="css-1dbjc4n r-18u37iz r-1pi2tsx r-13qz1uu r-417010" data-at-shortcutkeys="{&quot;n&quot;:&quot;新しいツイート&quot;,&quot;Cmd Enter&quot;:&quot;ツイートを送信&quot;,&quot;m&quot;:&quot;ダイレクトメッセージを作成&quot;,&quot;/&quot;:&quot;検索&quot;,&quot;l&quot;:&quot;いいね&quot;,&quot;r&quot;:&quot;返信&quot;,&quot;t&quot;:&quot;リツイート&quot;,&quot;s&quot;:&quot;ツイートを共有&quot;,&quot;b&quot;:&quot;ブックマーク&quot;,&quot;u&quot;:&quot;アカウントをミュート&quot;,&quot;x&quot;:&quot;アカウントをブロック&quot;,&quot;Enter&quot;:&quot;ツイートの詳細を開く&quot;,&quot;o&quot;:&quot;画像を開く&quot;,&quot;?&quot;:&quot;ショートカットのヘルプ&quot;,&quot;j&quot;:&quot;次のツイート&quot;,&quot;k&quot;:&quot;前のツイート&quot;,&quot;Space&quot;:&quot;ページ下へ移動&quot;,&quot;.&quot;:&quot;最新ツイートを読み込む&quot;,&quot;g h&quot;:&quot;ホーム&quot;,&quot;g e&quot;:&quot;話題を検索&quot;,&quot;g n&quot;:&quot;通知&quot;,&quot;g r&quot;:&quot;@ツイート&quot;,&quot;g p&quot;:&quot;プロフィール&quot;,&quot;g l&quot;:&quot;いいね&quot;,&quot;g i&quot;:&quot;リスト&quot;,&quot;g m&quot;:&quot;ダイレクトメッセージ&quot;,&quot;g s&quot;:&quot;設定&quot;,&quot;g b&quot;:&quot;ブックマーク&quot;,&quot;g u&quot;:&quot;プロフィールページを見る...&quot;,&quot;g d&quot;:&quot;表示設定&quot;}" aria-hidden="false" style="min-height: 789px;">
          <!-- 省略 -->
          <main role="main" class="css-1dbjc4n r-1habvwh r-16y2uox r-1wbh5a2">
            <div class="css-1dbjc4n r-150rngu r-16y2uox r-1wbh5a2 r-1obr2lp">
              <div class="css-1dbjc4n r-aqfbo4 r-16y2uox">
                <div class="css-1dbjc4n r-1oszu61 r-1niwhzg r-18u37iz r-16y2uox r-1wtj0ep r-2llsf r-13qz1uu">
                  <div class="css-1dbjc4n r-14lw9ot r-1tlfku8 r-1ljd8xs r-13l2t4g r-1phboty r-1jgb5lz r-11wrixw r-61z16t r-1ye8kvj r-13qz1uu r-184en5c" data-testid="primaryColumn">
                    <div class="css-1dbjc4n">
                      <!-- 省略 -->
                      <div class="css-1dbjc4n r-1jgb5lz r-1ye8kvj r-13qz1uu">
                        <div class="css-1dbjc4n">
                          <div class="css-1dbjc4n">
                            <section aria-labelledby="accessible-list-1" role="region" class="css-1dbjc4n">
                              <h1 aria-level="1" dir="auto" role="heading" class="css-4rbku5 css-901oao r-4iw3lz r-1xk2f4g r-109y4c4 r-1udh08x r-wwvuq4 r-u8s1d r-92ng3h" id="accessible-list-1">ホームタイムライン</h1>
                              <div aria-label="タイムライン: ホームタイムライン" class="css-1dbjc4n">
                                <div style="padding-bottom: 0px;">
                                  <div style="padding-top: 0px; padding-bottom: 9800px;">
                                    <!-- ここから1つのTweetはじまり -->
                                    <div>
                                      <div class="css-1dbjc4n r-my5ep6 r-qklmqi r-1adg3ll">
                                        <article aria-haspopup="false" role="article" data-focusable="true" tabindex="0" class="css-1dbjc4n r-1loqt21 r-1udh08x r-o7ynqc r-1j63xyz">
                                          <div class="css-1dbjc4n">
                                            <div class="css-1dbjc4n r-1j3t67a">
                                              <div class="css-1dbjc4n r-18u37iz r-thb0q2">
                                                <div class="css-1dbjc4n r-1iusvr4 r-16y2uox r-5f2r5o r-m611by">
                                                  <!-- 省略(●●がリツイート、など) -->
                                                </div>
                                              </div>
                                              <div class="css-1dbjc4n r-18u37iz r-thb0q2" data-testid="tweet">
                                                <div class="css-1dbjc4n r-1awozwy r-18kxxzh r-5f2r5o" style="flex-basis: 49px;">
                                                  <!-- 省略(ユーザーアイコン) -->
                                                </div>
                                                <div class="css-1dbjc4n r-1iusvr4 r-16y2uox r-1777fci r-5f2r5o r-1mi0q7o">
                                                  <div class="css-1dbjc4n">
                                                    <!-- 省略(ユーザー名など) -->
                                                  </div>
                                                  <div class="css-1dbjc4n">
                                                    <div class="css-1dbjc4n">
                                                      <div lang="ja" dir="auto" class="css-901oao r-hkyrab r-gwet1z r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-bnwqim r-qvutc0">
                                                        <span class="css-901oao css-16my406 r-gwet1z r-ad9z0x r-bcqeeo r-qvutc0">ここにTweetの文字列が表示されます!!</span>
                                                      </div>
                                                    </div>
                                                    <div class="css-1dbjc4n">
                                                      <!-- 省略(リンク・画像など) -->
                                                    </div>
                                                    <div role="group" class="css-1dbjc4n r-18u37iz r-1wtj0ep r-156q2ks r-1mdbhws">
                                                      <!-- 省略(リツイートアイコンなど) -->
                                                    </div>
                                                  </div>
                                                </div>
                                              </div>
                                            </div>
                                          </div>
                                        </article>
                                      </div>
                                    </div>
                                    <!-- ここまで1つのTweet -->
                                    <!-- あとはこのTweetブロックの繰り返し -->
                                  </div>
                                </div>
                              </div>
                            </section>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="css-1dbjc4n r-aqfbo4 r-zso239 r-1jocfgc" data-testid="sidebarColumn">
                    <!-- 省略(サイドメニューなど) -->
                  </div>
                </div>
              </div>
            </div>
          </main>
        </div>
      </div>
    </div>
  </div>
  <!-- 省略(scripts) -->
</body>

なにこれ長っ!深っ!複雑!

「ここにTweetの文字列が表示されます!!」っていうところが実際のTweetの文字列なんですが、深すぎる。複雑すぎる!すごくやめたいと思いました笑

ここは泥臭く構造を辿っていくしかないですね...特出すべきは各Tweetの文字列のspanの直前のdivタグにだけlang="ja"属性がついていることでしょうか。これをキーワードにして属性を取り出し、その中のテキストをtext()メソッドを使って取り出してみます。

js/content.js
...
function get_tweets() {
  tweets = []
  $("div[lang='ja']").each(function(i, target) {
    tweets.push($(target).text())
  })
  return tweets
}
...

これで表示されているTweetの文字列を配列で関数の呼び出し元に返してあげるプログラムを組むことができました。

このtweetsをCOTOHA APIで解析するために次はアクセストークンの払い出しを実行します。

get_access_token

ここからはCOTOHA APIと通信を行っていくのですが、CROBが効いてしまうのでcontent.jsからbackground.jsにメッセージを送り、background.js経由でCOTOHA APIを利用するようにします。

content.js
...
function get_access_token() {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage(
      {
        contentScriptQuery: "get_access_token"
      },
      (response) => { resolve(response) }
    )  
  })
}
...

content.jsからはcontentScriptQuery: "get_access_token"をキーワードにしてメッセージを送ります。

background.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    cotoha_api_base_url = "https://xxxxxxxxxx.com/" // COTOHA APIのアカウントホームページで確認

    // アクセストークンの払い出し
    if (request.contentScriptQuery == "get_access_token") {
    cotoha_access_token_publish_url = "https://xxxxxxxxxx.com/access_token" // COTOHA APIのアカウントホームページで確認
    cotoha_client_id = "xxxxxxxxxx" // COTOHA APIのアカウントホームページで確認
    cotoha_client_secret = "xxxxxxxxxx" // COTOHA APIのアカウントホームページで確認

    fetch(cotoha_access_token_publish_url, {
      method: 'POST',
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        grantType: "client_credentials",
        clientId: cotoha_client_id,
        clientSecret: cotoha_client_secret
      })
    })
    .then(response => response.json())
    .then(body => sendResponse(body.access_token))
    }

    return true
  }
)

background.jsでは、chrome.runtime.onMessage.addListenerでメッセージを受け取ります。requestの中に先ほどのcontentScriptQueryといった引数を受け取ることができ、sendResponseにデータを入れることでメッセージをお返しします。

APIはfetchを使って実行しています。
使い方は「Fetch を使う - Web API | MDN」などを参考に。
APIはかなりシンプルなので、特に迷うことはないかと思います。 => 「アクセストークン取得 | リファレンス | COTOHA API

最後にsendResponse(body.access_token)でAPIのレスポンスからaccess_tokenを取得してcontent.jsに投げ返してあげています。

これで他のAPIを使う準備ができたので、次はTweetsのネガポジ判定をしてみましょう!

get_tweet_sentiment

content.jsではtweetsaccess_tokenを取得した状態になっており、get_tweet_sentimentが実行されるタイミングではtweetsからひとつずつtweetを取り出してget_tweet_sentimentaccess_tokentweetを引き渡しています。

今回もAPIはかなりシンプルですので、とくに難しいところはないです。 => APIリファレンス | COTOHA API(感情分析)
sentenceに対象の文章を与えるだけで、その文章の感情分析や感情を強く感じる単語を抽出してくれます。レスポンスとして、result.sentimentでその文章が『Positive』なのか『Negative』なのか『Neutral』なのかを教えてくれます。

content.js
...
function get_tweet_sentiment(access_token, tweet) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage(
      {
        contentScriptQuery: "get_tweet_sentiment",
        access_token: access_token,
        tweet: tweet
      },
      (response) => { resolve(response) }
    )
  })
}
...
background.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    ...
    // ツイートのネガポジ判定
    if (request.contentScriptQuery == "get_tweet_sentiment") {
      cotoha_sentiment_api_endpoint = "nlp/v1/sentiment"

      fetch(cotoha_api_base_url + cotoha_sentiment_api_endpoint, {
        method: 'POST',
        headers: {
          "Content-Type": "application/json; charset=UTF-8",
          Authorization: `Bearer ${request.access_token}`
        },
        body: JSON.stringify({
          sentence: request.tweet
        })
      })
      .then(response => response.json())
      .then(body => {
        sendResponse(body.result.sentiment)
      })
    }
    ...
    return true
  }
)

content.jsから送っているtweetを検査対象として、sendResponseで感情分析結果をcontent.jsに返しています。
感情分析結果はPositive, Neutral, Negativeの3種類から判定されますが、今回はPositiveなTweetであふれかえすのが目的なのでNeutralNegativeを松岡修造語録へ変換する対象とします。

content.js
...
        get_tweet_sentiment(access_token, tweet).then(sentiment => {

          // tweetがポジティブでないと判断した場合、処理を継続。
          if (sentiment != "Positive") {

            // ポジティブでないtweetと近しい内容の松岡修造語録を取得する(`get_similar_shuzo_quote`)
            get_similar_shuzo_quote(access_token, tweet).then(new_tweet => {

              // ポジティブでないtweetの内容を松岡修造語録に変換する(`change_tweet`)
              change_tweet(tweet, new_tweet)
            })
          }
        })
...

そのため、content.jsでも!= "Positive"として、Positiveでなければその次のget_similar_shuzo_quoteに進むようにコーディングしています。

get_similar_shuzo_quote

次に、Positiveではないと判定されたtweetに対して、一番内容の近い松岡修造語録を取得する関数を作っていきます。
COTOHA APIの類似度算出APIを利用しますが、これも大変シンプルで2つの文章(s1, s2)を与えるだけでその2つの文章の内容の類似度を教えてくれます。 => APIリファレンス | COTOHA API(類似度算出)

松岡修造さんのポジティブな名言は色々とあるのですが、COTOHA APIの無料枠では1日1000リクエスト/APIまでと決められていることもあり、5つに厳選してみました。

100回叩くと壊れる壁があったとする。でもみんな何回叩けば壊れるかわからないから、90回まで来ていても途中であきらめてしまう。

みんな!!竹になろうよ。竹ってさあ台風が来てもしなやかじゃない。台風負けないんだよ。雪が来てもね。おもいっきりそれを跳ね除ける!!力強さがあるんだよ。そう、みんな!!!竹になろう!!!バンブー!!!

ベストを尽くすだけでは勝てない。僕は勝ちにいく。

もっと熱くなれよ!熱い血燃やしてけよ!!人間熱くなったときがホントの自分に出会えるんだ!!

一番になるっていったよな?日本一なるっつったよな!ぬるま湯なんかつかってんじゃねぇよお前!!

どれも元気が出ますね。
Positiveと判断されなかったtweetとこの5つの名言をひとつずつ類似度算出APIで比較し、最も類似度の高かった名言を返却する関数を作ります。

content.js
...
function get_similar_shuzo_quote(access_token, tweet) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage(
      {
        contentScriptQuery: "get_similar_shuzo_quote",
        access_token: access_token,
        tweet: tweet
      },
      (response) => { resolve(response) }
    )
  })
}
...

content.jsからはPositive判定されなかったtweetをメッセージングします。

background.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
...
    // ポジティブでないツイートを松岡修造の熱い名言から最も近しいものに変える
    if (request.contentScriptQuery == "get_similar_shuzo_quote") {
      cotoha_similarity_api_endpoint = "nlp/v1/similarity"

      function get_similarity(s1, s2) {
        return new Promise((resolve, reject) => {
          fetch(cotoha_api_base_url + cotoha_similarity_api_endpoint, {
            method: 'POST',
            headers: {
              "Content-Type": "application/json; charset=UTF-8",
              Authorization: `Bearer ${request.access_token}`
            },
            body: JSON.stringify({
              s1: s1,
              s2: s2
            })
          })
          .then(response => response.json())
          .then(body => resolve([s2, body.result.score]))
        })
      }

      Promise.all([
        get_similarity(request.tweet, "100回叩くと壊れる壁があったとする。でもみんな何回叩けば壊れるかわからないから、90回まで来ていても途中であきらめてしまう。"),
        get_similarity(request.tweet, "みんな!!竹になろうよ。竹ってさあ台風が来てもしなやかじゃない。台風負けないんだよ。雪が来てもね。おもいっきりそれを跳ね除ける!!力強さがあるんだよ。そう、みんな!!!竹になろう!!!バンブー!!!"),
        get_similarity(request.tweet, "ベストを尽くすだけでは勝てない。僕は勝ちにいく。"),
        get_similarity(request.tweet, "もっと熱くなれよ!熱い血燃やしてけよ!!人間熱くなったときがホントの自分に出会えるんだ!!"),
        get_similarity(request.tweet, "一番になるっていったよな?日本一なるっつったよな!ぬるま湯なんかつかってんじゃねぇよお前!!")
      ]).then((results) => {
        // 類似度を比較して一番類似している名言をsendResponseで返却する。
        most_similar_quote = ["", 0] // [名言, 類似度(score)]
        for (result of results) {
          if (most_similar_quote[1] < result[1]) {
            most_similar_quote = result
          }
        }
        sendResponse(result[0])
      })
    }
    return true
  }
)

これはかなり試行錯誤しました...javascript不慣れなもので非同期処理がかなりつまづいた...

類似度算出APIについては先ほどものべた通りとてもシンプルです。APIを叩く部分をget_similarityに関数化しました。
今回は5つの文章との比較が全部終わったあとで類似度を比較して一番近い名言を決める、ということがしたかったのですが、javascriptは非同期に処理を勧めてくれるおかげで、他の言語を書いているイメージのまま書くとAPIのレスポンスが返ってくる前に次の処理が進んでしまうなんてことがありました。

これを回避するために、今回はPromisePromise.allを使っています。
細かい使い方は「JavaScriptのPromise - Qiita」を参考にさせていただきました!

Promise.allのおかげで、5つの名言とtweetの類似度比較が全て終わってから類似度の比較を行うことができるようになっています。
比較の結果、最も類似度の高い名言がcontent.jsにレスポンスとしてメッセージングされています。

さて、これでPositiveではないTweetに変わる松岡修造語録をゲットすることができました!
あとはこれをViewに反映するだけです!!

change_tweet

最後にDOM操作です。Positiveではないと判定されたtweetの文字列を、それに類似した松岡修造語録new_tweetに書き換えてあげます。
ついでに文字も真っ赤に燃やしてみましょう。

content.js
...
function change_tweet(tweet, new_tweet) {
  target = $(`div[lang='ja']:contains(${tweet})`)
  $(target).text(new_tweet)
  $(target).css("color", "red")
}
...

最初の方で解析した通り、Tweetの文字列はlang='ja'属性がついているdivタグの配下にあります。containsを使ってPositiveではないと判断されたtweetをもつ要素を探してtargetに格納します。
そしてそのtargetに対してtextで文字列を松岡修造語録に変え、cssでテキストカラーを真っ赤に変えてみました。

あんなにネガティブだった僕のプロフィールページがこんなにもポジティブに!!

さて、ここまでできたので、Chrome extensionをchrome://extensions/からインストールして動作確認をしてみます!
他の方のtweetを出すのもあれなので自分のアカウントのプロフィールページでやっていますが、トップページでもどうように動きます。

output.gif

各tweetに対して非同期で処理を実行しているのでAPIの処理などが終わったtweetから反映されていくんですね。

まとめ

あんなにネガティブだった僕のTLをCOTOHA API松岡修造さんのおかげでこんなにもポジティブで熱いTLに変えることができました。
類似度のところは毎回 tweets x quotes 回API叩くのも無駄ですね...Doc2Vecとかならうまくいくのかな...(初心者)

とりあえず自信を持って生きていこうと思います!!

ソースコード

今回のソースコードをGitHubにあげました。

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9