分类: 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 保存到文件的函数。
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 个命令(!출석 签到、!포인트 积分、!랭킹 排行榜)就完成了。这段代码是可以直接运行的完整版。
# 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 文件。用记事本打开是这个样子。
{
"123456789012345678": {
"points": 30,
"last_check": "2026-07-30"
}
}如果只有 10 个用户左右,这样就够用了。但用户一多到几百人、记录越积越多,问题就来了。因为这种结构是签到一次就要重写整个文件,看一次排行榜就要把全部数据重新读一遍。而且如果保存到一半机器人被强制关闭,文件可能会整个损坏。这时候就需要 SQLite 了!
📌 第 3 步 - 把同一个机器人升级成 SQLite 版
SQLite 用 Python 内置的 sqlite3 模块就能直接使用。先连接 DB 文件,再创建表(table)。
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()签到命令会变成这样。
@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 就搞定了。
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 步的代码就行。)
# 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)📌 运行结果
启动机器人,在服务器里逐个确认一下吧。
- 输入
!출석(签到)→ 机器人会回复"✅ 签到成功!+10P(共 10P)"。同一天再输入一次,会回复"今天已经签到过了!😅" - 输入
!포인트(积分)→ 显示你的累计积分。 - 输入
!랭킹(排行榜)→ 弹出一条挂着 🥇🥈🥉 奖牌的 TOP 5 嵌入消息。 - 真正的重头戏来了!把机器人关掉再重新打开。 输入
!포인트,积分还原封不动地在那里。这都多亏机器人文件夹里的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 机器人开发系列
- #5 - 按钮(Button)交互
- #6 - 下拉菜单(Select Menu)
- #7 - 用模态框(Modal)接收输入
- #8 - 美化嵌入消息(Embed)
- #9 - 接入 AI(Claude·ChatGPT API)
- #10 - 支持多语言(i18n)
- #11 - 用 Flask 制作网页仪表盘
- #12 - 管理命令冷却时间·权限
- #13 - 身份组·频道自动化
- #14 - 数据存储(JSON vs SQLite)← 就是这一篇!
连载 · 共 32 篇中的第 14 篇
Discord 机器人开发
- #1Discord 机器人开发 - Slash Command 完全攻略!
- #2为 Slash Command 添加选项(Arguments)!
- #3在 Slash Command 中添加子命令(Subcommand)!
- #4用 Slash Command 发送 Embed 嵌入消息!
- #5实现按钮(Button)交互!
- #6实现下拉菜单(Select Menu)!
- #7用模态框(Modal)接收用户输入(Python 示例)
- #8用嵌入消息(Embed)的图片和字段美化消息(Python 示例)
- #9接入 AI(Claude·ChatGPT API)!(Python 示例)
- #10支持多语言(i18n)!(Python 示例)
- #11用 Flask 制作网页仪表盘!(Python 示例)
- #12管理命令冷却时间(Cooldown)与权限(Permission)!(Python 示例)
- #13用身份组·频道自动化管理服务器!(Python 示例)
- #14保存数据!JSON vs SQLite(Python 示例)当前文章
- #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제) (韩语)
- #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제) (韩语)
- #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제) (韩语)
- #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제) (韩语)
- #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제) (韩语)
- #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제) (韩语)
- #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제) (韩语)
- #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제) (韩语)
- #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제) (韩语)
- #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제) (韩语)
- #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제) (韩语)
- #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제) (韩语)
- #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제) (韩语)
- #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제) (韩语)
- #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제) (韩语)
- #30打造投票·问卷(Poll)机器人!原生投票 + 按钮实时统计(Python 示例)🗳️
- #32打造服务器日志·审核日志(Audit Log)频道!自动记录消息删除、编辑、成员进出和踢出(Python 示例)🕵️
- #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉 (韩语)
