Node.jsでツイートをできるだけ(200件以上)検索するには

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 444

elpha

score 12

JavaScript、Node.jsともに初心者になります。
(Windows7のコマンドプロンプトから実行しています)

 やりたいこと

「特定の30分間につぶやかれた、特定のハッシュタグを含んだツイート」をすべて一気に検索
したいと思っています。
(手動で検索した限りではおよそ800~2000ツイートほどになるので、分割やスクロールなどの手間を省きたく…)

Stream APIは今年8月に廃止されたそうなので、REST APIを使うしかないようですが… こちらは一度に200件までしか検索できないようですね。

 試したこと

Node.jsでTwitter検索から100件を超えるツイートを取得したい
https://qiita.com/ryo-a/items/53fe9eadcf719b817c9a

こちらを参考にしてしてみたところ、countの値をいくらにしても100件以内しか取得できませんでした。

Twitter API Timeline解説
http://nonbiri-tereka.hatenablog.com/entry/2014/03/06/220015

とりあえず検索するたびに最後のツイートのIDを格納し、次はそのIDをmax_idにして検索すればいいということはわかったのですが…

const Twit = require('twit');

const T = new Twit({
  各種キー
  app_only_auth: true
});
let lastId = '';

let params = {
    q: '#ハッシュタグ since:2018-11-28_00:00:00_JST until:2018-11-28_00:30:00_JST',
    count: 3,
    max_id: lastId,
    result_type: 'recent',
    include_entities: false
}

for(let i=0; i<3; i++){
    console.log('検索開始');
    params.max_id = lastId;
    T.get('search/tweets', params, (err, data, response) => {
        data.statuses.forEach(function(val, index, ar){
            console.log(index);
            console.log('@' + val.user.screen_name);
            console.log(val.text);
            lastId = val.id.str;
        });
    });
    console.log('最後のIDは' + lastId);
}

と3ツイート×3回の検索をしようとしてみると、コンソールはまず

検索開始
最後のIDは
検索開始
最後のIDは
検索開始
最後のIDは


と、まだ代入されていない状態のものが最初に3回ぶん表示されてしまい、そのあとで検索結果(3回とも同じ結果)が出てきます。

これはNode.jsゆえの、非同期だからこそ起こることでしょうか?
それともJavaScriptの何か初歩的な間違い(スコープなど)を犯しているのでしょうか?

ここさえ解決できれば、800ツイートでも2000ツイートでも(規制のかからないかぎり)検索できるとは思うのですが…
ご教授よろしくお願いいたします。

 追記

やはり非同期通信が原因とのことでしたので、初めてながらasync/awaitというやつで書き換えてみましたが
何かが足りないのか、やはり検索結果は変わりません…

(略)
async function main() {
    console.log('ループ開始');
    for(let i=0; i<3; i++){
        await search();
    }
}

function search() {
    return new Promise((resolve, reject) => {
        console.log('検索開始');
        params.max_id = lastId;
        T.get('search/tweets', params, (err, data, response) => {
            data.statuses.forEach(function(val, index, ar){
                console.log(index);
                console.log('@' + val.user.screen_name);
                console.log(val.text);
                lastId = val.id.str;
            });
        });
        console.log('最後のIDは' + lastId);
        resolve();
    });
}

main();

 動作した版

いただいた回答を元にさらに修正したところ、無事動作しました

const Twit = require('twit');

const T = new Twit({
  consumer_key: "略",
  consumer_secret: "略",
  access_token_key: "略",
  access_token_secret: "略",
  app_only_auth: true
});

let num = 0;
let lastId = '';

let params = {
    q: '#ハッシュタグ since:2018-11-28_00:00:00_JST until:2018-11-28_00:30:00_JST',
    count: 3,
    max_id: lastId,
    result_type: 'recent',
    include_entities: false
}

!(async () => {
    for(let i=1; i<=3; i++){
        console.log('\n\n' + i + '回目の検索開始 ID' + lastId + '以前のツイートを検索');
        params.max_id = lastId;
        const result = await T.get('search/tweets', params);
        result.data.statuses.forEach(function(val, index, ar){
        num++;
            console.log(`\n${parseInt(index) + 1}個目のツイート (累計${num}個目)`);
            console.log('@' + val.user.screen_name);
            console.log(val.text.replace(/\r?\n/g, ''));
            lastId = val.id_str;
            console.log('ツイートのIDは' + lastId);
        })
        console.log('\n最後のツイートのIDは' + lastId);
    }
})();


このままだと再検索のたびに最初と最後のツイート内容が重複してしまうので、ほんとは再検索の際にIDを-1したいところですが
なまじIDの桁数が多いため計算に誤差が生じて面倒なので、ひとまず重複をよしとしています

なぜJavaScriptで「76287755398823936」が正しく表示できないか、あるいはなぜRubyでも表せないか。
https://7io.org/2011/07/02/21:11:55/

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • asahina1979

    2018/11/28 09:04

    つ 非同期通信

    キャンセル

  • elpha

    2018/11/28 09:47

    ありがとうございます。やはりそのへんですか…async/awaitを使って書いてみたものを追記いたしました。

    キャンセル

回答 2

checkベストアンサー

+1

まずJavaScriptの非同期処理とイベントループについて調べて理解されることをおすすめします。

非同期に呼び出される関数は、少なくとも現在のコールスタックが空になるまで呼び出されることはありません。今の場合、T.get()のコールバック((err, data, response) => {...}の部分)は3回設定されるわけですが、これらは少なくともfor文が終わったあと、任意の順番で実行されます。検索結果が3回とも同じになるのも、for文内でlastId''のままなのでparamsの更新ができていないことが原因です。

追記のコードでは、resolveする場所が間違っています。コールバックが呼び出された時点で完了したとみなしたいので、コールバックの中でresolveしないと意味がありません。

ですが、twitのUsageを見る限り Promise をサポートしているので自分でPromiseを作る必要がそもそもありません。

テストしてないですが、以下のコードのようにできると思います。

!(async () => {
  for (let i = 0; i < 3; i++) {
    params.max_id = lastId
    const result = await T.get('search/tweets', params)
    result.data.statuses.forEach(function (val, index, ar) {
      console.log(index);
      console.log('@' + val.user.screen_name);
      console.log(val.text);
      lastId = val.id.str
    })
  }
})()

イベントループの分かりやすい動画(英語)
Jake Archibald: In The Loop - JSConf.Asia 2018 (YouTube)

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/11/28 13:57

    !(async () => {})();
    ですね正確には。先頭の ! は、セミコロンなしで書くスタイルだと、parserに直前の文と合わせて一文とみなされてしまうことがあるので、それを防ぐためのものです。

    キャンセル

  • 2018/11/28 14:25

    即時関数というやつでしょうか。
    セミコロンがないのはあえてなのかなと気になっていましたが、やはりそうだったのですね。
    聞きかじりですが、CoffeScriptなどでもないかぎり基本的にセミコロンは必須(省略すると予期せぬ事態が起こる)という認識でしたので…

    キャンセル

  • 2018/11/28 14:29

    はい、まさに即時関数(IIFE)です。トップレベルではawaitを使用できないためよく使われます。

    キャンセル

0

追記のPromiseのアドバイス。
全体的に横着しすぎだね、もう少しPromiseを勉強してモノにすれば解決するから頑張って。
以下ちょっとしたアドバイス。

Promiseをnewする時に渡す関数内で、
resolve(value)という書き方で引数として設定したvalueを外に持ち出す事が出来るのね。
そのPromiseを.then(value => { console.log(value); })という風に次のthenに指定した関数の第一引数でvalueを取り出してリレーできるんだよ。

要するにTwitterにHTTPリクエストを投げて、
受け取った結果からlastIdを取り出してリレーさせたいんだよね。
だったら結果が帰ってきた所で、なんとかしてlastIdを取り出して、resolve(lastId)ってやらないとダメだよ。

以下は面倒だからざっくり書いちゃおう

// 関数宣言は巻き上げやらなんやらで面倒だから、この形式が一般的
// 値を即返すなら{}不要
// lastIdは関数実行時に外から持ち込むようにすること、むしろparamsを持ち込んだ方が良さげだね
const search = (params = {}) => new Promise((resolve, reject) => {
  console.log('検索開始');
  T.get('search/tweets', params, (err, data, response) => {
    // お目当てはlastIdじゃなくて、皆のツイート結果一覧も含まれてるんだからdataを持ち帰る仕組みにしたほうがよくね?
    if (err) {
      reject(err);
      return;
    }
    resolve(data);
  });
})

const main = async () => {
  let lastId = '';

  console.log('ループ開始');
  for(let i=0; i<3; i++){
    // このようにdataを持ち帰るのだ
    // paramsはこの中で毎回作った方が綺麗じゃね?
    const data = await search({
      q: '#ハッシュタグ since:2018-11-28_00:00:00_JST until:2018-11-28_00:30:00_JST',
      count: 3,
      max_id: lastId,
      result_type: 'recent',
      include_entities: false,
    });

    // ここまでくればあとは単なる同期処理みたいなもんだから自分で出来るでしょ?
    // TODO: ここでlastIdを抜き出して、次のループで使えるよう仕込む
    // TODO: 更にツイート一覧を取り出してくっつける
  }
}

main();

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.21%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる