分类: Discord 机器人开发
Discord 机器人开发#30打造投票·问卷(Poll)机器人!原生投票 + 按钮实时统计(Python 示例)🗳️
本文译自韩语原文。
目录10 项
大家好!我是 Mojangcam 😎
上一篇我们做了货币·商店(Economy)系统,完成了一个能赚金币、买道具的机器人。服务器里算是多了一样"好玩的东西"!
这一篇我们来做服务器运营中真正常用的功能——投票·问卷(Poll)机器人。两种方式都会讲到。
- 原生投票 — 由机器人弹出 Discord 自带的投票 UI(最简单!)
- 按钮自定义投票 — 用按钮自己做,实时统计·防止重复投票·截止投票全都随心控制
定服务器活动日期、决定下一期做什么内容、就规则修改征求赞成与反对……如果你一直在给消息加表情再手动数票,今天这篇就能帮你彻底解决!🙌

📌 准备工作
- Python 3.8+(建议 3.10 及以上)
- discord.py 2.x — 原生 Poll 需要 2.5 及以上
- python-dotenv — 为了不把机器人令牌直接写进代码,这个是必需的!
pip install -U "discord.py>=2.5" python-dotenv可以这样确认已安装的版本。
python -c "import discord; print(discord.__version__)"另外,机器人令牌千万不要硬编码在代码里。请在项目文件夹里创建一个 .env 文件。
token=여기에_봇_토큰_붙여넣기💡 提示:一定要在
.gitignore里加上一行.env!令牌一旦被传到 GitHub,Discord 会自动让这个令牌失效。(这倒也算不幸中的万幸 😅)
🧩 设置 Intents(意图)
要接收原生投票的投票事件,需要 polls intent。如果只用按钮方式,不开也没关系。
intents = discord.Intents.default()
intents.message_content = True
intents.polls = True # 要接收投票事件(on_poll_vote_add 等)时需要📌 第 1 步 - 弹出原生投票(最简单的路)
从 discord.py 2.5 开始新增了 discord.Poll 类,机器人可以直接弹出 Discord 应用自带的投票 UI。
@bot.tree.command(name="투표", description="弹出 Discord 自带的投票")
async def native_poll(interaction: discord.Interaction, 질문: str, 항목: str):
# 选项用逗号分隔接收:"炸鸡,披萨,猪蹄"
choices = [c.strip() for c in 항목.split(",") if c.strip()][:10]
if len(choices) < 2:
await interaction.response.send_message("请至少输入 2 个选项!", ephemeral=True)
return
poll = discord.Poll(question=질문, duration=datetime.timedelta(hours=24))
for c in choices:
poll.add_answer(text=c)
await interaction.response.send_message(poll=poll)下面逐一拆解 💡
- discord.Poll(question, duration):创建投票对象。
question最多 300 个字符,duration用datetime.timedelta指定,最长可以到 32 天(768 小时)。 - poll.add_answer(text=..., emoji=...):添加选项。最多只能 10 个。还可以用
emoji参数加上表情。 - send_message(poll=poll):把它用
poll=传给消息就完事了!统计、截止、防重复全由 Discord 自动处理。 - 另外,机器人需要有创建投票(Send Polls)权限。
如果想在有人投票或撤回投票的那一刻做点什么,用事件就行。
@bot.event
async def on_poll_vote_add(user, answer):
print(f"{user} 投给了 '{answer.text}'")💬 这个事件只有在该消息还在机器人缓存里时才会触发。想连旧消息也捕获的话,请用
on_raw_poll_vote_add。
原生投票确实很方便,但也有局限:最多 10 个选项、不能按身份组限制、没法把结果漂亮地画进自己的嵌入消息、投票进行中无法加工统计数据。所以我们进入第 2 步!
📌 第 2 步 - 按钮自定义投票(实时统计)
结构是一个按钮对应一个选项,再把谁按了什么存进字典里。
class PollView(discord.ui.View):
def __init__(self, question: str, choices: list[str], author_id: int, timeout: float = 3600):
super().__init__(timeout=timeout)
self.question = question
self.choices = choices
self.author_id = author_id
self.votes: dict[int, int] = {} # {用户ID: 选项 index}
for idx, label in enumerate(choices):
self.add_item(VoteButton(label=label, index=idx))下面逐一拆解 💡
- self.votes:这是核心。键是用户 ID,所以同一个人再按一次时,值会被覆盖。也就是说,从结构上就杜绝了重复投票。改主意了?按另一个按钮就行!
- timeout:过了这个时间(秒),按钮会自动停止工作。3600 = 1 小时。
- author_id:保存下来,是为了只让发起投票的人能按"提前截止"。
按钮本体长这样。
class VoteButton(discord.ui.Button):
def __init__(self, label: str, index: int):
super().__init__(label=label, style=discord.ButtonStyle.secondary)
self.index = index
async def callback(self, interaction: discord.Interaction):
view: PollView = self.view
view.votes[interaction.user.id] = self.index
await interaction.response.edit_message(embed=view.build_embed())- callback:按下按钮的瞬间执行。记录选票后马上重绘消息。这就是"实时统计"的真面目!
- edit_message:不发新消息,而是直接替换原来的消息。聊天频道不会被刷屏 👍
📌 第 3 步 - 把结果画成柱状图
只显示数字太单调了,我们用方块字符来画个柱状图。
def build_embed(self) -> discord.Embed:
counts = [0] * len(self.choices)
for choice_idx in self.votes.values():
counts[choice_idx] += 1
total = sum(counts)
lines = []
for label, cnt in zip(self.choices, counts):
ratio = (cnt / total) if total else 0
bar = "█" * round(ratio * 12) or "░"
lines.append(f"`{bar:<12}` **{cnt}票** ({ratio*100:.0f}%) · {label}")
embed = discord.Embed(title=f"🗳️ {self.question}", description="\n".join(lines))
embed.set_footer(text=f"共 {total} 票 · 点击按钮投票(再点一次可以更改)")
return embed- bar:<12:用反引号包起来并把宽度固定为 12 格,这样柱子的长度就不会参差不齐。
- or "░":0 票时为了不变成空行,放一个空白格字符进去。
- 总票数和
len(self.votes)相同。毕竟一人一票嘛!

📌 完整代码(直接复制 OK)
# discord.py 2.5+ / Python 3.10+
# pip install -U "discord.py>=2.5" python-dotenv
import os
import datetime
import discord
from discord.ext import commands
from dotenv import load_dotenv
load_dotenv()
token = os.getenv("token")
intents = discord.Intents.default()
intents.message_content = True
intents.polls = True
bot = commands.Bot(command_prefix="!", intents=intents)
class VoteButton(discord.ui.Button):
def __init__(self, label: str, index: int):
super().__init__(label=label, style=discord.ButtonStyle.secondary)
self.index = index
async def callback(self, interaction: discord.Interaction):
view: "PollView" = self.view
if view.closed:
await interaction.response.send_message("这个投票已经截止了!", ephemeral=True)
return
view.votes[interaction.user.id] = self.index
await interaction.response.edit_message(embed=view.build_embed(), view=view)
class CloseButton(discord.ui.Button):
def __init__(self):
super().__init__(label="截止投票", style=discord.ButtonStyle.danger, row=4)
async def callback(self, interaction: discord.Interaction):
view: "PollView" = self.view
if interaction.user.id != view.author_id:
await interaction.response.send_message("只有发起投票的人才能截止!", ephemeral=True)
return
view.closed = True
for item in view.children:
item.disabled = True
await interaction.response.edit_message(embed=view.build_embed(), view=view)
view.stop()
class PollView(discord.ui.View):
def __init__(self, question: str, choices: list[str], author_id: int, timeout: float = 3600):
super().__init__(timeout=timeout)
self.question = question
self.choices = choices
self.author_id = author_id
self.votes: dict[int, int] = {}
self.closed = False
for idx, label in enumerate(choices):
self.add_item(VoteButton(label=label, index=idx))
self.add_item(CloseButton())
def build_embed(self) -> discord.Embed:
counts = [0] * len(self.choices)
for choice_idx in self.votes.values():
counts[choice_idx] += 1
total = sum(counts)
lines = []
for label, cnt in zip(self.choices, counts):
ratio = (cnt / total) if total else 0
bar = "█" * round(ratio * 12) or "░"
lines.append(f"`{bar:<12}` **{cnt}票** ({ratio * 100:.0f}%) · {label}")
title = ("🔒 [마감] " if self.closed else "🗳️ ") + self.question
embed = discord.Embed(title=title, description="\n".join(lines), color=0x5865F2)
footer = f"共 {total} 票"
footer += " · 这是最终结果" if self.closed else " · 点击按钮投票(再点一次可以更改)"
embed.set_footer(text=footer)
return embed
async def on_timeout(self):
self.closed = True
for item in self.children:
item.disabled = True
@bot.event
async def on_ready():
await bot.tree.sync()
print(f"{bot.user} 已就绪!")
@bot.tree.command(name="투표", description="开始按钮投票(选项用逗号分隔)")
async def poll_command(interaction: discord.Interaction, 질문: str, 항목: str, 시간: int = 1):
choices = [c.strip() for c in 항목.split(",") if c.strip()][:5]
if len(choices) < 2:
await interaction.response.send_message("请至少输入 2 个选项!例:炸鸡,披萨,猪蹄", ephemeral=True)
return
view = PollView(질문, choices, interaction.user.id, timeout=시간 * 3600)
await interaction.response.send_message(embed=view.build_embed(), view=view)
@bot.tree.command(name="기본투표", description="弹出 Discord 原生投票")
async def native_poll(interaction: discord.Interaction, 질문: str, 항목: str, 시간: int = 24):
choices = [c.strip() for c in 항목.split(",") if c.strip()][:10]
if len(choices) < 2:
await interaction.response.send_message("请至少输入 2 个选项!", ephemeral=True)
return
poll = discord.Poll(question=질문, duration=datetime.timedelta(hours=시간))
for c in choices:
poll.add_answer(text=c)
await interaction.response.send_message(poll=poll)
@bot.event
async def on_poll_vote_add(user, answer):
print(f"[投票] {user} → {answer.text}")
bot.run(token)📌 运行结果
启动机器人,在服务器里这样试试。
/투표 질문:오늘 저녁 뭐 먹을까요? 항목:치킨,피자,족발 시간:2会弹出一个带有 3 个选项按钮和"截止投票"按钮的嵌入消息。只要有人按下按钮,柱状图就会立即更新。同一个人按另一个按钮时,选票会转移过去;再按同一个按钮,票数也不会增加。
发起投票的人按下"截止投票"后,所有按钮都会被禁用,标题也会变成 🔒 [마감](已截止)。就算没人按,过了 시간(投票时长,默认 1 小时)后,on_timeout 也会自动把它锁上。
原生投票可以用 /기본투표 来试试。Discord 应用里熟悉的那个投票 UI 会原样弹出!

📌 常见错误
1. AttributeError: module 'discord' has no attribute 'Poll'
- 原因:discord.py 版本低于 2.5。
Poll类是在 2.5 中新增的。 - 解决:执行
pip install -U "discord.py>=2.5"后,用python -c "import discord; print(discord.__version__)"确认一下。
2. AttributeError: 'Intents' object has no attribute 'polls'
- 原因:和上面一样是版本问题。
pollsintent 也是 2.5 才加入的。 - 解决:升级一下;如果只用按钮方式,把
intents.polls = True这一行删掉也可以。
3. 按了投票按钮却出现"此交互失败(상호작용이 실패했습니다)"
- 原因:因为机器人重启过了。
View对象只在内存中存活期间才有效。 - 解决:短时间的投票重新发起就行;如果想要机器人关掉也能保留的投票,就要指定
timeout=None+custom_id,做成持久化 View(persistent view),并把选票存进数据库(SQLite)。如果选票只放在内存字典里,一重启就全没了!
4. Missing Permissions — 原生投票发不出去
- 原因:机器人的身份组没有创建投票(Send Polls)权限。
- 解决:在服务器设置(서버 설정) → 身份组(역할) → 机器人的身份组里打开这个权限。顺便也检查一下频道级别的权限覆盖。
5. 斜杠命令在服务器里看不到
- 原因:全局同步(
bot.tree.sync())最多可能要 1 小时才生效。 - 解决:测试时用
await bot.tree.sync(guild=discord.Object(id=서버ID))只同步到指定服务器,就会立即生效。
📌 总结
今天我们用两种方式做了投票·问卷机器人 🙌
- 原生投票 — 5 行代码搞定。统计和截止都由 Discord 负责,最省事
- 按钮自定义投票 — 实时柱状图、防重复投票、提前截止,全都能随心控制
"想快速问一下大家" → 原生投票;"想把结果展示得好看、还要加条件" → 按钮。这样记就行了!
下一篇我们会讲怎么把投票结果存进数据库,让机器人重启后数据也不会丢。算是上面第 3 个错误的根本解决篇。敬请期待!😎
📚 Discord 机器人开发系列
- 系列总整理 - 从 #1 开始按顺序看的路线图 🗺️
- #28 - 打造等级·经验值(XP)系统 → https://mojangcam.tistory.com/113
- #29 - 打造货币·商店(Economy)系统(即将公开)
- #30 - 打造投票·问卷(Poll)机器人 ← 就是这篇!
- #31 - 把投票结果存进数据库(预告)
连载 · 共 32 篇中的第 30 篇
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임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉 (韩语)
