拡張機能なしで、ツイートを一括削除

Chromeのコンソールに貼り付けるだけの無料スクリプトで、古いX投稿を片づけましょう。まずシミュレーション、最近のツイートは保護、削除は上限つきのバッチ処理。拡張機能もサードパーティのサービスも不要で、データがブラウザの外に出ることは一切ありません。

始める前に:ツイートの削除は取り消せません。スクリプトはシミュレーションモード(DRY_RUN = true)で提供されています。何にも触れずに、削除される予定のツイートを一覧表示するだけです。先にXのアーカイブをダウンロードし、シミュレーションを確認して、それからだけ実際の削除に切り替えてください。ウェブ画面の自動操作はXの利用規約で想定されていません。控えめに、自己責任でお進めください。

手順

  1. プロフィールを開くx.com/anata_no_username にアクセスし、ポストタブを開いて、アカウントにログインしておきます。
  2. コンソールを開くF12(Macでは Cmd+Option+J)を押し、Console タブをクリックします。
  3. スクリプトを設定する:下のコードをコピーし、anata_no_username をあなたのXのユーザー名(@なし)に置き換え、KEEP_RECENT(残す最近のツイートの数)を調整します。必要なら、絶対に削除しないツイートのIDを KEEP_STATUS_IDS に並べます。
  4. シミュレーションする:スクリプトをコンソールに貼り付け、Enterで実行します。DRY_RUN = true(既定の設定)では、何も削除せずに、削除される予定のツイート一覧を表示します。
  5. 削除する:シミュレーションの内容でよければ、DRY_RUNfalse にして再実行します。スクリプトは1回の実行で最大40件を削除し、6〜14秒の待機をはさみます。
  6. 繰り返す:残りをバッチで処理するために、必要な回数だけスクリプトを再実行します。

スクリプト

Chromeコンソール · x.com
(() => {
  // ============ 設定 ============
  const HANDLE          = "anata_no_username"; // あなたのXのユーザー名、@なし
  const KEEP_RECENT     = 15;           // 最新の15件のツイート(一番上)は絶対に削除しない
  const KEEP_STATUS_IDS = [             // 追加の保護(任意):絶対に削除しないツイートのID
    "1234567890123456789",
  ];
  let   DRY_RUN         = true;         // true = シミュレーション(一覧表示のみ)。false = 実際に削除。
  const MIN_DELAY_MS    = 6000;
  const MAX_DELAY_MS    = 14000;
  const MAX_DELETIONS   = 40;           // 1回の実行あたりの上限(続けるには再実行)
  // ================================

  const me = HANDLE.toLowerCase();
  const KEEP = new Set(KEEP_STATUS_IDS.map(String));
  const protectedRecent = new Set();
  const RX_DELETE = /(削除|delete|supprimer)/i;
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  const rand  = (a, b) => Math.floor(a + Math.random() * (b - a));
  const skipped = new Set();

  if (!location.pathname.toLowerCase().startsWith("/" + me)) {
    console.warn(`%c[停止] https://x.com/${HANDLE}(ポストタブ)へ移動してから再実行してください。`,
      "color:#e0245e;font-weight:bold");
    return;
  }

  const articles = () => [...document.querySelectorAll('article[data-testid="tweet"]')];

  const idOf = (art) => {
    const href = [...art.querySelectorAll('a[href*="/status/"]')]
      .map((x) => x.getAttribute("href"))
      .find((h) => new RegExp(`^/${HANDLE}/status/\\d+`, "i").test(h));
    if (!href) return null;
    const m = href.match(/status\/(\d+)/);
    return m ? m[1] : null;
  };

  // 最新のKEEP_RECENT件(一番上のツイート)を読み込んで保護する。
  async function protectRecent() {
    const order = [], seen = new Set();
    let noNew = 0;
    window.scrollTo(0, 0);
    await sleep(1200);
    while (order.length < KEEP_RECENT && noNew < 6) {
      let added = 0;
      for (const art of articles()) {
        const id = idOf(art);
        if (id && !seen.has(id)) { seen.add(id); order.push(id); added++; }
      }
      noNew = added ? 0 : noNew + 1;
      if (order.length >= KEEP_RECENT) break;
      const cs = articles();
      (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
      await sleep(1400);
    }
    for (const id of order.slice(0, KEEP_RECENT)) protectedRecent.add(id);
    return order.slice(0, KEEP_RECENT);
  }

  const eligible = (art) => {
    const id = idOf(art);
    if (!id) return null;
    if (protectedRecent.has(id)) return null;   // 最新のうちの1件
    if (KEEP.has(id)) return null;              // 追加で保護されたID
    if (skipped.has(id)) return null;
    return id;
  };

  async function deleteOne(art, id) {
    const caret = art.querySelector('[data-testid="caret"]');
    if (!caret) { skipped.add(id); return false; }
    caret.click();
    await sleep(700);
    const del = [...document.querySelectorAll('[role="menuitem"]')]
      .find((i) => RX_DELETE.test((i.innerText || "").trim()));
    if (!del) { document.body.click(); skipped.add(id); return false; }
    del.click();
    await sleep(700);
    const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
    if (!confirmBtn) { skipped.add(id); return false; }
    confirmBtn.click();
    await sleep(1500);
    return true;
  }

  async function dryScan() {
    const seen = new Set();
    let noNew = 0;
    console.log("%c[シミュレーション] 何も削除されません。カウント中...", "color:#1d9bf0;font-weight:bold");
    while (noNew < 4) {
      let added = 0;
      for (const art of articles()) {
        const id = eligible(art);
        if (id && !seen.has(id)) { seen.add(id); added++; }
      }
      noNew = added ? 0 : noNew + 1;
      const cs = articles();
      (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
      await sleep(1200);
    }
    console.log(`%c[シミュレーション] ${seen.size}件のツイートが削除される予定です(最新のものは保護されています)。`,
      "color:#1d9bf0;font-weight:bold");
    console.log([...seen].slice(0, 60));
  }

  async function run() {
    let count = 0, guard = 0, noHit = 0, started = false;
    while (count < MAX_DELETIONS && guard++ < 1500) {
      const pair = articles().map((a) => [a, eligible(a)]).find(([, id]) => id);
      if (!pair) {
        if ((started && noHit >= 6) || noHit >= 40) break;
        const cs = articles();
        (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
        await sleep(1500);
        noHit++;
        continue;
      }
      started = true; noHit = 0;
      const [node, id] = pair;
      node.scrollIntoView({ block: "center" });
      await sleep(600);
      const ok = await deleteOne(node, id);
      if (ok) count++;
      console.log(`${ok ? "削除" : "スキップ"} ${id}  (合計: ${count}/${MAX_DELETIONS})`);
      await sleep(rand(MIN_DELAY_MS, MAX_DELAY_MS));
    }
    console.log(`%c[完了] 今回は${count}件削除しました。残りがあればスクリプトを再実行してください。`,
      "color:#00ba7c;font-weight:bold");
  }

  (async () => {
    console.log(`最新の${KEEP_RECENT}件のツイートを読み込んで保護しています...`);
    const recent = await protectRecent();
    if (recent.length < KEEP_RECENT) {
      console.warn(`%c[停止] ${recent.length}件しか読み込めませんでした(Xが制限をかけています)。ページを再読み込み(F5)してもう一度お試しください。`,
        "color:#e0245e;font-weight:bold");
      return;
    }
    console.log("%c保護済み(最新):", "color:#00ba7c;font-weight:bold", recent);
    if (DRY_RUN) { await dryScan(); return; }
    await run();
  })();
})();

スクリプトがあなたを守るしくみ

4つの安全装置が組み込まれています。まず既定でシミュレーションDRY_RUNtrue のあいだは何も削除されず、対象となるツイートの一覧が表示されるだけです。次に最近のツイートの保護。スクリプトはまず最新のツイート(既定は15件)を記憶し、それらには決して触れません。さらにホワイトリストKEEP_STATUS_IDS に入れたIDはどれも削除されず、固定した投稿やお知らせに便利です。最後に慎重なペース。各削除の間に6〜14秒のランダムな待機をはさみ、40件で自動的に停止して、Xの制限を招かないようにします。

大掃除の前に、大切なものはアーカイブしておきましょう。メールアドレス・URL抽出ツールはツイートのエクスポートからすべてのリンクを取り出し、文字数カウンターは何年分の投稿がどれだけの分量だったかを教えてくれ、テキスト整形ツールはアーカイブのコピペをきれいに整えます。これからのツイートには、Unicode装飾文字ツール大文字小文字変換ツールがお役に立ちます。ツール集のすべてのツールは、このガイドと同じく、プライバシーポリシーの考え方に沿って、あなたのブラウザ内だけで動作します。

よくある質問

削除は取り消せますか?

いいえ。削除したツイートは完全に消え、Xにはゴミ箱のような機能はありません。だからこそスクリプトはシミュレーションモード(DRY_RUN = true)で提供されています。まずはそのまま実行して削除される予定のツイート一覧を確認し、内容に納得できてからだけ false に切り替えてください。事前にXのアーカイブをダウンロードしておくこともおすすめします(設定、次にアカウント、次にデータのアーカイブをダウンロード)。

他人のツイートを削除できてしまいますか?

いいえ。Xは自分の投稿にしか削除ボタンを表示しません。スクリプトはログイン中のアカウントの、あなた自身のプロフィールページ上でしか動作しません。他人の返信やサードパーティのコンテンツには一切触れません。

なぜ削除は40件が上限で、待機時間もランダムなのですか?

プラットフォームに負担をかけないためです。削除が速すぎたり大量すぎたりすると、Xの制限がかかったり、アカウントが一時的にロックされたりすることがあります。スクリプトは各削除の間に6〜14秒待機し、40件で停止します。続きを処理するには、もう一度実行してください。

これはXで認められていますか?

自分のコンテンツを削除することはあなたの権利です。一方で、ウェブ画面の自動操作はXの利用規約で想定されていません。このスクリプトは控えめに、自己責任でお使いください。公式な代替手段は手動での削除か、APIを利用する専用サービスです。

特定のツイートだけを守るには?

2つの保護が併用できます。KEEP_RECENT は最近のツイートを自動的に残し(既定は15件)、KEEP_STATUS_IDS には絶対に削除しないツイートのIDのリストを指定できます。ツイートのIDは、アドレスの末尾にある大きな数字です。x.com/username/status/1234567890123456789。