※この記事はUdon Advent Calendar 2024 - Adventarの11日目の記事です。
はじめに
こんにちは。Udonです。
自分はUDCというサークルに所属しており、そこではデュエル・マスターズというカードゲームをしています。
デュエルマスターズをする上で欠かせないのは、現在の環境でどのようなデッキが活躍しているのか、そして新カードがどのような性能をしているのかをいち早く知ることです。
これらの情報をまとめている田園補完計画というサイトがあります。
新カードについての情報は公式がYouTubeにアップしたりするのですが、それらの情報とともに、各地で行われた大会で活躍したデッキ、デッキの入賞数ランキングなどの情報がまとめられています。
というわけで、このサイトの情報をスクレイピングしてその結果をDiscordに通知するBotを作成しました。
リポジトリ
リポジトリは以下の通りです。
上位ディレクトリにはほかのBotのディレクトリもありますが、とりあえずはUDC_Information
のみに絞って話をしようと思います。ほかのBotについてはまた別の機会に話ができればいいなと思います。
やったこと
まずは田園補完計画のサイトがどうなっているのかを「ページのソース表示」で確認しました。
このサイトは様々なカードゲームを扱っているので、デュエマ関係のカテゴリのページのみを扱うこととしました。
なんかたくさん書いてありましたが、どうやら記事のタイトルとそこへのリンクがEntryTitle
というクラスのdiv
タグの中にあるようでした。それを取得すれば、記事のタイトルとリンクが取得でき、どんな記事かくらいはわかると思いました。
次に、実際の記事の中身を見てみました。各カテゴリごとに、欲しい情報は以下のように格納されていました。
- 新カードの画像:
card_image
というクラスのdiv
タグの中にあるimg
要素のsrc
属性 - 入賞数ランキング:````EntryBody
というクラスの
div``タグの中にある``a``要素の``href``属性 - 大会結果概要:
caption_white
というクラスの一つ目のdiv
タグの中にあるtext
要素 - 入賞者一覧:
caption_white
というクラスの二つ目のdiv
タグの中にあるtext
要素、またはdm_deck_name
というクラスのp
タグの中にあるtext
要素 - 入賞者のデッキ:
dm_deck_image
というクラスのdiv
タグの中にあるimg
要素のsrc
属性
というわけで、こいつらをいい感じに取得できれば良いわけです。
また、実行の流れを以下のように規定しました。
- 起動時、エントリーされた記事のリストを取得し、「が公開」「が優勝」「入賞数ランキング」のうちどれかが含まれている記事のみを取得し、リンクを送信済みリストに入れる
- 60秒ごとにまたエントリーされた記事のリストを取得し、まだ送信済みでなく、かつ上記3つの文字列のいずれかを含んでいる記事を取得し、データを抜き取る
- 抜き取ったデータをもとにメッセージを作成し、Discordに送信する
- 送信した記事のみを送信済みリストに入れる
- 2に戻る
という感じです。なぜ「が公開」「が優勝」「入賞数ランキング」のうちどれかが含まれている記事のみを送信済みリストに追加するのかというと、それ以外の記事まで送信済みに含めてしまうと、記事のタイトルが更新され条件を満たしたときに見逃してしまうからです。そして、記事が一気に複数投稿されることもあるので、60秒ごとにエントリーされた記事のリスト(1ページだけなので15個くらい)を取得し、条件を満たすものが複数あっても大丈夫なようにしています。
まず1.に関してですが、以下のようなプログラムで実装しました。
async def ready():
global latest_articles
response = requests.get(denen_url)
soup = BeautifulSoup(response.text, "html.parser")
articles = soup.find_all("div",class_="EntryTitle")
for a in articles:
title = a.text
if ("入賞数ランキング" in title) or ("が優勝" in title) or ("が公開" in title):
latest_articles.append(a.find("a").get("href"))
2.~4.については、それぞれ以下の関数たちで実装しています。
async def get_new_articles():
response = requests.get(denen_url)
soup = BeautifulSoup(response.text, "html.parser")
article = soup.find_all("div",class_="EntryTitle")
articles = []
for a in article:
articles.append(a.find("a").get("href"))
article_title = soup.find_all("div",class_="EntryTitle")
article_titles = []
for t in article_title:
article_titles.append(t.find("a").text)
return articles, article_titles
トップページ(denen_url
)から記事のリンクとタイトルを取得する関数です。
async def ranking_check(new_article):
response = requests.get(new_article)
soup = BeautifulSoup(response.text, "html.parser")
ranking_img = soup.find("div",class_="EntryBody").find("a").get("href")
return ranking_img
受け取ったリンクから入賞数ランキングの画像を取得する関数です。
async def result_check(new_article):
response = requests.get(new_article)
soup = BeautifulSoup(response.text, "html.parser")
result_div = soup.find("div", class_="caption_white")
# <br> タグを \n に置き換え
for br in result_div.find_all("br"):
br.replace_with("\n")
# テキストを取得
result_sentence = result_div.text
result_names = soup.find_all("p", class_="dm_deck_name")
names = [name.text for name in result_names]
# 複数の画像を含む <img> 要素を取得
result_imgs = soup.find_all("div", class_="dm_deck_image")
imgs = [img.find("img").get("src") for img in result_imgs if img.find("img") is not None]
return result_sentence, names, imgs
受け取ったリンクから大会概要、入賞者一覧、入賞者のデッキの画像を取得する関数です。入賞数や画像は複数あるので、リストにします。
async def newcard_check(new_article):
response = requests.get(new_article)
soup = BeautifulSoup(response.text, "html.parser")
newcard= soup.find_all("div",class_="card_image")
newcard_img = [img.find("img").get("src") for img in newcard if img.find("img") is not None]
return newcard_img
受け取ったリンクから新カードの画像を取得する関数です。画像は複数あることがあるので、リストにします。
async def hacchi_result(new_article):
response = requests.get(new_article)
soup = BeautifulSoup(response.text, "html.parser")
result_div = soup.find("div", class_="caption_white").find_next("div")
for br in result_div.find_all("br"):
br.replace_with("\n")
# テキストを取得
names = result_div.text
result_url=soup.find("blockquote",class_="twitter-tweet").find("a").get("href")
return names,result_url
大会によっては(はっちCS)、田園補完計画のサイトに入賞者一覧だけしか書いていないときがあります。そういう時は、外部リンクを取得します。result_check
では入賞者デッキ画像のキャプションから入賞者一覧を取得していまずが、この場合はcaption_white
の中の2番目のdiv
タグに入賞者一覧が書かれているので、それを取得します。
async def check_new_article():
global latest_articles
new_articles, article_titles = await get_new_articles()
page_length=len(new_articles)
for i in range(page_length):
new_article=new_articles[i]
article_title=article_titles[i]
if new_article not in latest_articles:
if "入賞数ランキング" in article_title:
channel = client.get_channel(DISCORD_CHANNEL_ID)
await channel.send(await ranking_check(new_article))
latest_articles = [new_article]+latest_articles
elif "が優勝" in article_title:
channel = client.get_channel(DISCORD_CHANNEL_ID_3)
result_sentence, names, imgs = await result_check(new_article)
txt=result_sentence+"\n"
if "はっち" in article_title:
names,result_url=await hacchi_result(new_article)
txt+="\n"
txt+=names
txt+=result_url
await channel.send(txt)
else:
for name in names:
txt+=("\n"+name)
await channel.send(txt)
for img in imgs:
await channel.send(img)
latest_articles = [new_article]+latest_articles
elif "が公開" in article_title:
channel = client.get_channel(DISCORD_CHANNEL_ID_2)
newcard_img = await newcard_check(new_article)
if newcard_img !=[]:
latest_articles = [new_article]+latest_articles
for img in newcard_img:
await channel.send(img)
return
メインプログラムが60秒ごとに呼び出す関数です。新しい記事があるかどうかを確認し、それらを一つ一つ見ていって、まだ送信済みではなく、かつ特定の文字列を含んでいる記事があれば、それに応じた処理を行います。サークルの人の要望で、記事ごとに送信先のチャンネルを分けています。
@client.event
async def on_ready():
print("Bot is ready!")
await ready()
while True:
await check_new_article()
await asyncio.sleep(60)
これがBot本体となります。起動時にready
関数を呼び出し、60秒ごとにcheck_new_article
関数を呼び出します。
最終的にDockerでホストするわけですが、そのあたりはここに書いてあることを行いました。
稼働させると、以下のような感じで情報が取得されます。
今後の課題点
まだ本格的に稼働してから時間が経っていないので、稼働の過程で何か問題が発生する可能性があります。そうなったときは逐一対応する必要があります。
また、一部の大会結果は画像がなく外部サイトへのリンクのみ貼ってあることがあります。現在はこのリンクのみ取得していますが、今後はこの外部サイトをスクレイピングし、画像を取得できるようにできたらいいなと思っています。
加えて、プログラム内にはYouTubeからの情報取得をする関数も実装しています。しかし、話し合いの結果あまり必要がないということで現在は使っていません。もし必要があれば、この関数を使って情報を取得するようにしたいと思います。
おわりに
Discord Botは何個か開発してきたのですが、今回はかなり実用的なものが作れたかなと思います。
田園補完計画のサイトが割と静的に書かれていて助かりました。動的に書かれていた場合Seleniumを使う必要があり、実装が難しくなっていたと思います。
サークル内でも割と評判がいいので、今後も改善していきたいと思います。
ではまた、明日の記事でお会いしましょう。