python - Discord py - 如何提高代码速度?

标签 python performance discord.py database-performance aio-mysql

我有一个问题。我想为我的赠品机器人创建一个任务来检查赠品是否结束。因此,我创建了一个任务来执行此操作,它运行许多行代码,并且一切正常。但我注意到我的代码非常慢。谁能帮助我并告诉我可以改进哪些地方以及如何加快速度?

我使用 aiomysql 连接到我的 mariadb 数据库,并使用 time.time() 检查代码速度。

抱歉,如果我做错了什么,我是这个网站的新人,如果您需要我提供任何帮助,请随时发表评论。 :)

我的 Discord-py 任务:

@tasks.loop(minutes=5.5)
    async def end_check(self):
        await self.client.wait_until_ready()
        start = time.time()

        mydb = await getConnection()
        mycursor = await mydb.cursor()
        current = datetime.now().timestamp()
        current = str(current).split(".")
        # get the main giveaway-data
        await mycursor.execute("SELECT guild_id, channel_id, message_id, gw_req FROM guild_giveaways WHERE end_date < %s", (current[0],))
        in_database = mycursor.fetchall()

        for entry in in_database:
            guild = self.client.get_guild(int(entry[0]))
            channel = guild.get_channel(int(entry[1]))
            message = await channel.fetch_message(int(entry[2]))

            emb = message.embeds[0].description.split("**")
            creator_id = emb[7].replace("<@", "").replace(">", "").replace("!", "")

            count = 0
            gwrole = None
            users = []
            async for user in message.reactions[0].users():

                if guild.get_member(int(user.id)) is None:
                    continue

                if user.bot:
                    continue

                # check if a user has the role/s from the database
                bypass_status = False
                await mycursor.execute("SELECT bypass_role_id FROM guild_role_settings WHERE guild_id = %s AND bypass_role_id IS NOT NULL", (guild.id,))
                role_exist = await mycursor.fetchone()
                if role_exist:
                    rolelist = role_exist[0].split(" ")
                    for role1 in rolelist:
                        role = guild.get_role(int(role1))
                        if role in user.roles:
                            bypass_status = True
                            break

                if "no_nitro" in entry[3].lower():
                    if user.avatar_url is not None and bypass_status is False:
                        if "gif" in str(user.avatar_url):
                            continue

                    if user.premium_since is not None and bypass_status is False:
                        continue

                elif "msg" in entry[3].lower():
                    msg = entry[3].replace("MSG: ", "")
                    # get the required message count to participate
                    await mycursor.execute("SELECT message_count FROM guild_message_count WHERE guild_id = %s AND user_id = %s", (guild.id, user.id))
                    data = await mycursor.fetchone()
                    if data:
                        if int(data[0]) < int(msg) and bypass_status is False:
                            continue
                    else:
                        if bypass_status is False:
                            continue

                elif "voicetime" in entry[3].lower():
                    seconds = entry[3].replace("VOICETIME: ", "")
                    # get the right voice_time to participate
                    await mycursor.execute("SELECT voice_time FROM guild_voice_time WHERE guild_id = %s AND user_id = %s", (guild.id, user.id))
                    data = await mycursor.fetchone()
                    if data:
                        if int(data[0]) < int(seconds) and bypass_status is False:
                            continue
                    else:
                        if bypass_status is False:
                            continue

                elif "role_id" in entry[3].lower():
                    roleid = entry[3].replace("ROLE_ID: ", "")
                    role = guild.get_role(int(roleid))
                    if role not in user.roles and bypass_status is False:
                        continue

                elif "mitglied" in entry[3].lower():
                    reqtime = entry[3].replace("MITGLIED:", "")
                    if time.time() - user.joined_at.timestamp() < int(reqtime) and bypass_status is False:
                        continue

                if int(user.id) == int(creator_id):
                    continue

                await mycursor.execute("SELECT ignore_role_id FROM guild_role_settings WHERE guild_id = %s", (guild.id,))
                find_data = await mycursor.fetchone()
                if find_data:
                    if find_data[0] is not None and len(find_data[0]) >= 3:
                        rolelist = find_data[0].split(" ")
                        for role1 in rolelist:
                            role = guild.get_role(int(role1))
                            if role in user.roles:
                                continue
                users.append(user)
                count += 1

            if int(count) < int(emb[5]):
                winners = random.sample(users, k=int(count))
                if count <= 0:
                    await mycursor.close()
                    mydb.close()
                    return await message.reply(f"`📛` › **Zu wenig Teilnehmer:** Ich konnte nur `{count}` Gewinner ziehen, {emb[7]}! <:AmongUs:774306215848181760>")
                zuwenig = True

            else:
                zuwenig = False
                winners = random.sample(users, k=int(emb[5]))

            status = True
            # check if server bot private messages are enabled
            await mycursor.execute("SELECT dm_status FROM guild_misc_settings WHERE guild_id = %s", (entry[0],))
            myresult = await mycursor.fetchone()
            if myresult:
                if myresult[0] == "False":
                    status = False

            role_status = False
            # check if the winner should receive a role
            await mycursor.execute("SELECT win_role_id FROM guild_role_settings WHERE guild_id = %s", (entry[0],))
            myresult = await mycursor.fetchone()
            if myresult:
                if myresult[0] is not None:
                    gwrole = guild.get_role(int(myresult[0]))
                    if gwrole is not None:
                        role_status = True

            for winner in winners:
                if status is True:
                    try:
                        done = discord.Embed(title="<a:COOL:805075050368598036> › **GEWINNSPIEL GEWONNEN!** <a:COOL:805075050368598036>",
                                             description="`🤖` › Lade den Bot **[hier](https://bl4cklist.de/invites/gift-bot)** ein.\n\n"
                                                         f"<a:gift:843914342835421185> › Du hast bei dem Gewinnspiel auf **[{guild.name}]({message.jump_url})** gewonnen!\n"
                                                         f"<a:love:855117868256198767> › Ein Teammitglied wird sich **demnächst** bei dir melden.",
                                             color=0x778beb)
                        done.set_image(url="https://i.imgur.com/fBsIE3R.png")
                        await winner.send(content="Du hast bei einem Gewinnspiel **GEWONNEN!!** <a:blobbeers:862780904112128051>", embed=done)
                    except discord.Forbidden:
                        pass

                if role_status is True:
                    try:
                        await winner.add_roles(gwrole, reason="Gewinnspiel gewonnen!")
                    except discord.Forbidden:
                        pass

            database_winners = " ".join([str(winner.id) for winner in winners])
            winners = ", ".join([winner.mention for winner in winners])

            if winners.count(",") >= 1:
                winnersdesc = f"{winners} haben **{message.embeds[0].title}** gewonnen! <a:love:855117868256198767>"
            else:
                winnersdesc = f"{winners} hat **{message.embeds[0].title}** gewonnen! <a:love:855117868256198767>"

            embed = discord.Embed(title=message.embeds[0].title,
                                  description="`🤖` › Lade den Bot **[hier](https://bl4cklist.de/invites/gift-bot)** ein.\n\n"
                                              "<a:trophy:867917461377404949> **__Gewinnspiel - Gewinner__**\n"
                                              f"<:arrow2:868989719319564359> Gewinner: {winners}\n"
                                              f"<:arrow2:868989719319564359> Erstellt von {emb[7]}\n⠀⠀",
                                  color=0xff4d4d)
            embed.set_footer(text=f"{self.client.user.name} - Bot", icon_url=str(self.client.user.avatar_url))
            embed.timestamp = datetime.now()
            embed.set_thumbnail(url=message.embeds[0].thumbnail.url)

            if zuwenig is False:
                await message.edit(content=":name_badge: **GEWINNSPIEL VORBEI!** :name_badge:", embed=embed)
            else:
                await message.edit(content=f"`📛` › **Zu wenig Teilnehmer:** Ich konnte nur `{count}` Gewinner ziehen! <:whut:848347703217487912>", embed=embed)

            await mycursor.execute("INSERT INTO guild_finished_giveaways (guild_id, channel_id, message_id, winner_id) VALUES (%s, %s, %s, %s)", (entry[0], entry[1], entry[2], database_winners))
            await mycursor.execute("SELECT COUNT(*) FROM guild_finished_giveaways WHERE guild_id = %s", (entry[0],))
            gcount = await mycursor.fetchone()
            count2 = '{:,}'.format(int(gcount[0])).replace(",", ".")
            count1 = '{:,}'.format(int(count)).replace(',', '.')

            await message.reply(content=f"<a:blobbeers:862780904112128051> **Herzlichen Glückwunsch**, {winnersdesc}\n"
                                        f"› Es gab `{count1}` **gültige** Teilnehmer. Dieses Gewinnspiel war das `{count2}`. auf dem Server. <a:PETTHEPEEPO:772189322392371201>")

            if status is True:
                try:
                    done = discord.Embed(
                        title="<a:Info:810178313733013504> › **GEWINNSPIEL VORBEI!**",
                        description="`🤖` › Lade den Bot **[hier](https://bl4cklist.de/invites/gift-bot)** ein.\n\n"
                                    f"`✅` › Das Gewinnspiel auf **[{guild.name}]({message.jump_url})** ist vorbei!\n"
                                    f"`📌` › Da du das Event gestartet hast, habe ich **dich informiert.**\n\n"
                                    f"`💸` › **Zahle das Gewinnspiel** selbst aus, oder kümmere dich\n"
                                    f"`💸` › darum, dass es die zuständige Person erledigt.",
                        color=0xffa502)
                    done.set_image(url="https://i.imgur.com/fBsIE3R.png")

                    creator = guild.get_member(int(creator_id))
                    await creator.send(content="Ein Gewinnspiel ist **VORBEI!**", embed=done)
                except discord.Forbidden:
                    pass

            await mycursor.execute("DELETE FROM guild_giveaways WHERE guild_id = %s AND channel_id = %s AND message_id = %s", (entry[0], entry[1], entry[2]))

            await mydb.commit()
            mydb.close()
            await mycursor.close()

        print(f"EndTask: {time.time() - start}")

最佳答案

您最重要的问题是您在异步代码中使用了阻塞方法 - cursor.execute()。这是您不应该做的事情,因为这样您的程序在等待查询结果时就无法执行任何其他操作。

async代码中,其思想是每个长操作都被抽象为 awaitable 。该函数将一直执行,直到遇到 await 关键字。当发生这种情况时,该函数的执行将被挂起,直到可等待的结果可用为止 - 但至关重要的是,它释放了“事件循环”以同时处理其他事情。

对于您的程序,这意味着在等待数据库结果时,您可以做一些有用的事情,例如监听新消息。或发送heartbeat packets ,因为如果您不这样做,Discord 就会断开您的机器人连接,您必须等到重新连接为止。

为了避免这种阻塞,您需要使用某种方法将阻塞调用转变为异步调用。其中一些是 asyncio.to_threadasyncio.loop.run_in_executor ,后者被认为是低级别的。这是前者的示例:

import asyncio

async def slow_insert(value):
    """ Bad example, do not do this! """
    # these methods are fast, so they are not an issue
    mydb = getConnection()
    mycursor = mydb.cursor()

    # but this hangs the event loop, and prevents the program from doing anything else
    result = mycursor.execute("INSERT INTO data VALUES (%s)", value)
    return

async def async_insert(value):
    """ Do this instead """
    # these methods are fast, so they can be done directly
    mydb = getConnection()
    mycursor = mydb.cursor()

    # then we create a function that does what we need
    # note that this does not execute the blocking function, it just wraps it into another function
    blocking_insert = lambda: mycursor.execute("INSERT INTO data VALUES (%s)", value)

    # we schedule this function to run on a thread, and only come back here once that thread has completed
    result = await asyncio.to_thread(blocking_insert)
    return result

当然,理想情况下,您可以使用一个允许您以异步方式访问数据库的库,而不是像这样包装每个调用。事实上,MySQL/MariaDB 有一个名为 aiomysql 的库。 .

这些都不会让你的程序本身运行得更快,但它会使得任何发生的缓慢都不会导致 Discord 断开连接,并且不会阻止你的机器人响应消息。为了真正提高速度,您必须优化查询。


一般来说,更少、更复杂的查询比更多、更简单的查询要好——数据库服务器足够智能,能够优化更复杂的查询。在您的代码中,有很多像这样的简单查询:

SELECT ignore_role_id FROM guild_role_settings WHERE guild_id = %s
SELECT bypass_role_id FROM guild_role_settings WHERE guild_id = %s AND bypass_role_id IS NOT NULL
SELECT dm_status FROM guild_misc_settings WHERE guild_id = %s
SELECT win_role_id FROM guild_role_settings WHERE guild_id = %s

请注意所有这些查询如何仅依赖于 group_id。这意味着它们甚至根本不必位于循环内部,并且可以将它们移到循环外部。也许更重要的是,您可以将它们全部连接到一个查询中:

-- SETUP
CREATE TABLE IF NOT EXISTS guild_role_settings (guild_id INTEGER, ignore_role_id INTEGER, bypass_role_id INTEGER, win_role_id INTEGER);
CREATE TABLE IF NOT EXISTS guild_misc_settings (guild_id INTEGER, dm_status BOOLEAN);
CREATE TABLE IF NOT EXISTS some_new_table (guild_id INTEGER, some_new_property INTEGER);
INSERT INTO guild_role_settings VALUES (1, 2, 3, 4);
INSERT INTO guild_role_settings VALUES (2, 0, 0, 0);

INSERT INTO guild_misc_settings VALUES (1, 1);
INSERT INTO guild_misc_settings VALUES (2, 0);
INSERT INTO some_new_table VALUES (1, 1337);
INSERT INTO some_new_table VALUES (2, 0);
-- END SETUP

SELECT
  guild_role_settings.ignore_role_id,
  guild_role_settings.bypass_role_id,
  guild_role_settings.win_role_id,
  guild_misc_settings.dm_status,
  some_new_table.some_new_property
FROM
  guild_misc_settings NATURAL JOIN guild_role_settings NATURAL JOIN some_new_table
WHERE guild_id=1;

Try it online!另外,如果您不清楚 NATURAL JOIN 的作用,请查看 the Wikipedia page with examples 。简而言之,这允许您将不同的表视为单个表并进行查询,从而跨不同的表执行查询。

对于包含成员 ID 的查询,您还应该将两个查询合并为一个,但最好完全重构这部分代码,首先获取所有成员的列表,然后对所有这些一起执行查询,然后使用该查询的结果执行操作:

# instead of this:
for entry in in_database:
  guild = self.client.get_guild(int(entry[0]))
  channel = guild.get_channel(int(entry[1]))
  message = await channel.fetch_message(int(entry[2]))
  async for user in message.reactions[0].users():
    await mycursor.execute("SELECT guild_message_count.message_count, guild_voice_time.voice_time FROM guild_message_count NATURAL JOIN guild_message_count WHERE guild_id = %s AND user_id = %s", (guild.id, user.id))
    msg_count, voice_time = await mycursor.fetchone()
    # do something...

# do this instead:
for entry in in_database:
  guild = self.client.get_guild(int(entry[0]))
  channel = guild.get_channel(int(entry[1]))
  message = await channel.fetch_message(int(entry[2]))

  user_objects = dict()
  user_ids = []
  async for user in message.reactions[0].users():
    user_ids.append(user.id)
    user_objects[user.id] = user

  # now we have the list of user IDs, as well as their corresponding objects

  # we will now form our query of the form
  # SELECT ... WHERE user_id IN (%s, %s, %s, %s)
  # to query all the user IDs at once
  # the idea comes from https://stackoverflow.com/a/283801/5936187
  
  # note that while manipulating SQL strings is usually dangerous
  # because of SQL injections, here we are only using the length
  # of the list as a parameter, so it is okay.

  query = "SELECT user_id, guild_message_count.message_count, guild_voice_time.voice_time FROM guild_message_count NATURAL JOIN guild_message_count WHERE guild_id = %s AND user_id IN "
  parameters = ["%s" for _ in user_ids]
  parameter_string = "(" + ( ", ".join(parameters) ) + ")"
  query += parameter_string

  await mycursor.execute(query, [guild.id] + user_ids)
  # now the cursor has the resultset of (user_id, msg_count, voice_time)
  async for user_id, msg_count, voice_time in mycursor:
    # do something...

这样,您就可以在函数开始时执行大型查询,而不是在每次迭代时执行小型查询。

除了这些之外,您可能还可以进行其他优化,但到目前为止,它们的具体用途还不是很明显。即便如此,它们将是 SQL 优化而不是 Python 代码优化,因此它们可能更适合放入另一个问题中。

关于python - Discord py - 如何提高代码速度?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68732200/

相关文章:

c# - WCF cometd 和线程

python - 不和谐.py |从 url 播放音频

python - 用于在 mac os X 上使用 Python 以编程方式访问邮件的 API

python - 如何在 python (anaconda) 中使用旧版本的 GLIBC?

python - 狮身人面像/autodoc : how to cross-link to a function documented on another page

C++ Performance 从磁盘写入和读取

performance - 强制预计算一个常数

python - 每当我输入某些命令时,我的机器人就会重复自己(discord.py,Python v3.7)

python - 使用discord.py获取 channel 的名称

python - 该代码片段在 tensorflow 代码中表示 "tf.logging.set_verbosity(tf.logging.INFO)"是什么意思?