歌ネットから歌詞を取得して穴埋めクイズを作成する

カラオケで穴埋めクイズ風に歌詞を隠しつつを歌うというのが面白かったので作りました。

※Pythonで作成したプログラムをWindows実行用にPyinstallerでexe化していますが、
 ウィルス対策ソフトによってマルウェアなどに分類されてしまうことがあるそうです。
 当然、そのような機能は実装していませんが。。。

■ファイル
https://drive.google.com/file/d/1wKf-ZSTDrIE2CUmj81Ls_6uaudk7Uy_v/view?usp=drive_link

【exeファイルの使い方】

  1. ダウンロードしたzipを解凍して、「quiz_creater.exe」を起動します。
    ※セキュリティの警告が表示される場合は、「詳細情報→実行する」
  2. URLに歌ネットの歌詞ページのURLを貼り付けて、GETボタンを押下するとテキストエリアに歌詞が表示されます。
  3. テキストエリアの歌詞から、穴埋めにしたいところを選択するとテキストボックスが追加されます。
  4. テキストボックス右側のDELボタンを押下すると穴埋めが解除されます。
  5. SAVEボタンを押下するとファイル保存ダイアログが表示されるので、HTMLファイルを出力したいフォルダとファイル名を設定してください。

【HTMLについて】

  1. HTMLを起動すると穴埋め状態で表示されます。
  2. ブラウザがChromeなら、F11キーを押下することで全画面表示になるかと思います。
  3. 画面内をクリックすることで、上から順番に穴埋め箇所が解除されます。

コードも載せておきます。

import os
import tkinter as tk
from tkinter import filedialog
import requests
from bs4 import BeautifulSoup
import copy

replace_list = {}
replace_list_index = 0

class ReplaceItem :
    def __init__(self, text_box, start_index, end_index, id_index) :
        self.text_box = text_box
        self.start_index = start_index
        self.end_index = end_index
        self.id_index = id_index

    def get_text(self) :
        result = "<span id=\"text" + str(self.id_index) + "\" style=\"background-color:#000;color:#000\">" + self.text_box.get() + "</span>"
        return result

def replace_text(event):
    global replace_list
    global replace_list_index

    # テキストエリアからテキストを取得
    text = text_area.get("1.0", "end-1c")

    # テキストの長さが0の場合、何もせずに終了
    if len(text) == 0:
        return

    # テキストエリアで選択されたテキストの開始位置と終了位置を取得
    try:
        start_index = text_area.index(tk.SEL_FIRST)
        end_index = text_area.index(tk.SEL_LAST)
    except ValueError:
        # テキストが選択されていない場合、何もせずに終了
        return

    selected_text = text_area.get(start_index, end_index)

    # フレームを作成してテキストボックスとボタンを横に並べる
    frame = tk.Frame(window)
    frame.pack(fill=tk.X, expand=True)

    # テキストボックスを配置
    text_box = tk.Entry(frame)
    text_box.insert(0, selected_text)
    text_box.pack(fill=tk.X, expand=True, side = 'left')

    # 置換アイテムリストに追加
    id_index = replace_list_index + 1 - 1
    replace_list[replace_list_index] = ReplaceItem(text_box, start_index, end_index, id_index)
    replace_list_index = replace_list_index + 1

    # 削除ボタンを配置
    del_button = tk.Button(frame, text="DEL")
    del_button.configure(command=lambda: del_text_box(frame, text_box, del_button, text_area, start_index, end_index, id_index))
    del_button.pack(side = 'right')

    # テキストエリアを更新
    text_area.delete(start_index, end_index)
    text_area.insert(start_index, "■" * len(selected_text))

# テキストボックスを削除する関数
def del_text_box(frame, text_box, del_button, text_area, start_index, end_index, del_index) :
    text_area.delete(start_index, end_index)
    text_area.insert(start_index, text_box.get())
    text_box.destroy()
    del_button.destroy()
    frame.destroy()
    replace_list.pop(del_index)

# 歌詞を取得
def getKashi(text_box, text_area) :
    response = requests.get(text_box.get())
    if response.status_code == 200:
        # Beautiful Soupを使用してHTMLを解析
        soup = BeautifulSoup(response.text, 'html.parser')

        # 歌詞がどの要素に格納されているかを調査し、適切な要素を選択
        # 例: 歌詞が <div class="lyrics"> タグの中にある場合
        lyrics_element = soup.find('div', id='kashi_area')
        
        if lyrics_element:
            # 歌詞テキストを取得
            lyrics_text = "\n".join(lyrics_element.stripped_strings)
            
            # 歌詞を表示またはファイルに保存
            text_area.insert(tk.END, lyrics_text)

        else:
            print("歌詞が見つかりませんでした。")
    else:
        print("URLにアクセスできませんでした。")

# HTML出力
def createHtml(text_area) :
    global replace_list

    # スクリプトのディレクトリを取得
    script_directory = os.path.dirname(os.path.abspath(__file__))

    # ファイル保存ダイアログを表示
    file_path = filedialog.asksaveasfilename(initialdir=script_directory, defaultextension=".html", filetypes=[("HTMLファイル", "*.html")])
    if not file_path:
        return

    # ファイル名(拡張子を除く)を取得
    filename = os.path.splitext(os.path.basename(file_path))[0]

    # 出力文字列を生成
    store_text = text_area.get("1.0", "end-1c")
    sorted_items = sorted(replace_list.items(), key=lambda item: (int(item[1].start_index.split(".")[0]), int(item[1].start_index.split(".")[1])), reverse=True)
    index_strs = []
    for key, item in sorted_items :
        text_area.delete(item.start_index, item.end_index)
        text_area.insert(item.start_index, item.get_text())
        index_strs.append(str(item.id_index))
    output_str = text_area.get("1.0", "end-1c").replace("\n", "<br/>\n")
    text_area.delete("1.0", "end")
    text_area.insert("1.0", store_text)

    # HTMLを成形
    html_str = "<html>\n"
    html_str = html_str + "  <head>\n"
    html_str = html_str + "    <title>\n"
    html_str = html_str + "      " + filename + "\n"
    html_str = html_str + "    </title>\n"
    html_str = html_str + "    <script>\n"
    html_str = html_str + "      var list = [" + ", ".join(list(reversed(index_strs))) + "];\n"
    html_str = html_str + "      var currentIndex = 0;\n"
    html_str = html_str + "\n"
    html_str = html_str + "      function changeStyles() {\n"
    html_str = html_str + "        if (currentIndex < list.length) {\n"
    html_str = html_str + "          var elementId = \"text\" + list[currentIndex];\n"
    html_str = html_str + "          var element = document.getElementById(elementId);\n"
    html_str = html_str + "\n"
    html_str = html_str + "          if (element) {\n"
    html_str = html_str + "            element.style.backgroundColor = \"transparent\"; // 背景色を無色に設定\n"
    html_str = html_str + "            element.style.color = \"black\"; // 文字色を黒色に設定\n"
    html_str = html_str + "          }\n"
    html_str = html_str + "\n"
    html_str = html_str + "          currentIndex++;\n"
    html_str = html_str + "        }\n"
    html_str = html_str + "      }\n"
    html_str = html_str + "\n"
    html_str = html_str + "      window.addEventListener(\"click\", changeStyles);\n"
    html_str = html_str + "    </script>\n"
    html_str = html_str + "  </head>\n"
    html_str = html_str + "  <body>\n"
    html_str = html_str + output_str + "\n"
    html_str = html_str + "  </body>\n"
    html_str = html_str + "</html>\n"

    with open(file_path, 'w') as file:
        file.write(html_str)
        print(f"ファイル '{file_path}' に保存しました")

# Tkinterウィンドウの作成
window = tk.Tk()
window.title("Text Replacement")

# テキストエリアを作成
text_area = tk.Text(window)
text_area.pack(fill=tk.BOTH, expand=True, side = 'top')  # 画面の伸長に合わせてサイズを変更

# フレームを作成
frame = tk.Frame(window)
frame.pack(fill=tk.X, expand=False)

# ラベルを作成
label1 = tk.Label(frame, text="URL:")
label1.pack(side = 'left')

# テキストボックスを作成
text_box = tk.Entry(frame)
text_box.insert(0, "https://www.uta-net.com/song/344723/")
text_box.pack(fill=tk.X, expand=True, side = 'left')

# GETボタンを作成
up_button = tk.Button(frame, text="GET", command=lambda: getKashi(text_box, text_area))
up_button.pack(side = 'right')

# SAVEボタンを作成
save_button = tk.Button(frame, text="SAVE", command=lambda: createHtml(text_area))
save_button.pack(side = 'right')

# テキスト選択時のイベントハンドラを設定
text_area.bind("<ButtonRelease-1>", replace_text)

# プログラムの実行
window.mainloop()

Python練習問題

Chainer Tutorialに記載されている練習問題をやってみたので、結果をメモとして残しておきます。

問2.8 (制御構文)

2以上の整数 p が素数であるとは、「どんな 2 以上 p-1 以下の整数 k に対しても p は k で割り切れない」が成り立つことを指します。素数を小さい順から列挙すると、2357111317, … となります。 チュートリアルで学んだ制御構文である if や for を用いて、2 から 100 からまでに含まれる素数を列挙して下さい。

◆回答

a = [2]
for x in list(range(3, 101)):
    if all([x % y > 0 for y in list(range(2, x))]):
        a.append(x)
print(a)

◆実行結果

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

Pythonでクローリング&スクレイピングのお勉強

1.経緯

本屋でPythonの本を眺めてふと、勉強してみようと思ったので。

2.購入

というわけで、そのままの流れで書籍を購入。
書籍代はもちろん、教育費。

3.まずは実行まで

なにやらVirtualBoxなるものでWindowsにLinuxの仮想環境を作成し、仮想環境でPythonを実行するようだ。

本に記載されているバージョンでインストールして、なんとかHello, Python!を出すところまで出来ました。

終わったらVirtualBoxで起動しているLinuxを保存&シャットダウン!

毎回起動&シャットダウンはちょっと面倒かも。。。