首页 > 解决方案 > 无法使用 win32com.client 打开只读 Microsoft Word 文件

问题描述

我有数千个 docx 文件,我需要通过 Python 提取其中的某些元素。我在我的 Python 脚本中使用 win32com.client 来完成这项任务。

import win32com.client
doc = win32com.client.GetObject(some_path_to_a_docx_file)

这适用于 99% 的文件;但是,对于一些文件,Microsoft Word 对话框会启动:“作者希望您以只读方式打开此文件,除非您需要进行更改。以只读方式打开? ”。脚本此时停止等待用户输入。

在对话框上按是继续根据需要运行脚本;但是,这是低于标准的,因为我需要它完全自动化,而不会弹出任何对话框。有没有办法通过 win32com Python 或通过 MS Word 永久禁用 MS Word 提示?(注意:这里不能选择导入 docx 作为 win32com 的替代品。)

标签: pythonms-wordwin32com

解决方案


Office 应用程序是最终用户应用程序,并未作为开发工具进行优化。这意味着它们在等待用户输入时可能会出现挂起,如问题中所述。没有简单、干净的方法可以解决这个问题,这就是为什么建议将 Open XML 文件格式用于消除对话框存在问题的需求...

如果必须使用自动化,那么我知道有两种可能性。详细信息不是 python,但这确实记录了基本方法。

  1. 如果代码无法继续,请使用 Timer 函数和 SendKeys 自动关闭对话框。这有点像彩票,因为不可能知道哪个对话框被关闭了。通常,发送“Escape”键。曾几何时,有一组针对各种编程语言的知识库文章,但 Microsoft 站点上不再提供这些文章。我找到了一个存档C-Bit并正在复制演示经典 VB6 原理的相关示例内容:

本节中的步骤演示了 Microsoft Word 打印文档的自动化。自动化客户端调用 Word 文档对象的 PrintOut 方法。如果用户的默认打印机配置为打印到 FILE 端口,则调用 PrintOut 会生成一个对话框,提示用户输入文件名。若要确定 PrintOut 方法是否导致出现此对话框,Visual Basic 自动化客户端使用 Timer 控件来检测调用 PrintOut 方法后的空闲时间。在调用 PrintOut 之前,定时器已启用并设置为在 5 秒内触发。打印输出完成后,计时器被禁用。因此,如果 PrintOut 方法在五秒内完成,则不会发生 Timer 事件并且不会采取进一步的操作。文档被打印并且代码执行在 PrintOut 方法之外继续。但是,如果 Timer 事件发生在五秒间隔内,则假定 PrintOut 方法尚未完成,并且延迟是由等待用户输入的对话框引起的。当 Timer 事件发生时,自动化客户端将焦点赋予 Word 并使用 SendKeys 关闭对话框。

注意 出于演示目的,此示例使用 PrintOut 方法,以便在打印到设置为 FILE 端口的打印机时有意显示一个对话框。请注意,PrintOut 方法有两个参数,OutputfileName 和 PrintToFile,您可以提供它们以避免出现此对话框。

此外,当使用这种“计时器”方法时,您可以自定义等待时间大于或小于五秒,以及自定义发送到对话框的击键。

该演示包含两个 Visual Basic 项目:

  1. 提供用于检测延迟的 Timer 类的 ActiveX EXE。为 Timer 类使用 ActiveX EXE 的原因是在单独的进程中运行 Timer 代码,因此在单独的线程中运行。这使得 Timer 类可以在暂停的自动化调用期间引发事件。

  2. 使用 Word 自动化并调用 PrintOut 方法来打印文档的标准 EXE。它使用 ActiveX EXE 来检测调用 PrintOut 方法时的延迟。创建 ActiveX EXE 项目

  3. 启动 Visual Basic 并创建一个 ActiveX EXE 项目。Class1 是默认创建的。
  4. 在项目菜单上单击以选择属性,然后将项目名称更改为 MyTimer。
  5. 将以下代码复制并粘贴到 Class1 模块中:Option Explicit
 Public Event Timer() Private oForm1 As Form1

 Private Sub Class_Initialize()
     Set oForm1 = New Form1
     oForm1.Timer1.Enabled = False 
End Sub

 Private Sub Class_Terminate()
     Me.Enabled = False
     Unload oForm1
     Set oForm1 = Nothing 
End Sub

Public Property Get Enabled() As Boolean
     Enabled = oForm1.Timer1.Enabled 
End Property

Public Property Let Enabled(ByVal vNewValue As Boolean)
     oForm1.Timer1.Enabled = vNewValue
     If vNewValue = True Then
         Set oForm1.oClass1 = Me
     Else
         Set oForm1.oClass1 = Nothing
     End If 
End Property

 Public Property Get Interval() As Integer
     Interval = oForm1.Timer1.Interval 
End Property

 Public Property Let Interval(ByVal vNewValue As Integer)
     oForm1.Timer1.Interval = vNewValue End Property

 Friend Sub TimerEvent()
     RaiseEvent Timer 
End Sub                 
  1. 在“项目”菜单上,选择“添加表单”以将新表单添加到项目中。
  2. 将 Timer 控件添加到窗体。
  3. 将以下代码复制并粘贴到 Form1 的代码模块中:Option Explicit
 Public oClass1 As Class1

 Private Sub Timer1_Timer()
     oClass1.TimerEvent 
End Sub
  1. 将此项目保存在名为 Server 的新子文件夹中。
  2. 在“文件”菜单上,选择“生成 MyTimer.Exe”以构建和注册组件。创建自动化客户端
  1. 使用 Windows API 来识别和消除可能的问题对话框。我在MSDN 论坛上找到了一些代码,我在这里复制。归因于用户名yet

这是一个通过 C# 中的 pinvoke 使用 Win32 API 的示例。我可以通过 FindWindow 和 SendMessage 或 PostMessage 处理已知的 Word 窗口,例如 Word->File->Options 对话框窗口。请仔细阅读示例,看看它是否适合您。由于您知道要处理哪些对话框,请使用 spy++ 查找窗口标题和窗口类并在此示例中使用。

对于您的场景,可能不需要 SendKeys。

希望这可以帮助。

using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Data;
 using System.Drawing;
 using System.Linq;
 using System.Text;
 using System.Windows.Forms;

using System.Runtime.InteropServices;

namespace SendKeys
 {

    public partial class Form1 : Form
     {
         // For Windows Mobile, replace user32.dll with coredll.dll

         [DllImport("user32.dll", SetLastError = true)]
         static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        // Find window by Caption only. Note you must pass IntPtr.Zero as the first parameter.

        [DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
         static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName);

        [DllImport("user32.dll")]
         public static extern bool SetForegroundWindow(IntPtr hWnd);


         [return: MarshalAs(UnmanagedType.Bool)]
         [DllImport("user32.dll", SetLastError = true)]
         static extern bool PostMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
         static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

        static uint WM_CLOSE = 0x10;

        public Form1()
         {
             InitializeComponent();
         }

        private void button1_Click(object sender, EventArgs e)
         {

             // the caption and the className is for the Word -> File -> Options window
             // the caption and the className are got by using spy++ application and focussing on the window we are researching.
             string caption = "Word Options";
             string className = "NUIDialog";
             IntPtr hWnd= (IntPtr)(0);

             // Win 32 API being called through PInvoke
             hWnd = FindWindow(className, caption);

            /*bool retVal = false;
             if ((int)hWnd != 0)
             {
                // Win 32 API being called through PInvoke 
               retVal = SetForegroundWindow(hWnd);
             }*/



            if ((int)hWnd != 0)
             {
                 CloseWindow2(hWnd);
                 //CloseWindow(hWnd); // either sendMessage or PostMessage can be used.
             }
         }



        static bool CloseWindow(IntPtr hWnd)
         {
             // Win 32 API being called through PInvoke
             SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
             return true;
         }

        static bool CloseWindow2(IntPtr hWnd)
         {
             // Win 32 API being called through PInvoke
             PostMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
             return true;

         }

    }
 }

推荐阅读