【自動化】Yahooトップ画面で不快なニュース配信元の記事を消すChrome拡張

こたつ記事とか釣りタイトルの記事で稼ぐしょうもないビジネスしている人はここでお引き取りください。
くれぐれもスクロールしないでください。くれぐれも。m(__)m

---

「釣りタイトル」で煽る記事、不快ですよね。
インフルエンサーのSNSでの発言を拾っているだけの「こたつ記事」、嫌いですよね。
大谷翔平は好きでも、誇大表現を使って、中身がぺらっぺらっの配信元うんざりしますよね？

こういった記事・配信元をYahooのTOPページから自動で非表示にするChrome拡張です。

非表示にしたい記事配信元(`blockProviders`)とキーワード(`blockKeywords`)に任意の値を設定するだけです。

個人レベルで精神衛生上もよいのはもちろん、社会的意義もありそうです。
社会的意義とは具体的に、低品質なメディアのトラフィックを減らすことで淘汰がすすんで、劣化する一方のWeb体験やネットメディアの質の向上が期待できます。

さらに、なんちゃって学者タレントやインフルエンサーがよくやる「炎上商法」（悪名は無名に勝る、アテンション経済）が経済的に報われない（むしろ負の効果）になれば、極論・暴論・汚い言葉が隅っこに追いやられて、感情的にならずに建設的な議論が交わされるようになることも期待できます。

# 手順

- 後述する`manifest.json` と `content.js` をフォルダに保存（例: yahoo_news_filter/）<br>[![Image from Gyazo](https://i.gyazo.com/21b5330882cb7be534d87fb87bbd313f.png)](https://gyazo.com/21b5330882cb7be534d87fb87bbd313f)
- Google Chrome の chrome://extensions/ にアクセス<br>[![Image from Gyazo](https://i.gyazo.com/85c048375ff467d9ceadc110072b051e.png)](https://gyazo.com/85c048375ff467d9ceadc110072b051e)
- デベロッパーモード をオンにする。<br>[![Image from Gyazo](https://i.gyazo.com/3852ab05622def445bfa55627e4182f0.png)](https://gyazo.com/3852ab05622def445bfa55627e4182f0)
- パッケージ化されていない拡張機能を読み込む から yahoo_news_filter/ フォルダを選択<br>[![Image from Gyazo](https://i.gyazo.com/1a315b7478ff20d7149625d1142c1641.png)](https://gyazo.com/1a315b7478ff20d7149625d1142c1641)

正常に読み込まれるとこうなります。
[![Image from Gyazo](https://i.gyazo.com/984ec5820f504f8f8508b0fbd910ede9.png)](https://gyazo.com/984ec5820f504f8f8508b0fbd910ede9)

これだけで、 Yahoo! JAPANのトップページを開くと、指定したワードを含む記事が非表示になります。

# コード

```json:manifest.json
{
  "manifest_version": 3,
  "name": "Yahoo News Filter",
  "version": "1.0",
  "description": "特定の単語を含むニュース記事を非表示にする",
  "permissions": ["scripting"],
  "host_permissions": ["https://www.yahoo.co.jp/"],
  "content_scripts": [
    {
      "matches": ["https://www.yahoo.co.jp/"],
      "js": ["content.js"],
      "run_at": "document_end"
    }
  ]
}
```

```js:content.js
(() => {
    // フィルタリング対象のキーワード
    const blockKeywords = ["堀江", "成田"];
    const blockProviders = ["デイリー", "ABEMA", "プレジデント","PRESIDENT", "ゴールドオンライン", "FRIDAY"];

    // ブロック統計情報
    let providerBlockCounts = {};  // providerごとの件数
    let providerCounts = {};  // providerごとの件数
    let reasonBlockCounts = { "provider": 0, "title": 0 };  // 理由ごとの件数

    function hideArticles() {
        document.querySelectorAll("#Stream article").forEach(article => {
            const titleElement = article.querySelector("h1 span");
            const providerElement = article.querySelector("cite");

            const title = titleElement ? titleElement.innerText : "";
            const provider = providerElement ? providerElement.innerText : "";

            let blocked = false;
            let blockReason = "";

            // 条件1: providerName にブロック対象のキーワードが含まれる
            if (blockProviders.some(keyword => provider.includes(keyword))) {
                reasonBlockCounts["provider"]++;
                blockReason = "provider";
                blocked = true;
            }

            // 条件2: articleTitle にブロック対象のキーワードが含まれる
            if (blockKeywords.some(keyword => title.includes(keyword))) {
                reasonBlockCounts["title"]++;
                blockReason = "title";
                blocked = true;
            }

            // ブロックされた場合
            if (blocked) {
                article.style.display = "none";

                // provider ごとのブロック件数を更新
                if (provider in providerBlockCounts) {
                    providerBlockCounts[provider]++;
                } else {
                    providerBlockCounts[provider] = 1;
                }
            }
            // provider ごとの記事件数を更新
            if (provider in providerCounts) {
                providerCounts[provider]++;
            } else {
                providerCounts[provider] = 1;
            }
        });

        // 統計情報をログ出力
        console.log("=== ブロック統計情報 ===");
        console.log("■ Providerごとのブロック件数:", providerBlockCounts);
        console.log("■ ブロック理由ごとの件数:", reasonBlockCounts);
        console.log("配信元件数=", Object.keys(providerCounts).length)
        const sortedProviderCounts = Object.fromEntries(
            Object.entries(providerCounts).sort((a, b) => b[1] - a[1])
          );
        console.table(sortedProviderCounts);
    }
    
    // 初回実行
    hideArticles();

    // 定期的に監視（Ajaxで更新される可能性があるため）
    const observer = new MutationObserver(hideArticles);
    observer.observe(document.body, { childList: true, subtree: true });

})();
```

実行結果例(開発者ツールのコンソール）

[![Image from Gyazo](https://i.gyazo.com/67fbcc915495d117c168a2378fc93c2f.png)](https://gyazo.com/67fbcc915495d117c168a2378fc93c2f)

# 余計なあとがき

Chromeの公式にアップロードすると、人気が出た後に悲劇（売られてバックドアつくられたりする）につながるので、それぞれが自作して利用するのがよいのかなと思います。

このコードは、どの記事配信元を何件ブロックしたかもコンソールに表示します。
Yahooのアルゴリズムや契約（によって配信記事数が決定？）がちょっと見えて、なかなか興味深いですね。

あー、すっきり！！

ちなみに、開発者ツールのコンソールタブでログを見ているとわかりますが、Yahooは非同期で記事を読み込んでくるのですが、その動作にも対応（＝非同期読み込み時に再実行）しています。

[![Image from Gyazo](https://i.gyazo.com/35c2e23fe6d1093f8a01d0796e4e9c78.png)](https://gyazo.com/35c2e23fe6d1093f8a01d0796e4e9c78)

# おまけ

## ソースコード

以下の機能を追加したコードを[Github](https://github.com/skatsumata/yahoo_filter)にあげました。
ご利用する場合は自己責任でお願いします。

Web黎明期のWebサイトってこんな感じでしょうか？

- ヘッダー削除
- サイドバー削除して記事部の幅を100%
    - 画像サイズを小さく(80x80→40x40)
        - これで一記事の高さが縮小されて表示される１ページあたりに表示される記事件数が倍くらいに
- 「エンタメ」タブ削除

## フィルター適用後のページ

[![Image from Gyazo](https://i.gyazo.com/9c480954e7fbe769953c94e857cb5d61.png)](https://gyazo.com/9c480954e7fbe769953c94e857cb5d61)

## フィルター適用前のページ

[![Image from Gyazo](https://i.gyazo.com/0e24ea8b03a3217d1e05ef4cdada3a2d.png)](https://gyazo.com/0e24ea8b03a3217d1e05ef4cdada3a2d)