【问题标题】:discord py - how can i boost the speed of my code up?discord py - 我怎样才能提高我的代码速度?
【发布时间】:2021-10-14 08:03:48
【问题描述】:

我有问题。我想为我的赠品机器人制定一项任务,以检查赠品是否结束。所以我创建了一个任务来完成它,它运行了很多行代码并且一切正常。但我注意到我的代码很慢。谁能帮助我并说出我可以改进的地方以及如何加快速度?

我使用 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}")

【问题讨论】:

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


    【解决方案1】:

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

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

    对于您的程序,这意味着在等待来自数据库的结果时,您可以做一些有用的事情,例如收听新消息。或者发送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
    

    当然,理想情况下,您应该使用允许您以async 方式访问数据库的库,而不是像这样包装每个调用。事实上,有一个这样的 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 代码优化,因此它们可能更适合放在另一个问题中。

    【讨论】:

    • 谢谢!我将 cmets 添加到我的代码中,让您知道这些 MySQL 语句的作用。 :)
    • 所以我切换到aiomysql,但我的代码仍然很慢。
    • @NewPythonAgent 我添加了一个关于如何尝试优化 SQL 查询的部分。
    【解决方案2】:

    我看到你的循环中有很多采石场,这就是它如此缓慢的原因。你不能做一个大采石场吗?你的mysql数据库对python的数据进行排序会更快。

    也许有一些开始的提示:

    • 我看到您整理了非 nitro 帐户,但我在您的第一个采石场中没有看到它。
    • 我看到你选择了所有消息,也许只在赠送开始后才拿走消息?因此您的脚本不必遍历所有数据。
    • 几乎所有的 if 语句(关于数据排序)都可以写在采石场的

    我自己的数据库不是那么好,但如果我能看到你的数据库并且特别知道你想要从查询中得到什么(我怀疑可能的获胜者列表),也许我可以提供帮助。这个脚本现在需要多长时间?

    【讨论】:

    • 我不知道如何将所有这些(或许多)结合起来,因为我提出了不同的 WHERE 请求。我该如何组合它?
    猜你喜欢
    • 2021-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多