python - Discord.gateway 警告“Shard ID 无心跳阻塞超过 10 秒。” 在使用熊猫时
问题描述
所以我在 python 中使用 discord.py 制作了一个不和谐机器人,并且已经运行了一段时间。但是,最近该机器人开始随机死亡。所以我将日志库添加到我的程序中,试图找出发生了什么,今天早上我得到了这个日志:
此错误回溯一直引用多个 pandas 文件。我的不和谐机器人代码:
# Import libraries
import asyncio
import random
import AO3
import pandas as pd
from discord.ext import commands
import logging
# Function to setup the dataframe
def dataframeSetup():
# Create the dataframe
df = pd.read_csv(
"https://docs.google.com/spreadsheets/d/16QtBJEtvV5a5DheR78x5AsoVA5b2DpXD1mq-x3lCFiA/export?format=csv",
names=["NaN", "Title", "Author", "Ship(s)", "Type", "Series", "Status", "Smut", "No of words", "No of chapters",
"Link"])
# Remove first two lines
df = df.iloc[2:]
# Remove the first column
df.drop("NaN", axis=1, inplace=True)
# Create variable to store the index of the first empty row
firstEmptyRow = 0
# Iterate over every row
for index, row in df.iterrows():
# Test if every cell is empty
if row.isnull().all():
# Set firstEmptyRow to the index (it is minus 2 because the index of the dataframe starts at 2)
firstEmptyRow = index - 2
break
# Return the final dataframe
return df.iloc[0:firstEmptyRow]
# Function to make random quotes
def quoteMaker(df):
# Grab a random fic
randomFic = df.iloc[random.randint(2, len(df))]
# Create AO3 session
ao3Session = AO3.Session("username", "password")
# Create work object
work = AO3.Work(AO3.utils.workid_from_url(randomFic["Link"]), ao3Session)
# Get chapter amount
chapterAmount = work.chapters
# Get chapter text for a random chapter
randomChapter = random.randint(1, chapterAmount)
randomChapterText = work.get_chapter_text(randomChapter)
# Convert the chapter text into a list
textList = list(filter(None, randomChapterText.split("\n")))
# Return random string
return textList[random.randint(0, len(textList) - 1)], work, randomChapter, ao3Session
# Function to create trivia
def triviaMaker(triviaDone):
# Test if all trivia questions have been done
if len(triviaDone) == len(df1):
# They've all been done, so clear the list and start again
triviaDone.clear()
# Generate a random index and use that to get a random trivia question
randomIndex = random.randint(0, len(df1)) - 1
randomTrivia = df1.iloc[randomIndex]
# Test if the selected trivia question has been done before
while randomIndex in triviaDone:
# Trivia has already been done recently so try another one
randomTrivia = df.iloc[random.randint(0, len(df1))]
# Add the selected trivia question's index to the list
triviaDone.append(randomIndex)
# Return the formatted string as well as the correct index to allow for validation
return f'''{randomTrivia["Question"]}:
1. {randomTrivia["Option 1"]}
2. {randomTrivia["Option 2"]}
3. {randomTrivia["Option 3"]}
4. {randomTrivia["Option 4"]}''', randomTrivia, randomTrivia["Correct Option"]
def record(work):
# Create initial array to store results
ficResults = []
# Open file and write existing results to ficResults
with open("QuoteResults.txt", "r") as file:
for line in file.readlines():
ficResults.append(line)
# Test if fic already exists in the results
found = False
for count, fic in enumerate(ficResults):
if str(work.workid) in fic:
# Fic already exists
found = True
break
# Assign the new result
if found == True:
# Increment the result
ficResults[count] = f"22561831, {int(ficResults[count][-2:]) + 1}\n"
else:
# Create new result
ficResults.append(f"{work.workid}, 1\n")
# Write to file
with open("QuoteResults.txt", "w") as file:
for result in ficResults:
file.write(result)
def authorGrab(work, session):
# Function to grab only the authors
return session.request(work.url).findAll("h3", {"class": "byline heading"})[0].text.replace("\n", "")
# Initialise discord variables
token = "discord token"
client = commands.Bot(command_prefix="!", case_insensitive=True)
# Initialise the dataframe
df = dataframeSetup()
# Initialise trivia variables
df1 = pd.read_csv("Trivia.txt", delimiter="/",
names=["Question", "Option 1", "Option 2", "Option 3", "Option 4", "Correct Option"])
# Initialise asked trivia questions list
triviaDone = []
# Initialise channel ID variables using a file
with open("IDs.txt", "r") as file:
channelIDs = file.read().splitlines()
# Initialise logging
logger = logging.getLogger("discord")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename="quoteBot.log", encoding="utf-8", mode="a")
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
logger.addHandler(handler)
# Register !quote command
@client.command()
@commands.cooldown(1, 10, commands.BucketType.default)
async def quote(ctx):
if ctx.channel.id == int(channelIDs[0][10:]):
quote = ""
# Test whether the quote is longer than 10 words
while len(quote.split()) < 10:
# Grab quote and related attributes
quote, work, randomChapter, session = quoteMaker(df)
# Grab authors
authors = authorGrab(work, session)
# Print quote and attributes
await ctx.channel.send(quote)
await ctx.channel.send(f"-{work.title} chapter {randomChapter} by {authors}. Link {work.url}")
record(work)
# Register !trivia command
# This command can only be used once every 60 seconds server-wide
@client.command()
@commands.cooldown(1, 60, commands.BucketType.default)
async def trivia(ctx):
shortenedIDString = channelIDs[1][11:]
for id in shortenedIDString.split(", "):
if ctx.channel.id == int(id):
# Display trivia question
triviaString, randomTrivia, correctIndex = triviaMaker(triviaDone)
await ctx.channel.send(triviaString)
# Function to check if an answer is correct
def check(message):
# Check if answer is correct
if "!answer" in message.content:
return message.content == f"!answer {randomTrivia.iloc[int(correctIndex)]}" or message.content == f"!answer {int(correctIndex)}"
# Try and except statement to catch timeout error
try:
# Wait for user response
await client.wait_for("message", check=check, timeout=15)
# User response is correct
await ctx.channel.send("Correct answer")
except asyncio.TimeoutError:
# Time has run out
await ctx.channel.send("Times up, better luck next time")
# Register empty !answer command
# This is only needed to stop an error being returned
@client.command()
async def answer(ctx):
return None
# Register !cruzie command
@client.command()
@commands.cooldown(1, 5, commands.BucketType.default)
async def cruzie(ctx):
# User has types !cruzie so do secret
await ctx.channel.send("https://giphy.com/gifs/midland-l4FsJgbbeKQC8MGBy")
# Register !murica command
@client.command()
@commands.cooldown(1, 5, commands.BucketType.default)
async def murica(ctx):
# User has typed !murica so play murica gif
await ctx.channel.send("https://tenor.com/view/merica-gif-9091003")
# Register !gamer command
@client.command()
@commands.cooldown(1, 5, commands.BucketType.default)
async def gamer(ctx):
# User has typed !gamer so play gamers gif
await ctx.channel.send("https://tenor.com/view/hello-gamers-hello-hi-howdy-whats-up-gif-12988393")
# Register !stinky command
@client.command()
@commands.cooldown(1, 5, commands.BucketType.default)
async def stinky(ctx):
# User has typed !stinky so play srinky gif
await ctx.channel.send("https://tenor.com/view/monke-uh-oh-stinky-uh-oh-stinky-monke-gif-18263597")
# Run when discord bot has started
@client.event
async def on_ready():
# Get channel ID for test channel
channel = client.get_channel("debug channel")
# Send message to user signalling that the bot is ready
await channel.send("Running")
# Catch discord errors
@client.event
async def on_command_error(ctx, error):
if isinstance(error, commands.CommandOnCooldown):
# CommandOnCooldown error detected
await ctx.channel.send(f"Command is on cooldown, try again in {round(error.retry_after, 2)} seconds")
# Start discord bot
client.run(token)
如果有人能弄清楚为什么会发生这个错误,那将不胜感激。
解决方案
该警告本质上意味着您的代码阻塞超过 x 秒,它阻塞心跳并触发该警告(您可以使用 重现此警告time.sleep(x)
)。要修复它,您必须以非阻塞方式运行阻塞函数(熊猫函数):
import time # To reproduce the error
import typing # For typehinting
import functools
def blocking_func(a, b, c=1):
"""A very blocking function"""
time.sleep(a + b + c)
return "some stuff"
async def run_blocking(blocking_func: typing.Callable, *args, **kwargs) -> typing.Any:
"""Runs a blocking function in a non-blocking way"""
func = functools.partial(blocking_func, *args, **kwargs) # `run_in_executor` doesn't support kwargs, `functools.partial` does
return await client.loop.run_in_executor(None, func)
@client.command()
async def test(ctx):
r = await run_blocking(blocking_func, 1, 2, c=3) # Pass the args and kwargs here
print(r) # -> "some stuff"
await ctx.send(r)
您应该以这种方式运行所有阻塞功能
另一种(更简单)的方法是简单地创建一个装饰器
import functools
import typing
import asyncio
def to_thread(func: typing.Callable) -> typing.Coroutine:
@functools.wraps(func)
async def wrapper(*args, **kwargs):
return await asyncio.to_thread(func, *args, **kwargs)
return wrapper
@to_thread
def blocking_func(a, b, c=1):
time.sleep(a + b + c)
return "some stuff"
await blocking_func(1, 2, 3)
如果您使用的是 python <3.9,您应该使用loop.run_in_executor
而不是asyncio.to_thread
def to_thread(func: typing.Callable) -> typing.Coroutine:
@functools.wraps(func)
async def wrapper(*args, **kwargs):
loop = asyncio.get_event_loop()
wrapped = functools.partial(func, *args, **kwargs)
return await loop.run_in_executor(None, func)
return wrapper
@to_thread
def blocking_func(a, b, c=1):
time.sleep(a + b + c)
return "some stuff"
await blocking_func(1, 2, 3)
推荐阅读
- rust - 从 Rust 调用 Lua 函数时出错:`*mut rlua::ffi::lua_State` 不能在线程之间安全共享
- imagemagick - 使用 ImageMagick Convert 将大小调整为英寸?
- php - 根据当前语言更改徽标
- html - 选择元素无法与 CSS 网格一起正常工作
- php - 如何在我的帖子中使用来自 wordpress 的 Rest API?
- android - 系统用户访问令牌令牌无效凭证
- javascript - 根据不同的条件创建分隔字符串
- python - Apache 错误日志——如何修复“numpy ImportError”或禁用它?
- c - 将可变参数列表提供给C中的被调用函数
- javascript - 将 Webpack 与“webpack serve”一起使用时未生成源映射