Categoría: Bots de Discord
Crear un bot de Discord#14¡Guarda tus datos! JSON vs SQLite (ejemplo en Python)
Esta entrada es una traducción del original en coreano.
Índice11 secciones
¡Hola! Soy Mojangcam 😎
En el #13 dejamos la administración del servidor por completo en manos del bot, automatizando roles y canales. Pero quedó una cosa pendiente. Igual que temp_channels del #13, los datos que solo guardas en variables desaparecen por completo en cuanto reinicias el bot.
Por eso, en este #14 le vamos a dar memoria al bot. Vamos a crear un bot de asistencia + puntos + ranking con dos métodos que ya vienen incluidos en Python, JSON y SQLite, y de paso dejaremos claro cuándo conviene usar cada uno.
Registro de asistencia, sistema de puntos (economía), niveles, acumulación de advertencias… todas las funciones populares de los servidores de comunidad necesitan un "bot que recuerde datos". ¡En cuanto superes este paso, la cantidad de funciones que puedes crear se dispara!

🧠 ¿Por qué hace falta guardar los datos?
Todas las variables de Python existen solo en la memoria (RAM). En cuanto reinicias el bot, se reinicia el VPS o el bot se cae por un error, los puntos y registros que había en las variables se borran sin dejar rastro. Por eso hay que dejar los datos en un archivo y volver a leerlos cuando el bot arranca. A esto se le llama, con una palabra más técnica, persistencia (Persistence).
🧩 JSON vs SQLite, ¿en qué se diferencian?
¡Vamos directo a la conclusión! Para datos pequeños y simples, como la configuración, usa JSON; para datos que se van acumulando y necesitan búsquedas o rankings, usa SQLite.
| Aspecto | JSON | SQLite |
|---|---|---|
| Qué es | Un archivo de texto (guarda el dict tal cual) | Una base de datos de verdad en un solo archivo |
| Instalación | No hace falta (módulo integrado json) |
No hace falta (módulo integrado sqlite3) |
| Lectura y escritura | Siempre el archivo completo de una vez | Solo las filas que necesitas |
| Búsqueda y orden | Lo programas tú mismo en Python | Una línea de SQL (ORDER BY, etc.) |
| Cuando crecen los datos | Se vuelve cada vez más lento | Rápido gracias a los índices |
| Si algo falla al guardar | Riesgo de que el archivo se corrompa | Seguro gracias a las transacciones |
| Dónde encaja | Configuración del bot, registros pequeños | Datos que no paran de crecer, como puntos, economía o logs |
JSON es básicamente un dict de Python pasado a un archivo, así que es intuitivo, y su mayor ventaja es que puedes abrirlo con el Bloc de notas y revisarlo a simple vista. A cambio, como reescribe el archivo entero cada vez que guardas, se vuelve más lento a medida que crecen los datos. SQLite es una base de datos de verdad que funciona con un solo archivo y sin instalar ningún servidor, así que puedes leer solo los datos que necesitas y dejarle a SQL hasta el ordenamiento y los cálculos.
📌 Lo que necesitas
- Python 3.8 o superior
- discord.py 2.x (cualquier versión 2.x reciente sirve)
- Módulos
jsonysqlite3— vienen integrados en Python, ¡así que no hay nada que instalar!
pip install -U discord.pyComo vamos a usar comandos con prefijo (!출석), tienes que activar Message Content Intent en el Discord Developer Portal → pestaña Bot. (Si es la primera vez que tocas este ajuste, ¡revisa los capítulos anteriores de la serie!)
📌 Paso 1 - Lo básico de guardar en JSON: las funciones load y save
La clave del método JSON son solo dos funciones: una que lee el dict desde el archivo y otra que guarda el dict en el archivo.
import json
import os
DATA_FILE = "points.json"
def load_data():
"""Si el archivo existe lo lee; si no, devuelve un dict vacío."""
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):
"""Guarda el dict completo en el archivo JSON."""
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)Vamos a verlo parte por parte 💡
- json.load(f) : lee el contenido del archivo y lo convierte en un dict de Python.
- json.dump(data, f) : convierte el dict en texto con formato JSON y lo escribe en el archivo.
- encoding="utf-8" + ensure_ascii=False : es el combo para que los caracteres no ASCII (coreano, ñ, tildes…) no se rompan. Si quitas cualquiera de los dos, el texto se guarda en un idioma alienígena como
\uD55C\uAE00. - indent=2 : añade sangría para que el archivo quede fácil de leer para las personas.
¡Un detalle importante! Las claves (key) de JSON solo pueden ser cadenas. El ID de usuario en realidad es un número, pero lo más seguro es convertirlo siempre a cadena al guardarlo, como str(ctx.author.id).
📌 Paso 2 - Bot de asistencia y puntos completo en versión JSON
Si a esas dos funciones les sumas tres comandos (!출석 = asistencia, !포인트 = puntos, !랭킹 = ranking), ya está listo. Este código es la versión completa y puedes ejecutarlo tal cual.
# discord.py 2.x / Python 3.8+ / json viene integrado en Python
import json
import os
from datetime import date
import discord
from discord.ext import commands
TOKEN = "PON_AQUI_EL_TOKEN_DE_TU_BOT"
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 # para comandos con prefijo
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"✅ ¡Sesión iniciada como {bot.user}!")
@bot.command(name="출석")
async def attend(ctx: commands.Context):
data = load_data()
uid = str(ctx.author.id)
today = date.today().isoformat() # ej.: '2026-07-30'
user = data.get(uid, {"points": 0, "last_check": ""})
if user["last_check"] == today:
await ctx.send(f"{ctx.author.mention} ¡Hoy ya registraste tu asistencia! 😅")
return
user["points"] += 10
user["last_check"] = today
data[uid] = user
save_data(data)
await ctx.send(f"✅ {ctx.author.mention} ¡Asistencia registrada! +10P (total {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"💰 Puntos de {ctx.author.mention}: {total}P")
@bot.command(name="랭킹")
async def ranking(ctx: commands.Context):
data = load_data()
if not data:
await ctx.send("Todavía nadie ha registrado asistencia. ¡Tu oportunidad de ser el número 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="🏆 Ranking de puntos TOP 5",
description="\n".join(lines),
color=0x5865F2)
await ctx.send(embed=embed)
bot.run(TOKEN)Vamos a verlo parte por parte 💡
- date.today().isoformat() : convierte la fecha de hoy en una cadena con el formato
2026-07-30. Al compararla conlast_check, es la clave para permitir una sola asistencia al día. - data.get(uid, valor por defecto) : si es la primera vez que el usuario registra asistencia, crea un dict por defecto que empieza con 0 puntos.
- sorted(..., reverse=True)[:5] : para sacar el ranking, cargamos todos los datos en Python y los ordenamos nosotros mismos. ¡Este es justo el límite del método JSON! Compáralo más adelante con SQLite.
- Mención <@ID_del_usuario> dentro del embed : las menciones dentro de un embed no envían notificación (ping), así que son perfectas para mostrar un ranking.
Cuando lo ejecutes y escribas !출석, se creará el archivo points.json en la carpeta del bot. Si lo abres con el Bloc de notas, se ve así.
{
"123456789012345678": {
"points": 30,
"last_check": "2026-07-30"
}
}Para un servidor de unos 10 usuarios, con esto basta. Pero cuando hay cientos de usuarios y los registros se acumulan, empiezan los problemas. La estructura es así: con cada asistencia se reescribe el archivo entero, y con cada ranking se vuelve a leer todo. Además, si el bot se cierra a la fuerza mientras guarda, el archivo puede corromperse por completo. ¡Para estos casos necesitas SQLite!
📌 Paso 3 - Pasar el mismo bot a SQLite
SQLite se puede usar directamente con el módulo integrado sqlite3 de Python. Primero te conectas al archivo de la base de datos y creas una tabla.
import sqlite3
con = sqlite3.connect("points.db") # ¡si el archivo no existe, se crea solo!
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()El comando de asistencia queda así.
@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} ¡Hoy ya registraste tu asistencia! 😅")
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()Vamos a verlo parte por parte 💡
- sqlite3.connect("points.db") : se conecta al archivo de la base de datos. Si el archivo no existe, lo crea por ti.
- CREATE TABLE IF NOT EXISTS : crea la tabla solo si no existe. Por eso es seguro reiniciar el bot las veces que quieras.
- PRIMARY KEY : es la clave principal que impide que
user_idse repita. - Marcador de posición con signo de interrogación (?) : pasa los valores de forma segura sin pegarlos directamente en la sentencia SQL. Si armas el SQL con f-strings, quedas expuesto a ataques de inyección SQL, ¡así que usa siempre
?! - INSERT OR IGNORE → UPDATE : primero "si no existe, lo crea; si existe, lo deja como está", y después suma los puntos. Es el patrón con el que menos se equivocan los principiantes.
- con.commit() : confirma los cambios en el archivo. ¡Si te lo saltas, no se guarda nada!
¿Y el ranking? Con JSON cargábamos todo y lo ordenábamos en Python, pero con SQLite basta una línea de SQL.
rows = con.execute(
"SELECT user_id, points FROM points ORDER BY points DESC LIMIT 5"
).fetchall()ORDER BY points DESC LIMIT 5 — significa "ordena por puntos de mayor a menor y dame solo los 5 primeros". Aunque haya 100.000 usuarios, la base de datos los selecciona sola. ¡Ese es el poder de una base de datos! 💪
📌 Código completo (listo para copiar y pegar)
Esta es la versión completa con SQLite. Solo cambia TOKEN y funciona al instante. (Para la versión completa con JSON, usa tal cual el código del Paso 2 de arriba).
# discord.py 2.x / Python 3.8+ / sqlite3 viene integrado en Python
import sqlite3
from datetime import date
import discord
from discord.ext import commands
TOKEN = "PON_AQUI_EL_TOKEN_DE_TU_BOT"
DB_FILE = "points.db"
# ── Preparar la base de datos ──────────────────────────
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 # para comandos con prefijo
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"✅ ¡Sesión iniciada como {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} ¡Hoy ya registraste tu asistencia! 😅")
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} ¡Asistencia registrada! +10P (total {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"💰 Puntos de {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("Todavía nadie ha registrado asistencia. ¡Tu oportunidad de ser el número 1! 🏃")
return
medals = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣"]
lines = [f"{medals[i]} <@{uid}> — {pts}P"
for i, (uid, pts) in enumerate(rows)]
embed = discord.Embed(title="🏆 Ranking de puntos TOP 5",
description="\n".join(lines),
color=0x5865F2)
await ctx.send(embed=embed)
bot.run(TOKEN)📌 Resultado
Enciende el bot y prueba los comandos uno por uno en tu servidor.
- Escribe
!출석→ responde "✅ ¡Asistencia registrada! +10P (total 10P)". Si lo vuelves a escribir el mismo día: "¡Hoy ya registraste tu asistencia! 😅" - Escribe
!포인트→ muestra tus puntos acumulados. - Escribe
!랭킹→ aparece un embed con el TOP 5 y las medallas 🥇🥈🥉. - ¡Y aquí viene lo mejor! Apaga el bot y vuelve a encenderlo. Si escribes
!포인트, tus puntos siguen ahí. Es gracias a que la memoria quedó guardada en el archivopoints.db(opoints.json) de la carpeta del bot. 🎉


📌 Errores comunes
1. json.decoder.JSONDecodeError: Expecting value: line 1 column 1
Causa: points.json está vacío, o el bot se cerró a la fuerza mientras guardaba y el archivo se corrompió.
Solución: borra el archivo y vuelve a ejecutar el bot; se creará uno nuevo. Si no quieres perder los datos, ábrelo con el Bloc de notas y arregla la parte dañada. Para prevenirlo, apaga el bot con un solo Ctrl+C cuando no esté guardando, para que se cierre de forma normal.
2. El texto se guarda en un idioma alienígena como \uc548\ub155
Causa: olvidaste poner ensure_ascii=False en json.dump.
Solución: json.dump(data, f, ensure_ascii=False, indent=2) — y al abrir el archivo, no olvides añadir encoding="utf-8".
3. sqlite3.OperationalError: no such table: points
Causa: hiciste un SELECT antes de ejecutar CREATE TABLE, o ejecutaste el bot desde otra carpeta y se creó un archivo de base de datos vacío en otro lugar.
Solución: comprueba que el código que prepara la base de datos (CREATE TABLE) se ejecute antes de bot.run(), y entra a la carpeta del bot con cd en la terminal antes de ejecutarlo.
4. sqlite3.OperationalError: database is locked
Causa: un programa visor de bases de datos tiene abierto points.db, u otra conexión sin commit() tiene el archivo bloqueado.
Solución: cierra el visor y acostúmbrate a poner siempre con.commit() después del código que modifica datos.
💡 Consejos extra
- Evita el spam de asistencia : además del límite de una vez al día, si le pones el cooldown del #12, bloqueas también el spam por completo.
- Para hacer una copia de seguridad, basta con copiar un archivo : ¡copia solo
points.dby listo! Si lo tienes funcionando 24 horas en un VPS, ten en cuenta que la fecha de asistencia se basa en la hora del servidor, así que en un VPS en el extranjero ajusta la zona horaria a Seúl (Asia/Seoul). - Si los datos crecen muchísimo :
sqlite3funciona de forma síncrona, así que con operaciones muy grandes el bot puede quedarse congelado un momento. A escala de principiante no se nota, ¡pero más adelante en la serie veremos cómo pasar a la librería asíncrona aiosqlite!
📌 Para terminar
Hoy aprendimos a guardar datos para darle memoria al bot. Para datos pequeños y simples, JSON; para datos que se van acumulando y necesitan búsquedas o rankings, SQLite — con recordar solo este criterio es suficiente. Ya creamos el bot de asistencia, puntos y ranking con los dos métodos, así que ahora puedes aplicarlo a lo que quieras, ¡ya sea un sistema de niveles o un contador de advertencias!
En el próximo capítulo veremos los grupos de comandos slash (Group), que sirven para ordenar un bot con muchos comandos. ¡No te lo pierdas! 🙌
📚 Serie Crear un bot de Discord
- #5 - Interacciones con botones (Button)
- #6 - Menús desplegables (Select Menu)
- #7 - Pedir datos con un modal (Modal)
- #8 - Decorar embeds (Embed)
- #9 - Integración con IA (API de Claude y ChatGPT)
- #10 - Soporte multilingüe (i18n)
- #11 - Panel web con Flask
- #12 - Gestión de cooldowns y permisos de comandos
- #13 - Automatización de roles y canales
- #14 - Guardar datos (JSON vs SQLite) ← ¡estás leyendo este!
Serie · Parte 14 de 32
Crear un bot de Discord
- #1Crear un bot de Discord - ¡Domina por completo los Slash Commands!
- #2¡Añade opciones (argumentos) a tus Slash Commands!
- #3¡Añade subcomandos (Subcommand) a tus Slash Commands!
- #4¡Envía embeds con Slash Commands!
- #5¡Implementa interacciones con botones (Button)!
- #6¡Implementa menús desplegables (Select Menu)!
- #7Pide texto al usuario con un modal (Modal) (ejemplo en Python)
- #8Decora embeds (Embed) con imágenes y campos (ejemplo en Python)
- #9¡Integra la IA (API de Claude y ChatGPT)! (ejemplo en Python)
- #10¡Añade soporte multilingüe (i18n)! (ejemplo en Python)
- #11¡Crea un panel web con Flask! (ejemplo en Python)
- #12¡Gestiona los cooldowns y permisos de tus comandos! (ejemplo en Python)
- #13¡Administra tu servidor automatizando roles y canales! (ejemplo en Python)
- #14¡Guarda tus datos! JSON vs SQLite (ejemplo en Python)Estás aquí
- #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제) (en coreano)
- #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제) (en coreano)
- #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제) (en coreano)
- #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제) (en coreano)
- #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제) (en coreano)
- #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제) (en coreano)
- #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제) (en coreano)
- #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제) (en coreano)
- #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제) (en coreano)
- #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제) (en coreano)
- #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제) (en coreano)
- #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제) (en coreano)
- #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제) (en coreano)
- #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제) (en coreano)
- #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제) (en coreano)
- #30¡Crea un bot de encuestas y votaciones (Poll)! Encuestas nativas + botones con recuento en tiempo real (ejemplo en Python) 🗳️
- #32¡Crea un canal de registros y auditoría (Audit Log) del servidor! Registra automáticamente mensajes borrados y editados, entradas, salidas y expulsiones (ejemplo en Python) 🕵️
- #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉 (en coreano)
