Twitterアカウントがシャドウバンされてないか自動で確認する【TwitterAPI不要】


#要約
Twitterアカウントがシャドウバンされているかを[https://shadowban.eu/](https://shadowban.eu/)を使って定期的に自動で確認し、Discord等に通知する方法を紹介します。

#はじめに
Twitterで、シャドウバン、というものがあります。

シャドウバンはアカウント凍結の一歩手前の状態と言われており、
普通のアカウント凍結とは違い、

* シャドウバンされた当人はシャドウバンされたことが知らされない
* 検索窓からシャドウバンされた垢のツイートを検索しても出てこなくなる
* 他のアカウントへのリプライが表示されなくなる

といった症状になります。
ちなみにTwitter公式はシャドウバンは存在しない、と主張してますが、検索結果に順位付けをしていることは認めているようです。
また、[シャドウバンについて研究した論文](https://arxiv.org/abs/2012.05101)でも、シャドウバンが存在していることを示唆してます。

一言でシャドウバン、といっても4種類あり、

* Search Suggestion Ban 
    * 検索窓のおすすめに表示されない
* Search Ban
    * 検索結果に表示されない
* Ghost Ban
    * リプライが表示されなくなる
* Reply Deboosting
    * リプライが積極的に表示されなくなる

があります。

そしてそのシャドウバンの原因は、

* 同じハッシュタグのツイートをし過ぎた
* 同じURLのツイートをしすぎた
* フォローやフォロー解除をしすぎた
* アカウント開設直後はなりやすい

などいろいろあるといわれています。
本当はもっと言いたいことがいろいろあるのですが、長くなってしまうので、ここまでにしときます。

# シャドウバンのチェック方法(手動)

シャドウバンをチェックする有名なサイトとして、
[https://shadowban.eu/](https://shadowban.eu/)
があります。
使い方は簡単、シャドウバンされているか調べたいアカウントを入力してチェックを押すと、シャドウバンされているか確認できます。

例えば、Twitter公式アカウント(＠Twitter)をテストした場合は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565132/5c98a73c-77fa-31a7-c2fe-c817137cc910.png)

当然ながら、シャドウバンはされていません。

# シャドウバンのチェック方法(自動)

ちまちまと[https://shadowban.eu/](https://shadowban.eu/)に手動でアカウントを入力→確認する、というのは面倒ですよね。
そこで、自動でシャドウバンされてないかチェック→Discordに通知、できるようにします。

前提条件

* pythonの実行環境がある
* Discordのアカウントがある

として、
1、discordにメッセージを送る関数の定義
2、自動でシャドウバンをチェックする関数の定義
をしたのちに
3、1と2の関数を使って、自動でシャドウバンされてないかチェックする(←これがやりたいこと)

を紹介します。

## Discordにメッセージを送る

有用な記事が他にもありますが、最低限のメッセージのみ送る機能のみ実装しました。
[pythonで一方的に通知するdiscord botをつくる（requests, jsonのみ使用）](https://qiita.com/iroiro_bot/items/48e8a8a9754aacaf7ec9)などを参考にしました。

```python
import requests
import json

def send_discord_message(msg, webhook):
    main_content = {'content': msg}
    headers      = {'Content-Type': 'application/json'}
    response     = requests.post(webhook, json.dumps(main_content), headers=headers)

if __name__ == "__main__":
    # テスト用
    WEBHOOK = "webhookをここにペーストする"
    msg = "こんにちは"

    send_discord_message(msg, WEBHOOK)
```

"webhookをここにペーストする"の箇所にdiscordのwebhookを記載してください。
上のテストコードで、こんにちは、という投稿がされたら成功です。
ちなみに、今回は私が慣れているからDiscordで通知という形にしていますが、
LINE通知、メール通知、slack通知なども可能です
(Qiitaには有用な記事がたくさんあるので、色々試してみてください)

## シャドウバンされているか自動で確認する

次にTwitterが凍結されているか確認します。
実は、https://shadowban.eu/.api/(ユーザー名)
にアクセスすると処理しやすい形でbanされているかの確認ができるのでこれを使っていきます
(ただし、高頻度にアクセスするのは良くないので注意！)

```python 
import requests
import json
import time

def analyze_shadowban_data(data, account_name):
    is_touketsu = is_touketsu = is_searchsuggestban = is_searchban = is_ghostban = None # 初期化
    marubatsu = lambda x: "○" if x else "×" # バンされている場合(True)は○、されていない場合(False)は×
    try:
        is_exists = data.get("profile").get("exists") is True
    except:
        pass
    if is_exists is False:
        return f"@{account_name} does not exists!"

    try:
        is_touketsu = marubatsu(data.get("profile").get("protected") is True)
    except:
        pass

    try:
        is_ghostban = marubatsu(data.get("tests").get("ghost").get("ban") is True)
    except:
        pass

    try:
        is_searchban = marubatsu(data.get("tests").get("search") is False)
    except:
        pass

    try:
        is_searchsuggestban = marubatsu(data.get("tests").get("typeahead") is False)
    except:
        pass
    return f"@{account_name} Suspend：{is_touketsu}, SearchSuggestBan：{is_searchsuggestban}, SearchBan:{is_searchban}, GhostBan:{is_ghostban}"

def check_shadowban(username):
    username = username.replace("@", "").replace("＠", "") # @マーク(全角、半角)を削除
    headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'}
    url = "https://shadowban.eu/.api/" + username
    try_max = 5 # サイトから取得する最大試行回数
    time_sleep = 10 # 取得に失敗した場合の待機時間
    for i in range(try_max):
        res = requests.get(url, headers=headers)
        if res.status_code == 200:
            return analyze_shadowban_data(res.json(), username)
        if i == try_max - 1:
            return None
        time.sleep(time_sleep*(i+1))

if __name__ == "__main__":
    print(check_shadowban("@twitter")) # @付きでもよい
    time.sleep(10)
    print(check_shadowban("nhk_news")) # @なしでもよい

```
上のコードでは、関数check_shadowbanがメイン処理で、実際にサイトにアクセスします。
うまく情報を取得できたら関数analyze_shadowban_dataに投げて、データを文章に変換します。
もし、取得に失敗した場合(ステータスコードが200でなかった場合)は間をおいて再取得します。

上のコードでは、試しに、Twitter公式アカウント、NHKニュースのアカウントを確認しています。

```text:実行結果
@twitter Suspend：×, SearchSuggestBan：×, SearchBan:×, GhostBan:×
@nhk_news Suspend：×, SearchSuggestBan：×, SearchBan:×, GhostBan:×
```

×となったら、Banされていない、という意味ですので、どちらもシャドウバンされていないアカウントであることが確認できます。
ちなみに、リプライツイートがないときはそもそもGhostBanかどうかを確認できないので、○×ではなくNoneが入ります。
そのほか、例外的な値が入ってきた場合はとりあえずNoneが入るようになっています。(そのためのtry～except分です。)

## 上2つのコードをくっつける

```python
import requests
import json
import time

def send_discord_message(msg, webhook):
    main_content = {'content': msg}
    headers      = {'Content-Type': 'application/json'}
    response     = requests.post(webhook, json.dumps(main_content), headers=headers)

def analyze_shadowban_data(data, account_name):
    is_touketsu = is_touketsu = is_searchsuggestban = is_searchban = is_ghostban = None # 初期化
    marubatsu = lambda x: "○" if x else "×" # バンされている場合(True)は○、されていない場合(False)は×
    try:
        is_exists = data.get("profile").get("exists") is True
    except:
        pass
    if is_exists is False:
        return f"@{account_name} does not exists!"

    try:
        is_touketsu = marubatsu(data.get("profile").get("protected") is True)
    except:
        pass

    try:
        is_ghostban = marubatsu(data.get("tests").get("ghost").get("ban") is True)
    except:
        pass

    try:
        is_searchban = marubatsu(data.get("tests").get("search") is False)
    except:
        pass

    try:
        is_searchsuggestban = marubatsu(data.get("tests").get("typeahead") is False)
    except:
        pass
    return f"@{account_name} Suspend：{is_touketsu}, SearchSuggestBan：{is_searchsuggestban}, SearchBan:{is_searchban}, GhostBan:{is_ghostban}"

def check_shadowban(username):
    username = username.replace("@", "").replace("＠", "") # @マーク(全角、半角)を削除
    headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'}
    url = "https://shadowban.eu/.api/" + username
    try_max = 5 # サイトから取得する最大試行回数
    time_sleep = 10 # 取得に失敗した場合の待機時間
    for i in range(try_max):
        res = requests.get(url, headers=headers)
        if res.status_code == 200:
            return analyze_shadowban_data(res.json(), username)
        if i == try_max - 1:
            return None
        time.sleep(time_sleep*(i+1))

if __name__ == "__main__":
    WEBHOOK = "(WEBHOOKを入力)"
    WAITING_INTERVAL = 60*60*12 # 取得する周期[sec]
    WAITING_INTERVAL_EACH_ACCOUNT = 60*10 # アカウントごとの取得する周期[sec]
    account_list = ["nhk_news", "twitter"] # チェックしたいアカウント[@付きでもなしでも良い]

    while True:
        for account in account_list:
            msg = check_shadowban(account)
            send_discord_message(msg, WEBHOOK)
            time.sleep(WAITING_INTERVAL_EACH_ACCOUNT)
        time.sleep(WAITING_INTERVAL)

```

本記事の結論は上のコードです。
WAITING_INTERVAL でチェックを取得する周期
WAITING_INTERVAL_EACH_ACCOUNTでアカウントごとの周期(上の例だと、＠nhk_newsをチェックした後に何秒間をおいてから＠twitterのチェックを行うか)
を設定します。
とりあえず、上の例だとWAITING_INTERVAL は半日、WAITING_INTERVAL_EACH_ACCOUNTは10分としてますが、
この値を小さくしすぎると運営から怒られると思うので、変えない方がいいです。

## 注意点
このサイトは、shadowban.euに頼ってチェックしていますので、shadowban.euのサービス終了や一時的なメンテナンス・サーバーダウン時には取得できなくなります。
(実際に、2021年8月5日～8月8日に、shadowban.euから取得できなくなりました。)
ですので、確実に確認したい場合は、自身でTwitterAPIを使って実装されることをお勧めします。



#最後に
Twitterの自動運用は難しく、シャドウバンをはじめとした日々変更されるTwitterの裏側のアルゴリズムを理解しないといけません。
この記事冒頭には、シャドウバンについて簡単にしか説明していませんが、単に記事を読んだだけではシャドウバンを回避することはうまくできません。
そのための有用な知見を自身のものにするためには、自分自身で手を動かすしかないのです。
と偉そうに書きましたが、正直、私も完全に理解はしていない状態です。
今回、Twitterの自動運用について、有用な情報をともに共有できるやる気のある協力者がいましたら、
ぜひ私の[Twitter](https://twitter.com/eg_i_eg)まで連絡ください。

#追記20210919
[LINE bot バージョン](https://github.com/a-suzuki-py/myLab/blob/master/shadow_ban_checker/shadow_ban_checker.py)を作っている方もいました。


#Twitter関連記事

* [Twitter Botで長い文章をツイート可能ギリギリの長さに調節する](https://qiita.com/eg_i_eg/items/179e49918b230caf4bd6)
* [グラビア⇒グ〇ビア、みたいにpythonで禁止ワード/NGワードを置換/隠す](https://qiita.com/eg_i_eg/items/f102c9f7533188ce780a)