Python으로 Discord 봇 만들기! - 3
Python으로 Discord 봇을 만드는 건 생각만큼 어렵지 않습니다. 준비 조금과 몇 줄의 코드로 봇을 만들고, 실행 할 수 있습니다! 세 번째 시간에는 외부에 데이터를 저장하고, 불러와보겠습니다.
저번 시간에 특정 구조의 채팅을 입력 했을 때, 봇이 답장을 보내게하는 코드를 작성해서 실행까지 해 보았습니다.
하지만 그 코드는 명령어를 추가 할 때, 직접 코드에 명령어와 답장을 하나하나 적어야 한다는 문제가 있습니다.
이번 시간에는, 외부 파일에 정보를 저장하고, 읽고, 쓰게 하는 코드를 통해 명령어를 저장하고, 동적으로 추가할 수 있는 코드를 작성해보겠습니다.
1. Python에서 파일 읽고 쓰기
기본적으로 Python에서는 open 함수를 통해 파일을 읽고 씁니다.

Python 공식 문서의 open 함수를 보면 다음과 같은 인자를 받습니다.
- file
- mode
- buffering
- encoding
- errors
- newline
- closefd
- opener
그리고, mode는 다음과 같은 값이 들어갈 수 있습니다.
문자 |
의미 |
|---|---|
|
읽기용으로 엽니다 (기본값) |
|
쓰기용으로 엽니다, 파일을 먼저 자릅니다. |
|
독점적인 파일 만들기용으로 엽니다, 이미 존재하는 경우에는 실패합니다. |
|
쓰기용으로 엽니다, 파일의 끝에 내용을 추가합니다. |
|
바이너리 모드 |
|
텍스트 모드 (기본값) |
|
갱신(읽기 및 쓰기)용으로 엽니다 |
그 중에서 file과 mode, encoding이 가장 많이 사용됩니다.
file은 문자열로 전달받은 파일 위치에 파일을 엽니다."./data/test.json"으로 지정했다면,data폴더의test.json을 연다는 뜻입니다.
mode는 파일을 어떻게 사용할지 정합니다."r"을 통해 파일을 읽고,"w"를 통해 파일에 쓸 수 있습니다.
encoding은 파일의 인코딩 방식을 지정합니다.UTF-8을 많이 사용하며, 한국에서는cp949나euc-kr을 사용하는 경우도 있습니다.
이 open 함수를 사용하는 방식은 2가지가 있습니다.
a. 그냥 열고, 닫기
file = open("test.txt", "r", encoding="UTF-8")
print(file.read())
file.close()위 코드는 test.txt를 UTF-8로 읽어, print하는 간단한 코드입니다.
끝에 file.close()를 했는데, 파일을 열고 사용을 완료하면 close 해야 합니다.
왜냐하면, 이미 열린 파일을 열려고 할 때 문제가 발생할 수 있기 때문이고,
또는 어떤 프로그램에서 파일을 읽고 쓰는 중에는 그 파일에 접근 할 수 없는 경우가 있기 때문입니다.
게임을 업데이트 하는 동안, 그 게임을 꺼야하는 것과 비슷합니다.
하지만 코드에서 끝에 file.close()를 추가하는건 조금 귀찮습니다.
그래서 with문을 이용하는 경우가 더 많습니다.
b. with문 사용하여 열고, 닫기
with open("test.txt", "r", encoding="UTF-8") as file:
print(file.read())위 코드는 1번 코드와 완전히 동일한 코드입니다.
하지만 더 간결하고, close가 없어졌습니다.
with문에서 open 함수를 사용하면, with문에서 벗어났을 때, close가 알아서 실행됩니다.
+ encoding은 왜 쓸까?
기본적으로 Python에서는 UTF-8을 사용합니다.
하지만 한국에서는 몇몇 경우에, cp949나 euc-kr을 사용하기도 합니다.
이는 프로그램에서 언어를 사용함에 있어서, 사용되는 표준이 다르기 때문에 발생하는 문제입니다.
이제는 기본적으로 UTF-8을 사용하지만, 예전에는 Microsoft에서 만든 euc-kr등을 사용하는 경우가 있었습니다.
euc-kr을 이용하여 작성된 파일은 똑같은 한글이 적힌 파일이지만, 구조가 달라서 오류가 생기기도 합니다.
2. JSON으로 데이터를 외부에 저장하기
파이썬에서 명령어를 사용하는 것에는 제한이 있습니다.
변수들은 모두 메모리에 저장되기 때문에, 실행이 끝나면 데이터가 저장되지 않고 삭제됩니다.
그렇기 때문에 중요한 데이터는 외부에, 파일로 저장해야 합니다.
저번 시간에 만들었던 봇은 명령어를 코드 내부에 정확히 지정하고, 답변 또한 지정했습니다.
하지만 그렇게 된다면 Prefix를 바꿀 때, 하나하나 바꿔야 하고
명령어를 추가 할 때마다 봇을 껐다가 켜야합니다.
코드가 엄청나게 길어지는 것은 덤이죠.
그렇기 때문에 JSON 파일에 저장하여 명령어를 효율적으로 관리하고, 코드도 줄여보겠습니다.
import jsonjson 모듈은 Python에 기본적으로 내장되어 있습니다.
그렇기 때문에 Pycord를 설치했을 때처럼, pip로 귀찮게 설치하지 않아도 괜찮습니다!

Python 공식 문서의 json 라이브러리를 보면 다음과 같은 사용법이 있습니다.
a. JSON 형식으로 인코딩(dumps)
import json
json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
# 결과
# '["foo", {"bar": ["baz", null, 1.0, 2]}]'json.dumps 함수를 이용하여 파이썬의 데이터 형식을 JSON 문법의 문자열로 바꿀 수 있습니다.
이렇게 바뀐 문자는 말 그대로 문자(str)로 처리됩니다.
이 문자를 파일에 저장하는 용도로 사용됩니다.
b. JSON 형식에서 디코딩(loads)
import json
json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]')
# 출력
# ['foo', {'bar': ['baz', None, 1.0, 2]}]json.loads 함수를 이용하여 JSON 문법의 문자열을 파이썬의 데이터로 불러올 수 있습니다.
이렇게 불러온 데이터는 파이썬 데이터(int, str, dict 등)의 사용방식과 동일하게 사용 할 수 있습니다.
+ dump, load
공식 문서를 보면 알 수 있겠지만, dumps와 loads 함수는 각각 dump, load 함수를 기반으로 하는 함수입니다.
dump, load 함수는 file-like object를 받습니다.file-like object는 말 그대로, 파일을 의미합니다.
open 함수를 사용하여 파일을 읽거나 쓸 때, file.write나 file.read를 사용하여 파일을 읽고 씁니다.
file.write를 사용하여, file.write(json.dumps(data))를 통해 data 변수의 내용을 JSON 문법의 문자열로 변환하여 file에 저장할 수 있겠지만
json.dump(data, file)을 한다면 굳이 JSON 문법으로 변환하는 과정 없이 바로 저장할 수 있고
file.read 또한 파일을 파이썬의 문자 형식으로 불러오니, 그것을 이용해서 json.loads(file.read())과 같은 방식으로 불러올 수 있겠지만
json.load(file)을 한다면 굳이 문자 형식으로 불러올 필요 없이 바로 불러올 수 있습니다.
3. 코드 작성
import json
import discord
intent = discord.Intents.all()
client = discord.Client(intents=intent)
with open('commands.json', 'r', encoding="UTF-8") as file:
commands = json.load(file)
@client.event
async def on_ready():
print(client.user.name)
await client.change_presence(activity=discord.Game("햄버거"))
@client.event
async def on_message(msg: discord.Message):
if msg.author.bot:
return
if msg.content.startswith("%명령어추가"):
oper, cmd, rep = msg.content.split(' ')
commands[cmd] = rep
with open('commands.json', 'w', encoding="UTF-8") as file:
json.dump(commands, file, ensure_ascii=False, indent=2)
await msg.reply("명령어: " + cmd + "\n내용: " + rep)
if msg.content in commands.keys():
await msg.reply(commands[msg.content])
client.run('<여기에 TOKEN 입력>')전체 코드는 다음과 같습니다.
import json위 코드는 Json 파일을 파이썬의 Dictionary로 불러오기 위해, 기본 모듈인 json 모듈을 import 합니다.
with open('commands.json', 'r', encoding="UTF-8") as file:
commands = json.load(file)위 코드는 commands.json 파일에서 데이터를 불러와, commands 변수에 Dictionary 형태로 저장합니다.
if msg.content.startswith("%명령어추가"):
oper, cmd, rep = msg.content.split(' ')
commands[cmd] = rep
with open('commands.json', 'w', encoding="UTF-8") as file:
json.dump(commands, file, ensure_ascii=False, indent=2)
await msg.reply("명령어: " + cmd + "\n내용: " + rep)위 코드는 기존의 코드와 다르게, %명령어추가 명렁어가 생겼습니다.
%명령어추가 명령어는 %명령어추가 <명령어> <내용>의 구조로 사용되는 명령어입니다.
이를 위해 메세지의 내용(msg.content)을 공백 단위로 잘라(.split(' ')) 각각 oper, cmd, rep에 저장합니다.
만약 명령어가 %명령어추가 햄버거 맛있어였다면, 각 변수에 다음과 같이 저장됩니다.
oper:"%명령어추가"cmd:"햄버거"rep:"맛있어"
그리고 그 명령어를 commands 변수에 cmd를 Key로, rep를 Value로 저장하고, 명령어가 재실행 시 사라지지 않게 commands.json에 저장하기 위해서
with open('commands.json', 'w', encoding="UTF-8") as file:
json.dump(commands, file, ensure_ascii=False, indent=2)open 함수로 commands.json을 쓰기 모드('w')로 열어서 file에 저장한 후, JSON 형식으로 저장합니다.
여기에서 json.dump에 사용된 옵션에 대해 설명하자면, 다음과 같습니다.
commands: 프로그램에서 사용되는commandsdict타입 변수입니다.file:open을 통해 연commands.json파일의 객체입니다.ensure_ascii=False: 영어 이외의 데이터를 16진수로 표시하는 옵션입니다. 꺼야 한글로 보입니다.indent=2: JSON 문자를 저장할 때 공백 크기 설정입니다. 보기 편하게 표시되므로 지정했습니다.
await msg.reply("명령어: " + cmd + "\n내용: " + rep)마지막으로 명령어가 잘 추가되었음을 알리기 위해서, 명령어를 추가한 메세지에 명령어 이름과 내용을 포함하여 답장을 보냅니다.
if msg.content in commands.keys():
await msg.reply(commands[msg.content])명령어의 저장 방식이 바뀌었으므로, 명령어를 저장하는 dict의 특성을 활용하여 코드를 간결하게 해보겠습니다.
만약 메세지의 내용(msg.content)이 commands 변수의 Key들 중(commands.keys())에 있다면
메세지의 답장으로(msg.reply) 그 명령어의 값(commands[msg.content])을 보내는 방식입니다.
이렇게 한다면 이제는 JSON 파일에 추가하거나, 명령어를 통해서 추가하면 알아서 저장되기 때문에 코드를 더 바꿀 필요가 없습니다.
이렇게 코드를 개선하여, 외부 파일에 저장하는 기능까지 써 봤습니다.
다음 시간에는 지금까지 작성한 코드를 Git을 통해 관리하고, Github에 올려서 저장해보겠습니다.