2022/04/28:追記
公開していたzipファイルのリンクを削除しました。
バックエンドのサーバーを停止しました。
伴ってもう星が流れなくなります。
※この記事にはちょっとだけエッチな内容が含まれます!苦手な方はご注意ください。
こんにちは。あんど(@ampersand_xyz)と申します。
いきなりすみません、タイトルにエッチとか入ってて驚かれた方もいらっしゃることでしょう。どういうことなのか説明させていただきます。
概要説明
画像出典: 吸血鬼すぐ死ぬ 9巻 P134 盆ノ木至 秋田書店
__要するにこれです。__さすがに宙に星を降らせるわけにはいきませんので今回はブラウザ内に星を降らせていきます。
漫画のコマを見ただけでは何を言ってるのかご理解いただくのが難しいかもしれませんが、これ以上説明のしようがありませんのでついてきてください。
実現方法
いかにしてエッチなことを考えているときに星を降らせるのか、その実現方法についてです。
まず、前提として エッチなことを考えている = エロサイトを見ているとき と定義します。
- Chrome拡張を用いてエッチなサイトを見ていることを検知する
- 閲覧しているのがエッチなサイトであるかどうかは、コンテンツ内に存在する語句とその出現数で判断する。語句の出現数が多いほどすっごいエッチなサイトだと判断する。そのさい、コンテンツ内の画像やその人の性癖による盛り上がり度合いなどについては加味しない。
- 検知が行われたらブラウザ内に流れ星を降らせる。また、すっごいエッチなサイトを見ている場合はその度合いに応じた巨大な星を降らせる。
方法についてはごく単純なこの2つの手順となりますが、それらを実現するための方法を以下に解説します。
1.Chrome拡張のための雛形を用意
chrome-extension-cliはChrome拡張の初期セットアップとビルド環境の構築をしてくれるCLIツールです。今回はこちらを使用させていただきます。
2.ブロードキャスト用のサーバの準備
Socket.ioを用いて適当にブロードキャストするサーバーを用意します。
また、Socket.ioはバージョン3以降からcorsを明示的に有効にする必要があるのですが、今回はクソアプリアドベントカレンダー作品なのでバージョン2を用いてそのへんを気にせずブロードキャストするようにしています。
/** @format */
"use strict";
const express = require('express');
const socketIO = require('socket.io');
const PORT = process.env.PORT || 3000;
const server = express().listen(PORT, () => console.log(`Listening on ${PORT}`));
const io = socketIO(server);
io.sockets.on("connection", function (socket) {
// 誰かがえっちなことを考えているのを受信
socket.on("i_think_h", function (data) {
const message = {
value: data.value, // えっち度合い
};
// クライアントへブロードキャスト
io.emit("brodecast_htp", message);
});
});
3.kuromoji.jsを用いて形態素解析を行う
kuromoji.jsはクライアントサイドのみで日本語の形態素解析を行うことができるオープンソースライブラリです。こちらを使用させていただき、えっちな語句を検出するための作業を進めていきます。
3-1. エッチなサイトに出てきがちなえっちな語句の辞書を作成する
__エッチなサイトはSEOにメチャクチャ力が入ってる__ため、画像とか動画以外の文章だけでも充分にコンテンツがえっちであると判断出来ると考えられます。
ページ内に含まれる文章を単純な文字列として取得するのはdocument.getElementsByTagName("body")[0].outerText
で行うことが可能です。コンテンツから得られた文字列をkuromoji.jsで解析し、名詞として抽出することが出来るえっちな語句を集めます。
解析結果のsurface_formにそれらしい語句があるのが確認できますので、えっちだと思う語句を選っていきます。この作業中にkuromoji.jsは__結構な数のエロサイト名を固有名詞として識別できる__ことがわかったのでF◯NZAやP◯rnHubなどのサイト名もえっちな語句として集めておきます。
作業し始めた当初は誰が見ているわけでもなくとも、とても気恥ずかしい気持ちでいっぱいでしたが解析作業も3サイト目ぐらいになると語句が重複し始め使えそうな語句の取れ高が少ないことのほうが気になるようになってくるので、目的を持って作業を行うということの重要性がわかります。
こうして、200個弱のえっちな語句を集めてテキストファイルにしたのがこちらになります。
ぱっと見は全然えっちではありません。なぜなら暗号化を施したからです。
別にこの工程は必要なものではないのですが、生々しい単語がいっぱい並んでいるファイルがあるのが嫌だったのと、なによりも自分がどんなものをえっちな単語だと思って集めたのかがわかってしまうのが恥ずかしいからです。とはいえ、復号にサーバーを介しているわけではないのでただの悪あがきなんですが…。暗号化にはcrypto-jsというオープンソースライブラリを使用しました。
使い方はこんな感じです
const CryptoJS = require("crypto-js");
// 復号用のキー文字列
const SECRET_KEY = "xxxxxxxxxxx";
// 暗号化
const encodeWord = CryptoJS.AES.encrypt("元の文字列", SECRET_KEY).toString();
// 復号
const bytes = CryptoJS.AES.decrypt(encodeWord, SECRET_KEY);
const decodeWord = bytes.toString(CryptoJS.enc.Utf8); // 元の文字列 が得られる
3-2. コンテンツの内容とえっちな辞書の内容を突合してえっちかどうかチェックする
まずは画面表示時に3-1で作成したテキストファイルの内容を読み込み配列に保持しておきます
const CryptoJS = require("crypto-js");
// 暗号化されたアレな単語リストのパス
const textPath = chrome.extension.getURL("/words.txt");
// 保持用配列
const words = [];
// 単語リストをフェッチ
fetch(textPath)
.then((r) => r.text())
.then((result) => {
const lines = result.split("\n");
for (let i = 0; i < lines.length; ++i) {
const bytes = CryptoJS.AES.decrypt(lines[i], SECRET_KEY);
const word = bytes.toString(CryptoJS.enc.Utf8);
words.push(word); // 複合した文字列を配列にほ
}
});
そして、コンテンツの内容をえっち辞書に出現した単語と突合するにあたって、どの程度でえっちなサイトであるかを判断するための基準を考えます。
例えば、一つ以上含まれているというゆるい判断条件にしてしまうと、下図のように__一般ページにもさり気なく潜んでいるエロっぽいワード__に引っかかってしまい、__誤エロ判定__してしまう可能性があります。
とはいえページ内の文脈から判断するなど高度な判定は難しいため、さしあたりえっち辞書に登録した単語のうち、重複を除いた10個以上の単語がコンテンツ内に含まれていた場合にえっちなサイトであると判断することにしました。
本文の解析処理はこんな感じです
const kuromoji = require("kuromoji");
// 形態素解析用の辞書ファイルを格納しているディレクトリのパス
const dicPath = chrome.extension.getURL("/dict/");
// kuromojiの使用準備
const kuromojiBuilder = kuromoji.builder({
dicPath: dicPath,
});
// スクロールして0.1秒後に解析処理を実行
const debounceFunc = _.debounce(morphologicalAnalysis, 1000);
window.addEventListener("scroll", debounceFunc);
function morphologicalAnalysis() {
// スクロール後に追加されるコンテンツを加味するためにコンテンツ内容を都度取得する
const content = document.getElementsByTagName("body")[0].outerText;
kuromojiBuilder.build((error, tokenizer) => {
// 表示中のページ内に存在する文字列を形態素解析した結果
const parsed = tokenizer.tokenize(content);
// 名詞のみを抽出
const nouns = parsed.filter((v) => {
return v.pos === "名詞";
});
// 重複を排除した文字列の配列
const nounWords = [...new Set(nouns.map((v) => v.surface_form))];
// 2つの配列に含まれる要素で重複する要素を得る
// 参考 https://www.dkrk-blog.net/javascript/duplicate_an_array
const _words = [...words, ...nounWords];
const duplicatedArr = [...new Set(_words.filter((item) => words.includes(item) && nounWords.includes(item)))];
if (duplicatedArr.length >= 10) {
// 10個以上辞書に合致する単語がある場合恐らくえっちなコンテンツを見ている
// えっち辞書と合致した文字列が多いほどHTP(H Thinking Power)が高いものとして扱い
// えっちな情報を見ていることをブロードキャストする
socket.emit("i_think_h", {
value: duplicatedArr.length / 10 // 一致した語句の数/10をHTPとして送信
});
} else if (duplicatedArr.length == 0) {
// 1つもえっちな単語がコンテンツ内になければ、多分えっちなコンテンツではないと判断して監視をやめる
window.removeEventListener("scroll", debounceFunc);
}
});
}
4. 星を降らせる
誰かがえっちな事を考えていることがブロードキャストされたらブラウザに星を降らせます
星を降らせるアニメーションにはTWEENを使っています
// 誰かがえっちな情報を見ていることを感じ取った
socket.on('brodecast_htp', function (data) {
// 星を降らせる処理
shootStar(data.value)
})
// 指定範囲でのランダムな値を得る 参考 https://qiita.com/uto-usui/items/7193db237175ba15aaa3
const randRange = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
// htp: エッチシンキングパワー 大きいほど派手な流れ星を降らせる
function shootStar(htp) {
// 星を降らせるアニメーションを動作させるかどうかの設定値を受け取る(後述)
chrome.runtime.sendMessage({ method: "getShowSetting" }, function (response) {
// 未設定状態または明示的に受け取る設定になっている場合
if (!response || response === "true") {
// HTP(H Thinking Power)による係数、取り敢えず最大5まで
htp = htp > 5 ? 5 : htp;
const fontSize = randRange(12, 30) * htp;
const startLeft = randRange(0, window.innerWidth);
const endLeft = randRange(0, window.innerWidth * 0.8);
const speed = randRange(1000, 3000) * htp;
const star = document.createElement("div");
star.style.setProperty("position", "fixed");
star.style.setProperty("top", "0");
star.style.setProperty("left", startLeft + "px");
star.style.setProperty("font-size", fontSize + "px");
star.style.setProperty("z-index", "9999999");
const starText = document.createTextNode("🌟");
star.appendChild(starText);
document.body.appendChild(star);
const coords = { x: 0, y: 0 };
new TWEEN.Tween(coords)
.to({ x: endLeft, y: window.innerHeight, opacity: 0 }, 1000)
.easing(TWEEN.Easing.Quadratic.In)
.onUpdate(() => {
star.style.setProperty("transform", `translate(${coords.x}px, ${coords.y}px)`);
})
.start()
.onComplete(() => {
document.body.removeChild(star);
});
function animate(time) {
requestAnimationFrame(animate);
TWEEN.update(time);
}
requestAnimationFrame(animate);
}
});
}
5. 流れ星の表示可否を設定できるようにする
拡張をより楽しむために、星を流すかどうかを設定で切り替えられるようにします。
リモート会議の多くなった昨今、画面共有中にいきなり星が流れてきてどういうことなのか聞かれたときに「いま誰かがえっちなことを考えていて…」とか言おうものなら会議が終わってしまいかねません。
幸い、手順1で作成されるひな形に拡張機能のポップアップ画面が含まれるので、今回用に改造します。で、出来た画面がこちらです。
いらすとやさんの星の画像を使ってかわいく仕上げました。クリックすると非表示設定に切り替わります。
えっちなことを考えると流れ星を降らせる拡張 完成
動作の様子は下図になります。ファーストビューにえっちな画像のないえっちなサイトで撮影しているのでご安心ください。
えっちなサイトのコンテンツを見ようとする動き(スクロール操作)が行われると星が流れます。
ダークモードに対応してる画面でたくさん星が流れると結構きれいです。
あなたもブラウザに流れ星を降らせましょう
GoogleのChrome Webストアから拡張をご利用いただけるように申請中なのですが、現在審査待ちとなっています。
Googleの審査の人、年末にこんなもんの審査の依頼してごめんね…
公開され次第(されるかわかんないけど)、リンクを差し替えします。
ストア公開が拒否されました(2021/12/21 19:18 追記)
__拒否__て。そんなステータスあるんですね。これとかこれとか他にも色々とChromeWebストアにデブリを撒いてきましたが、これ以上ストアを散らかすなということかもしれません。
拒否はされてしまいましたが、天地神明に誓って何らかの悪さをしたりはしていないので、この拡張をご利用になりたい場合は下記項目のZIPファイルからお試しいただければと思います。
実行ファイルを用意しましたので直接拡張をインストールしてお楽しみください
公開を停止しました(2022/04/28)
上記のリンクよりzipファイルをダウンロードいただき、解凍して作成されたディレクトリをChromeの拡張機能の管理画面から「パッケージ化されていない拡張機能を読み込む」で選択することでインストールできます。
ブラウザにえっちなサイトの閲覧履歴が残るのはちょっと…という方へ
ブラウザのシークレットモードで拡張を実行する方法をご紹介します。
Chromeの拡張機能の管理画面から、当該拡張の「詳細」をクリックします。
シークレットモードでの実行を許可する、の項目をONにすることでシークレットモードのブラウザでも拡張を利用することが出来るようになります
さあ安心して一緒に流星群を作りましょう。
特記事項
信じてくれとは言えないのですが、普段マジでエロサイトとかは見ないので、ググって1ページ目に出てくるページの内容ぐらいしか集められていません。そのためマニアックな性癖とかのえっちサイトの検出は出来ません。 あと、本来エロくないコンテンツなのにエロく見えている人の感情とかも検出はできません。あしからずご了承ください。
まとめ
ギャグ漫画のネタをマジでやろうとすると大変だということがわかりました。
あと、元ネタの漫画「吸血鬼すぐ死ぬ」は頭スッカラカンにして読むことが出来る漫画なので疲れてしまったときとかに読むのにピッタリです。既刊の一部がKindle Unlimitedになってるので未読の方は是非お試しください。
ここまでお読みくださりありがとうございました。