分类: Discord 机器人开发

Discord 机器人开发#14保存数据!JSON vs SQLite(Python 示例)

本文译自韩语原文。

目录11 项

大家好!我是 Mojangcam 😎

上一篇 #13 我们用身份组·频道自动化把服务器管理整个交给了机器人。不过还留有一点遗憾。像 #13 的 temp_channels 那样,只放在变量里的数据,机器人一重启就会全部消失。

所以这一篇 #14 我们要给机器人装上记忆力。我们会用 Python 内置的 JSON 和 SQLite 两种方法,亲手做一个签到 + 积分 + 排行榜机器人,同时把什么时候该用哪个彻底讲清楚。

签到、积分(经济)系统、等级、警告累计…… 社区服务器里受欢迎的功能,全都需要一个"能记住数据的机器人"才做得出来。今天只要跨过这道坎,你能做的功能就会一下子多出很多!

🧠 为什么需要保存数据?

Python 的变量全都只存在于内存(RAM)中。机器人一重启、VPS 一重启,或者机器人因为报错而关掉,变量里的积分和记录就会被清空得一干二净。所以需要把数据写进文件保存下来,等机器人启动时再读回来。这个过程用专业术语来说叫持久化(Persistence)。

🧩 JSON vs SQLite,有什么区别?

先说结论!像设置这样又小又简单的数据用 JSON,会不断累积、需要搜索·排名的数据用 SQLite。

对比项 JSON SQLite
本质 文本文件(把 dict 原样保存) 只有一个文件的真正数据库
安装 不需要(内置 json 模块) 不需要(内置 sqlite3 模块)
读写 每次都要把整个文件一起读写 只挑需要的行来读写
搜索·排序 用 Python 代码自己实现 一行 SQL(ORDER BY 等)
数据变多时 越来越慢 有索引,所以很快
保存时出意外 文件有损坏的风险 有事务保护,很安全
适合场景 机器人设置值、小规模记录 积分·经济·日志这类不断累积的数据

JSON 就是把 Python 的 dict 原样搬进文件,所以很直观,最大的优点是可以用记事本打开直接用眼睛确认。不过每次保存都要把整个文件重写一遍,数据越大就越慢。SQLite 是不用安装服务器、一个文件就能运行的真正的数据库,可以只挑需要的数据来读,排序·统计也都能交给 SQL。

📌 准备工作

  • Python 3.8 及以上
  • discord.py 2.x(最新的 2.x 版本就行)
  • json·sqlite3 模块 —— Python 自带,不用另外安装!
终端
pip install -U discord.py

因为要用前缀命令(!출석,签到),所以需要在 Discord 开发者门户 → Bot 标签页 → 打开 Message Content Intent。(如果是第一次设置,请参考本系列前面的文章!)

📌 第 1 步 - JSON 存储的基本功:load & save 函数

JSON 方式的核心就只有两个函数。从文件读取 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 版签到·积分机器人

在上面两个函数的基础上加上 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 = "在这里输入机器人 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("还没有人签到哦。抢第一的机会来了!🏃")
        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 比较,就能实现每天只能签到一次,这是关键所在。
  • 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 个用户左右,这样就够用了。但用户一多到几百人、记录越积越多,问题就来了。因为这种结构是签到一次就要重写整个文件,看一次排行榜就要把全部数据重新读一遍。而且如果保存到一半机器人被强制关闭,文件可能会整个损坏。这时候就需要 SQLite 了!

📌 第 3 步 - 把同一个机器人升级成 SQLite 版

SQLite 用 Python 内置的 sqlite3 模块就能直接使用。先连接 DB 文件,再创建表(table)。

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 就搞定了。

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 也会自动帮你挑出来。这就是数据库的力量!💪

📌 完整代码(可直接复制)

这是 SQLite 版的完整代码。只要改掉 TOKEN 就能直接运行。(JSON 版的完整代码直接用上面第 2 步的代码就行。)

Python
# discord.py 2.x / Python 3.8+ / sqlite3 是 Python 内置模块
import sqlite3
from datetime import date

import discord
from discord.ext import commands

TOKEN = "在这里输入机器人 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("还没有人签到哦。抢第一的机会来了!🏃")
        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 正常退出。

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() 的习惯。

💡 额外小技巧

  • 防止连续刷签到:在每天一次的限制之外,再加上 #12 的冷却时间,连刷屏也能彻底挡住。
  • 备份只要复制一次文件:把 points.db 文件复制一份,备份就完成了!如果你正在 VPS 上 24 小时运行机器人,签到日期是按服务器时间算的,所以记得把 VPS 的时区调成你的用户所在的时区(例如首尔 Asia/Seoul)。
  • 数据真的变得很多时:sqlite3 是同步方式,遇到很大的操作时机器人可能会卡一下。入门规模下感觉不到,不过之后本系列会讲怎样换成异步库 aiosqlite!

📌 结语

今天我们学习了给机器人装上记忆力的数据存储。又小又简单的数据用 JSON,不断累积、需要搜索·排名的数据用 SQLite —— 只要记住这一个标准就够了。签到·积分·排行榜机器人两种方式都做过了,接下来不管是等级系统还是警告累计,都可以举一反三!

下一篇我们会讲怎样用斜杠命令组(Group)把命令越来越多的机器人整理得清清楚楚。敬请期待!🙌

📚 Discord 机器人开发系列

连载 · 共 32 篇中的第 14 篇

Discord 机器人开发

  1. #1Discord 机器人开发 - Slash Command 完全攻略!
  2. #2为 Slash Command 添加选项(Arguments)!
  3. #3在 Slash Command 中添加子命令(Subcommand)!
  4. #4用 Slash Command 发送 Embed 嵌入消息!
  5. #5实现按钮(Button)交互!
  6. #6实现下拉菜单(Select Menu)!
  7. #7用模态框(Modal)接收用户输入(Python 示例)
  8. #8用嵌入消息(Embed)的图片和字段美化消息(Python 示例)
  9. #9接入 AI(Claude·ChatGPT API)!(Python 示例)
  10. #10支持多语言(i18n)!(Python 示例)
  11. #11用 Flask 制作网页仪表盘!(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 实战博客