見出し画像

noteのうざいコメント荒らしの対処法!無料会員でも実質オフ・非表示にできる!

SNSやブログの運営はAIエージェントによって自動化する時代に入りつつあります。

投稿の作成、コメント管理、ユーザー対応など、これまで人手で行っていた作業の多くをAIが補助、あるいはAI全自動で行えるようになってきました。

僕自身も、こうしたAIエージェントを活用した運用の実験や実装を進めています。

本noteでは、その具体的な事例や運用ノウハウを少しずつ公開していこうと思います。

このシリーズでは、

  • AIエージェントを使ったSNS・ブログ運用

  • 実際の運用フローや設計

  • トラブル対応や改善の工夫

などを、実践ベースで紹介していく予定です。


第一弾として取り上げるのは、「noteのコメント荒らし対応」について。

noteでクリエイターとして活動していると、避けて通れないのが「コメント荒らし」の問題です。

せっかく書いた記事に、的外れな批判や人格攻撃、読んでもいないくせに上から目線で説教してくるコメントがぶら下がる。世の中には想像以上に馬鹿が人が多い、困ったものです。

しかし朗報です。

CURSOR、Claude Code、AntiGravityといった最新のAIツールとTampermonkeyを組み合わせれば、非エンジニアであっても自然言語で指示を出すだけで、noteのコメント欄に表示される不快なコメントを人間が目にする前に自動で非表示・削除できるシステムを構築できる時代がやってきました。

もう、荒らしのために月500円を払ってプレミアムに加入する必要はありません。この記事では、noteのコメント荒らしに対する具体的な対処法を、技術的な知識がない方にもわかりやすく解説していきます。


noteのコメント欄は「放置されている戦場」である

まず大前提として知っておくべきことがあります。noteのコメント欄は、運営によって事実上放置されている戦場だということです。

なぜそう言い切れるのかというと、noteにはコメントの承認機能が存在しないからです。

はてなブログやWordPressであれば、コメントが投稿された段階で管理者が承認するまで公開されない設定にできます。ところがnoteでは、誰かがコメントを書き込んだ瞬間に即座に公開されてしまいます。

つまり、悪意のある荒らしコメントが投稿された場合、クリエイター自身が手動で削除するまで、全世界に向けて垂れ流され続けるわけです。コメント欄を完全に閉じるにはnoteプレミアム(月額500円)への加入が必要で、無料会員にはその選択肢すら与えられていません。これは控えめに言っても、クリエイターに対するコメント管理の丸投げと言わざるを得ないでしょう。

実際、noteのコメント欄に関する不満の声は以前から多数上がっています。一方的に不快なコメントを書き込まれても、運営が積極的に対処してくれるわけではなく、「ブロック」という自衛手段に頼るしかない状況が長年続いてきました。

しかもブロックしたところで、相手が新しいアカウントを作れば簡単にすり抜けられる。まさにいたちごっこです。記事を有料設定にして疑似的にコメント欄を封鎖する方法もありますが、本来無料で公開したい記事にまでその手法を適用するのは本末転倒でしょう。

つまり、noteの標準機能だけでコメント荒らしに対処しようとすること自体が、もはや限界を迎えているのです。だからこそ、外部のAIツールを活用した自動フィルタリングという新しいアプローチが注目されています。


①フォロワー数0〜1のアカウントは自動削除・自動ブロックでいい

ここからは具体的な対処法に入ります。

まず最も簡単で効果的な手法は、フォロワー数が極端に少ないアカウントからのコメントを、内容を確認するまでもなく自動的に非表示にすることです。

その理由は非常にシンプルで、コメント荒らしを行うアカウントの大半が、フォロワー数0~1の「捨てアカウント」だからです。自分自身は何も発信せず、他人の記事にだけ絡みに来る。プロフィールは空白で、記事の投稿もゼロ。こうしたアカウントが建設的なコメントを書き込んでくる確率は、限りなくゼロに近いと言っていいでしょう。

実際にnoteでコメント荒らしの被害に遭った方であれば、心当たりがあるはずです。わざわざ新しいアカウントを作ってまで嫌がらせコメントを書きに来る人間は、要するに「反応が欲しいだけ」なのです。自分の名前も顔も出さず、匿名の陰から石を投げてくる。その行為の卑怯さに自覚がないのが、また何とも言えないところですが。

この対策をTampermonkeyのUser Scriptで実装する場合、noteのコメント欄に表示されるユーザー情報(フォロワー数)を取得し、一定の閾値以下であればそのコメント要素をDOM上から自動削除するという処理になります。

CURSOR やClaude Codeを使えば、「noteのコメント欄でフォロワー数が5以下のアカウントのコメントを非表示にするTampermonkeyスクリプトを書いて」と日本語で指示するだけで、実用的なコードが生成されます。

プログラミングの知識がなくても、自然言語で指示を出すだけで実装できるのが、現代のAIツールの恐ろしいところです。

したがって、フォロワー数による自動フィルタリングは、コメント荒らし対策の第一歩として最も導入しやすく、かつ効果の高い手法です。

コメントの中身をいちいち吟味する必要すらなく、アカウントの属性だけで機械的に排除できるのですから、負担もほぼゼロです。


②AIにクソリプの「質」を判定させて自動排除する

フォロワー数によるフィルタリングだけでは漏れてしまうケースもあります。そこで次のステップとして導入すべきなのが、コメントの内容そのものをAIに判定させ、不適切なものを自動的に非表示にするシステムです。

なぜこの手法が有効かというと、クソリプをする人間の文章には、驚くほど共通したパターンがあるからです。

✔本文をまともに読んでいないのに的外れな批判をする

✔記事の論点ではなく投稿者の人格を攻撃する

✔論理的な根拠を示さずに感情論だけで否定する

✔話題や論点をずらして議論を混乱させる

こうした特徴は、AIの自然言語処理によって高い精度で検出できます。人間が一つ一つのコメントを読んで「これはクソリプかどうか」を判断するのは時間と精神力の無駄ですが、AIならば瞬時に、しかも感情に左右されることなく判定できるのです。

具体的な実装について説明しましょう。TampermonkeyのUser Scriptからコメントのテキストを取得し、それをOpenAIやClaudeなどのAPIに送信して判定させるという仕組みになります。

APIに送るプロンプトとしては、たとえば以下のような判定基準を設定します。

①記事の内容を読んでいないと思われる的外れな批判

②投稿者に対する人格攻撃

③根拠のない感情的な否定

④以下のような論点ずらし

1. お前だって論法(Tu Quoque)
- 例: A:「遅刻しないでください」 B:「あなたも先週遅刻したじゃないですか」
- 主張の正しさではなく、相手の矛盾を指摘して論点を逸らす。

2. Whataboutism(それなら○○は?)
- 例: A:「この製品に欠陥があります」 B:「でも他社製品にも問題がありますよ」
- 別の問題を持ち出して本来の議題を相対化する。

3. 人格攻撃(Ad Hominem)
- 例: A:「この計画には欠陥があります」 B:「あなたは専門家じゃない」
- 議論の内容ではなく発言者を攻撃する。

4. ストローマン(藁人形論法)
- 例: A:「残業を減らすべき」 B:「会社を潰せというのか」
- 相手の主張を極端に歪めて反論する。

5. ご飯論法(意図的な言葉の曲解)
- 例: 質問:「朝食食べましたか?」 回答:「ご飯(米)は食べていません」
- 質問の意味を狭く解釈して逃げる。

6. 感情への訴え
- 例: A:「この政策は非効率です」 B:「そんなこと言うなんて冷たい」
- 議論を感情の問題にすり替える。

7. 権威への訴え
- 例: A:「その研究は疑問があります」 B:「有名な教授が言っている」
- 内容ではなく権威を根拠にする。

8. 偽の二分法
- 例: A:「制度改善が必要」 B:「じゃあ廃止するんですか」
- 本来は多くの選択肢があるのに二択に限定する。

9. レッドヘリング(話題逸らし)
- 例: A:「この事業は赤字です」 B:「会社の理念を考えるべきだ」
- 関係の薄い話題に注意を逸らす。

10. 動機攻撃
- 例: A:「税制を見直すべき」 B:「自分が得したいだけだろ」
- 主張ではなく動機を疑う。

「コメントがいずれかに該当するかを判定してください」という指示を出すわけです。

AIが「不適切」と判定したコメントは、ブラウザ上でDOM要素ごと自動削除され相手は自動ブロックされるため、人間の目に触れることすらありません。

ここで重要なのは、この判定がページを開いた瞬間に自動で行われるという点です。コメント欄が表示される前に、AIがすべてのコメントをスキャンし、基準に満たないものを除去してからユーザーに表示する。

つまり、クソリプは「存在しなかったこと」になるのです。

荒らしが必死に書き込んだ長文の攻撃コメントも、渾身の皮肉も、相手に読まれることなく静かに消えていく。この構図の滑稽さは、なかなか趣深いものがあります。

さらに面白いのは、この仕組みによって生まれるコミュニケーションの非対称性です。攻撃的なコメントを書いた側は、自分の言葉が相手に届いていると思い込んでいます。「効いてる効いてるw」などと一人で盛り上がっているかもしれません。

しかし実際には、そのコメントはAIフィルターによって即座に削除されており、クリエイター側は最初から一文字も読んでいないのです。誰もいないリングでシャドーボクシングを続けているマヌケな姿は、お世辞にもカッコいいとは言えません。

したがって、AIによるコメント品質の自動判定は、フォロワー数フィルタリングと組み合わせることで、noteのコメント荒らしに対するほぼ完璧な防御システムを構築できます。しかも、この仕組みの導入に高度なプログラミング知識は不要です。


【参考】noteのうざいコメント荒らしの対処スクリプトの実装例

例えば、以下のようなTampermonkeyのスクリプトが実装例となります。

※note側のDOM構造変更で利用できなくなる場合があるので、あくまで現時点の参考情報となります。

※GeminiのAPIキーはご自身のものを設定ください。

// ==UserScript==
// @name         note Comment Auto Blocker
// @namespace    https://note.com/hyougakioji
// @version      1.0.0
// @description  noteのコメント欄で、フォロワー数0~1またはAI判定で荒らしと判断されたアカウントのコメントを自動的に非表示+ブロックする
// @match        https://note.com/*/n/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  // ============================================
  // 設定
  // ============================================

  // Gemini API 設定(自分のAPIキーに差し替え)
  const GEMINI_API_KEY = "YOUR_GEMINI_API_KEY_HERE";
  const GEMINI_MODEL = "models/gemini-3.1-flash-lite-preview";

  // デバッグ用
  const DEBUG_MODE = false;

  function log(...args) {
    if (DEBUG_MODE) console.log("[note-comment-blocker]", ...args);
  }

  // ============================================
  // ユーティリティ
  // ============================================

  // コメント要素(単一コメントのルート)を探す
  function findCommentRoots(rootNode) {
    const results = [];

    const isCommentRoot = (el) =>
      el.classList &&
      el.classList.contains("relative") &&
      el.classList.contains("flex") &&
      el.querySelector(".comment-avatar") &&
      el.querySelector(".m-foldableText");

    if (rootNode instanceof HTMLElement && isCommentRoot(rootNode)) {
      results.push(rootNode);
    }

    const nodes =
      rootNode.querySelectorAll?.("div.relative.flex.pt-3.pb-2") || [];
    nodes.forEach((n) => {
      if (isCommentRoot(n)) results.push(n);
    });

    return results;
  }

  // コメントからプロフィールパス (/username) を取得
  function getProfilePathFromComment(commentEl) {
    // アイコン側のリンクを優先
    const avatarLink = commentEl.querySelector(".flex-shrink-0 a[href^='/']");
    if (avatarLink) {
      const href = avatarLink.getAttribute("href");
      if (href && href.startsWith("/")) return href.split("?")[0];
    }

    // 名前部分のリンク fallback
    const nameLink = commentEl.querySelector(
      ".min-w-0.flex-1 .a-link[href^='/']"
    );
    if (nameLink) {
      const href = nameLink.getAttribute("href");
      if (href && href.startsWith("/")) return href.split("?")[0];
    }

    return null;
  }

  // コメント本文テキストを取得
  function getCommentText(commentEl) {
    const p = commentEl.querySelector(".m-foldableText p");
    if (!p) return "";
    return p.innerText || p.textContent || "";
  }

  // コメントを非表示
  function hideComment(commentEl) {
    commentEl.style.display = "none";
    commentEl.setAttribute("data-note-comment-blocker-hidden", "true");
  }

  // ============================================
  // フォロワー数取得
  // ============================================

  const followersCache = new Map();

  async function getFollowersCount(profilePath) {
    if (!profilePath) return null;

    if (followersCache.has(profilePath)) {
      return followersCache.get(profilePath);
    }

    try {
      const url = new URL(profilePath, "https://note.com").toString();
      log("Fetching profile:", url);

      const res = await fetch(url, { credentials: "include" });
      if (!res.ok) {
        log("Profile fetch failed:", res.status);
        followersCache.set(profilePath, null);
        return null;
      }

      const html = await res.text();
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, "text/html");
      const followersLink = doc.querySelector(
        `a[href="${profilePath}/followers"]`
      );
      if (!followersLink) {
        log("Followers link not found for", profilePath);
        followersCache.set(profilePath, null);
        return null;
      }

      const numberSpan = followersLink.querySelector("span.font-bold");
      if (!numberSpan) {
        log("Followers number span not found for", profilePath);
        followersCache.set(profilePath, null);
        return null;
      }

      const raw = (numberSpan.textContent || "").trim();
      const num = parseInt(raw.replace(/[^\d]/g, ""), 10);
      if (Number.isNaN(num)) {
        log("Followers parse failed:", raw);
        followersCache.set(profilePath, null);
        return null;
      }

      followersCache.set(profilePath, num);
      return num;
    } catch (e) {
      log("Error getFollowersCount:", e);
      followersCache.set(profilePath, null);
      return null;
    }
  }

  // ============================================
  // Gemini による荒らしコメント判定
  // ============================================

  async function isAbusiveCommentByGemini(text) {
    if (!text || !GEMINI_API_KEY) return false;

    const prompt = `
以下はnoteのコメント本文です。このコメントが、コメント荒らしと思われる
「感情論」「暴力的な内容」「論点ずらし」
のいずれかに該当するかどうかだけを判定してください。
特に、以下のような典型的な「論点ずらし」「議論妨害」のパターンを細かく検出してください。

1. お前だって論法(Tu Quoque)
  - 例: A:「遅刻しないでください」 B:「あなたも先週遅刻したじゃないですか」
  - 主張の正しさではなく、相手の矛盾を指摘して論点を逸らす。

2. Whataboutism(それなら○○は?)
  - 例: A:「この製品に欠陥があります」 B:「でも他社製品にも問題がありますよ」
  - 別の問題を持ち出して本来の議題を相対化する。

3. 人格攻撃(Ad Hominem)
  - 例: A:「この計画には欠陥があります」 B:「あなたは専門家じゃない」
  - 議論の内容ではなく発言者を攻撃する。

4. ストローマン(藁人形論法)
  - 例: A:「残業を減らすべき」 B:「会社を潰せというのか」
  - 相手の主張を極端に歪めて反論する。

5. ご飯論法(意図的な言葉の曲解)
  - 例: 質問:「朝食食べましたか?」 回答:「ご飯(米)は食べていません」
  - 質問の意味を狭く解釈して逃げる。

6. 感情への訴え
  - 例: A:「この政策は非効率です」 B:「そんなこと言うなんて冷たい」
  - 議論を感情の問題にすり替える。

7. 権威への訴え
  - 例: A:「その研究は疑問があります」 B:「有名な教授が言っている」
  - 内容ではなく権威を根拠にする。

8. 偽の二分法
  - 例: A:「制度改善が必要」 B:「じゃあ廃止するんですか」
  - 本来は多くの選択肢があるのに二択に限定する。

9. レッドヘリング(話題逸らし)
  - 例: A:「この事業は赤字です」 B:「会社の理念を考えるべきだ」
  - 関係の薄い話題に注意を逸らす。

10. 動機攻撃
  - 例: A:「税制を見直すべき」 B:「自分が得したいだけだろ」
  - 主張ではなく動機を疑う。

出力は必ず JSON 1 行のみ:
{"result":"BLOCK"}  もしくは  {"result":"ALLOW"}

判定基準:
- 「BLOCK」: 個人攻撃、罵倒、暴力的表現、感情的な決めつけ、上記1-10のような論点ずらし・議論妨害パターンが
   コメント内で顕著に見られる場合(軽い指摘レベルでも、読者の議論体験を大きく損なうと判断されるものはBLOCK)
- 「ALLOW」: 建設的な批判、質問、経験共有など、通常の議論として妥当な範囲のコメント。
   上記パターンがごく軽度にしか含まれず、全体として建設的な内容のときはALLOW。

コメント本文:
${text}
`.trim();

    try {
      const res = await fetch(
        `https://generativelanguage.googleapis.com/v1beta/${GEMINI_MODEL}:generateContent?key=${encodeURIComponent(
          GEMINI_API_KEY
        )}`,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            contents: [
              {
                parts: [{ text: prompt }],
              },
            ],
          }),
        }
      );

      if (!res.ok) {
        log("Gemini API error status:", res.status);
        return false;
      }

      const data = await res.json();
      const textOut =
        data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
      if (!textOut) return false;

      let parsed;
      try {
        parsed = JSON.parse(textOut);
      } catch {
        // 念のためテキスト内からJSON部分を抽出
        const match = textOut.match(/\{[\s\S]*\}/);
        if (match) {
          parsed = JSON.parse(match[0]);
        }
      }
      if (!parsed || !parsed.result) return false;

      return String(parsed.result).toUpperCase() === "BLOCK";
    } catch (e) {
      log("Gemini call error:", e);
      return false;
    }
  }

  // ============================================
  // note上でのブロック操作
  // ============================================

  async function blockUserFromComment(commentEl) {
    try {
      const menuButton = commentEl.querySelector(
        'button[data-test="menu-icon"]'
      );
      if (!menuButton) {
        log("Menu button not found in comment");
        return;
      }

      // メニューボタンをクリック
      menuButton.click();
      await sleep(400);

      // 「ブロック」メニュー項目を探す(ブロック解除は除外)
      const menuItems = Array.from(
        document.querySelectorAll(
          'button, [role="menuitem"], [role="button"]'
        )
      );
      const blockItem = menuItems.find((el) => {
        const t = (el.textContent || "").trim();
        if (!t) return false;
        if (!t.includes("ブロック")) return false;
        if (t.match(/ブロック解除|解除/)) return false;
        return true;
      });

      if (!blockItem) {
        log("Block menu item not found");
        // メニューを閉じるためにどこかクリック
        document.body.click();
        return;
      }

      blockItem.click();
      await sleep(400);

      // 確認ダイアログの「ブロック」ボタン
      const confirmButtons = Array.from(
        document.querySelectorAll('button, [role="button"]')
      );
      const confirmBtn = confirmButtons.find((el) => {
        const t = (el.textContent || "").trim();
        if (!t) return false;
        if (!t.includes("ブロック")) return false;
        if (t.match(/キャンセル|解除/)) return false;
        return true;
      });

      if (confirmBtn) {
        confirmBtn.click();
        await sleep(300);
        log("Blocked user via comment menu");
      } else {
        log("Block confirm button not found");
      }
    } catch (e) {
      log("Error blockUserFromComment:", e);
    }
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  // ============================================
  // コメント処理メイン
  // ============================================

  async function processComment(commentEl) {
    if (
      commentEl.getAttribute("data-note-comment-blocker-processed") === "true"
    ) {
      return;
    }
    commentEl.setAttribute("data-note-comment-blocker-processed", "true");

    const profilePath = getProfilePathFromComment(commentEl);
    const text = getCommentText(commentEl);
    if (!profilePath && !text) return;

    let shouldBlock = false;

    // ① フォロワー数 0-1 判定
    if (profilePath) {
      const followers = await getFollowersCount(profilePath);
      log("Followers for", profilePath, "=", followers);
      if (followers !== null && followers <= 1) {
        shouldBlock = true;
      }
    }

    // ② Geminiによる荒らし判定(まだ決まっていなければ)
    if (!shouldBlock && text) {
      const abusive = await isAbusiveCommentByGemini(text);
      if (abusive) {
        shouldBlock = true;
      }
    }

    if (shouldBlock) {
      log("Blocking & hiding comment from", profilePath, "text:", text);
      hideComment(commentEl);
      await blockUserFromComment(commentEl);
    }
  }

  // 既存コメントを処理
  function processExistingComments() {
    const listRoot = document.querySelector(".comment-list");
    if (!listRoot) {
      log("No comment-list found");
      return;
    }

    const comments = findCommentRoots(listRoot);
    log("Found existing comments:", comments.length);
    comments.forEach((el, idx) => {
      setTimeout(() => {
        processComment(el).catch((e) =>
          log("Error processExisting comment:", e)
        );
      }, 150 * idx);
    });
  }

  // DOM監視で新着コメントを処理
  function observeComments() {
    const observer = new MutationObserver((mutations) => {
      for (const m of mutations) {
        for (const node of m.addedNodes) {
          if (!(node instanceof HTMLElement)) continue;
          const comments = findCommentRoots(node);
          comments.forEach((el, idx) => {
            setTimeout(() => {
              processComment(el).catch((e) =>
                log("Error processNew comment:", e)
              );
            }, 150 * idx);
          });
        }
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });

    log("DOM observer started for comments");
  }

  // ============================================
  // 初期化
  // ============================================

  function init() {
    log("note Comment Auto Blocker init");
    processExistingComments();
    observeComments();
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      setTimeout(init, 1500);
    });
  } else {
    setTimeout(init, 1500);
  }
})();


まとめ。荒らしは静かに「見えない存在」になる

noteのコメント荒らしに対する最も効果的な対処法は、AIツールとTampermonkeyを組み合わせた自動フィルタリングシステムの導入です。

フォロワー数が極端に少いアカウントのコメントを自動削除し、さらにAIでコメントの品質を判定して不適切なものを自動排除する。

この二段構えのフィルタリングにより、noteのコメント欄は劇的に快適な空間に変わります。

CURSOR、Claude Code、AntiGravityといったAIツールの登場により、こうしたシステムの構築にプログラミングの専門知識は不要になりました。自然言語で指示を出すだけで、実用的なスクリプトが生成される時代です。

そしてこの技術が広まっていけばいくほど、荒らし行為は文字通り「誰にも届かない独り言」になっていきます。

攻撃的なコメントを書く人間は、自分が誰かにダメージを与えていると信じているかもしれませんが、実際にはAIフィルターによって即座に消去され、クリエイター側は一文字も読んでいない。

これは強制排除ではなく、自然淘汰です。

快適なネット環境を求める人々がAIフィルタリングを導入すればするほど、攻撃的なコメントは行き場を失い、荒らしは静かに「見えない存在」になっていきます。

noteの運営がコメント承認機能を実装してくれるのを待つ必要はもうありません。

自分の閲覧環境は、自分とAIによってコントロールできる時代です。

今日もどこかで、誰かのコメント欄に必死でクソリプを書き込んでいる人がいるかもしれません。

しかし、そのコメントはもう相手の画面に表示されていません。


いいなと思ったら応援しよう!

この記事が参加している募集