Twitterの検索結果からユーザー名を除外するブックマークレット 完成版

2017年8月20日追記
7月19日の追記でmobileへの対応を諦めたと書きましたが、その後1ヶ月以上そのまま使えています。
この様子だとメンテナンスする意味はありそうなので、mobile側の更新も継続することにしました。
2017年7月19日追記
mobile.twitter.comのHTMLの記述が頻繁に変わってしまうため、対応を諦めました。
現時点では使用できますが、ほどなく使用できなくなると思われます。
twitter.com(デスクトップブラウザ)では問題なく使用できると思います。

更新履歴

日付をタップすると展開します。


ステータスの変化をスキャンの繰り返しで検出するのは筋の悪い手法なのでなんとかならないかなあと思っていたのですが、調べてみたらちゃんとしたやり方がありましたのでブックマークレットを書き直しました。
プログラムとしてようやくマトモになりました。

前回からの変更点

  • ちゃんとした方法で無限スクロールへ完全追随するようにした
  • Twitterの全ての検索演算子に対応
  • ツイートの着色表示をちょっとだけ見栄え良くした
  • 検索画面を抜けても処理を終了しないようにした
    → ページをリロードするか、Twitter外へ抜けると終了します

前回の記事をご覧になっていない方のためにこのブックマークレットについて説明しておきます。

↓ 検索結果の画面でブックマークレットを実行すると

↓ このように着色します

Twitterの検索は、ツイート本文だけでなく、情報として価値のないユーザー名や表示名も検索の対象としてしまうため、非常に使いづらいものとなっています。
このブックマークレットを使用することで、必要なツイートをひと目で捉えることができるようになります。たかがブックマークレットですが、Twitterの情報検索ツールとしての価値を限りなく高めてくれると思います。

着色するもの

  • 発言者の情報(表示名、ユーザー名)、返信先、本文中の@ツイートの文字列によりヒットしたツイートではないもの

着色できないもの

  • 「ポケモン」と検索してヒットした「Pokémon」「ポケモン」「ポケットモンスター」など
  • 「workflow」と検索してヒットした「work flow」など

要するに、あいまい検索によるヒットを「必要なツイート」として検出することはできません。
アルファベットの大文字・小文字の違いは大丈夫。

動作確認済み

  • iOS版Safari
  • iOS版Chrome
  • Android版Chrome
  • Mac版Safari
  • Mac版Chrome

Windows版のChromeでも動作すると思います。
Firefoxでは動作しませんでした。
IEとEdgeもたぶん動作しません。

コード

Ohajikiを起動する
 

コピーがうまくいかない場合はこちら。
ここをタップすると別窓でDropboxのページが開きます。
右上の「↓」ボタン --> 直接ダウンロード と進むとコードが表示されますので、コピペでご使用ください。

スポンサーリンク

 

▼圧縮前のコード

javascript:void((function(undefined) {

// 着色表示の色設定
var highlight_color = '#F7A9D1',
    card_background = '#E0F5E0',
    card_border = '#99DA99',
    hover_background = '#E0F5E0',
    hover_border = '#1B95E0';
// テキストのANDマッチ確認
function andmatch(target_str, word_list) {
  for (var i = 0; i < word_list.length; i++) {
    var reg = new RegExp(word_list[i], 'i');
    if (!target_str.match(reg)) {
      return false;
    }
  }
  return true;
}
// テキストのORマッチ確認
function ormatch(target_str, word_list) {
  for (var i = 0; i < word_list.length; i++) {
    var reg = new RegExp(word_list[i], 'i');
    if (target_str.match(reg)) {
      return true;
    }
  }
  return false;
}
// URLから検索語を抜き出す
function get_wordlists() {
  // 検索語の候補リストから高度な検索演算子を除去する関数
  function sanitize_unnecessary(word_list) {
    var  ret = [];
    for (var i = 0; i < word_list.length; i++) {
      var str = word_list[i];
      if (!str.match(/^(-|#|from:|to:|@|since:|until:|lang:|near:|within:|:\)|:\(|geocode:|filter:|min_retweets:|min_faves:|min_replies:)/)) {
        ret.push(str.replace(/^"(.+)"$/,'$1'));
      }
    }
    return ret;
  }
  var hashes = location.search.slice(1).split('&');
  for (var i = 0; i < hashes.length; i++) {
    var hash = hashes[i].split('=');
    if (hash[0] === 'q') {
      var q_value = decodeURIComponent(hash[1]);
      break;
    }
  }
  // クエリ文字列がなければnullを返す
  if (q_value === undefined) {
    return null;
  }
  var list1 = q_value.replace(/\+/g, ' ').replace(/ {2,}/g, ' ').split(' OR ');
  var or_list = [];
  for (var i = 0; i < list1.length; i++) {
    if (i === 0) {
      var and_list = sanitize_unnecessary(list1[i].split(' '));
    } else {
      or_list = or_list.concat(sanitize_unnecessary(list1[i].split(' ')));
    }
  }
  return [and_list, or_list];
}
// エレメント内の検索語文字列をspanで囲う
function highlight(elem, word_list) {
  var new_inner = elem.innerHTML;
  for (var i = 0; i < word_list.length; i++) {
    var reg = new RegExp('(>[^<]*?)(' + word_list[i] + ')', 'gi');
    new_inner = new_inner.replace(reg, '$1<span class="excluder-highlight">$2</span>').replace(/<strong>/g, '<span class="excluder-highlight"><strong>').replace(/<\/strong>/g, '</strong></span>');
  }
  elem.innerHTML = new_inner;
}
// ツイートリストを処理(デスクトップ)
function process_tweets_desktop(tweet_list, and_list, or_list) {
  var all_words = and_list.concat(or_list);
  var tryagain_list = [];
  label_A:
  for (var i = 0; i < tweet_list.length; i++) {
    if (tweet_list[i].getAttribute('data-item-type') !== 'tweet') {
      continue;
    }
    var tweet_elem = tweet_list[i].getElementsByClassName('js-tweet-text-container')[0],
        tweet_text_elem = tweet_elem.getElementsByClassName('tweet-text')[0],
        // @ツイートとstrongタグを除去
        text_removed_unnecessary = tweet_text_elem.innerHTML.replace(/<a href[^>]+?><s>@<\/s>.+?<\/a>/g, '').replace(/<\/?strong>/g, ''),
        parent_children = tweet_elem.parentNode.children;
    for (var j = 0; j < parent_children.length; j++) {
      // 兄弟にカードがあるかどうか確認
      if (parent_children[j].classList.contains('card2')) {
        var twittercard_elem = parent_children[j];
        // summaryかplayerならスキャンの対象に
        if (twittercard_elem.getAttribute('data-card2-name').match(/summary/) || twittercard_elem.getAttribute('data-card2-name').match(/player/)) {
          var iframe_elem = twittercard_elem.getElementsByTagName('iframe')[0];
          // カードの中身の読み込みが完了していなければ再処理に回す
          if (iframe_elem === undefined) {
            tryagain_list.push(tweet_list[i]);
            continue label_A;
          }
          var iframe_summary_elem = iframe_elem.contentWindow.document.getElementsByClassName('SummaryCard-content')[0];
          // 読み込みが完了していなければ再処理に回す
          if (iframe_summary_elem === undefined) {
            tryagain_list.push(tweet_list[i]);
            continue label_A;
          }
          // カード内のテキストをスキャンにかけるテキストに追加
          var iframe_text = iframe_summary_elem.textContent;
          text_removed_unnecessary = text_removed_unnecessary + '\n' + iframe_text;
          break;
        }
      }
    }
    // 必要なツイートなら着色処理
    if (andmatch(text_removed_unnecessary, and_list)) {
      highlight(tweet_text_elem.parentNode, all_words);
      tweet_list[i].firstElementChild.classList.add('excluder-card');
    } else if (or_list !== [] && ormatch(text_removed_unnecessary, or_list)) {
      highlight(tweet_text_elem.parentNode, all_words);
      tweet_list[i].firstElementChild.classList.add('excluder-card');
    }
  }
  // 再処理リストがあればディレイを設けて再投入
  if (tryagain_list.length !== 0) {
    setTimeout(function() {
      process_tweets_desktop(tryagain_list, and_list, or_list);
    }, 300);
  }
}
// スクロールで新しいツイートが表示された時に実行される関数(デスクトップ)
function observe_desktop(mutation_records, observer) {
  var tweet_list =[];
  for (var i = 0; i < mutation_records.length; i++) {
    for (var j = 0; j < mutation_records[i].addedNodes.length; j++) {
      tweet_list.push(mutation_records[i].addedNodes[j]);
    }
  }
  var word_lists = get_wordlists(),
      and_list = word_lists[0],
      or_list = word_lists[1];
  process_tweets_desktop(tweet_list, and_list, or_list);
}
// ツイートリストを処理(モバイル)
function process_tweets_mobile(tweet_list, and_list, or_list) {
  var all_words = and_list.concat(or_list);
  for (var i = 0; i < tweet_list.length; i++) {
    var data_testid_tweet = tweet_list[i].querySelector('[data-testid="tweet"]');
    // ツイート以外のノードをスキップ
    if (data_testid_tweet === null) {
      continue;
    }
    // ツイート本文のノードを探す
    var tweettext_candidate_list = data_testid_tweet.children[1].firstElementChild.children;
    for (var j = 0; j < tweettext_candidate_list.length; j++) {
      if (tweettext_candidate_list[j].getAttribute('lang') !== null) {
        var tweet_text_elem = tweettext_candidate_list[j];
        break;
      }
    }
    // @ツイートを除去
    var  text_removed_reply = tweet_text_elem.innerHTML.replace(/<a [^>]+?>@.+?<\/a>/g,''),
        next_elem = tweet_text_elem.nextElementSibling,
        twittercard_exists = false;
    // 本文の次にカードがあればスキャンの対象に
    if (next_elem !== null && !next_elem.textContent.match(/^.+を読み込む$/)) {
      text_removed_reply = text_removed_reply + '\n' + next_elem.textContent;
      twittercard_exists = true;
    }
    // 必要なツイートなら着色処理
    if (andmatch(text_removed_reply, and_list)) {
      highlight(tweet_text_elem, all_words);
      data_testid_tweet.parentNode.classList.add('excluder-card');
      if (twittercard_exists === true) {
        highlight(next_elem, all_words);
      }
    } else if (or_list !== [] && ormatch(text_removed_reply, or_list)) {
      highlight(tweet_text_elem, all_words);
      data_testid_tweet.parentNode.classList.add('excluder-card');
      if (twittercard_exists === true) {
        highlight(next_elem, all_words);
      }

    }
  }
}
// スクロールで新しいツイートが表示された時に実行される関数(モバイル)
function observe_mobile(mutation_records, observer) {
  var tweet_list =[];
  for (var i = 0; i < mutation_records.length; i++) {
    for (var j = 0; j < mutation_records[i].addedNodes.length; j++) {
      tweet_list.push(mutation_records[i].addedNodes[j]);
    }
  }
  var word_lists = get_wordlists(),
      and_list = word_lists[0],
      or_list = word_lists[1];
  process_tweets_mobile(tweet_list, and_list, or_list);
}

function main() {
  var current_url = location.href,
      word_lists = get_wordlists();
  // 検索以外のページなら終了
  if (word_lists === null) {
    return;
  }
  // スタイルを追加
  if (!document.getElementById('excluder-style')) {
    var inner = '.excluder-highlight { background-color:' + highlight_color + '; }';
    inner += '.excluder-card { background-color: ' + card_background +'; border-radius: 10px; box-shadow: 0 0 0 2px' + card_border + ' inset; }';
    inner += '.excluder-card:hover { background-color: ' + hover_background + '; box-shadow: 0 0 0 2px ' + hover_border + ' inset; }';
    var style = document.createElement('style');
    style.setAttribute('id', 'excluder-style');
    style.innerHTML = inner;
    document.getElementsByTagName('head')[0].appendChild(style);
  }
  var and_list = word_lists[0],
      or_list = word_lists[1];
  switch (location.hostname) {
    case 'twitter.com':
      // 監視対象のノード
      var stream_items = document.getElementById('stream-items-id'),
          tweet_list = stream_items.children;
      process_tweets_desktop(tweet_list, and_list, or_list);
      // スクロールによるツイートの出現を監視
      var tweet_observer = new MutationObserver(observe_desktop);
      tweet_observer.observe(stream_items, { childList: true });
      // titleタグの変更を監視して、function以下(URLの変更チェックおよびツイートの再処理)をトリガー
      var page_observer = new MutationObserver(function(mutation_records, observer) {
        // URLが変わっていなければ何もしない
        if (current_url === location.href) {
          return;
        }
        current_url = location.href;
        $(function() {
          var word_lists = get_wordlists();
          // 検索以外のページならtweet_observerを停止するだけ
          if (word_lists === null) {
            tweet_observer.disconnect();
            return;
          }
          var and_list = word_lists[0],
              or_list = word_lists[1],
              stream_items = document.getElementById('stream-items-id'),
              tweet_list = stream_items.children;
          process_tweets_desktop(tweet_list, and_list, or_list);
          tweet_observer.disconnect();
          tweet_observer = new MutationObserver(observe_desktop);
          tweet_observer.observe(stream_items, { childList: true });
        });
      });
      page_observer.observe(
        document.getElementsByTagName('head')[0].getElementsByTagName('title')[0],
        { childList: true, subtree: true }
      );
      break;
    case 'mobile.twitter.com':
      var data_testid_tweet = document.querySelector('[data-testid="tweet"]');
      // 読み込みが完了していなければディレイを設けて最初からやり直し(mobileではjQueryが使えず、また、読み込みが全く終わっていない状態でloadedを返してくるので)
      if (data_testid_tweet === null) {
        setTimeout(main, 1000);
        break;
      }
      // 監視対象のノード(mobileではデータ節減のためIDが全く使われていない)
      // このあたりがよく変わる
      var stream_items = data_testid_tweet.parentNode.parentNode.parentNode,
          tweet_list = stream_items.children;
      process_tweets_mobile(tweet_list, and_list, or_list);
      // スクロールによるツイートの出現を監視
      var tweet_observer = new MutationObserver(observe_mobile);
      tweet_observer.observe(stream_items, { childList: true });
      // titleタグの変更を監視して、function以下(URLの変更チェックおよびツイートの再処理)をトリガー
      var page_observer = new MutationObserver(function(mutation_records, observer) {
        // URLが変わっていなければ何もしない
        if (current_url === location.href) {
          return;
        }
        current_url = location.href;
        var word_lists = get_wordlists();
        // 検索以外のページならtweet_observerを停止するだけ
        if (word_lists === null) {
          tweet_observer.disconnect();
          return;
        }
        var and_list = word_lists[0],
            or_list = word_lists[1];
        // 読み込みが完了していなければディレイを設けて繰り返す(mobileではjQueryが使えず、また、読み込みが全く終わっていない状態でloadedを返してくるので)
        function waitloop() {
          var data_testid_tweet = document.querySelector('[data-testid="tweet"]');
          if (data_testid_tweet === null) {
            setTimeout(waitloop, 1000);
            return;
          }
          // 監視対象のノード(mobileではデータ節減のためIDが全く使われていない)
          // このあたりがよく変わる
          var stream_items = data_testid_tweet.parentNode.parentNode.parentNode,
              tweet_list = stream_items.children;
          process_tweets_mobile(tweet_list, and_list, or_list);
          tweet_observer.disconnect();
          tweet_observer = new MutationObserver(observe_mobile);
          tweet_observer.observe(stream_items, { childList: true });
        };
        waitloop();
      });
      page_observer.observe(
        document.getElementsByTagName('head')[0].getElementsByTagName('title')[0],
        { childList: true, subtree: true }
      );
      break;
  }
}

if (location.hostname !== 'twitter.com' && location.hostname !== 'mobile.twitter.com') {
  return;
}
if (document.readyState === 'complete') {
  setTimeout(main, 10);
} else {
  document.addEventListener('DOMContentLoaded', main);
}

})());

ブックマークレットの起動方法について

iOSでは、検索開始と同時にブックマークレットを起動できるOhajiki Webブラウザ(有料版)との組み合わせが最高です。
ブックマークレットの登録とURLスキームについて、こちらの記事で紹介しています。
Ohajiki WebブラウザでTwitter検索とブックマークレット起動を同時に実行するURLスキーム


▼ ここから下は前回の記事と同じ記述です。 ▼

iOS版のSafariでブックマークレットを起動するおすすめの方法はこちら。
iOSのSafariにおけるベストなブックマークレットの起動方法 – hitoriblog

--

検索ランチャーなどのin-app Safariではブックマークを呼び出すことが出来ませんので、その場合はExtensionでJavaScriptを実行できる『Safari Snippets』というアプリの利用がおすすめです。
Safari Snippetsについてはこちらのサイトで詳しく紹介されています。

このアプリがすごい No.24 Safari Snippets - ブックマークレットをExtensionで呼び出せるアプリ

ブックマークレットって便利なんですけど、登録方法がやたらめんどくさい。 もっと手軽に登録できたり呼び出せたらいいのにと思ってきましたが、このアプリを使うとその願いが叶います。 ydangle ユーティリティ, 仕事効率化 評価: - こんなアプリが欲しかった。 セールなのか無料になっていたので取り急ぎ。   WorkflowのようにブラウザのExtensionから呼び出します。 ...

スポンサーリンク
スポンサーリンク

コメントを残す

メールアドレスが公開されることはありません。