カテゴリー: Discordボット開発

Discordボット開発#30投票・アンケート(Poll)ボットを作ろう!ネイティブ投票+ボタンでリアルタイム集計まで(Pythonサンプル) 🗳️

この記事は韓国語の原文から翻訳したものです。

目次10項目

こんにちは!Mojangcamです 😎

前回は通貨・ショップ(Economy)システムを作って、コインを稼いでアイテムを買えるボットを完成させました。サーバーに「遊び要素」が1つ増えたわけですね!

今回は、サーバー運営で本当によく使われる機能、投票・アンケート(Poll)ボットを作ってみます。2つの方式を両方扱います。

  1. ネイティブ投票 — Discordが標準で用意している投票UIをボットが表示する方式(いちばん簡単!)
  2. ボタンのカスタム投票 — ボタンで自作して、リアルタイム集計・重複防止・投票締切まで自由に制御する方式

サーバーイベントの日程決め、次のコンテンツ決め、ルール改定の賛否確認……こういうのを毎回絵文字リアクションを付けて手で数えていたなら、今日の記事で卒業できます! 🙌

![](./img-01.png)

📌 準備するもの

  • 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 の1行を必ず追加してください!GitHubにトークンが上がると、Discordが自動でトークンを無効化してしまいます。(不幸中の幸いではありますが 😅)

🧩 インテント(Intents)の設定

ネイティブ投票の投票イベントを受け取るには polls インテントが必要です。ボタン方式だけを使うなら、なくても大丈夫です。

Python
intents = discord.Intents.default()
intents.message_content = True
intents.polls = True  # 投票イベント(on_poll_vote_addなど)を受け取るのに必要

📌 ステップ1 - ネイティブ投票を表示する(いちばん簡単な方法)

discord.py 2.5から discord.Poll クラスが追加されたので、Discordアプリの標準の投票UIをボットがそのまま表示できます。

Python
@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)

1つずつ見ていきましょう 💡

  • 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)権限が必要です。

投票が入ったり取り消されたりする瞬間をキャッチしたいなら、イベントを使えばOKです。

Python
@bot.event
async def on_poll_vote_add(user, answer):
    print(f"{user}さんが「{answer.text}」に投票しました")

💬 このイベントは、そのメッセージがボットのキャッシュにあるときだけ呼ばれます。古いメッセージまで拾いたいなら on_raw_poll_vote_add を使ってください。

ネイティブ投票は本当に便利ですが、限界もあります。選択肢は10個まで、ロールで制限できない、結果を自分の埋め込みできれいに描けない、投票中に集計を加工できない。そこでステップ2に進みます!


📌 ステップ2 - ボタンのカスタム投票(リアルタイム集計)

ボタン1つにつき選択肢を1つ作り、誰が何を押したかを辞書に保存する構造です。

Python
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))

1つずつ見ていきましょう 💡

  • self.votes : ここが核心です。キーがユーザーIDなので、同じ人がもう一度押すと値が上書きされます。つまり重複投票が構造的に防がれます。気が変わったら別のボタンを押すだけ!
  • timeout : この時間(秒)が過ぎると、ボタンが自動で止まります。3600 = 1時間です。
  • author_id : 投票を始めた人だけが「早期締切」を押せるようにするために保存しておきます。

ボタン本体はこんな感じです。

Python
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 - 結果を棒グラフで描く

数字だけだと味気ないので、ブロック文字で棒グラフを作ってみましょう。

Python
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票のときに空の行にならないよう、空きマスの文字を1つ入れます。
  • 総投票数は len(self.votes) と同じです。1人1票ですからね!

![](./img-02.png)


📌 全体コード(コピペOK)

Python
# 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が、そのまま表示されます!

![](./img-03.png)


📌 よくあるエラー

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'

  • 原因: 上と同じバージョンの問題です。polls インテントも2.5で追加されました。
  • 解決: アップグレードするか、ボタン方式だけを使うなら intents.polls = True の行を削除すればOKです。

3. 投票ボタンを押したら「インタラクションに失敗しました」と表示される

  • 原因: ボットを再起動したからです。View オブジェクトはメモリ上に存在している間だけ動作するんです。
  • 解決: 短い投票ならもう一度開けば済みますが、ボットが止まっても維持される投票が欲しいなら、timeout=None + custom_id を指定した永続View(persistent view)にして、票をDB(SQLite)に保存する必要があります。票をメモリ上の辞書にだけ置いていると、再起動ですべて消えてしまいます!

4. Missing Permissions — ネイティブ投票が投稿されない

  • 原因: ボットのロールに投票の作成(Send Polls)権限がありません。
  • 解決: サーバー設定 → ロール → ボットのロールで、その権限をオンにしてください。チャンネルごとの権限の上書きも一緒に確認しましょう。

5. スラッシュコマンドがサーバーに表示されない

  • 原因: グローバル同期(bot.tree.sync())は、反映まで最大1時間かかることがあります。
  • 解決: テストするときは await bot.tree.sync(guild=discord.Object(id=서버ID)) で特定のサーバーだけに同期すれば、すぐに反映されます。

📌 まとめ

今日は投票・アンケートボットを2つの方式で作ってみました 🙌

  • ネイティブ投票 — コード5行で完成。集計・締切をDiscordが全部やってくれるので、いちばん楽です
  • ボタンのカスタム投票 — リアルタイム棒グラフ、重複防止、早期締切まで自由に制御できます

「さっと聞きたい」→ ネイティブ、「結果をきれいに見せて、条件も付けたい」→ ボタン。こう覚えておけばOKです!

次回は、この投票結果をデータベースに保存して、ボットを再起動しても消えないようにする方法を扱います。上のエラー3番の根本的な解決編になりますね。お楽しみに! 😎

📚 Discordボット開発シリーズ

連載 · 全32回中30回目

Discordボット開発

  1. #1Discordボット開発 - スラッシュコマンド(Slash Command)完全攻略!
  2. #2スラッシュコマンドにオプション(Arguments)を追加しよう!
  3. #3スラッシュコマンドにサブコマンド(Subcommand)を追加しよう!
  4. #4スラッシュコマンドで埋め込み(Embed)を送信しよう!
  5. #5ボタン(Button)のインタラクションを実装しよう!
  6. #6ドロップダウン(Select Menu)メニューを実装しよう!
  7. #7モーダル(Modal)で入力を受け取る方法 (Pythonサンプル付き)
  8. #8埋め込み(Embed)を画像・フィールドで飾る (Pythonサンプル付き)
  9. #9AI(Claude・ChatGPT API)と連携する! (Pythonサンプル付き)
  10. #10多言語(i18n)に対応する! (Pythonサンプル付き)
  11. #11FlaskでWebダッシュボードを作る! (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実践ブログ