어떤 웹페이지에서 특정 시간 정보를 가져올 일이 간간히 있는데,
지금까진 그냥 눈으로 보고 손으로 데이터를 변환한 후 사용했다.
너무 미련한 짓인 것 같아서 확실히 자동화해야겠다고 생각했다...
해당 기능을 구현해 봇에 올릴 것이다.
1. 어떤 데이터를 받아올 것인가?
FFXIV의 로드스톤엔 점검 관련 공지가 올라온다.
그 공지의 내용에 있는 일정에 대한 정보를 뜯어올 것이다.
[続報]全ワールド メンテナンス作業 終了時間変更のお知らせ(8/8)
全ワールド メンテナンス作業 終了時間変更のお知らせ(8/8) | FINAL FANTASY XIV, The Lodestone
全ワールド メンテナンス作業 終了時間変更のお知らせ(8/8)
jp.finalfantasyxiv.com
위 공지에서 시간을 나타내는 아래와 같다.
정규식을 이용해서 일정을 파싱 한 후, 타임스탬프로 변환해서 사용할 것이다.
그리고 패치 등 서버가 전부 다 다운되는 점검 공지엔 무조건 "全ワールド"라는 문구가 들어간다.
타이틀 맨 뒤에 (월/일)이 온다는 점도 확인했다.
2. 클래스 작성
import requests
import re
from bs4 import BeautifulSoup
from datetime import datetime
class Parser:
def get_recent_maintenance_title(url):
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser') if response.status_code == 200 else None
if not soup:
return None
target_prefix = "全ワールド"
news_list_elements = soup.find_all(class_="news__list")
current_year = datetime.now().year
for news_element in news_list_elements:
title_element = news_element.find(class_="news__list--title")
title_text = "".join(str(item) for item in title_element.contents if item.name is None).strip()
if title_text.startswith(target_prefix):
date_text = title_text.split('(')[-1].replace(')', '')
month, day = map(int, date_text.split('/')[0:2])
post_datetime = datetime(current_year, month, day)
if (datetime.now() - post_datetime).days < 1:
return title_text, news_element.find('a')['href']
return None
def get_maint_info():
target_url = 'https://jp.finalfantasyxiv.com/lodestone/news/category/2'
recent_title, recent_link = Parser.get_recent_maintenance_title(target_url) or (None, None)
return recent_title, recent_link
def get_html_content(url):
response = requests.get(url)
return response.content
def parse_html_content(html_content):
soup = BeautifulSoup(html_content, "html.parser")
news_detail_div = soup.find("div", class_="news__detail__wrapper")
return news_detail_div.get_text()
def extract_time_info(content):
time_match = re.search(r"日\s*時:(.*?)頃まで", content, re.DOTALL)
if time_match:
return time_match.group(1).strip()
else:
return None
def parse_time_string(time_string):
match = re.search(r"(\d{4})年(\d{1,2})月(\d{1,2})日", time_string)
year, month, day = map(int, match.groups())
match = re.search(r"(\d{1,2}):(\d{1,2})より(\d{1,2}):(\d{1,2})", time_string)
start_hour, start_minute, end_hour, end_minute = map(int, match.groups())
start_datetime = datetime(year, month, day, start_hour, start_minute)
end_datetime = datetime(year, month, day, end_hour, end_minute)
return int(start_datetime.timestamp()), int(end_datetime.timestamp())
기본적으로 클래스 내에서 사용될 함수들이다.
공지 타이틀을 뽑아오는 부분은 아래와 같이 동작한다.
BeautifulSoup
로 html을 파싱해 온다.- 「全ワールド」가 들어간 글을 위에서부터 찾는다.
- 찾았다면 스트링을 분해해 날짜를 뽑는다.
- 뽑힌 날짜와 현재의 차이가 1일 이상 날 경우 None을 리턴
- 1일 이내일 경우 타이틀과 해당 링크 리턴
def GetMaintTimeStamp():
# infos[0] = title, infos[1] = link
infos = Parser.get_maint_info()
if not infos[1]:
return None
url = f'https://jp.finalfantasyxiv.com{infos[1]}'
html_content = Parser.get_html_content(url)
content = Parser.parse_html_content(html_content)
time_string = Parser.extract_time_info(content)
if time_string:
start_unix_timestamp, end_unix_timestamp = Parser.parse_time_string(time_string)
if end_unix_timestamp < datetime.now().timestamp():
return None
print(f"Parsed time: {start_unix_timestamp} to {end_unix_timestamp}")
return start_unix_timestamp, end_unix_timestamp, infos[0], url
else:
return None
실제로 명령어에서 호출할 함수.
넘겨받은 링크로 가서 시간 정보를 뽑아온다.
만약 점검 종료 시각이 현재보다 이전이면 None
을 리턴한다.
@client.tree.command()
async def maintinfo(interaction: discord.Interaction):
"""공지 관련 정보를 임베드로 작성"""
lg.info(f"{interaction.user.display_name} request maintinfo()")
# 0 = Start, 1 = End, 2 = Title, 3 = Link
time_info = pa.GetMaintTimeStamp()
if not time_info:
embed = discord.Embed(title="점검 일정이 없습니다!", colour=discord.Colour.dark_red())
embed.set_thumbnail(url="https://cdn.discordapp.com/attachments/1138398345065414657/1138398369929244713/0001061.png")
embed.add_field(name="", value="현재 확인할 수 있는 점검 공지가 없습니다.\n무언가 문제가 있다면 공식 로드스톤을 참고해 주세요.")
await interaction.response.send_message(embed=embed)
return
output = Translator.Translate(time_info[2])
embed = discord.Embed(title=time_info[2], url=time_info[3], description=output, colour=discord.Colour.dark_blue())
embed.set_thumbnail(url="https://cdn.discordapp.com/attachments/1138398345065414657/1138398369929244713/0001061.png")
embed.add_field(name="일정", value=f'시작 : <t:{time_info[0]}:F> \n종료 : <t:{time_info[1]}:F> \n\n<t:{time_info[1]}:R>', inline=False)
lg.info(f'공지 링크 : {time_info[3]}')
넘겨받은 데이터로 임베드를 작성해 응답한다.
3. 출력 확인
잘 되니 다행이다.
4. 개선 사항들
A. 환율 정보 테이블
여러 화폐의 환율을 한방에 보고 싶어서 만들기로 했다.
기존 방식이 번거로웠던 것도 있고.
아래는 클래스 내부에서 실제로 데이터를 처리할 함수이다.
def exchCurList(src, amount):
dst_currencies = ['usd', 'krw', 'jpy', 'eur', 'gbp', 'cny', 'try', 'ars', 'twd', 'mnt']
exchange_rates = {}
for dst in dst_currencies:
try:
request = requests.get(f"https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/{src}/{dst}.min.json")
result = request.json()
total = result[f'{dst}'] * amount
total = '{:,.2f}'.format(total)
exchange_rates[dst] = total
except:
lg.error("Something went wrong while processing exchCur()!!")
return None
return exchange_rates
내가 확인할 필요가 있는 여러 화폐들을 배열처럼 저장하고 환율 데이터를 받아온 후, 딕셔너리 형태로 저장한다.
굳이 딕셔너리로 할 필요는 없었지만 하다보니 그리 됐다...
@client.tree.command()
@app_commands.describe(src='Source Currency. Default : USD', amount='Amount of Source Currency. Default : 1')
async def ratetable(interaction: discord.Interaction, src: str=None, amount: app_commands.Range[float, 0, None]=None):
"""Show the exchange rate table."""
await interaction.response.defer()
# Set default values if not provided
src = src.lower() if src else 'usd'
amount = amount if amount else 1
try:
# Get exchange rates
result = ex.exchCurList(src, amount)
if result is None:
raise ValueError("No exchange rates found for the given source and amount.")
exchange_rates = list(result.values())
# Create an embed message with the exchange rates
embed = discord.Embed(title="**Exchange Rate**", colour=discord.Colour.dark_green())
embed.description = f'Exchange rates for **[ {amount} {src.upper()} ]**'
embed.set_thumbnail(url="https://cdn.discordapp.com/attachments/1138398345065414657/1138816034049105940/gil.png")
embed.add_field(name="", value="", inline=False) # Padding
# Add fields for each currency
currencies = ['USD', 'KRW', 'JPY', 'EUR', 'GBP', 'CNY', 'TRY', 'ARS', 'TWD', 'MNT']
flags = [':flag_us:', ':flag_kr:', ':flag_jp:', ':flag_eu:', ':flag_gb:', ':flag_cn:', ':flag_tr:', ':flag_ar:', ':flag_tw:', ':flag_mn:']
for flag, currency, rate in zip(flags, currencies, exchange_rates):
embed.add_field(name=f'{flag} {currency}', value=rate, inline=True)
embed.add_field(name="", value="", inline=False) # Padding
embed.add_field(name="", value="Powered by [fawazahmed0/currency-api](https://github.com/fawazahmed0/currency-api)", inline=False)
embed.set_footer(text=f'{datetime.datetime.now().strftime("%Y-%m-%d")} 기준')
# Send embed message
await interaction.followup.send(embed=embed)
except:
lg.error("Something went wrong while processing ratetable()")
embed = rs.error("처리 중 오류가 발생했습니다. USD, KRW, JPY와 같은 제대로 된 통화코드를 입력했는지 확인하세요.")
await interaction.followup.send(embed=embed, ephemeral=True)
인자에 어떤 입력도 없다면 1 USD
로 계산한다.
받아온 데이터를 Embed
에 Field
형태로 정리한다.
반복적인 작업은 반복문으로 간소화한다.
A-1. 결과
데스크탑 환경에선 잘 보인다마는...
모바일 환경에선 3개씩 끊는 게 아니라 inline = False
처럼 하나씩 끊어서 나온다.
다른 봇들도 그렇게 나오는 것을 보면 디스코드 자체의 사양인 듯싶다.
B. 임베드 리스폰스
그냥 쌩 메세지로 보내는 건 보기에 좋지 않은 것 같아 모든 반응을 임베드로 하게 했다.
from datetime import datetime
import discord
class Response:
@staticmethod
def create_embed(title, description, color):
embed = discord.Embed(
title = title,
description = description,
colour = color
)
embed.set_footer(text=f'현재 시각 : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
return embed
@staticmethod
def general(title, content):
return Response.create_embed(title, content, discord.Colour.dark_blue())
@staticmethod
def info(content):
return Response.create_embed(":large_blue_diamond: **INFO**", content, discord.Colour.dark_blue())
@staticmethod
def error(content):
return Response.create_embed(":warning: **ERROR**", content, discord.Colour.red())
임베드를 그때그때 코드 상에 만들 순 없으니 클래스로 양식을 만들었다.
embed = rs.general(title=f'{value} {imp} **IN METRIC**', content=f'**{resVal:.2f} {resMet}**')
await interaction.response.send_message(embed=embed, ephemeral=True)
위와 같은 느낌으로 임베드를 작성해 넘겨주게 된다.
B-1. 결과
훨씬 보기 좋은 것 같다.
푸터의 시간을 GMT+9
로 바꾸는 것은 조만간 할 것이다.
'Study > Python' 카테고리의 다른 글
[Python] Aim Smoothing (0) | 2023.10.19 |
---|---|
[Python] 봇 개선 및 커맨드 추가 (0) | 2023.10.16 |
[Python] DeepL 번역 API 써보기 (0) | 2023.06.02 |
[Python] 단위 변환 (0) | 2023.05.21 |
[Python / GPT] 봇 코드 리팩터링 (0) | 2023.05.17 |