概要
今回、Ubuntu OSを搭載したVPSにおいて、24時間365日、高レスポンスで動作するDiscord Botを構築します。 特定のゲームサーバー内の経済データ(価格相場)をCSVから高速検索する機能と、サーバーの稼働状況をリアルタイムでBotのステータスに反映する機能を統合しました。
規約により詳細が公開できないので、ふんわりとした開発内容だけ記載していきます。
開発環境
- OS: Ubuntu Server 24.04 (LTS)
- Language: Python 3.8.10
- Library:
discord.py,aiohttp,python-dotenv - Database: CSV-based Lightweight DB
- Infra: Memory 512MB VPS
- SSH Cliant: Termius
実装方法
Python環境の構築
システム全体のライブラリ汚染を防ぎつつ、軽量な実行環境を作るため venv を使用しました。
# 依存パッケージのインストール
sudo apt update
sudo apt install -y python3-venv python3-pip
# 仮想環境の作成とライブラリインストール
python3 -m venv venv
source venv/bin/activate
pip install discord.py aiohttp python-dotenv
systemdによるサービス永続化
# /etc/systemd/system/discord-bot.service
[Unit]
Description=Discord Price Search Bot
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/user/my-bot
ExecStart=/home/user/my-bot/venv/bin/python3 bot.py
Restart=always
[Install]
WantedBy=multi-user.target
外部公開用に軽量化したPythonプログラム
import os
import csv
import unicodedata
import asyncio
import aiohttp
import discord
from discord import app_commands
from dotenv import load_dotenv
# 環境設定の読み込み
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), ".env"), override=True)
# ─── 設定項目(環境変数から取得) ───
TOKEN = os.getenv("DISCORD_TOKEN")
CSV_PATH = os.getenv("CSV_PATH")
GUILD_ID = int(os.getenv("GUILD_ID")) if os.getenv("GUILD_ID") else None
# ブログ掲載用にURLや名前を定数化
SERVICE_NAME = "アイテム価格検索"
LATEST_INFO_URL = "https://example.com/price-list"
GAME_SERVER_ADDR = "mc.example.com"
MC_API_URL = f"https://api.mcsrvstat.us/2/{GAME_SERVER_ADDR}"
# 表示用の装飾
SEP = "────────────────────"
def normalize(text: str) -> str:
"""テキストを検索用に正規化(半角・小文字化)"""
if not text:
return ""
return unicodedata.normalize("NFKC", text).lower()
def load_data_from_csv():
"""CSVからデータをパースしてリストで返す"""
rows = []
current_category = ""
with open(CSV_PATH, newline="", encoding="utf-8") as f:
reader = csv.reader(f)
for row in reader:
if len(row) < 4:
continue
# カラムの定義(用途に合わせて調整)
name, detail_a, detail_b, note = [s.strip() for s in row[:4]]
# カテゴリ行の判定ロジック(例:名前が空で詳細がある場合)
if not name and detail_a:
current_category = detail_a
continue
if not name:
continue
rows.append({
"name": name,
"detail_a": detail_a or "—",
"detail_b": detail_b or "—",
"category": current_category or "一般",
"note": note
})
return rows
async def fetch_server_status(session: aiohttp.ClientSession):
"""外部APIからサーバーのオンライン人数を取得"""
# 内部処理により略
class MyBot(discord.Client):
def __init__(self):
intents = discord.Intents.default()
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
# スラッシュコマンドの同期
if GUILD_ID:
await self.tree.sync(guild=discord.Object(id=GUILD_ID))
else:
await self.tree.sync()
# バックグラウンドタスクの開始
self.loop.create_task(self.update_presence_loop())
async def update_presence_loop(self):
"""30秒ごとにステータス(ボットの活動)を更新"""
# 内部処理により略
bot = MyBot()
@bot.tree.command(name="search", description="データベースから情報を検索します")
@app_commands.describe(query="検索キーワード")
async def search(interaction: discord.Interaction, query: str):
await interaction.response.defer(thinking=True)
query_n = normalize(query)
data = load_data_from_csv()
# 全フィールドを結合してあいまい検索
results = [
r for r in data
if query_n in normalize(" ".join(r.values()))
]
if not results:
embed = discord.Embed(
title="結果が見つかりませんでした",
description=f"最新のリストは以下を確認してください:\n{LATEST_INFO_URL}",
color=discord.Color.red()
)
await interaction.followup.send(embed=embed)
return
# 検索結果の構築
# 内部処理により略
if __name__ == "__main__":
if not TOKEN or not CSV_PATH:
print("Error: 環境変数が設定されていません。")
else:
bot.run(TOKEN)実行結果
本実装により、以下の成果を得られました。
- リアルタイム監視: 30秒周期でのサーバー監視を行い、Botの活動ステータスへ常に最新のプレイヤー数を反映。
- 高速検索: 膨大なアイテムデータの中から、スラッシュコマンド入力後3秒以内に関連情報を抽出・Embed表示。

まとめ
今回は、「ソフトウェアの設計(非同期処理・軽量DB)」と「OS層のチューニング(Swap・systemd)」でを行いました。スペックに依存せず、安定性と利便性を両立させるアプローチは、商用環境におけるコスト最適化にも応用可能な技術スタックです。


