カテゴリー: Discordボット開発

Discordボット開発#14データを保存しよう!JSON vs SQLite (Pythonサンプル付き)

この記事は韓国語の原文から翻訳したものです。

目次11項目

こんにちは!Mojangcamです 😎

前回の#13では、ロール・チャンネルの自動化でサーバー管理をまるごとボットに任せてみました。でも、ひとつ残念な点がありましたよね。#13のtemp_channelsのように、変数にだけ入れておいたデータは、ボットを再起動した瞬間にすべて消えてしまうのです。

そこで今回の#14では、ボットに記憶力を持たせます。Pythonに標準搭載されているJSONとSQLiteの2つの方法で出席チェック + ポイント + ランキングボットを実際に作りながら、どんなときに何を使えばいいのかをしっかり整理していきます。

出席チェック、ポイント(経済)システム、レベル、警告の累積… コミュニティサーバーで人気の機能は、どれも「データを覚えているボット」があってこそ実現できます。今日この関門さえ越えれば、作れる機能が一気に増えますよ!

🧠 なぜデータの保存が必要なのでしょうか?

Pythonの変数は、すべてメモリ(RAM)の上にしか存在しません。ボットを再起動したり、VPSが再起動したり、エラーでボットが落ちたりした瞬間、変数に入っていたポイントや記録は跡形もなくリセットされます。そこで、データをファイルに残しておき、ボットが起動したときに読み込み直す仕組みが必要になります。これを難しい言葉で永続化(Persistence)と呼びます。

🧩 JSON vs SQLite、何が違うの?

結論から言いますね!設定のように小さくてシンプルなデータはJSON、どんどん増えていき検索・ランキングが必要なデータはSQLiteです。

区分 JSON SQLite
正体 テキストファイル (dictをそのまま保存) ファイル1つで動く本物のデータベース
インストール 不要 (標準のjsonモジュール) 不要 (標準のsqlite3モジュール)
読み書き 毎回ファイル全体をまるごと 必要な行だけを選んで
検索・並べ替え Pythonのコードで自分で実装 SQL 1行 (ORDER BYなど)
データが増えると だんだん遅くなる インデックスのおかげで速い
保存中にトラブルが起きると ファイルが壊れるおそれ トランザクションで安全
向いている用途 ボットの設定値、小規模な記録 ポイント・経済・ログのように増え続けるデータ

JSONはPythonのdictをそのままファイルに移したものなので直感的で、メモ帳で開いて目で確認できるのが最大の長所です。そのかわり、保存のたびにファイル全体を書き直すので、データが大きくなるほど遅くなります。SQLiteはサーバーをインストールせずにファイル1つで動く本物のデータベースなので、必要なデータだけを選んで読み込み、並べ替えや集計までSQLに任せられます。

📌 準備するもの

  • Python 3.8以上
  • discord.py 2.x (最新の2.xバージョンならOKです)
  • json・sqlite3モジュール — Pythonに標準搭載なので、別途インストールするものはありません!
ターミナル
pip install -U discord.py

プレフィックスコマンド(!출석)を使うので、Discord Developer Portal → Botタブ → Message Content Intentをオンにしておいてください。(この設定が初めてなら、シリーズの前の回を参考にしてください!)

📌 ステップ1 - JSON保存の基本: load & save関数

JSON方式の核心は、たった2つの関数です。ファイルからdictを読み込む関数と、dictをファイルに保存する関数。

Python
import json
import os

DATA_FILE = "points.json"

def load_data():
    """ファイルがあれば読み込み、なければ空のdictを返します。"""
    if not os.path.exists(DATA_FILE):
        return {}
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        return json.load(f)

def save_data(data):
    """dictをまるごとJSONファイルに保存します。"""
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

ひとつずつ見ていきましょう 💡

  • json.load(f) : ファイルの内容をPythonのdictに変換して読み込みます。
  • json.dump(data, f) : dictをJSON形式のテキストに変換してファイルに書き込みます。
  • encoding="utf-8" + ensure_ascii=False : 日本語や韓国語などの文字化けを防ぐセットです。どちらか一方でも抜けると、文字が\uD55C\uAE00のような謎の文字列で保存されてしまいます。
  • indent=2 : インデントを入れて、人が読みやすい形で保存します。

ひとつ注意!JSONのキー(key)には文字列しか使えません。ユーザーIDは本来数字ですが、保存するときはstr(ctx.author.id)のように文字列にそろえて使うのが安全です。

📌 ステップ2 - JSON版の出席・ポイントボットを完成させる

上の2つの関数にコマンドを3つ(!출석、!포인트、!랭킹 — 韓国語でそれぞれ「出席」「ポイント」「ランキング」)載せれば完成です。このコードはこのまま実行できる完成版です。

Python
# discord.py 2.x / Python 3.8+ / jsonはPythonに標準搭載
import json
import os
from datetime import date

import discord
from discord.ext import commands

TOKEN = "ここにボットのトークン"
DATA_FILE = "points.json"

def load_data():
    if not os.path.exists(DATA_FILE):
        return {}
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        return json.load(f)

def save_data(data):
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

intents = discord.Intents.default()
intents.message_content = True  # プレフィックスコマンド用

bot = commands.Bot(command_prefix="!", intents=intents)

@bot.event
async def on_ready():
    print(f"✅ {bot.user} でログインしました!")

@bot.command(name="출석")
async def attend(ctx: commands.Context):
    data = load_data()
    uid = str(ctx.author.id)
    today = date.today().isoformat()   # 例: '2026-07-30'

    user = data.get(uid, {"points": 0, "last_check": ""})
    if user["last_check"] == today:
        await ctx.send(f"{ctx.author.mention} 今日はもう出席済みです! 😅")
        return

    user["points"] += 10
    user["last_check"] = today
    data[uid] = user
    save_data(data)
    await ctx.send(f"✅ {ctx.author.mention} 出席完了! +10P (合計 {user['points']}P)")

@bot.command(name="포인트")
async def my_points(ctx: commands.Context):
    data = load_data()
    user = data.get(str(ctx.author.id))
    total = user["points"] if user else 0
    await ctx.send(f"💰 {ctx.author.mention} さんのポイント: {total}P")

@bot.command(name="랭킹")
async def ranking(ctx: commands.Context):
    data = load_data()
    if not data:
        await ctx.send("まだ誰も出席していません。1位のチャンス! 🏃")
        return

    top = sorted(data.items(), key=lambda x: x[1]["points"], reverse=True)[:5]
    medals = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣"]
    lines = [f"{medals[i]} <@{uid}> — {info['points']}P"
             for i, (uid, info) in enumerate(top)]
    embed = discord.Embed(title="🏆 ポイントランキング TOP 5",
                          description="\n".join(lines),
                          color=0x5865F2)
    await ctx.send(embed=embed)

bot.run(TOKEN)

ひとつずつ見ていきましょう 💡

  • date.today().isoformat() : 今日の日付を2026-07-30形式の文字列にします。last_checkと比べて1日1回の出席に制限する、核心の部分です。
  • data.get(uid, デフォルト値) : 初めて出席するユーザーなら、ポイント0から始まるデフォルトのdictを作ってくれます。
  • sorted(..., reverse=True)[:5] : ランキングを出すために、全データをPythonにすべて読み込んで自分で並べ替えました。これがJSON方式の限界ポイント!あとでSQLiteと比べてみてください。
  • 埋め込み内の <@ユーザーID> メンション : 埋め込みの中のメンションは通知(ping)が飛ばないので、ランキング表示にぴったりです。

実行して!출석と入力すると、ボットのフォルダにpoints.jsonファイルができます。メモ帳で開くと、こんな感じです。

JSON
{
  "123456789012345678": {
    "points": 30,
    "last_check": "2026-07-30"
  }
}

ユーザーが10人規模なら、これで十分です。でも、ユーザーが数百人になって記録がたまってくると問題が出てきます。出席1回のたびにファイル全体を書き直し、ランキング1回のたびに全体を読み直す構造だからです。しかも、保存の途中でボットが強制終了すると、ファイルがまるごと壊れることもあります。こんなときに必要なのがSQLiteです!

📌 ステップ3 - 同じボットをSQLiteにアップグレード

SQLiteはPython標準のsqlite3モジュールですぐに使えます。まずDBファイルに接続して、表(テーブル)を作ります。

Python
import sqlite3

con = sqlite3.connect("points.db")   # ファイルがなければ自動で作成!
con.execute("""
CREATE TABLE IF NOT EXISTS points (
    user_id    TEXT PRIMARY KEY,
    points     INTEGER NOT NULL DEFAULT 0,
    last_check TEXT    NOT NULL DEFAULT ''
)
""")
con.commit()

出席コマンドはこう変わります。

Python
@bot.command(name="출석")
async def attend(ctx: commands.Context):
    uid = str(ctx.author.id)
    today = date.today().isoformat()

    row = con.execute(
        "SELECT last_check FROM points WHERE user_id = ?", (uid,)
    ).fetchone()
    if row is not None and row[0] == today:
        await ctx.send(f"{ctx.author.mention} 今日はもう出席済みです! 😅")
        return

    con.execute("INSERT OR IGNORE INTO points (user_id) VALUES (?)", (uid,))
    con.execute(
        "UPDATE points SET points = points + 10, last_check = ? WHERE user_id = ?",
        (today, uid),
    )
    con.commit()

ひとつずつ見ていきましょう 💡

  • sqlite3.connect("points.db") : DBファイルに接続します。ファイルがなければ自動で作ってくれます。
  • CREATE TABLE IF NOT EXISTS : 表がないときだけ作ります。ボットを何度再起動しても安全な理由です。
  • PRIMARY KEY : user_idが重複しないように守ってくれる代表のキーです。
  • クエスチョンマーク(?)のプレースホルダー : 値をSQL文に直接くっつけず、安全に渡します。f-stringでSQLを組み立てるとSQLインジェクション攻撃を受けるおそれがあるので、必ず?を使いましょう!
  • INSERT OR IGNORE → UPDATE : 「なければ作り、あればそのまま」のあとでポイントを足します。初心者にとっていちばんミスの少ないパターンです。
  • con.commit() : 変更をファイルに確定します。これを忘れると保存されません!

ランキングはどうでしょう?JSONでは全体を読み込んでPythonで並べ替えましたが、SQLiteならSQL 1行で終わりです。

Python
rows = con.execute(
    "SELECT user_id, points FROM points ORDER BY points DESC LIMIT 5"
).fetchall()

ORDER BY points DESC LIMIT 5 — 「ポイントの降順で上から5人だけ」という意味です。ユーザーが10万人いても、DBが自動で選び出してくれます。これがデータベースの力です! 💪

📌 全体コード (コピペOK)

SQLite版の完成版です。TOKENだけ変えれば、すぐに動きます。(JSON版の完成版は、上のステップ2のコードをそのまま使えばOKです。)

Python
# discord.py 2.x / Python 3.8+ / sqlite3はPythonに標準搭載
import sqlite3
from datetime import date

import discord
from discord.ext import commands

TOKEN = "ここにボットのトークン"
DB_FILE = "points.db"

# ── DBの準備 ──────────────────────────
con = sqlite3.connect(DB_FILE)
con.execute("""
CREATE TABLE IF NOT EXISTS points (
    user_id    TEXT PRIMARY KEY,
    points     INTEGER NOT NULL DEFAULT 0,
    last_check TEXT    NOT NULL DEFAULT ''
)
""")
con.commit()

intents = discord.Intents.default()
intents.message_content = True  # プレフィックスコマンド用

bot = commands.Bot(command_prefix="!", intents=intents)

@bot.event
async def on_ready():
    print(f"✅ {bot.user} でログインしました!")

@bot.command(name="출석")
async def attend(ctx: commands.Context):
    uid = str(ctx.author.id)
    today = date.today().isoformat()

    row = con.execute(
        "SELECT last_check FROM points WHERE user_id = ?", (uid,)
    ).fetchone()
    if row is not None and row[0] == today:
        await ctx.send(f"{ctx.author.mention} 今日はもう出席済みです! 😅")
        return

    con.execute("INSERT OR IGNORE INTO points (user_id) VALUES (?)", (uid,))
    con.execute(
        "UPDATE points SET points = points + 10, last_check = ? WHERE user_id = ?",
        (today, uid),
    )
    con.commit()

    total = con.execute(
        "SELECT points FROM points WHERE user_id = ?", (uid,)
    ).fetchone()[0]
    await ctx.send(f"✅ {ctx.author.mention} 出席完了! +10P (合計 {total}P)")

@bot.command(name="포인트")
async def my_points(ctx: commands.Context):
    row = con.execute(
        "SELECT points FROM points WHERE user_id = ?", (str(ctx.author.id),)
    ).fetchone()
    total = row[0] if row else 0
    await ctx.send(f"💰 {ctx.author.mention} さんのポイント: {total}P")

@bot.command(name="랭킹")
async def ranking(ctx: commands.Context):
    rows = con.execute(
        "SELECT user_id, points FROM points ORDER BY points DESC LIMIT 5"
    ).fetchall()
    if not rows:
        await ctx.send("まだ誰も出席していません。1位のチャンス! 🏃")
        return

    medals = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣"]
    lines = [f"{medals[i]} <@{uid}> — {pts}P"
             for i, (uid, pts) in enumerate(rows)]
    embed = discord.Embed(title="🏆 ポイントランキング TOP 5",
                          description="\n".join(lines),
                          color=0x5865F2)
    await ctx.send(embed=embed)

bot.run(TOKEN)

📌 実行結果

ボットを起動して、サーバーでひとつずつ確認してみましょう。

  1. !출석と入力 → 「✅ 出席完了! +10P (合計 10P)」と返事をします。同じ日にもう一度入力すると「今日はもう出席済みです! 😅」
  2. !포인트と入力 → 自分の累計ポイントを表示します。
  3. !랭킹と入力 → 🥇🥈🥉のメダルが付いたTOP 5の埋め込みが表示されます。
  4. ここが本当のハイライト!ボットを一度止めてから、もう一度起動してみてください。 !포인트を打つと、ポイントがそのまま残っています。ボットのフォルダのpoints.db(またはpoints.json)ファイルに記憶が保存されているおかげです。 🎉

📌 よくあるエラー

1. json.decoder.JSONDecodeError: Expecting value: line 1 column 1 原因: points.jsonが空のファイルか、保存の途中でボットが強制終了してファイルが壊れています。 解決: ファイルを消して再実行すれば、新しく作られます。データを失いたくなければ、メモ帳で開いて壊れた部分を直します。予防するには、保存中でないときにCtrl+Cを1回押して、ボットを正常に終了させましょう。

2. 日本語などの文字が\uc548\ub155のような謎の文字列で保存される 原因: json.dumpにensure_ascii=Falseを付け忘れています。 解決: json.dump(data, f, ensure_ascii=False, indent=2) — ファイルを開くときもencoding="utf-8"を必ず付けてください。

3. sqlite3.OperationalError: no such table: points 原因: CREATE TABLEの実行前にSELECTしたか、別のフォルダからボットを実行したせいで、空のDBファイルが思わぬ場所に新しくできています。 解決: DBの準備コード(CREATE TABLE)がbot.run()より先に実行されているか確認し、ターミナルでcdを使ってボットのフォルダに移動してから実行してください。

4. sqlite3.OperationalError: database is locked 原因: DBビューアーがpoints.dbを開いたままになっているか、commit()していない別の接続がファイルをつかんでいます。 解決: DBビューアーを閉じて、データを変更するコードのあとには必ずcon.commit()を付ける習慣をつけましょう。

💡 ボーナスTips

  • 出席の連打を防ぐ : 1日1回の制限に加えて#12のクールダウンをかければ、スパムまで完全にブロックできます。
  • バックアップはファイルをコピーするだけ : points.dbファイルをコピーしておけばバックアップ完了!VPSで24時間動かしているなら、出席の日付はサーバーの時刻が基準になるので、海外のVPSではタイムゾーンを自分の地域(筆者の場合はソウル、Asia/Seoul)に合わせてください。
  • データが本当に多くなったら : sqlite3は同期方式なので、とても大きな処理ではボットが一瞬止まることがあります。入門規模では体感できませんが、あとで非同期ライブラリのaiosqliteに移行する方法をシリーズで扱う予定です!

📌 まとめ

今回は、ボットに記憶力を持たせるデータ保存を学びました。小さくてシンプルなデータはJSON、増え続けて検索・ランキングが必要なデータはSQLite — この基準ひとつだけ覚えておけばOKです。出席・ポイント・ランキングボットを両方の方式で作ってみたので、これでレベルシステムでも警告の累積でも、何にでも応用できます!

次回は、コマンドが増えてきたボットをすっきり整理するスラッシュコマンドのグループ(Group)を扱います。お楽しみに! 🙌

📚 Discordボット開発シリーズ

連載 · 全32回中14回目

Discordボット開発

  1. #1Discordボット開発 - スラッシュコマンド(Slash Command)完全攻略!
  2. #2スラッシュコマンドにオプション(Arguments)を追加しよう!
  3. #3スラッシュコマンドにサブコマンド(Subcommand)を追加しよう!
  4. #4スラッシュコマンドで埋め込み(Embed)を送信しよう!
  5. #5ボタン(Button)のインタラクションを実装しよう!
  6. #6ドロップダウン(Select Menu)メニューを実装しよう!
  7. #7モーダル(Modal)で入力を受け取る方法 (Pythonサンプル付き)
  8. #8埋め込み(Embed)を画像・フィールドで飾る (Pythonサンプル付き)
  9. #9AI(Claude・ChatGPT API)と連携する! (Pythonサンプル付き)
  10. #10多言語(i18n)に対応する! (Pythonサンプル付き)
  11. #11FlaskでWebダッシュボードを作る! (Pythonサンプル付き)
  12. #12コマンドのクールダウン(Cooldown)・権限(Permission)を管理しよう! (Pythonサンプル付き)
  13. #13ロール・チャンネルの自動化でサーバーを管理しよう! (Pythonサンプル付き)
  14. #14データを保存しよう!JSON vs SQLite (Pythonサンプル付き)現在の記事
  15. #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제) (韓国語)
  16. #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제) (韓国語)
  17. #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제) (韓国語)
  18. #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제) (韓国語)
  19. #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제) (韓国語)
  20. #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제) (韓国語)
  21. #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제) (韓国語)
  22. #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제) (韓国語)
  23. #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제) (韓国語)
  24. #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제) (韓国語)
  25. #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제) (韓国語)
  26. #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제) (韓国語)
  27. #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제) (韓国語)
  28. #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제) (韓国語)
  29. #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제) (韓国語)
  30. #30投票・アンケート(Poll)ボットを作ろう!ネイティブ投票+ボタンでリアルタイム集計まで(Pythonサンプル) 🗳️
  31. #32サーバーログ・監査ログ(Audit Log)チャンネルを作ろう!メッセージの削除・編集・入退室・キックまで自動記録(Pythonサンプル) 🕵️
  32. #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉 (韓国語)
全32回を表示閉じる

著者

Mojangcam

自分でコードを書き、サーバーを動かし、詰まったエラーを解決しながら学んだことを記録する開発・IT実践ブログ