从Excel实例池转为动态创建实例时,xlwings打开工作簿失败的问题求助
大家好,我目前在Windows服务器上运行一个Flask API,用来对Excel文件进行增删改操作以及运行宏,交互部分用的是xlwings。
之前的方案是预先创建一个Excel实例池,每个请求携带user_id,我用它来给用户分配对应的实例;如果没有可用实例的话,就用一个备份实例兜底,这套方案运行得还不错。
现在我想改成动态创建实例——用户需要的时候再创建。因为Excel不支持多线程和同一个实例交互,所以我没法在请求线程里直接创建实例。我本来想沿用之前重启实例的思路:在Redis里写入一个信号,让主线程创建新实例,然后把实例的PID存到Redis里,再让请求线程取出来用。但这么做之后,打开工作簿时直接报错了,错误信息如下:
info:app found f<App [excel] 9232>
ERROR:custom_logger:Error opening workbook: (-2147352567, 'Exception occurred.', (0, 'Microsoft Excel', 'La méthode Open de la classe Workbooks a échoué.', 'xlmain11.chm', 0, -2146827284), None)
下面是我之前用实例池时的核心代码,以及尝试动态创建时沿用的重启/创建逻辑:
初始化与实例池创建代码
import os import sys import logging import pythoncom import xlwings as xw from flask import Flask from redis import Redis SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.dirname(SCRIPT_DIR)) # Initialize Flask app app = Flask(__name__) app.logger.setLevel(logging.DEBUG) BUCKET_NAME = os.getenv('AWS_BUCKET') # Initialize COM once during app startup pythoncom.CoInitialize() redis_instance = Redis(host='your-redis-host', port=6379, db=0) def create_excel_instances(number_of_instances): xl_apps = [] number_of_instances = int(number_of_instances) for i in range(1, number_of_instances + 1): xl_app = xw.App(visible=False) xl_app.screen_updating = False xl_app.display_alerts = False xl_app.enable_events = False xl_app.ask_to_update_links = False xl_app.ignore_remote_requests = True redis_instance.set(f'XL_APP{i}_PID', xl_app.pid) print(f'xl_app{i} pid: {xl_app.pid}') xl_apps.append(xl_app) # 创建备份实例 backup_app = xw.App(visible=False) backup_app.screen_updating = False backup_app.display_alerts = False backup_app.enable_events = False backup_app.ask_to_update_links = False backup_app.ignore_remote_requests = True redis_instance.set('XL_APP_BACK_UP_PID', backup_app.pid) print(f'backup app pid: {backup_app.pid}') xl_apps.append(backup_app) return xl_apps # 创建初始实例池 xl_apps = create_excel_instances(os.getenv('NUMBER_OF_EXCEL_INSTANCES')) setup_routes(app) # 调度器相关(略) from apscheduler.schedulers.background import BackgroundScheduler scheduler = BackgroundScheduler() if scheduler.running: scheduler.shutdown(wait=False) scheduler.start() def create_app(): return app
实例重启/动态创建逻辑
import time import psutil import threading def restart_excel_instance(instance_key): try: custom_logger.info(f"Starting restart of instance {instance_key}") old_pid = int(redis_instance.get(instance_key)) custom_logger.info(f"Old PID: {old_pid}") # 尝试通过xlwings关闭旧实例 try: if old_pid in xw.apps: custom_logger.info(f"Closing via xlwings Excel instance (PID: {old_pid})") for book in xw.apps[old_pid].books: try: book.close() except Exception as e: custom_logger.error(f"Error closing workbook: {e}") try: xw.apps[old_pid].quit() except Exception as e: custom_logger.error(f"Error closing via xlwings: {e}") except Exception as e: custom_logger.error(f"Error during xlwings closure: {e}") time.sleep(2) # 强制终止进程(如果还在运行) try: process = psutil.Process(old_pid) if process.is_running(): custom_logger.warning(f"Excel instance still running, attempting forced termination") process.terminate() process.wait(timeout=5) custom_logger.info("Excel instance successfully terminated") except psutil.NoSuchProcess: custom_logger.info(f"Process {old_pid} no longer exists") except Exception as e: custom_logger.error(f"Error during forced process termination: {e}") # 创建新实例 custom_logger.info("Creating new Excel instance") xl_app = xw.App(visible=False) xl_app.screen_updating = False xl_app.display_alerts = False new_pid = xl_app.pid redis_instance.set(instance_key, new_pid) custom_logger.info(f"New instance successfully created (PID: {new_pid})") return True except Exception as e: custom_logger.error(f"Error during instance restart {instance_key}: {e}") return False # 监听Redis的重启请求 def check_for_restart_requests(): while True: instance_keys = [f'XL_APP{i}_PID' for i in range(1, int(os.getenv('NUMBER_OF_EXCEL_INSTANCES')) + 1)] + ['XL_APP_BACK_UP_PID'] for instance_key in instance_keys: restart_flag = redis_instance.get(f'{instance_key}_needs_restart') if restart_flag and restart_flag.decode('utf-8') == '1': print(f"Restart required for instance {instance_key}") restart_excel_instance(instance_key) redis_instance.delete(f'{instance_key}_needs_restart') time.sleep(5) # 启动监听线程 restart_checker = threading.Thread(target=check_for_restart_requests) restart_checker.daemon = True restart_checker.start()
我猜测问题可能出在COM线程模型上?因为预先创建实例时是在Flask启动的主线程里做的,而动态创建是在后台线程里完成的,xlwings和Excel的COM交互是不是对线程有严格要求?
有没有朋友遇到过类似的问题?或者能给我一些排查方向?感谢大家!
内容来源于stack exchange

