使用Python读取元数据

6
过去两天,我一直在搜索互联网,试图找到解决我的问题的方法。我有一个不同文件类型的文件夹。我正在尝试编写一个Python脚本,它将读取每个文件的元数据(如果存在)。这样做的目的是最终将数据输出到一个文件中,以便与另一个程序的元数据提取进行比较。
我已经找到了一些示例,可以处理目录中很少量的文件。我发现所有的方法都涉及打开一个存储容器对象。我是Python新手,不知道什么是存储容器对象。我只知道当我尝试使用它时,大多数文件会出错。
pythoncom.StgOpenStorage(<File Name>, None, flags)

只有少数能够正常工作,我可以获取主要的元数据标签,例如标题、主题、作者、创建时间等。

除了存储容器之外,还有其他方法可以获取元数据吗?如果有其他语言更容易实现这一点的方法,请务必提出建议。

谢谢


3
“元数据”?实际上有数千种可以存储它们的元数据集和格式(如Dublin Core、ID3、Exif等)。你所指的是哪个元数据集,并以何种格式存储? - Lukas Graf
4
“Metadata”是一个非常泛泛的术语。你需要哪种类型的元数据?(如果你不知道答案,请告诉我们你想要与之比较的程序。)同时,StgOpenStorage打开Windows结构化存储文件(以及旧式OLE复合文件),并且可以获取该格式定义的元数据。 - abarnert
看起来你需要使用Windows API。这里提到了一种笼统的方法。http://win32com.goermezer.de/content/view/269/285/ - specialscope
非常抱歉我在提问时表述得不够清楚。我确实提到了一些元数据标签,例如标题、主题、作者、创建时间。但是我无法列出所有我需要的标签,因为在特定的时间内我永远也不可能知道它们全部是什么。有时候我只需要作者、创建时间、最后作者、最后保存、修订号码。有时候我需要上述所有标签以及页面计数、字数统计、字符计数、上次打印时间、编辑时间。其他时候我需要获取电子邮件主题、收件人、发件人、密送、抄送、发送时间、接收时间等信息。 - Bob
6个回答

15
您可以使用Shell com对象检索在资源管理器中可见的任何元数据:
import win32com.client
sh=win32com.client.gencache.EnsureDispatch('Shell.Application',0)
ns = sh.NameSpace(r'm:\music\Aerosmith\Classics Live!')
colnum = 0
columns = []
while True:
    colname=ns.GetDetailsOf(None, colnum)
    if not colname:
        break
    columns.append(colname)
    colnum += 1

for item in ns.Items():
    print (item.Path)
    for colnum in range(len(columns)):
        colval=ns.GetDetailsOf(item, colnum)
        if colval:
            print('\t', columns[colnum], colval)

1
请注意,如果您只需要特定文件的信息,则可以使用item = ns.ParseName('song.mp4')而不是循环。实际上,如果您有from pathlib import Path,则尝试file = Path(r'C:\path\to\file.ext')并使用ns = sh.NameSpace(str(file.parent))item = ns.ParseName(str(file.name)) - Greedo

7

为了试图将上面的答案结合并澄清,我决定写下自己的答案(这些答案在很大程度上帮助了我解决问题)。

我认为解决这个问题有两种方法。

情况1:你知道文件包含哪些元数据(你感兴趣的元数据)。

在这种情况下,假设您有一个字符串列表,其中包含您感兴趣的元数据。我在这里假设这些标签是正确的(即您对.txt文件的像素数不感兴趣)。

metadata = ['Name', 'Size', 'Item type', 'Date modified', 'Date created']

现在,使用Greedo和Roger Upole提供的代码,我创建了一个函数,该函数接受文件的完整路径和名称,并返回一个包含所需元数据的字典:
def get_file_metadata(path, filename, metadata):
    # Path shouldn't end with backslash, i.e. "E:\Images\Paris"
    # filename must include extension, i.e. "PID manual.pdf"
    # Returns dictionary containing all file metadata.
    sh = win32com.client.gencache.EnsureDispatch('Shell.Application', 0)
    ns = sh.NameSpace(path)

    # Enumeration is necessary because ns.GetDetailsOf only accepts an integer as 2nd argument
    file_metadata = dict()
    item = ns.ParseName(str(filename))
    for ind, attribute in enumerate(metadata):
        attr_value = ns.GetDetailsOf(item, ind)
        if attr_value:
            file_metadata[attribute] = attr_value

    return file_metadata

# *Note: you must know the total path to the file.*
# Example usage:
if __name__ == '__main__':
    folder = 'E:\Docs\BMW'
    filename = 'BMW series 1 owners manual.pdf'
    metadata = ['Name', 'Size', 'Item type', 'Date modified', 'Date created']
    print(get_file_metadata(folder, filename, metadata))

使用以下内容:

{'Name': 'BMW series 1 owners manual.pdf', 'Size': '11.4 MB', 'Item type': 'Foxit Reader PDF Document', 'Date modified': '8/30/2020 11:10 PM', 'Date created': '8/30/2020 11:10 PM'}

这是正确的,因为我刚创建了该文件并使用Foxit PDF阅读器作为我的主要pdf阅读器。 因此,此函数返回一个字典,其中键是元数据标签,值是给定文件的那些标签的值。

情况2:您不知道文件包含哪些元数据

这是一个稍微困难一些的情况,特别是在优化方面。我分析了Roger Upole提出的代码,基本上他试图读取None文件的元数据,结果得到了所有可能的元数据标签列表。所以我想可能更容易的办法是硬拷贝这个列表,然后尝试读取每个标签。这样,完成后,您将拥有一个包含文件实际拥有的所有标签的字典。

只需复制我认为是每个可能的元数据标签,并尝试从文件中获取所有标签。 基本上,只需复制这个声明Python列表的代码,并使用上面的代码(将metadata替换为这个新列表):

metadata = ['Name', 'Size', 'Item type', 'Date modified', 'Date created', 'Date accessed', 'Attributes', 'Offline status', 'Availability', 'Perceived type', 'Owner', 'Kind', 'Date taken', 'Contributing artists', 'Album', 'Year', 'Genre', 'Conductors', 'Tags', 'Rating', 'Authors', 'Title', 'Subject', 'Categories', 'Comments', 'Copyright', '#', 'Length', 'Bit rate', 'Protected', 'Camera model', 'Dimensions', 'Camera maker', 'Company', 'File description', 'Masters keywords', 'Masters keywords']

我不认为这是一个很好的解决方案,但另一方面,你可以把这个列表作为全局变量,并且无需在每次函数调用时传递它。为了完整起见,以下是使用这个新元数据列表的前一个函数的输出:

{'Name': 'BMW series 1 owners manual.pdf', 'Size': '11.4 MB', 'Item type': 'Foxit Reader PDF Document', 'Date modified': '8/30/2020 11:10 PM', 'Date created': '8/30/2020 11:10 PM', 'Date accessed': '8/30/2020 11:10 PM', 'Attributes': 'A', 'Perceived type': 'Unspecified', 'Owner': 'KEMALS-ASPIRE-E\\kemal', 'Kind': 'Document', 'Rating': 'Unrated'}

如你所见,现在返回的字典包含了文件中所有的元数据。 这个方法可行的原因是由于 if 语句

if attribute_value:

这意味着每当一个属性等于None时,它不会被添加到返回的字典中。

需要强调的是,如果要处理许多文件,在函数中传递列表而不是在每次调用函数时传递它将更加高效。


2
问题在于Windows存储文件元数据的方式有两种。您正在使用的方法适用于由COM应用程序创建的文件;这些数据包含在文件本身中。然而,随着NTFS5的引入,任何文件都可以作为备用数据流的一部分包含元数据。因此,成功的文件可能是COM应用程序创建的文件,而失败的文件可能不是。
以下是处理COM应用程序创建的文件的可能更健壮的方法:从任何文件获取文档摘要信息
对于备用数据流,可以直接读取它们:
meta = open('myfile.ext:StreamName').read()
< p > < em > 更新: 好的,现在我看到这些都不相关,因为您需要的是< em>文档元数据而不是< em>文件元数据。问题清晰明了可以带来很大的差异:|

请尝试这个链接:如何在Python中检索办公文件的作者?


正如我所说,我对Python完全是新手。如果我的文件名为test1.doc,那么我应该在单引号中输入什么? - Bob
尝试使用“ZoneInfo”。问题在于这并不能帮助你得到你想要的。 - Matthew Trevor

1

Windows API Code Pack可以与Python for .NET一起使用,用于读写文件元数据。

  1. 下载WindowsAPICodePack-CoreWindowsAPICodePack-Shell的NuGet包。

  2. 使用压缩工具(如7-Zip)提取.nupkg文件到脚本路径或系统路径变量中定义的某个位置。

  3. 使用pip install pythonnet安装Python for .NET。

获取并设置MP4视频标题的示例代码:

import clr
clr.AddReference("Microsoft.WindowsAPICodePack")
clr.AddReference("Microsoft.WindowsAPICodePack.Shell")
from Microsoft.WindowsAPICodePack.Shell import ShellFile

# create shell file object
f = ShellFile.FromFilePath(r'movie..mp4')

# read video title
print(f.Properties.System.Title.Value)

# set video title
f.Properties.System.Title.Value = 'My video'

使用Hack来检查可用的属性:

dir(f.Properties.System)

0

我知道这是一个老问题,但我曾经遇到过同样的问题,并最终创建了一个软件包来解决我的问题:windows-metadata

附带一提,Roger Upole的答案是一个很好的起点,但它并没有捕捉到文件可以拥有的所有属性(如果不是列名,那么中断会太早结束循环,因为Windows跳过了某些列号,出于某种原因。所以Roger的答案只给出了前30个左右的属性,实际上有近320个)。

现在,使用这个软件包来回答问题:

from windows_metadata import WindowsAttributes

attr = WindowsAttributes(<File Name>)  # this will load all the filled attributes a file has
title = attr["Title"] # dict-like access
title = attr.title # attribute like access -> these two will return the same value
subject = attr.subject
author = attr.author
...

同样适用于文件具有的任何可用属性。


0
Roger Upole的回答帮了我很多。不过,我还需要读取“.xls”文件中“最后保存者”的详细信息。
可以使用win32com读取XLS文件属性。 Workbook对象有一个BuiltinDocumentPropertieshttps://gist.github.com/justengel/87bac3355b1a925288c59500d2ce6ef5
import os
import win32com.client  # Requires "pip install pywin32"


__all__ = ['get_xl_properties', 'get_file_details']


# https://learn.microsoft.com/en-us/dotnet/api/microsoft.office.tools.excel.workbook.builtindocumentproperties?view=vsto-2017
BUILTIN_XLS_ATTRS = ['Title', 'Subject', 'Author', 'Keywords', 'Comments', 'Template', 'Last Author', 'Revision Number',
                     'Application Name', 'Last Print Date', 'Creation Date', 'Last Save Time', 'Total Editing Time',
                     'Number of Pages', 'Number of Words', 'Number of Characters', 'Security', 'Category', 'Format',
                     'Manager', 'Company', 'Number of Btyes', 'Number of Lines', 'Number of Paragraphs',
                     'Number of Slides', 'Number of Notes', 'Number of Hidden Slides', 'Number of Multimedia Clips',
                     'Hyperlink Base', 'Number of Characters (with spaces)']


def get_xl_properties(filename, xl=None):
    """Return the known XLS file attributes for the given .xls filename."""
    quit = False
    if xl is None:
        xl = win32com.client.DispatchEx('Excel.Application')
        quit = True

    # Open the workbook
    wb = xl.Workbooks.Open(filename)

    # Save the attributes in a dictionary
    attrs = {}
    for attrname in BUILTIN_XLS_ATTRS:
        try:
            val = wb.BuiltinDocumentProperties(attrname).Value
            if val:
                attrs[attrname] = val
        except:
            pass

    # Quit the excel application
    if quit:
        try:
            xl.Quit()
            del xl
        except:
            pass

    return attrs


def get_file_details(directory, filenames=None):
    """Collect the a file or list of files attributes.
    Args:
        directory (str): Directory or filename to get attributes for
        filenames (str/list/tuple): If the given directory is a directory then a filename or list of files must be given
    Returns:
         file_attrs (dict): Dictionary of {filename: {attribute_name: value}} or dictionary of {attribute_name: value}
            if a single file is given.
    """
    if os.path.isfile(directory):
        directory, filenames = os.path.dirname(directory), [os.path.basename(directory)]
    elif filenames is None:
        filenames = os.listdir(directory)
    elif not isinstance(filenames, (list, tuple)):
        filenames = [filenames]

    if not os.path.exists(directory):
        raise ValueError('The given directory does not exist!')

    # Open the com object
    sh = win32com.client.gencache.EnsureDispatch('Shell.Application', 0)  # Generates local compiled with make.py
    ns = sh.NameSpace(os.path.abspath(directory))

    # Get the directory file attribute column names
    cols = {}
    for i in range(512):  # 308 seemed to be max for excel file
        attrname = ns.GetDetailsOf(None, i)
        if attrname:
            cols[i] = attrname

    # Get the information for the files.
    files = {}
    for file in filenames:
        item = ns.ParseName(os.path.basename(file))
        files[os.path.abspath(item.Path)] = attrs = {}  # Store attributes in dictionary

        # Save attributes
        for i, attrname in cols.items():
            attrs[attrname] = ns.GetDetailsOf(item, i)

        # For xls file save special properties
        if os.path.splitext(file)[-1] == '.xls':
            xls_attrs = get_xl_properties(item.Path)
            attrs.update(xls_attrs)

    # Clean up the com object
    try:
        del sh
    except:
        pass

    if len(files) == 1:
        return files[list(files.keys())[0]]
    return files


if __name__ == '__main__':
    import argparse

    P = argparse.ArgumentParser(description="Read and print file details.")
    P.add_argument('filename', type=str, help='Filename to read and print the details for.')
    P.add_argument('-v', '--show-empty', action='store_true', help='If given print keys with empty values.')
    ARGS = P.parse_args()

    # Argparse Variables
    FILENAME = ARGS.filename
    SHOW_EMPTY = ARGS.show_empty
    DETAILS = get_file_details(FILENAME)

    print(os.path.abspath(FILENAME))
    for k, v in DETAILS.items():
        if v or SHOW_EMPTY:
            print('\t', k, '=', v)

这种方法对我没有起作用。尽管找到了许多其他标签,但它无法找到“最后作者”标签。也许它不适用于xlsx,或者Python库有一个错误。这个方法行得通: https://dev59.com/BVrUa4cB1Zd3GeqPhCZF#69690437 (只需手动从xml数据中读取)。 - Dan
尝试直接调用“get_xls_properties”函数。这可能是Excel版本的问题。我有一个条件,只有在文件为.xls时才使用该函数,但它应该适用于所有Excel文件。主要问题是该函数需要很长时间,因为它实际上打开Excel来读取数据。 - justengel
哦,伙计...你是对的。它在xlsx上运行良好。 - Dan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接