Help us understand the problem. What is going on with this article?

YouTube Liveのスパムを自動で非表示にするChrome拡張機能を作った話

動機

YouTube Live Spam Killer - Chrome ウェブストア

突然ですが、自分はバーチャルYouTuber(VTuber)のライブストリーミングやそのアーカイブを複数窓垂れ流して作業するのが大好きです。
しかし、最近そのライブチャット欄に品のないユーザ名の出会い系スパムが多発しています。
これらのブロック作業で配信に集中できなかったりといった面倒が目立つようになりました。
せっかくの至福の時間を邪魔される、というのは人間誰しも嫌なものです。

人間様がクソプログラムに苛つかせられる必要はありません。
技術でなんとかしましょう。

YouTubeを利用している関係上、ブラウザ上でバックグラウンド動作してくれるタイプの技術を使うほうが見通しは良いです。
したがって今回、自分がメインで利用しているウェブブラウザであるGoogle Chrome上の拡張機能として実装することにしました。

なお、手元の事前調査などは使い慣れているPythonを利用しているため、Python(のREPLであるiPython)とJavaScriptのコードが混在する点のみご承知おきください。

実験

事前調査

これらのスパムは数種のパターンが存在しますが、大方以下のような特徴を持っています。

  • ユーザ名の形式: (英語圏の女性名) (スパムユーザのアカウントに誘導する文言)
    • 例: Lilah ʜ0'ᴛ ᴄʜᴀᴛ ᴊᴏɪɴ ᴍᴇ [ɪ ᴍ ʟɪᴠᴇ]
    • 最近ではコロナウィルス周りの文言になったスパムも登場している
  • 直前のチャット欄に現れた(人間の)コメントをコピーし、絵文字を追加したコメントを行う

上記の例で示したように、『スパムユーザのアカウントに誘導する文言』は単純なフィルタで取り除けないように、プレーンな英語ではなく記号類を用いています。

In [1]: import unicodedata

In [2]: target = "Lilah ʜ0'ᴛ ᴄʜᴀᴛ ᴊᴏɪɴ ᴍᴇ [ɪ ᴍ ʟɪᴠᴇ]"

In [3]: for i, c in enumerate(target):
   ...:     print(i, '"{}"'.format(c), '{:04x}'.format(ord(c)),
   ...:           unicodedata.category(c), unicodedata.name(c))
   ...:
0 "L" 004c Lu LATIN CAPITAL LETTER L
1 "i" 0069 Ll LATIN SMALL LETTER I
2 "l" 006c Ll LATIN SMALL LETTER L
3 "a" 0061 Ll LATIN SMALL LETTER A
4 "h" 0068 Ll LATIN SMALL LETTER H
5 " " 0020 Zs SPACE
6 "ʜ" 029c Ll LATIN LETTER SMALL CAPITAL H
7 "0" 0030 Nd DIGIT ZERO
8 "'" 0027 Po APOSTROPHE
9 "ᴛ" 1d1b Ll LATIN LETTER SMALL CAPITAL T
10 " " 0020 Zs SPACE
11 "ᴄ" 1d04 Ll LATIN LETTER SMALL CAPITAL C
12 "ʜ" 029c Ll LATIN LETTER SMALL CAPITAL H
13 "ᴀ" 1d00 Ll LATIN LETTER SMALL CAPITAL A
14 "ᴛ" 1d1b Ll LATIN LETTER SMALL CAPITAL T
15 " " 0020 Zs SPACE
16 "ᴊ" 1d0a Ll LATIN LETTER SMALL CAPITAL J
17 "ᴏ" 1d0f Ll LATIN LETTER SMALL CAPITAL O
18 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I
19 "ɴ" 0274 Ll LATIN LETTER SMALL CAPITAL N
20 " " 0020 Zs SPACE
21 "ᴍ" 1d0d Ll LATIN LETTER SMALL CAPITAL M
22 "ᴇ" 1d07 Ll LATIN LETTER SMALL CAPITAL E
23 " " 0020 Zs SPACE
24 "[" 005b Ps LEFT SQUARE BRACKET
25 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I
26 " " 0020 Zs SPACE
27 "ᴍ" 1d0d Ll LATIN LETTER SMALL CAPITAL M
28 " " 0020 Zs SPACE
29 "ʟ" 029f Ll LATIN LETTER SMALL CAPITAL L
30 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I
31 "ᴠ" 1d20 Ll LATIN LETTER SMALL CAPITAL V
32 "ᴇ" 1d07 Ll LATIN LETTER SMALL CAPITAL E
33 "]" 005d Pe RIGHT SQUARE BRACKET

これはまだ素直な方ですが、ものによってはUnicodeの隅を突くような記号を突っ込んでくることもあります。
このような手口に対応する手法はいくつか考えられますが、この場合正規化を行い適当な英語表記に変換してやるのが一番楽でしょう。

正規化概要

具体的な手法としては、次に示すような連想配列を用いてユーザ名を逐一英語小文字に変換していく手法を採っています。

const NORMALIZE_MAP = {
  a: ['', ''],
  c: ['', ''],
  e: ['3', 'ǝ', '', '', 'є'],
  h: ['ʜ', 'н', ''],
  i: ['ɪ', 'ι', '', 'í'],
  j: [''],
  k: [''],
  l: ['ʟ', '', ''],
  m: ['', ''],
  n: ['ɴ', ''],
  o: ['0', ''],
  p: [''],
  r: ['ʀ'],
  s: [''],
  t: ['т', ''],
  v: ['', ''],
} // 執筆時のバージョン

var normalizeWord = word => { // 単語単位で正規化を行う
  word = word.replace(/[!-/:-@\[-`{-~]/g, '') // 約物の除去
  Object.keys(NORMALIZE_MAP).forEach(key => {
    NORMALIZE_MAP[key].forEach(item => {
      word = word.replace(new RegExp(item, 'g'), key)
    })
  })
  return word.toLowerCase()
  // 英語圏以外の文字に作用し、連想配列をすり抜けてしまう事象が確認されたため、
  // 後付けでtoLowerCase()を作用させている
}

スパム検知手法

さらに、予め用意しておいたブラックリスト単語に合致するかを調べます。

const BLACKLIST_WORDS = ['chat', 'click', 'colona', 'here', 'hot', 'join', 'live', 'love',
                         'mask', 'me', 'photo', 'sex', 'tap'] // 執筆時のバージョン

先頭の人名を除去した部分のユーザ名を正規化し、これらのブラックリスト単語が多くヒットすればスパムと見なします。

旧検知手法

別の検出方法として、複数の言語圏の文字を利用していることを検出する手法も考えていました。
しかし、これはあまり一般性がないことが判明したためボツとしました。

ソース

https://github.com/Le96/youtube-live-spam-killer

参考

参考にした記事一覧です。
JavaScriptはほぼ触った経験がなかったので基本的なことも紛れています。

言語仕様

Array.filterが使えないNodeListに対してフィルターをかける方法 - Qiita
JavaScript で forEach を使うのは最終手段 - Qiita
Javascript 連想配列(オブジェクト)をforEachでループさせたい。 - かもメモ
Javascript:任意の文字列を含むかどうかの確認方法 | WWWクリエイターズ
JavaScriptにおける連想配列のforループ操作 - Qiita
JSのArray.prototype.filter.callとArray.from().filterどっちが早いか - Qiita
Unicode Property Escapesについての補説
Unicodeの文字プロパティを指定した正規表現をみてみる(ECMAScript2018) - TES Blog
特定の文字列を全て置換する[Javascript] - Qiita

(jQueryを利用しない)DOM操作

【Javascript】setInterval()で要素が現れるまで待つ - Qiita
iframeのonloadのタイミングでiframeのソースを確認する - みかづきブログ その3
JavaScript - js読み込みの際はnullになるが、devツールのコンソールではnullにならない|teratail
javascript - 読み込み完了の取得について - スタック・オーバーフロー
JavaScriptのMutationObserverでDOMの変化を監視する方法 - Qiita
ライブラリを使わない素のJavaScriptでDOM操作 - Qiita
要素の取得方法まとめ - Qiita

Chrome拡張機能

Chrome Extension を作って公開する - Qiita
Chrome 拡張機能のマニフェストファイルの書き方 - Qiita
Chromeブラウザの拡張機能を作ってみたい初心者向けに開発方法を紹介!【サンプルあり】 - Qiita
Chrome拡張 備忘録 1から10まで - Qiita
Chrome拡張の開発方法まとめ その1:概念編 - Qiita

類似の試み a.k.a.先行研究

YouTube Live の絵文字スパムをブロックする - Qiita
YouTubeコピペスパムを抽出する(モデレータなら自動ブロック?) - Qiita

アイコン

コメントアイコン7 | アイコン素材ダウンロードサイト「icooon-mono」 | 商用利用可能なアイコン素材が無料(フリー)ダウンロ ードできるサイト

あとがき

本当はスパム報告・ブロックまで行いたかったのですが、手元のアイデアでは実装方法が浮かびませんでした。
ご教示いただければ非常に助かります。

また、想定の甘さや新種スパムの発生などに伴い、False Positive/False Negativeなど発生している場合もぜひご教示ください。

Le96
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした