アルティメットウルトラスーパーでーじうすまさやっけーばんないエクスプロージョン
この投稿は ちゅらデータアドベントカレンダー2023 の26 - 6日目の記事です。
大遅刻です
アイパー隊長といいます。ちゅらデータを退職してから1年ちょっと経ちました。
ちゅらデータでやったことといえば、これです。
こいつをリリースしたんですが、5年間でこれしか成果を上げることができず、去年ついにレイオ◯されてしまいました🥺
とぼとぼしてたところ、運良く物流の会社に拾われてフロントエンドやったりバックエンドやったり、データパイプライン整備してたり、たまに配送したり倉庫いったり電話取ったりしています。
さて、今回の話ですが、タイトルにもある通り、 爆発 です。
エクス!プロージョンッ!!!!!です。
はい
正直勢いで決めたタイトルなので、全く意味は無いのですが、有言実行するために、 なんとか爆発させたいと思います。
まずは企画が大事です。ちゅらデータアドベントカレンダーなので、ちゅらデータらしさを体現しましょう。ちゅらデータらしさとは?
ちゅらデータらしさとは?????
酒? 居酒屋? チラデータ???? うううッ 頭が
∧∧
( ゚∀゚)
⊂ つ
(つ ノ
(ノ
\ ☆
| ☆
(⌒ ⌒ヽ /
\ (´⌒ ⌒ ⌒ヾ /
('⌒ ; ⌒ ::⌒ )
(´ ) ::: ) /
☆─ (´⌒;: ::⌒`) :; )
(⌒:: :: ::⌒ )
/ ( ゝ ヾ 丶 ソ ─
頭が沸騰ボーン!
...( ゚д゚)ハッ!危ないところでした。少し 記憶障害 が起こったようです。
さて、もう一年も離れてしまうと ちゅらデータらしさ というものの記憶が薄れてきているので、とりあえず画面いっぱいに 「ちゅらデータ」の文字を埋め尽くす ことで思い出せるんじゃないか?という 仮設 仮説を立てます。
最近良く触ることの多いJavaScriptを使ってできるか試してみましょう。
生贄 参考とさせてもらうサイトを選択することにします。そうだ、 Dockerが好きそうな このカエルのアイコンの人の記事はどうでしょうか。
テキストを置換する
まず、JavaScriptでテキスト置換する場合は replace
を利用します。
chrome devtoolsを起動して、consoleで試してみましょう。
できました。
簡単すぎて面白くないですね。 そうだ。形態素解析を使って、置換すべき単語の品詞を制限してみるのはどうでしょうか。
形態素解析を使ってみる
Ginzaという日本語用のNLPライブラリがあります。このOSSを利用してやりましょう。
Dockerfileを用意してやろうとしましたが、失敗したので諦めてColaboratoryで用意します。
!pip install ginza ja-ginza
import spacy
nlp = spacy.load('ja_ginza')
doc = nlp('今年の干支は庚子です。東京オリンピックたのしみだなあ。')
for sent in doc.sents:
for token in sent:
print(token.i, token.orth_, token.lemma_, token.pos_,
token.tag_, token.dep_, token.head.i)
nlp
に食わせた文章を形態素解析すると、こんな結果になります。
0 今年 今年 NOUN 名詞-普通名詞-副詞可能 nmod 2
1 の の ADP 助詞-格助詞 case 0
2 干支 干支 NOUN 名詞-普通名詞-一般 nsubj 4
3 は は ADP 助詞-係助詞 case 2
4 庚子 庚子 PROPN 名詞-普通名詞-一般 ROOT 4
5 です です AUX 助動詞 cop 4
6 。 。 PUNCT 補助記号-句点 punct 4
7 東京 東京 PROPN 名詞-固有名詞-地名-一般 compound 8
8 オリンピック オリンピック NOUN 名詞-普通名詞-一般 obl 9
9 たのしみ たのしみ PROPN 名詞-普通名詞-一般 ROOT 9
10 だ だ AUX 助動詞 cop 9
11 なあ なあ PART 助詞-終助詞 mark 9
12 。 。 PUNCT 補助記号-句点 punct 9
形態素解析すると、文章がトークン区切りで取り出せることや、そのトークンの品詞なども確認することができます。
ただただ置換すると面白くないので、ここで品詞も考慮して置換できるようにします。助詞(ADP)、句読点(PUNCT)、名詞-数詞(NUM)以外は置換するようにしてみます。
IGNORE_POS = ['PUNCT', 'NUM', 'ADP']
def convert_sentence_churadata_lang(sentence, verbose=False):
doc = nlp(sentence)
s = ''
for sent in doc.sents:
for token in sent:
if token.pos_ in IGNORE_POS:
s += token.orth_
continue
# s += なんらかの方法でテキストを「ちゅらデータ」にする関数(token.orth_)
# debug用
if verbose:
print(token.orth_, token.pos_)
return s
関数名は convert_sentence_churadata_lang
にしました。ナンノコッチャ
さて、次は s += なんらかの方法でテキストを「ちゅらデータ」にする関数(token.orth_)
の要件を満たす関数を実装しましょう。
とりあえず簡単にするために、「入ってきた文字数分、「ちゅらデータ」の文字からランダムに取得して用意した文字列に置き換える」という感じにしてみます。
numpyの random.choice
を使ってみます。
CHURA = list('ちゅらデータ')
def なんらかの方法でテキストを「ちゅらデータ」にする関数(s):
return ''.join(np.random.choice(CHURA, size=len(s)))
できました!簡単ですね!
名前は convert_churadata_lang
にしましょう。本当にナンノコッチャ
これらを組み合わせます。
import numpy as np
import random
CHURA = list('ちゅらデータ')
def convert_churadata_lang(s):
return ''.join(np.random.choice(CHURA, size=len(s)))
IGNOER_POS = ['PUNCT', 'NUM', 'ADP']
def convert_sentence_churadata_lang(sentence, verbose=False):
doc = nlp(sentence)
s = ''
for sent in doc.sents:
for token in sent:
if token.pos_ in IGNOER_POS:
s += token.orth_
continue
s += convert_churadata_lang(token.orth_)
# debug用
if verbose:
print(token.orth_, token.pos_)
return s
できました!試してみましょう。
convert_sentence_churadata_lang('上手く形態素に分割できています。左から「入力語」「見出し語(基本形)」「品詞」「品詞詳細」です(tokenの詳細はspaCyのAPIを参照してください)。GiNZAでは依存構造解析にも対応しており、依存関係にある単語番号とその単語との関係も推定されています(token.dep_の詳細はこちらを参照してください)。')
ーデちちちデにデタらデちーちゅ。らから「ゅデら」「ーーーゅ(デーデ)」「らタ」「タちタゅ」ーー(タゅタデーのーゅはタゅタデタのデーーをちちーらタゅータ)。らゅデーーではデちゅデーデにもちちゅタデゅ、タタターにらちーデちゅとゅゅータとのタゅもタゅゅーちちゅタ(ちちターゅデゅちゅ_のららはゅちタをらタタデちーゅら)。
えくせれんと!!!!!!(???)
できました。良さそうですね!!!(本当か?)
これができたら、次はブラウザから実行できるように、API化します。 クソアプリに お金かけるのは嫌なので、Colaboratoryとなにかを使って公開できるようにしてみます。
Cloudflareを使ってみる
Ngrokでできることは知っていましたが、軽く調べてみるとCloudflareを利用している人もいました。せっかくなんで、こちらを使ってみます。
調べてみると、対象OS用の手順に従って、コマンドをインストールし、公開したいローカルのポートを指定してコマンドを叩いたら、URLが払い出されるようです。
試してみましたが、問題にぶち当たりました。
Flaskのapp.run()
とCloudflareのコマンドは、それぞれプロセスが常駐するので、Colaboratoryで実行すると終了待ちで待機するようになってしまいます。
そのため、どれかをバックグラウンドで実行する必要がありますが、順番的には、①Flask起動後、②Cloudflareのコマンドが起動しているポートを参照しておkなら払い出す(もしポートへ接続できなければエラーで落ちる)ということになっているようで、Flaskをうまいことバックグラウンドで動かすようにしないといけないのかな。と思っていました。
Ngrokの場合、flask-ngrok
というpackageがあるので、それを使えばよしなにやってくれるのですが、今回はそういうものはなさそうです。
↑packageを真似て、PythonのSubprocessモジュールでCloudflareコマンドを別プロセスで実行してを試してみましたが、標準出力などうまいこと表示させることができずに払い出されたURLが参照できず苦しんでいました。
そして、色々ガチャガチャ試した結果、この方法ならうまくいきました。
-
nohup
でCloudflareのコマンドを起動する -
瞬時に Flaskの
app.run()
のセルを叩く - 1で実行したCloudflareのコマンドがポート接続おkになり、URLが
nohup.out
へ書き込まれる
ぱわー!!!!!
/フフ ム`ヽ
/ ノ) ∧∧ ) ヽ
゙/ | (´・ω・`)ノ⌒(ゝ._,ノ
/ ノ⌒7⌒ヽーく \ /
丶_ ノ 。 ノ、 。|/
`ヽ `ー-'_人`ーノ
丶  ̄ _人'彡ノ
ノ r'十ヽ/
/`ヽ _/ 十∨
できたソースコードとコマンド群がこちらです。
!rm -f nohup.out
!nohup cloudflared tunnel --url localhost:5000 &
# import Flask from flask module
from flask import Flask, jsonify, request, redirect
from flask_cors import CORS
import json
app = Flask(__name__) #app name
CORS(app)
# これは使わないけどテスト用
@app.route("/")
def hello():
return "Hello"
@app.route('/api/translate', methods=['POST'])
def translate():
print(request.json)
texts = request.json['texts']
result = convert_sentence_churadata_lang(texts)
return {'results': result}, 200
if __name__ == "__main__":
app.run()
これでAPI経由で変換できるようになりました!!!!!
試してみましょう。
実行するとドメインが払い出されました。
https://xxx-but-surrounding-shadows.trycloudflare.com/
$ curl -XPOST -s 'https://xxx-antique.trycloudflare.com/api/translate' -H 'Content-Type: application/json' -d '{"texts": "おはよう日本!"}' | jq .
{
"results": "タちちゅらゅ!"
}
\\\٩( 'ω' )و //// < \タちちゅらゅ!/
発音できねぇ → らゅ
ちなみに、Flaskの実行プロセスを落とすと、Cloudflareコマンドもエラーで落ちるので便利です(?)
Chrome Pluginを用意する
テキストを変換するのを簡単にするためにはどうしたら良いでしょうか。類似するアプリを考えてみると、そうですね。Google翻訳です。
ボタン押したらすぐ変換されるような仕組みを考えてみると、Chrome Pluginがいいんじゃないかなと思います。
しかし僕はChrome Pluginを開発した経験はありません。どうしたら良いでしょうか。外部委託するしかないでしょうか。
とりあえず ドラえもん ChatGPTに聞いてみました。
・・・天才か? (AIなんだけども)
その通り(にしたら動かんかったけどエラーみてなんとか治す)に実装してみたので、試しにCSSを当ててみます。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<button id="replaceText">変換します</button>
<script src="popup.js"></script>
</body>
</html>
{
"manifest_version": 3,
"name": "Text Replace Extension",
"version": "1.0",
"description": "Replace text on webpages.",
"permissions": [
"activeTab"
],
"action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
},
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"run_at": "document_start",
"js": [
"content.js"
]
}
],
"web_accessible_resources": [
{
"resources": [
"style.css"
],
"matches": [
"<all_urls>"
]
}
]
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
var link = document.createElement('link');
link.href = chrome.runtime.getURL('style.css');
link.type = 'text/css';
link.rel = 'stylesheet';
document.head.appendChild(link);
if (request.action === "replaceText") {
// なにか書く
}
});
span, p {
color: red;
}
準備ができました!
その他参考にした記事たち(ありがとうございます)
ちなみに、icon.png
を用意する必要がありましたが、適当に某会社のアイコンをスクショいたしました。
翻訳アプリに並び 文章構成 しそうな雰囲気です。
テキストを抽出して、APIをコールし変換するプログラムを書く
テキストを抽出する
テキストを集めてくるだけならこれでいけます。
function translateTextNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
// 翻訳APIを使用してテキストを翻訳するロジックをここに追加
node.textContent = '翻訳されたテキスト';
} else {
node.childNodes.forEach(translateTextNode);
}
}
translateTextNode(document.body);
これをchrome devtoolsのコンソールで実行すると、テキストが「翻訳されたテキスト」に置き換わります。
Nodeって何?とかなんでこんなことができるの?についてはこの辺から学ぶと良いです(たまに混乱する)
APIをコールする
抽出して置換するコードが用意できたので、そのタイミングでAPIをコールするようにしてみます。
const URL = 'https://xxx-wings.trycloudflare.com/api/translate';
async function doPost(data) {
const res = await fetch(URL, {
method: "POST",
body: JSON.stringify(data),
headers: {"Content-Type": "application/json"}
});
return res.json();
}
async function translateTextNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
// 翻訳APIを使用してテキストを翻訳するロジックをここに追加
node.textContent = await doPost({"texts": node.textContent});
} else {
node.childNodes.forEach(translateTextNode);
}
}
translateTextNode(document.body);
よしこれでおkです!
おkではない
引っかかる方は多いと思いますが、WebのページのNodeの数は結構多いので、doPost
が叩かれる回数が軽く1000件を超えます。叩く度にawait
していると全然置換されないし、また、ネットワークのレイテンシも馬鹿にならんし、なによりテキストNodeの数分APIをコールすることになるので、突然の数の暴力がColaboratoryを襲う!ことになります。
下記方向で修正します。
- 意味わからん短い文字列は変換しないようにする(リクエストしないようにする)
- ある程度のまとまりでリクエストするようにする(リクエスト回数を減らす)
const replaceNodes = [];
function translateTextNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
// 3文字以下は変換対象にしない
const is5Length = node.textContent.length > 3;
if (is5Length) {
replaceNodes.push([node, {"text": node.textContent}]);
}
}
node.childNodes.forEach(translateTextNode);
}
// 参考: https://qiita.com/yarnaimo/items/e92600237d65876f8dd8
function chunk(arr, size) {
return arr.reduce(
(newarr, _, i) => (i % size ? newarr : [...newarr, arr.slice(i, i + size)]),
[]
)
}
function requestPost() {
// Arrayを50個区切りにする(調整可能)
chunk(replaceNodes, 50).forEach(async (chunkNodes) => {
const res = await doPost({"texts": chunkNodes.map(n => n[1])});
if (chunkNodes.length === 1) {
return;
}
chunkNodes.forEach((node, index) => {
try {
node[0].textContent = res.results[index];
console.log(node[0].textContent, res.results[index]);
} catch (e) {
console.error(e);
}
});
})
}
translateTextNode(document.body);
まるっとリクエストするようにしたので、API側も変更します。
# import Flask from flask module
from flask import Flask, jsonify, request, redirect
from flask_cors import CORS
import subprocess
import json
app = Flask(__name__) #app name
CORS(app)
@app.route("/")
def hello():
return "Hello."
@app.route('/api/translate', methods=['POST'])
def translate():
print(request.json)
texts = request.json['texts']
# 下記を変更した
result = [convert_sentence_churadata_lang(t['text']) for t in texts]
return {'results': result}, 200
if __name__ == "__main__":
app.run()
$ curl -XPOST -s 'https://xxx-focal.trycloudflare.com/api/translate' -H 'Content-Type: application/json' -d '{"texts": [{"text": "おはよう日本!"}]}' | jq .
{
"results": [
"タゅちゅちタ!"
]
}
おk!配列で返ってくるようになりました。
では、こちらをPluginに反映させてみましょう。
やったね!!!
色々「ちゅらデータ」にしてみる
あの有名なサイトも
こう
カエルアイコンの記事も
こう
おや
わざわざ変換しなくてよい「ちゅらデータ」だった文字も「ちゅらデータ」に変換されています。こちらは Issue を積んでおきましょう。
Zennのトレンドページも
こう
ハッカーがスキなサイトも
こう!(なんか期待と違う...)
来年公開予定のあのガンダムのサイトも
(怒られたら消します)
僕のブログも
こう
できました!これが「ちゅらデータ」です!!!
いかがだったでしょうか。頼まれてもいないのにとりあえず勢いでやってみた記事です。くぅ~疲れました。
なにか忘れているような?
アッ
爆発を添える
爆発のこと完全に忘れていたのでやります
過去、こんな記事が話題になりました。
filter関数、すごいですね。こんなテクノロジ(?)がCSSにも実装されているなんて。
この関数を調べてみるとテキストにも使えることがわかったので、遊んでみました。
先人のひよこ爆発を参考にテキスト爆発を実装してみます。
パクる お借りする
先人に知恵を
確認してみると、先人のひよこ爆発は、いくつかdiv
で囲んで、それを@keyframe
で時間差でアニメーションをするような仕組みになっていました。
今回はテキストを検索し、その親Elementを必要なdiv
構造に置き換えるという案で進めたいと思います。
それでは先程のPluginにこの実装を足してみます。
爆発ボタンを用意します
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<style>
/* モダンなbuttonにしてほしいです */
#replaceText {
width: 100px;
height: 30px;
background-color: #00ff00;
}
#replaceText:hover, #replaceText:active {
background-color: #00dd00;
}
#explosion {
margin-top: 10px;
width: 100px;
height: 30px;
background-color: #ff0000;
}
#explosion:hover, #explosion:active {
background-color: #dd0000;
}
</style>
<button id="replaceText">変換します</button>
<button id="explosion">爆発します</button>
<script src="popup.js"></script>
</body>
</html>
ナイス爆発!ナイス爆裂!!
モダンなボタン ができました。
/* モダンなbuttonにしてほしいです */
Copilotに願いましたが これがでてきました。
爆裂ボタンがclickされたらイベントを叩く
そろそろ眠たいのでサクッと載せていきます。
とりあえずは用意したbuttonにeventを用意するとの、content.js
で用意する関数になんのactionかをわかるようにします。
document.getElementById('replaceText').addEventListener('click', function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {action: "replaceText"})
.then((response) => {
console.log(response);
})
.catch((error) => {
console.error(error);
})
});
});
document.getElementById('explosion').addEventListener('click', function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {action: "explosion"})
.then((response) => {
console.log(response);
})
.catch((error) => {
console.error(error);
})
});
});
// ...省略...
// 時間がないのでパワー!!!
function createElement(node) {
const expScale = document.createElement('div');
expScale.className = 'exp-scale';
const expShadow = document.createElement('div');
expShadow.className = 'exp-shadow';
const expLight = document.createElement('div');
expLight.className = 'exp-light';
const expHue = document.createElement('div');
expHue.className = 'exp-hue';
expHue.appendChild(node);
expLight.appendChild(expHue);
expShadow.appendChild(expLight);
expScale.appendChild(expShadow);
return expScale;
}
// ...省略...
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
var link = document.createElement('link');
link.href = chrome.runtime.getURL('style.css');
link.type = 'text/css';
link.rel = 'stylesheet';
document.head.appendChild(link);
if (request.action === "replaceText") {
// みんなちゅらデータに変換されるのはこちら
translateTextNode(document.body);
requestPost();
}
if (request.action === "explosion") {
// 爆発するのはこちら
// text nodeを判定してうまいことやりたかったけどうまくいかなかったので
// Element検索してやるようにした
['p', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'li', 'dt', 'a'].forEach((tagName) => {
document.querySelectorAll(tagName).forEach((ele) => {
ele.replaceWith(createElement(ele.cloneNode(true)));
});
});
}
});
先人のお知恵のCSSをほぼコピペします。
:root {
--drop-size: 10px;
--drop-radius: 10px;
}
@keyframes shake1 {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(0, 3px);
}
50% {
transform: translate(0, 0);
}
75% {
transform: translate(0, -3px);
}
100% {
transform: translate(0, 0);
}
}
@keyframes shake2 {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(0, 5px);
}
50% {
transform: translate(0, 0);
}
75% {
transform: translate(0, -5px);
}
100% {
transform: translate(0, 0);
}
}
@keyframes explosion-shadow {
0% {
filter: drop-shadow(0 0 0 0 white);
}
50% {
filter: drop-shadow(var(--drop-size) var(--drop-size) var(--drop-radius) red) drop-shadow(var(--drop-size) calc(-1 * var(--drop-size)) var(--drop-radius) yellow) drop-shadow(calc(-1 * var(--drop-size)) var(--drop-size) var(--drop-radius) yellow) drop-shadow(calc(-1 * var(--drop-size)) calc(-1 * var(--drop-size)) var(--drop-radius) red);
}
100% {
filter: drop-shadow(var(--drop-size) var(--drop-size) var(--drop-radius) white
/* var(--drop-color) */
) drop-shadow(var(--drop-size) calc(-1 * var(--drop-size)) var(--drop-radius) white) drop-shadow(calc(-1 * var(--drop-size)) var(--drop-size) var(--drop-radius) white) drop-shadow(calc(-1 * var(--drop-size)) calc(-1 * var(--drop-size)) var(--drop-radius)
/* var(--drop-color) */
white);
}
}
@keyframes explosion {
0% {
}
30% {
filter: hue-rotate(-50deg) contrast(100%);
}
60% {
filter: opacity(80%);
}
100% {
filter: hue-rotate(-50deg) contrast(100%) opacity(0%);
}
}
.target {
cursor: pointer;
}
.exp-hue {
animation: shake1 0.1s infinite, shake2 0.1s 2s infinite;
}
.exp-light {
animation: explosion 7s 1 forwards;
}
.exp-shadow {
animation: explosion-shadow 5s 1s;
}
.exp-scale {
transform: scale(200%);
transition: 3s;
transition-delay: 3s;
}
ナイス爆発!ナイス爆裂!!
試す
木っ端微塵です!ナイス爆発!!ナイス爆裂!!!
色々試す
爆発!
爆裂!!
エクスプローーーージョン!!!!!
ジョン!!!!
異常です 以上です。
ちなみに、パフォーマンス対策スルーなので、 何度か連打するとChromeがぐるぐるして落ちます。
まとめ
- レイオ◯は嘘です
- Google翻訳はすごい
- 思い出しました。ちゅらデータらしさ。「Be Crazy!」です
おまけ
Q. テキスト変換と爆発はあわせてできないの?
A. わかる。できるんだけど、なんか変
それでは良いお年を〜
Discussion
で大爆笑ww
初笑いでございますww