使用Telegram Bot返回Matplotlib图表时遇线程相关警告问题
解决Matplotlib图表通过Telegram Bot发送时的线程警告问题
问题说明
使用pandas_datareader获取股票数据、matplotlib生成图表,通过Telegram Bot的update.message.reply_photo方法发送图表时,触发以下警告:
用户警告:在主线程之外启动Matplotlib GUI很可能会失败。
核心代码如下:
# imports def generate_chart(update, context): stock = f'^GSPC' start = datetime.date(2000, 1, 1) end = datetime.date.today() data = web.DataReader(stock, 'yahoo', start, end) plot = data.plot(y='Open') img_buf = BytesIO() plt.savefig(img_buf, format='png') prepared_data = img_buf update.message.reply_photo(prepared_data) def main(): updater = Updater(keys.API_KEY, use_context=True) dp = updater.dispatcher dp.add_handler(CommandHandler("stocks", generate_chart)) dp.add_handler(MessageHandler(Filters.text, handle_message)) dp.add_error_handler(error) updater.start_polling() updater.idle() main()
问题原因
Telegram Bot的updater.start_polling()会启动多线程处理请求,而matplotlib默认的交互式后端(如TkAgg、QtAgg)要求在主线程运行,在Bot的处理线程中调用绘图逻辑时,就会触发线程不匹配的警告。
解决方案
方案1:指定非交互式后端
在导入matplotlib.pyplot前,设置使用无GUI的非交互式后端(如Agg),彻底避免GUI线程冲突:
import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt
注意:这行代码必须放在import matplotlib.pyplot as plt之前才会生效。
方案2:绘图后清理资源+修正缓冲区指针
每次绘图完成后关闭图表释放资源,同时将BytesIO缓冲区指针移到开头(否则Telegram读取的是空内容):
修改generate_chart函数:
def generate_chart(update, context): stock = f'^GSPC' start = datetime.date(2000, 1, 1) end = datetime.date.today() data = web.DataReader(stock, 'yahoo', start, end) # 创建独立绘图对象,避免依赖全局plt fig, ax = plt.subplots() data.plot(y='Open', ax=ax) img_buf = BytesIO() plt.savefig(img_buf, format='png') # 关闭图表释放资源 plt.close(fig) # 将缓冲区指针移到起始位置 img_buf.seek(0) update.message.reply_photo(img_buf)
方案3:主线程执行绘图(可选)
若必须使用交互式后端,可将绘图任务提交到主线程执行,但实现复杂,Bot场景下不推荐使用。
优化后完整代码示例
import datetime from io import BytesIO import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import pandas_datareader.data as web from telegram import Update from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackContext def generate_chart(update: Update, context: CallbackContext): stock = '^GSPC' start = datetime.date(2000, 1, 1) end = datetime.date.today() data = web.DataReader(stock, 'yahoo', start, end) fig, ax = plt.subplots() data.plot(y='Open', ax=ax) ax.set_title('标普500开盘价走势') ax.set_xlabel('日期') ax.set_ylabel('价格(美元)') img_buf = BytesIO() plt.savefig(img_buf, format='png', dpi=100, bbox_inches='tight') plt.close(fig) img_buf.seek(0) update.message.reply_photo(img_buf) def handle_message(update: Update, context: CallbackContext): update.message.reply_text("发送 /stocks 获取标普500走势图表!") def error(update: Update, context: CallbackContext): print(f'更新 {update} 触发错误 {context.error}') def main(): updater = Updater(keys.API_KEY, use_context=True) dp = updater.dispatcher dp.add_handler(CommandHandler("stocks", generate_chart)) dp.add_handler(MessageHandler(Filters.text, handle_message)) dp.add_error_handler(error) updater.start_polling() updater.idle() if __name__ == '__main__': main()
内容的提问来源于stack exchange,提问作者spal
相关产品推荐
相关产品推荐

