Discord上で動く辞書Bot作成話(後編)(Python[discord.py, beautifulsoup4, PyDrive] + GitHub + Heroku)

では後編に移ります。今回は備忘録的側面が強いため、分かりにくい所があるかもしれません。ご了承ください。

フレームデータを手に入れるために

フレームデータを手打ちするという作業は避けたかったため、DustLoopWikiのフレームデータをお借りすることにしました。ただWikiでのフレームデータは表の形で表示されているとはいえCSVの形式で転がっているはずもないわけです。ということでHTMLから該当する部分を抜き出してCSVにするプログラムが必要です。ここで登場するのがBeautifulSoup4というスクレイピングのライブラリになります。

import requests
from bs4 import BeautifulSoup

import csv

url = 'フレームデータのあるURL'
file = './data/[character name].csv'

r = requests.get(url)
 #解析 
soup = BeautifulSoup(r.text, 'html.parser')

tables = soup.find_all("table", {"class":"wikitable"})

table = list()

for t in tables:
   table.append(t.find_all("tr"))
   
data = []
   
for rows in table:
   for row in rows:
       datarow = list()
       for cell in row.find_all(["th", "td"]):
           if cell.find("span", {"class" : "tooltiptext"}) != None:
               cell.find("span", {"class" : "tooltiptext"}).decompose()
           text = cell.get_text().replace("\n","").replace(", ",",")
           datarow.append(text)
       data.append(datarow)

with open(file,'w', newline="", encoding='utf-8-sig') as csvfile:
   writer = csv.writer(csvfile)
   writer.writerows(data)

find_allメソッドを用いて徐々に対象となる要素に近づいていきます。まずtableタグでwikitableクラスに囲まれている中身を取り出し、そこから列ごとに取り出すためにtrタグに囲まれている中身を取り出します。さらにセルごとに中身を取り出すために…と続けていくと、最終的にget_text()でセルの中身をテキストとして取り出すことができます。後はセル部分をリストとしてまとめて、列情報としてdataに格納します。最後にそれをcsv.writerで書き込めばOKです。スクリーンショットはラグナのフレームデータのページから出力した結果の一部になります。うまく抽出できていますね。

スクリーンショット (130)

躓きやすい点として挙げられるのはfind_allメソッドの返り値でしょう。fild_all()は対象となる要素を抜き出し、リストの形式で返ってきます。雰囲気的にはfind_all("table").find_all("tr").find_all("td")…というように絞り込みたいところなのですが、find_allはそんなに都合よく解釈してくれません。リストの要素を指定するなり、forで回すなりして対処していきましょう(今回はwikitableクラス内のデータを取りたい、列ごとにリストでまとめたいためこのような処理にしていますが、単にtableタグの各セルの内容を取りたいのであればfind_all(["th", "td"])だけで十分でしょう)。

Googleドライブと連携しよう

格闘ゲーム用語の解説となるwordコマンドの充実化のため、Discordのメンバーにも手伝ってもらえるように共有ファイルのGoogleスプレッドシートから情報を取り込めるようにします。「編集協力お願いします!データ形式はcsvでお願いね!」はちょっと不親切ですし、googleスプレッドシートで編集するだけで更新が可能なら管理も楽ですよね。ということでGoogleドライブとの連携を行います。

この機能の実装に当たっては、以下の記事が非常に参考になりました。

最終的にはリモートサーバーであるHerokuからGoogleドライブに接続したいため、手法としては次のようになります。

0.(上の記事を参考にして)Googleドライブの認証の準備をする
1.auth.pyを作成し、実行してアクセストークン(DiscordのBotのトークンと同じですね、鍵となる認証コードです)を得る
2.アクセストークンを用いてメインのプログラムからGoogleドライブにアクセスする
3.対象のGoogleスプレッドシートをCSV形式で(Heroku上の一時ファイルとして)ダウンロードし、データベースを構築する

0.は記事を見ていただくことにして、1.から始めていきましょう。

メインと同ディレクトリに作成するauth.pyは次のようになります。(今回は真ん中のos.chdir…は要らないかも…)

import os

from pydrive.auth import GoogleAuth

os.chdir(os.path.dirname(os.path.abspath(__file__)))

gauth = GoogleAuth()
gauth.LocalWebserverAuth()

認証のためのsettings.yamlやjsonファイルを準備してこのファイルを実行すると、ブラウザが開いて認証確認画面が開きます。警告が出ますが気にせず「詳細」をクリックして先へ進みます。後は確認などの画面に従っていくと「認証が成功しました」との英文が表示されて認証が完了します。これでアクセストークンが発行され、プログラムからGoogleドライブにアクセスできるようになります。

次にGoogleドライブへのアクセスです。今回はPyDriveというラッパー(ライブラリを扱いやすくするライブラリで、自動車のハンドルカバーのようなものです)を用いているため、簡単にできます。

#Googleドライブ認証用
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive

gauth = GoogleAuth()
gauth.CommandLineAuth()

#driveにアクセスするためのオブジェクト
drive = GoogleDrive(gauth)

これだけです。後はdriveを操作して目当てのデータを探します。

def update_worddata():
   drive = GoogleDrive(gauth)
   
   dir_id = drive.ListFile({'q': 'title = "BBTAG_Database"'}).GetList()[0]['id']
   metadata = drive.ListFile({'q': '"{}" in parents and title = "word"'.format(dir_id)}).GetList()
   data_id = metadata[0]['id']
   
   f = drive.CreateFile({'id': data_id})
   data_path = os.path.join('/tmp', 'tmp.csv')
   f.GetContentFile(data_path, mimetype='text/csv')
   
   with open(data_path, newline='', encoding='utf-8-sig') as csvfile:
       readword = csv.reader(csvfile)
       for row in readword:
           wordlist[row[0]] = row[1]
           
   os.remove(data_path)

drive.ListFileは引数で指定したファイルのタイプに合致したファイル・フォルダの情報をリスト形式で得る関数です。今回はBBTAG_Databaseフォルダ内の「word」という名前のファイルの情報を得ています。

   f = drive.CreateFile({'id': data_id})
   data_path = os.path.join('/tmp', 'tmp.csv')
   f.GetContentFile(data_path, mimetype='text/csv')

この部分はファイルのダウンロード関連です。wordというファイルを/tmp/tmp.csvとしてcsvの形で保存します。(今回使用しているHerokuには/tmpという一時的にファイルを保存できる場所があります)

   with open(data_path, newline='', encoding='utf-8-sig') as csvfile:
       readword = csv.reader(csvfile)
       for row in readword:
           wordlist[row[0]] = row[1]
           
   os.remove(data_path)

最後に保存したファイルを開いてデータを読み出して辞書の形で格納し、ファイルを消去して終了です。後は手動更新なりスケジューリングなどでupdate_worddata()を実行すれば更新が行われる、という仕組みです。

おわりに

こんな感じでBBTAG辞書Botは作成されました。データベースという性質上頻繁に使われることはありませんが、自分で作ったBotが使われているのを見るとなんだかうれしくなります。皆さんもDiscordBot、作ってみてはいかがでしょうか。意外と簡単ですよ!

この記事が気に入ったらサポートをしてみませんか?