WPS COM 自动化对接手册
用 COM 自动化驱动 WPS Office 时,有四个反复踩到的坑。本手册给出判定方法与修法。
坑 1:现代 WPS 不注册 WPS.Application
症状:win32com.client.Dispatch("WPS.Application") 报
(-2147221005, '无效的类字符串') 或 (-2147221008, '尚未调用 CoInitialize。')。
事实:WPS Office 12.x 的三件套是三个彼此独立的 COM 类,没有任何单一 ProgID 同时提供 Documents + Workbooks + Presentations:
| ProgID | Documents | Workbooks | Presentations | 对应程序 |
|---|---|---|---|---|
Kwps.Application |
✅ | ❌ | ❌ | wps.exe(文字) |
Ket.Application |
❌ | ✅ | ❌ | et.exe(表格) |
Kwpp.Application |
❌ | ❌ | ✅ | wpp.exe(演示) |
核验命令(先跑这个,别猜):
import win32com.client as w
for pid in ["WPS.Application","Kwps.Application","Ket.Application","Kwpp.Application"]:
try:
app = w.Dispatch(pid)
print(pid, "OK", app.Path, app.Name, getattr(app,"Build","?"))
app.Quit()
except Exception as e:
print(pid, "FAIL", str(e)[:80])
坑 2:WPS 抢占 MS Office 的 ProgID,导致误判
症状:脚本报告"检测到 Microsoft Office",实际装的是 WPS;或 Word.Application 能用但 Name 返回 Microsoft Word、Version 返回 12.0。
原因:WPS 的"兼容第三方系统和软件"会把 Word/Excel/PowerPoint.Application 全部接管指向自己。
判据:不能只看 Dispatch 是否成功,必须核对可执行文件路径:
def app_is_wps(app) -> bool:
try:
p = str(app.Path).lower()
except Exception:
return False
return ("kingsoft" in p) or ("wps" in p)
WPS 的真实路径形如
C:\Users\<用户>\AppData\Local\Kingsoft\WPS Office\<版本>\office6。
据此判定才能把 MS Office 与 WPS 兼容层区分开。
坑 3:默认保存到桌面 → 工作簿未保存 → Quit() 永久阻塞
症状:wb.SaveAs(<桌面路径>) 看似返回,桌面却查不到文件;随后 app.Quit() 卡住,
把整个命令拖到 120 秒超时甚至永不返回。
原因:桌面若被重定向(junction / 符号链接,例如
C:\Users\X\Desktop → E:\Users_X\Desktop),WPS SaveAs 静默失败,
工作簿保持"已修改未保存",退出时弹出保存确认框;而应用是 Visible=False,
对话框没人能点 → 无限等待。
先检查桌面是不是链接:
ls -la ~/Desktop # 出现 "->" 即为 junction
修法(两条都要):
- 永远显式传输出路径,不要依赖"默认桌面"。
- 给 Quit 加限时,防止任何模态框挂死调用方:
import threading
def bounded_quit(app, timeout=8.0) -> bool:
"""限时退出,超时则放弃(残留进程事后清理),保证调用方不被挂死。"""
done = threading.Event()
def _run():
try:
import pythoncom; pythoncom.CoInitialize()
except Exception:
pythoncom = None
try:
app.Quit()
except Exception:
pass
finally:
if pythoncom:
try: pythoncom.CoUninitialize()
except Exception: pass
done.set()
t = threading.Thread(target=_run, daemon=True)
t.start()
return done.wait(timeout)
> 注意:不要在 Quit 前关闭文档、也不要设 DisplayAlerts=False 去"自动答框"——
> 若 COM 附着到的是用户正在使用的 WPS 实例,那会静默丢弃用户的未保存修改。
坑 4:残留进程让 COM 附着到坏实例
症状:app.Workbooks.Count 恒为 1、Workbooks.Add() 返回 None、ActiveWorkbook.Name
报 <unknown>.Name;或前一次超时后,后续调用全部异常。
原因:上一次操作被超时杀掉,留下一个持有孤儿文档的 et.exe/wpp.exe;
再次 Dispatch 会附着到这个坏实例,而不是新建。
处理:结束残留进程后重试(只杀自动化残留,不要动用户正在用的窗口):
# 列出
tasklist | grep -Ei "et\.exe|wpp\.exe|wps\.exe"
# 按 PID 精确结束(先确认该 PID 不是用户自己开的)
taskkill /PID <pid> /F
判断依据:在开始自动化之前先记一次进程清单,之后新出现的才是你制造的。
定位卡死位置:faulthandler
不要靠猜。用 faulthandler 直接把堆栈打出来:
import faulthandler, runpy
faulthandler.dump_traceback_later(25, exit=True) # 25 秒后打印堆栈并退出
runpy.run_path(r"...\target_script.py", run_name="__main__")
典型输出会直接指到 release_wps → Quit → <COMObject Ket.Application>.Quit,一步锁定问题。
通用改造模式:套件代理(一处改动,几十处调用点不动)
当既有代码用一个 get_wps() 对象同时访问 Documents / Workbooks / Presentations 时,
不要去改那几十处调用点,改为按属性名路由的代理:
_ROUTE = {"Documents": "word", "Workbooks": "excel", "Sheets": "excel",
"Presentations": "ppt"}
class WpsSuiteProxy:
def __init__(self):
object.__setattr__(self, "_apps", {})
def _app(self, suite):
apps = object.__getattribute__(self, "_apps")
if suite not in apps:
apps[suite] = _dispatch_suite(suite) # 按 suite 选 ProgID
return apps[suite]
def __getattr__(self, name):
if name.startswith("_"):
raise AttributeError(name)
return getattr(self._app(_ROUTE.get(name, "word")), name)
def __setattr__(self, name, value):
if name.startswith("_"):
object.__setattr__(self, name, value); return
try: setattr(self._app("word"), name, value)
except Exception: object.__setattr__(self, name, value)
def Quit(self):
for a in list(object.__getattribute__(self, "_apps").values()):
bounded_quit(a)
object.__getattribute__(self, "_apps").clear()
_dispatch_suite(suite) 内部按候选 ProgID 依次尝试,并用 app_is_wps() 过滤掉
MS Office 同名对象;Visible=False 用 try/except 包住(PowerPoint 类组件可能拒绝)。
环境准备清单
python -m venv <venv>
<venv>/Scripts/python.exe -m pip install pywin32 openpyxl python-pptx pandas Pillow PyYAML
pywin32是必需项,缺了报ModuleNotFoundError: win32com.client。- 从非主线程调 COM(如上面
bounded_quit)必须先pythoncom.CoInitialize(),
否则报-2147221008 尚未调用 CoInitialize。
验收清单(改完必须逐项实测,不要只看代码)
engine判定是否已变成WPS,且msword/msexcel/msppt为false。- Word:创建 → 编辑 → 导出 PDF,各返回
"engine": "WPS"。 - Excel:创建多工作表,用 openpyxl 打开核对
sheetnames。 - PPT:创建 → 加页(此功能常被
if engine != "WPS"门禁屏蔽,能通过才说明真的走 WPS)。 - 跑一次技能自带自检,确认
overall: pass。 - 确认没有新增残留
et.exe/wpp.exe。