Python:从Office / Excel文档访问嵌入式OLE而不使用剪贴板

问题描述 投票:9回答:2

我想使用Python从Office / Excel文档中添加和提取文件。到目前为止添加东西很容易但是为了提取我还没有找到一个干净的解决方案。

为了清楚我已经得到了什么,我没有写下下面的小例子test.py并进一步解释。

test.朋友

import win32com.client as win32
import os 
from tkinter import messagebox
import win32clipboard

# (0) Setup
dir_path = os.path.dirname(os.path.realpath(__file__))
print(dir_path)
excel = win32.gencache.EnsureDispatch('Excel.Application')
wb = excel.Workbooks.Open(dir_path + "\\" + "test_excel.xlsx")
ws = wb.Worksheets.Item(1)
objs = ws.OLEObjects()

# (1) Embed file
f = dir_path + "\\" + "test_txt.txt"
name = "test_txt_ole.txt"
objs.Add( Filename=f, IconLabel=name )

# (2) Access embedded file
obj = objs.Item(1) # Get single OLE from OLE list
obj.Copy()
win32clipboard.OpenClipboard()
data = win32clipboard.GetClipboardData(0xC004) # Binary access
win32clipboard.EmptyClipboard()
win32clipboard.CloseClipboard()
messagebox.showinfo(title="test_txt_ole.txt", message=str(data))

# (3) Press don't save here to keep 
# wb.Close() # Will close excel document and leave excel opened.
excel.Application.Quit() # Will close excel with all opened documents

对于准备(步骤0),它打开一个给定的excel文档,其中包含一个工作表,该工作表之前是使用excel中的新文档按钮创建的。在步骤(1)中,它使用API​​将给定的文本文件嵌入到excel文档中。使用文本编辑器在内容“TEST123”之前创建了文本文件。之后在步骤(2)中,它尝试使用剪贴板从嵌入式OLE读回内容,并打开一个消息框,显示剪贴板中OLE的内容。最后(3)程序关闭打开的文档。要保持不变的设置,请按此处。

这种解决方案的最大缺点是使用剪贴板来破坏剪贴板中的任何用户内容,这在生产环境中是不好的风格。此外,它使用未记录的选项进行剪贴板。

更好的解决方案是将安全的OLE或OLE嵌入文件安全到python数据容器或我选择的文件。在我的示例中,我使用了TXT文件来轻松识别文件数据。最后,我将使用ZIP作为一体化解决方案,但TXT文件解决方案足以支持base64数据。

来源0xC004 = 49156:https://danny.fyi/embedding-and-accessing-a-file-in-excel-with-vba-and-ole-objects-4d4e7863cfff

这个VBA示例看起来很有趣,但我对VBA没有任何线索:Saving embedded OLE Object (Excel doc) to file in Excel 2010 vs 2013

python excel com ms-office ole
2个回答
4
投票

好吧,我发现Parfait的解决方案有点hackish(在坏的意义上),因为

  • 它假定Excel将嵌入保存为临时文件,
  • 它假定此临时文件的路径始终是用户的默认临时路径,
  • 它假定你有权在那里打开文件,
  • 它假定您使用命名约定来标识您的对象(例如,'test_txt'总是在名称中找到,您不能插入对象'account_data'),
  • 它假设该约定不受操作系统的干扰(例如,它不会将其更改为'~test_tx(1)'以保存字符长度),
  • 它假设此约定已知并被计算机上的所有其他程序接受(没有其他人将使用包含'test_txt'的名称)。

所以,我写了一个替代解决方案。其实质如下:

  1. 将.xlsx文件(或基于新XML格式的任何其他Office文件,未受密码保护)解压缩到临时路径。
  2. 遍历'/ xxx / embeddings'('xxx'='xl'或'word'或'ppt')中的所有.bin文件,并创建一个字典,其中包含.bin文件的临时路径作为键和字典从步骤3返回为值。
  3. 根据(未详细记录的)Ole Packager格式从.bin文件中提取信息,并将信息作为字典返回。 (将原始二进制数据检索为“内容”,不仅来自.txt,还有任何文件类型,例如.png)

我还在学习Python,所以这不是完美的(没有错误检查,没有性能优化),但你可以从中获得想法。我在几个例子上测试了它。这是我的代码:

import tempfile
import os
import shutil
import zipfile
import glob
import pythoncom
import win32com.storagecon


def read_zipped_xml_bin_embeddings( path_zipped_xml ):
    temp_dir = tempfile.mkdtemp()

    zip_file = zipfile.ZipFile( path_zipped_xml )
    zip_file.extractall( temp_dir )
    zip_file.close()

    subdir = {
            '.xlsx': 'xl',
            '.xlsm': 'xl',
            '.xltx': 'xl',
            '.xltm': 'xl',
            '.docx': 'word',
            '.dotx': 'word',
            '.docm': 'word',
            '.dotm': 'word',
            '.pptx': 'ppt',
            '.pptm': 'ppt',
            '.potx': 'ppt',
            '.potm': 'ppt',
        }[ os.path.splitext( path_zipped_xml )[ 1 ] ]
    embeddings_dir = temp_dir + '\\' + subdir + '\\embeddings\\*.bin'

    result = {}
    for bin_file in list( glob.glob( embeddings_dir ) ):
        result[ bin_file ] = bin_embedding_to_dictionary( bin_file )

    shutil.rmtree( temp_dir )

    return result


def bin_embedding_to_dictionary( bin_file ):
    storage = pythoncom.StgOpenStorage( bin_file, None, win32com.storagecon.STGM_READ | win32com.storagecon.STGM_SHARE_EXCLUSIVE )
    for stastg in storage.EnumElements():
        if stastg[ 0 ] == '\1Ole10Native':
            stream = storage.OpenStream( stastg[ 0 ], None, win32com.storagecon.STGM_READ | win32com.storagecon.STGM_SHARE_EXCLUSIVE )

            result = {}
            result[ 'original_filename' ] = '' # original filename in ANSI starts at byte 7 and is null terminated
            stream.Seek( 6, 0 )
            while True:
                ch = stream.Read( 1 )
                if ch == '\0':
                    break
                result[ 'original_filename' ] += ch

            result[ 'original_filepath' ] = '' # original filepath in ANSI is next and is null terminated
            while True:
                ch = stream.Read( 1 )
                if ch == '\0':
                    break
                result[ 'original_filepath' ] += ch

            stream.Seek( 4, 1 ) # next 4 bytes is unused

            temporary_filepath_size = 0 # size of the temporary file path in ANSI in little endian
            temporary_filepath_size |= ord( stream.Read( 1 ) ) << 0
            temporary_filepath_size |= ord( stream.Read( 1 ) ) << 8
            temporary_filepath_size |= ord( stream.Read( 1 ) ) << 16
            temporary_filepath_size |= ord( stream.Read( 1 ) ) << 24

            result[ 'temporary_filepath' ] = stream.Read( temporary_filepath_size ) # temporary file path in ANSI

            result[ 'size' ] = 0 # size of the contents in little endian
            result[ 'size' ] |= ord( stream.Read( 1 ) ) << 0
            result[ 'size' ] |= ord( stream.Read( 1 ) ) << 8
            result[ 'size' ] |= ord( stream.Read( 1 ) ) << 16
            result[ 'size' ] |= ord( stream.Read( 1 ) ) << 24

            result[ 'contents' ] = stream.Read( result[ 'size' ] ) # contents

            return result

你可以像这样使用它:

objects = read_zipped_xml_bin_embeddings( dir_path + '\\test_excel.xlsx' )
obj = objects.values()[ 0 ] # Get first element, or iterate somehow, the keys are the temporary paths
print( 'Original filename: ' + obj[ 'original_filename' ] )
print( 'Original filepath: ' + obj[ 'original_filepath' ] )
print( 'Original filepath: ' + obj[ 'temporary_filepath' ] )
print( 'Contents: ' + obj[ 'contents' ] )

2
投票

考虑使用Windows临时目录,该目录将在嵌入工作簿时临时存储OLE对象的文件源。此解决方案中没有使用剪贴板,但物理文件。

使用此方法,您将需要检索当前用户的名称并遍历temp目录的所有文件:C:\ Documents and Settings \ {username} \ Local Settings \ Temp(Windows Vista / 7/8的标准Excel转储文件夹) / 10)。此外,使用包含in的条件同名搜索,其包含原始文件的基本名称,因为具有数字后缀(1),(2),(3),......的多个版本可能存在,具体取决于脚本运行的次数。在这里尝试甚至正则表达式搜索。

最后,下面的例程使用try...except...finally块来清楚地存在Excel对象而不管错误但是会输出任何异常消息。请注意,这只是使用文本文件的Windows解决方案。

import win32com.client as win32
import os, shutil
from tkinter import messagebox

# (0) Setup
dir_path = cd = os.path.dirname(os.path.abspath(__file__))
print(dir_path)

try:
    excel = win32.gencache.EnsureDispatch('Excel.Application')    
    wb = excel.Workbooks.Open(os.path.join(dir_path, "test_excel.xlsx"))
    ws = wb.Worksheets(1)
    objs = ws.OLEObjects()

    # (1) Embed file
    f = os.path.join(dir_path, "test_txt.txt")    
    name = "test_txt_ole.txt"
    objs.Add(Filename=f, IconLabel=name).Name = 'Test'

    # (2) Open file from temporary folder
    ole = ws.OLEObjects(1)        
    ole.Activate()

    # (3) Grab the recent like-named file
    user = os.environ.get('USERNAME')
    outfile = os.path.join(dir_path, "test_txt_out.txt")

    tempfolder = r"C:\Documents and Settings\{}\Local Settings\Temp".format(user)

    for subdir, dirs, files in os.walk(tempfolder):
        for file in sorted(files, reverse=True):
            if 'test_txt' in file:                
                tempfile = os.path.join(tempfolder, file)
                break

    shutil.copyfile(tempfile, outfile)

    # (4) Read text content
    with open(outfile, 'r') as f:        
        content = f.readlines()

    # (5) Output message with content
    messagebox.showinfo(title="test_txt_ole.txt", message="".join(content))

except Exception as e:
    print(e)

finally:
    wb.Close(True)      # CLOSES AND SAVES WORKBOOK
    excel.Quit          # QUITS EXCEL APP

    # RELEASES COM RESOURCES
    ws = None; wb = None; objs = None; ole = None; excel = None

Tkinter Messagebox

Message Output

© www.soinside.com 2019 - 2024. All rights reserved.