プログラミングをこれまで一切したことがない文系大学生です。文学部ではないです。
夏休みの自由研究ということで7日間Pythonを独学して、「世にも奇妙な物語」風タイトルメーカー を作ってみました。
あまりに無謀です。そしてきっかけは
コードはとにかく汚いと思います。行き当たりばったりで作ったのでラベリングもその場しのぎです。
もう疲れたので細かい説明は特にありません。記事もコード内の添え書きも表記揺れも酔うほどすごいです。常体と形体の区別もひどい。
記事の最後につまずいたところなど、雑な感想をだらだら述べています。
7日間の結果(exeの動画)
先に成果物を載せておきます。exeに変換したpyを実行する動画です。
ざっくりプロセス
例えば「不倫警察」が「週刊誌刑務所」になれば面白いですね。つまり、単語に分けてそれぞれ類語にすると良さそうです。したがって、今回することは
- 本家のタイトル一覧を形態素解析(janome)
- 名詞+助動詞+名詞など
- よく出てくる文構造を確認(Excel...泣)
- 本家のタイトルにはない「名詞」を2種類の方法で用意する★
- ランダムな単語をとってくる(webdriver)
- 本家のタイトルの名詞 の類語を探してくる(gensim)
- 本家のタイトルの名詞を上の2種に置き換える(ランダム)★
これだけです。
前半2つは毎回する作業ではないので、実行した結果をtxt化して保存してあります。
★の付いた後半2つが今回のコードになります。GUI作成も込みです(Tkinter)。
ちなみに類似語と類義語をそんな厳密に分けたり定義するつもりは無いですが、genism君は類似語の方を取りだしている感覚に近いです。
類義語
=置き換えても文意がほぼ変わらない言葉
例えば「今日」と「本日」
類似語
=置き換えると文意は変わるかもしれない言葉で、ジャンルや音が近い言葉
例えば「今日」と「昨日」
という理解でいくと、類似語は対義語を含むことがあります。genism君に「"男"とmost_similarな単語探してね」というと、「"女"」と返してきます。
冗長コード
クリックやタップで開きます。
コード本体
# -*- coding: utf-8 -*-
print('\n' + '起動したよ')
import tkinter as tk
import winsound
from sys import exit
root = tk.Tk()
menubar = tk.Menu(root)
root.config(menu=menubar)
root.geometry('1000x800')
root.configure(bg="black")
root.geometry('+0+0')
root.title('世にも奇妙なタイトルメーカー')
root.overrideredirect(True)
message = tk.Message(root, text= (" 世にも奇妙な" + '\n' +
"タイトルメーカー"),
font=("FOT-コミックミステリ Std DB",90), foreground="#e43d4e",
bg="black", width=1080)
message.pack(anchor='center', pady=100)
def close_window(): #
winsound.PlaySound('door.wav', winsound.SND_FILENAME)
root.destroy()
def finpy(): # 終了!
winsound.PlaySound('fin.wav', winsound.SND_FILENAME)
root.destroy()
exit()
button2 = tk.Button(root, text = 'メーカーを終了する', command=finpy,
font=("FOT-コミックミステリ Std DB",20),
foreground="#797979", bg="black").pack(side = tk.BOTTOM, fill = 'x', padx=60, pady=40)
button = tk.Button(root, text = 'つくる...', command=close_window,
font=("FOT-コミックミステリ Std DB",70),
foreground="#ffffff", bg="black").pack(side = tk.BOTTOM, fill = 'x', padx=60, ipady=20)
winsound.PlaySound("m_theme.wav", winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_LOOP)
root.mainloop()
winsound.PlaySound("m_theme.wav", winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_LOOP)
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from janome.tokenizer import Tokenizer
import random
from gensim.models import KeyedVectors
print('\n' + '膨大な量の日本語データベースを読み込んでるよ')
model_path = ('entity_vector.model.bin')# chiVeデータのPATH示すよ
wmodel = KeyedVectors.load_word2vec_format(model_path, binary=True)# モデルの読み込み
addwl = []
t = Tokenizer()
print('\n' + 'ランダムな単語を選んでるよ')
option = Options() # オプションを用意
option.add_argument('--headless') # ヘッドレスモード!!!
option.add_experimental_option('excludeSwitches', ['enable-logging'])
driver = webdriver.Chrome(options=option)
driver.get('https://tango-gacha.com/') # URLを指定してブラウザ開くよ
#-------------------------------------------------
#-------------------------------------------------
nroop = 1 # ワード取得を繰り返すよ。nroop x 3 回。★調整値★
#-------------------------------------------------
#-------------------------------------------------
print('\n' + f'あと{nroop*3}秒待っててね')
# 追加ワードのリストです
for i in range(nroop):
time.sleep(3) # 待機
# 取り出してw1,w2,...,w10って名前つけとくよ
# 属性、つまりattributeをwebelementsじゃなくてテキストでgetするよ
w1 = driver.find_element(by=By.XPATH, value='//*[@id="show_txt1"]') .get_attribute("textContent")
w2 = driver.find_element(by=By.XPATH, value='//*[@id="show_txt2"]') .get_attribute("textContent")
w3 = driver.find_element(by=By.XPATH, value='//*[@id="show_txt3"]') .get_attribute("textContent")
addwl.append(w1)
addwl.append(w2)
addwl.append(w3) # w1からw3をaddwlに突っ込みます
restore = driver.find_element(by=By.CSS_SELECTOR, value='#word > div.options > div.mouitido > a').click() # 引き直す
print('\n'+ "=====ランダムワード=====")
print(addwl)
print("========================"+'\n')
driver.close() # ブラウザ閉じるよ
#-------------------------------------------------
meishi_list = []
for aw in addwl:
#print("addwl_list:" + aw) # addwl_list:awの形でプリントするよ
awts = t.tokenize(aw)
for awt in awts:
awt_ana = awt.part_of_speech.split(',')[0]
#print(awt_ana)
if(awt_ana.startswith('名詞')):
meishi_list.append(aw)
break
#-------------------------------------------------
stext = open(r"sampoki.txt", "r", encoding="utf-8")
sa = list(stext.readlines()) # sample
rsa = (random.sample(sa,1))
resultm = ''
result_t= []
class Class :
def __init__(self, rrr, resultmm):
self.rsa = rrr
self.resultm = resultmm
def make(self):
print('\n'+ "=====もとのタイトル====="
'\n' + "".join(self.rsa)+
"========================"+'\n')
rsats = t.tokenize("".join(self.rsa))
cl = []
for rs in rsats:
rs_ana = rs.part_of_speech.split(',')[0]
if('名詞'in rs_ana) and (len(cl) == 0): # もし名詞なら
# 複合名詞が厄介なので複合名詞を1つの名詞だとみなす
dotti = random.randrange(4) # 調整してね。-1されるの忘れずに ★調整値★
if dotti < 3 : # 類語置換。★調整値★
if len(cl) == 0:
rwordlist = wmodel.most_similar(rs.surface,
topn = 5) # 類語の数 ★調整値★
wordsinfo_list = []
for rwords in rwordlist:
rwordsstr = str(rwords[0])
wordsinfo_list.append(rwordsstr.replace('[','').replace(']',''))
print('\n'+ "=======類語候補=======")
print(wordsinfo_list)
print("========================"+'\n')
ruigiw = "".join(random.sample(wordsinfo_list,1))
self.resultm += ruigiw
cl += ["mou_orude"]
else :
self.resultm += rs.surface
else : # ランダムワード置換
if len(cl) == 0:
raddwm = "".join(random.sample(addwl,1)) #ランダム追加ワードをもってきて
self.resultm += raddwm
cl += ["mou_orude"]
print (raddwm)
else :
self.resultm += rs.surface
elif('名詞'in rs_ana): # 複合名詞の2個目以降の名詞は
self.resultm += rs.surface
else: # それ以外はそのまま入れるよ、それ以外ということは複合名詞終わってるからclリセット
self.resultm += rs.surface
cl = [] #clをリセットするよ
print('\n'*8 + "======================")
print("======================" )
print("=====生成タイトル=====" +'\n'+
'\n' + '→→→' + self.resultm +'\n')
print("======================" )
print("======================" )
print("======================" + '\n')
result_t.append(self.resultm)
with open('made_tilte.txt', "a", encoding="utf-8")as mt: #作ったタイトルのログ
mt.write(self.resultm + '\n')
with open('used_samp_title.txt', "a", encoding="utf-8")as ust: #作ったタイトルのログ
ust.write("".join(self.rsa))
return self.resultm
def firstmain():
c = Class(rsa,resultm)
c.make()
firstmain()
def main():
with open(r'used_samp_title.txt', "r", encoding="utf-8")as ust2:
last_line2 = ust2.readlines()[-1]
c = Class(last_line2, resultm)
c.make()
def not_main():
stext = open(r"sampoki.txt", "r", encoding="utf-8")
sa = list(stext.readlines()) # sample
rrr = (random.sample(sa,1))
rrrrrr=''
c2 = Class(rrr, rrrrrr)
c2.make()
#-------------------------------------------------
subw = tk.Tk()
subw.geometry('900x900')
subw.configure(bg="black")
subw.title("世にも奇妙なタイトルメーカー")
subw.geometry('+0+0')
label00 = tk.Message(subw, text=result_t,
font=("FOT-コミックミステリ Std DB",80), foreground="#e43d4e",
bg="black", width=900)
label00.pack(anchor='s',pady=40)
frame01 = tk.Frame(subw,width=840,height=160,relief=tk.FLAT, bg='black', bd=0)
frame01.propagate(False)
frame00 = tk.Frame(frame01,width=550,height=160,relief=tk.FLAT, bg='black', bd=0) # 2つの文
frame00.propagate(False)
message11 = tk.Message(frame00, text="参考原作タイトル...... " + "".join(rsa),
font=("FOT-コミックミステリ Std DB",20), foreground="white",
bg="black", width=800)
message22 = tk.Message(frame00, text=addwl,
font=("FOT-コミックミステリ Std DB",12), foreground="#797979",
bg="black", width=800) # ランダムワード
subsubw = tk.Tk()
subsubw.geometry('600x900')
subsubw.configure(bg="black")
subsubw.title('つくったタイトルリスト')
subsubw.geometry('+900+0')
message001 = tk.Message(subsubw, text='今までつくったタイトル(最新 9 )',
font=("FOT-コミックミステリ Std DB",16), foreground="#797979",
bg="black", width=850)
message001.pack(anchor='ne',pady=5)
with open(r'made_tilte.txt', "r", encoding="utf-8")as mt3:
last10 = mt3.readlines()[-10:-1]
message002 = tk.Message(subsubw, text="".join(last10),
font=("FOT-コミックミステリ Std DB",40), foreground="white",
bg="black", width=850,justify=tk.RIGHT)
message002.pack(anchor='e',pady=40)
# 以下ボタンから使うメソッドがだぁーっと
def one_more(): # 途中から
main()
with open(r'made_tilte.txt', "r", encoding="utf-8")as mt2:
last_line = mt2.readlines()[-1]
label00.config(text=last_line)
with open(r'made_tilte.txt', "r", encoding="utf-8")as mt3:
last10 = mt3.readlines()[-10:-1]
message002.config(text="".join(last10))
winsound.PlaySound("m_theme.wav", winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_LOOP)
def forget1():# 忘却
winsound.PlaySound('po.wav', winsound.SND_FILENAME | winsound.SND_ASYNC)
message002.config(text='')
label00.config(text='')
subw.after(100,one_more)
message002.update()
label00.update()
def one_more2(): # 準備から
not_main()
with open(r'made_tilte.txt', "r", encoding="utf-8")as mt2:
last_line = mt2.readlines()[-1]
label00.config(text=last_line)
with open(r'made_tilte.txt', "r", encoding="utf-8")as mt3:
last10 = mt3.readlines()[-10:-1]
message002.config(text="".join(last10))
with open(r'used_samp_title.txt', "r", encoding="utf-8")as ust2:
last_line2 = ust2.readlines()[-1]
message11.config(text="参考原作タイトル...... " + last_line2)
winsound.PlaySound("m_theme.wav", winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_LOOP)
def forget2(): # 忘却2
winsound.PlaySound('popi.wav', winsound.SND_FILENAME | winsound.SND_ASYNC)
message002.config(text='')
label00.config(text='')
message11.config(text='')
subw.after(100,one_more2)
message002.update()
label00.update()
message11.update()
flag0 = []
class Class2:
def __init__(self, flag):
self.flag0 = flag
def ok(self):
if len(self.flag0)%2 == 0:
winsound.PlaySound(None, winsound.SND_FILENAME)
winsound.PlaySound('dj.wav', winsound.SND_FILENAME)
winsound.PlaySound('story_teller.wav', winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_LOOP)
self.flag0 += 'a'
else :
winsound.PlaySound(None, winsound.SND_FILENAME)
winsound.PlaySound('dj.wav', winsound.SND_FILENAME)
winsound.PlaySound('m_theme.wav', winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_LOOP)
self.flag0 += 'b'
def finpy2(): # 終了!
winsound.PlaySound('fin.wav', winsound.SND_FILENAME)
exit()
button11 = tk.Button(subw, text = 'つくりなおす', command=forget1,
font=("FOT-コミックミステリ Std DB",40),
foreground="#ffffff", bg="black")
button11.pack(side = tk.BOTTOM, fill = 'x', padx=30)
button33 = tk.Button(subw, text = '原作タイトルを選びなおす', command=forget2,
font=("FOT-コミックミステリ Std DB",20),
foreground="#ffffff", bg="black")
button33.pack(side = tk.BOTTOM, fill = 'x', padx=30)
button22 = tk.Button(subw, text = 'メーカーを終了する', command=finpy2,
font=("FOT-コミックミステリ Std DB",15),
foreground="#797979", bg="black")
button22.pack(side = tk.BOTTOM, fill = 'x', padx=30)
c2 = Class2(flag0)
button44 = tk.Button(frame01, text = 'BGM変更', command=c2.ok,
font=("FOT-コミックミステリ Std DB",20),
foreground="#797979", bg="black")
button44.pack(side = tk.RIGHT, fill = 'both', ipadx=25)
frame01.pack(side=tk.BOTTOM)
frame00.pack(side=tk.LEFT)
message11.pack(anchor='sw')
message22.pack(anchor='sw')
winsound.PlaySound("door2", winsound.SND_FILENAME)
winsound.PlaySound("m_theme.wav", winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_LOOP)
subw.mainloop()
〈補足〉ディレクトリの構造
構造と言うまでもなく1フォルダー内で作りました。
なぜならパスの参照が嫌いだからです。
- Yonimo_Kimyo_na_Title_Maker.py #.py本体
- entity_vector.model.bin #日本語辞書
- LICENSE #辞書のライセンス
- sampoki.txt #本家のタイトル
- made_tilte.txt #生成したタイトル
- used_samp_title.txt #本家のタイトル(直前に使ったもの)
- m_theme.wav #世にものメインテーマ
- story_teller.wav #ストーリーテラーのテーマ
- dj.wav #BGM切り替え音
- door.wav #生成開始音
- door2.wav #生成完了音
- fin.wav #メーカー終了音
- po.wav #再生成音
- popi.wav #再生成音2
- chromedriver.exe #スクレイピング用
EXE化
pyinstallerで行いました。条件としてはOSがWindows。あとはFOTのコミックミステリフォントがあったほうがいいです、なくても良いです。容量はでかいです。日本語辞書がでかい。
(著作権の問題と2GBとサイズがでかいのと、署名もなく安全性も確保できないので公開しない)
7日間の感想
以下難文が続きます。
感想 技術編
-
joinは便利です。困ったら試す。
-
イコール(=)の使い方なんなんですか。基本的に「イコールは等号だな」とざっくり捉えても問題ない場面が多いですが、左の変数に右の値を代入するって考え方がピンと来ません。
-
listを定義する位置で何度かつまずきました。for inの中にsample_list=[ ]っておいてると、毎回「sample_list=[ ]」って定義されてリスト内が初期化されるわけです。
-
文字列?str?いまだによくわかりません。
-
すぐエディターが教えてくれるけど、ifに:が必要。
-
if、ほんでif、ほんでelse。ってやってたけど、2回目以降はelif使わなきゃだめなのね。
-
append1つしかできんのかい。リストの扱いが癖がある。
-
人のコードを参考にしても、その人が作ったメソッドなのか、モジュールをインポートしなくても使える系のメソッドなのか分からない。
-
=や==や+=。未だにわかりません。
-
defの扱い(泣)。代入自体はわかるけど形態素解析から入ったから、ググってみて数字の例で説明されてもずっとピンとこない(泣)けどメソッド外で変数使いたい(泣)
-
途中で変数とかの名前を変えたとき。他の部分で変更し忘れる。甘い。
-
tkinerのスペルが覚えられない。1日かかりました。
-
.txtを扱う際、rかaかwか、気をつけとこう。
-
エラーを落ち着いて読めるようになるでしょ、そしてそして、検索して英語だらけのページへたどり着くが、逆に案外よめちゃう。こんな適当な文章書いておいてあれだけど、修飾語が多すぎて何言ってるかわかんない日本人よりそういうページ。
-
if {〈条件1〉and〈条件2〉} : ってやってて、条件が意味をなしてなかった。外側の { と } がいらなかった。
-
各所、色々定義し直しすぎなのはわかってます。
-
.exe化した際、「exitが読めまへん」とエラーが出てもうて、from sys import exitがそれを解決したんですよ。
感想 技術以外編
- 思い通りの動きしてくれたら素直に嬉しい。
- 「殺意取扱説明書」ってタイトルをそれぞれ類語などに置き換えた時に困りました。「殺気入荷未定マニュアル」みたいになり複合名詞が少し厄介。なので名詞が連続した場合、2個目以降は元の単語を送り出すという場合分けになってる。「殺意」だけを置換する形になる。
- マルコフ連鎖版のメーカーは2日目で完成してたけど、失敗。「サンプルが少ない」「タイトルだから長々されると困る」この2点が原因かな。以下マルコフで生成されたタイトルのサンプルと感想。
『恐怖の恋文』
私の感想「おー、いいかんじやん」
『恐怖の男超・能・力!遅すぎる家族猿の恋文』
私の感想「本家タイトルの"親切すぎる"と"遅すぎた"を混ぜれてるのはマルコフ連鎖の優秀さ!」
「恐怖の花子不眠症目の出負荷あなたの物語美女禁じられる!捨の記憶寺島声をきかせてるふり」
私の感想「......。」
友達の感想「聞かせてるふりなんや」
友達の感想「文章のぶち壊れ具合がAIコウメ太夫1」
- メーカーの仕組みを思いついた順番としては、
「ランダムワードを用いながらマルコフ連鎖する(ボツ)」
→「本家タイトルにランダムワードを置換する(ボツ)」
→「(今の形)」
でした。 - 今回類語に関してはオフラインの辞書を用いているので、ランダムにそれらを選択しても限りがある。そこでランダムワードもたまに紛れ込むようにした。
- 造語(ズンドコベロンチョ、ががばば とか)まで対応したかった。カタカナに崩してるだけでも形態素解析でエラーが出るので難しい。
- あと名詞以外も置換出来たら良かったけど、初めて書くコードとしてはおかしい長さだと思うのでやめた。形態素解析を細かくすればいけるとは思うけど用言には活用形ってもんがあって、、、。
- 「世にも」タイトルのどこがそれっぽいのか、単純な機械学習で判別するは難しい。そもそも作者がバラバラでタイトルの雰囲気もバラバラ。だから納得いかなければ複数作成してあとは勝手に判断してくださいという形になってしまった。
- ちなみに原作タイトル550のうち、(名詞+名詞)が118個、(名詞)が118個、この2つが同率で1位。次に(名詞+助詞+名詞)が70個。(名詞)の名詞をただ置き換えるプログラムは面白くないのでこれらは削除してます。
- 参考にするタイトルも 頻出文構造トップ5 、つまり約半分くらいにしぼった。でも使わなかった 少数派の構造の文 を見て、「世にもだ!」と思うだろうか?いや思わないはず。よく出る文構造だから「世にもだ!」となるわけだ、と思っておきたい。そもそも文構造と"それらしさ"の相関があるのか、根拠がなかった(大大大盲点)。
- 「"世にも"っぽさ」はさておき、どんなタイトルが出てきたら面白いのか。自分は原作の「ワタ毛男」「夢の検閲官」「墓友」「人間電子レンジ」のようなタイトルが作れたら面白いと思ってます。
- ちなみにジャルジャルファンです。「上の句」2と「下の句」をそれぞれ募ってつなぎ合わせる、そのタイトルでコントを即興でやるという企画をかなり前からジャルジャル(というか倉本美津留か?)が行ってます。もちろん2つの句はジャルファンの作る句なのでジャルからの影響は否めませんが、コント師ジャルジャルがいくら考えても思いつかないタイトルがその場で誕生しています。その圧倒的なボキャブラリー(=ファンの数×それぞれのファンの語彙)、偶然性、無造作性をもったタイトルはシュルレアリスムのデペイズマン的と言えるかもしれません。違和感、不適切さが面白い。タイトルがそうなれば、2人のコントも多くの場合違和感を引きずっているようですね。
- 関係ないですがジャルジャルは『タツヤとジョージ〜ヒゲ剃り滑らし〜』などデペイズマンを感じます。楽器ではないヒゲ剃り、さらに滑らすという演奏法、ヒゲ剃り滑らしがボーカルと並ぶ立場(B'zのギタリストみたいな)であること、こういった違和感を押し付けたまま説明や指摘もなく終わるコントです。ジャルジャル2人が考えてもデペイズマン的タイトル、内容のコントがあるようです。
- ほかに似たもののとしてコウメ太夫の「まいにちチクショー」3があります。面白いとは思いませんが、AIとの判別など話題ですね。まいチクの場合は 読者がなんとかして「上の句」「下の句」の行間を読む、というのが面白さの1つかと。哲学者コウ・メダユーさん4の論によればコウメ太夫は自動筆記の手法でまいチクをつくっています。「(コウメ太夫は)シュルレアリストである」と指摘されています。
- そして世にも奇妙な物語の原作タイトルの「人間電子レンジ」はタイトルだけ見るとデペイズマン的です。ジャルの企画、コウメのまいチク、世にものタイトル、この3つの共通集合にはデペイズマンがあるようですが、コウメが面白くないのは、デペイズマンのみだから、ということですね。ジャルにおけるコント、世にもにおける本編のドラマ、に該当するものがないんです。
- 「世にも奇妙なタイトルメーカー」って題名が本家のドラマで出たら激アツ。
参考、使ったものなど
→これをみていけるやんと思ったら様々な点で絶望。
-
コウメAI太夫
https://twitter.com/dayukoumeAI?t=6l51NpAjppCS1NbIj2PpCg&s=09 ↩ -
#ジャルジャルに上の句あげる奴 - Twitter検索 / Twitter
https://twitter.com/hashtag/%E3%82%B8%E3%83%A3%E3%83%AB%E3%82%B8%E3%83%A3%E3%83%AB%E3%81%AB%E4%B8%8A%E3%81%AE%E5%8F%A5%E3%81%82%E3%81%92%E3%82%8B%E5%A5%B4?src=hashtag_click ↩ -
#まいにちチクショー - Twitter検索 / Twitter
https://twitter.com/hashtag/%E3%81%BE%E3%81%84%E3%81%AB%E3%81%A1%E3%83%81%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%BC?src=hashtag_click ↩ -
哲学者コウ・メダユー
https://note.com/koume_philo ↩