获取可传递给实用程序进行处理的 Zip 存档中的文件路径。 -- python

Get file paths inside a Zip archive that can be passed to utilities for processing. -- python

使用 python 3.5

我需要找到存储在旧式 1997-2003 windows .doc 文件中的特定文本,并将其转储到 csv 文件中。我的约束是:

a) 文档文件在压缩存档中:我无法写入 disk/I 需要在内存中工作

b) 我需要使用正则表达式查找特定文本,因此需要将文档转换为 .txt

理想情况下,我可以使用 zipfile 读取文件,将数据传递给某些 doc-to-txt 转换器(例如 textract),并在 txt 上使用正则表达式。这可能看起来像

import zipfile
import textract
import re

    with zipfile.ZipFile(zip_archive, 'r') as f:
    for name in f.namelist():
        data = f.read(name)
        txt = textract.process(data).decode('utf-8')  
        #some regex on txt

这当然行不通,因为 textract(以及任何其他 doc-to-txt 转换器)的参数是文件路径,而 "data" 是字节。使用 "name" 作为参数给出 MissingFileError,可能是因为 zip 存档没有目录结构,只有文件名模拟路径。

有没有什么方法可以只在内存中通过压缩文档文件进行正则表达式,而不提取文件(并因此将它们写入磁盘)?

在不写入物理驱动器的情况下处理文件

在大多数情况下,必须首先提取 zip 中的文件才能进行处理。但这可以在内存中完成。障碍是如何调用一个实用程序,该实用程序仅将映射的文件系统路径作为参数来处理压缩文件中的文本,而不写入物理驱动器。

在内部 textract 调用命令行实用程序(antiword)来执行实际的文本提取。因此,解决此问题的方法可以普遍应用于需要通过文件系统路径访问 zip 内容的其他命令行工具。

以下是绕过此文件限制的几种可能的解决方案:

  1. 安装 RAM 驱动器。
    • 这很好用,但需要 sudo 提示,但可以自动执行。
  2. 将 zip 文件装载到文件系统。 (不错的选择)
    • 一个很好的 Linux 安装这些的工具是 fuse-zip
  3. 使用tempfile模块。 (最简单)
    • 确保自动删除文件。
    • 缺点,文件可能会写入磁盘。
  4. 访问 .docx 文件中的 XML。
    • 可以通过原始 XML 或使用 XML reader.
    • 进行正则表达式
    • 虽然只有一小部分文件是 .docx。
  5. 寻找另一个提取器。 (不包括)
    • 我找了找也没找到。
    • docx2txt 是另一个 Python 模块,但看起来它只能处理 .docx 文件(顾名思义)而不处理旧的 Word .doc 文件。

您可能想知道,为什么我要做所有这些跑腿的工作。实际上,我发现这对我自己的项目之一很有用。


1) RAM 驱动器

如果 tempfile 不满足文件约束目标,并且您希望确保该工具使用的所有文件都在 RAM 中,那么创建 RAM 驱动器是一个不错的选择。该工具应在完成后卸载驱动器,这将删除它存储的所有文件。

这个选项的一个优点是 Linux 系统都支持这个。它不会产生任何额外的软件依赖性;至少对于 Linux,Windows 可能需要 ImDisk。

这些是 Linux 上的相关 bash 命令:

$ mkdir ./temp_drive
$ sudo mount -t tmpfs -o size=512m temp_drive ./temp_drive
$ 
$ mount | tail -n 1     # To see that it was mounted.
$ sudo umount ./temp_drive   # To unmount.

在 Mac 上OS:

$ diskutil erasevolume HFS+ 'RAM Disk' `hdiutil attach -nomount ram://1048576 `
$ # 512M drive created: 512 * 2048 == 1048576

在 Windows:

在 Windows 上,您可能必须使用 ImDisk 等第三方应用程序:

为了使该过程自动化,这个简短的脚本会提示用户输入他们的 sudo 密码,然后调用 mount 来创建一个 RAM 驱动器:

import subprocess as sp
import tempfile
import platform
import getpass

ramdrv = tempfile.TemporaryDirectory()

if platform.system() == 'Linux':

    sudo_pw = getpass.getpass("Enter sudo password: ")

    # Mount RAM drive on Linux.
    p = sp.Popen(['sudo', '-S', 'bash', '-c', 
                 f"mount -t tmpfs -o size=512m tmpfs {ramdrv.name}"], 
                 stderr=sp.STDOUT, stdout=sp.PIPE, stdin=sp.PIPE, bufsize=1,
                 encoding='utf-8')

    print(sudo_pw, file=p.stdin)

    del sudo_pw

    print(p.stdout.readline())

elif platform.system() == 'Darwin':
    # And so on...

您的应用程序使用的任何 GUI 包都可能有一个密码对话框,但 getpass 适用于控制台应用程序。

要访问 RAM 驱动器,请像系统中的任何其他文件一样使用它所在的文件夹。向其中写入文件、从中读取文件、创建子文件夹等


2) 挂载 Zip 文件

如果 Zip 文件可以安装在 OS 文件系统上,那么它的文件将具有可以传递到 textract 的路径。这可能是最好的选择。

对于Linux,一个运行良好的实用程序是fuse-zip。下面几行安装它,并挂载一个 zip 文件。

$ sudo apt-get install fuse-zip
...
$ mkdir ~/archivedrive
$
$ fuse-zip ~/myarchive.zip ~/archivedrive
$ cd ~/archivedrive/myarchive           # I'm inside the zip!

从 Python,创建临时挂载点,挂载 zip,提取文本,然后卸载 zip:

>>> import subprocess as sp, tempfile, textract
>>>
>>> zf_path = '/home/me/marine_life.zip'
>>> zipdisk = tempfile.TemporaryDirectory()           # Temp mount point.
>>> 
>>> cp = sp.run(['fuse-zip', zf_path, zipdisk.name])  # Mount.
>>> cp.returncode
0
>>> all_text = textract.process(f"{zipdisk.name}/marine_life/octopus.doc")
>>> 
>>> cp = sp.run(['fusermount', '-u', zipdisk.name])   # Unmount.
>>> cp.returncode
0
>>> del zipdisk                                       # Delete mount point.
>>> all_text[:88]
b'The quick Octopuses live in every ocean, and different species have\n
adapted to different'
>>>
>>> # Convert bytes to str if needed.
>>> as_string = all_text.decode('latin-1', errors='replace')

使用此方法的一大优点是它不需要使用 sudo 来装载存档 - 无需提示输入密码。唯一的缺点是它增加了对项目的依赖。可能不是主要问题。 subprocess.run().

自动安装和卸载应该很容易

我相信 Linux 发行版的默认配置允许用户挂载 Fuse 文件系统而无需使用 sudo;但这需要针对支持的目标进行验证。

对于Windows,ImDisk 还可以挂载档案并具有命令行界面。这样就可以自动支持 Windows。 XML 方法和这种方法都很好,因为它们直接从 zip 文件中获取信息,而无需将其写入文件的额外步骤。

关于字符编码:我在示例中假设早于 2006 年的旧东欧 Word 文档可能使用 'utf-8'(iso-8859-2、latin-1、windows-1250,西里尔字母等)。您可能需要进行一些试验以确保每个文件都正确转换为字符串。

链接:


3) tempfile.NamedTemporaryFile

此方法不需要任何特殊权限。它应该可以正常工作。但是,它创建的文件不能保证只在内存中。

如果担心您的工具会使用户的驱动器中的文件过多,则此方法可以防止这种情况发生。临时文件可靠地自动删除。

一些示例代码,用于创建 NamedTemporaryFile、打开 zip 并将文件解压缩到其中,然后将其路径传递给 textract

>>> zf = zipfile.ZipFile('/temp/example.docx')
>>> wf = zf.open('word/document.xml')
>>> tf = tempfile.NamedTemporaryFile()
>>>
>>> for line in wf:
...     tf.file.write(line)
>>>
>>> tf.file.seek(0) 
>>> textract.process(tf.name)

# Lines and lines of text dumped to screen - it worked!

>>> tf.close()
>>>
>>> # The file disappears.

您可以反复使用相同的 NamedTemporaryFile 对象,使用 tf.seek(0) 重置其位置。

在完成之前不要关闭文件。当你关闭它时它会消失。 NamedTemporaryFile 的实例在关闭时自动删除,它们的引用计数变为 0,或者您的程序退出。

如果您想要一个确保在程序完成后消失的临时文件夹,一个选项是 tempfile.TemporaryDirectory

在同一个模块中,tempfile.SpooledTemporaryFile是内存中存在的文件。然而,这些的路径很难得到(我们只知道这些的文件描述符)。如果您确实找到了检索路径的好方法,那么 textract.

无法使用该路径

textract 在单独的进程中运行,但它继承了父进程的文件句柄。这就是可以在两者之间共享这些临时文件的原因。


4) Word.docx 通过 XML

提取文本

此方法试图通过在 Python 中完成工作或使用不需要 FS 路径的其他工具来消除对第 3 方实用程序的需求。

zip 文件中的 .docx 文件也是包含 XML 的 zip 文件。 XML 是文本,它可以用正则表达式进行原始解析,或者先传递给 XML reader。

Python 模块 docx2txt 与下面的第二个示例几乎相同。我查看了它的来源,它以 zip 格式打开 Word 文档,并使用 XML 解析器获取文本节点。由于与此方法相同的原因,它不会起作用。

下面的两个示例直接从 .docx 存档中读取文件 - 文件未提取到磁盘。

如果要将原始 XML 文本转换为字典和列表,可以使用 xmltodict:

import zipfile
import xmltodict

zf        = zipfile.ZipFile('/temp/example.docx')
data      = xmltodict.parse(zf.open('word/document.xml'))
some_text = data['w:document']['w:body']['w:p'][46]['w:r']['w:t']

print(some_text)

我发现这种格式有点笨拙,因为 XML 元素的嵌套结构很复杂,而且它并没有给你 XML reader 的优势作为定位节点。

使用xml.etree.ElementTree,一个XPATH表达式可以一次提取所有文本节点。

import re
import xml.etree.ElementTree as ET
import zipfile

_NS_DICT = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}

def get_docx_text(docx_path):
    """
    Opens the .docx file at 'docx_path', parses its internal document.xml
    document, then returns its text as one (possibly large) string.
    """
    with zipfile.ZipFile(docx_path) as zf:
        tree = ET.parse(zf.open('word/document.xml'))
    all_text = '\n'.join(n.text for n in tree.findall('.//w:t', _NS_DICT))
    return all_text

使用上面的xml.etree.ElementTree模块,只需几行代码就可以提取文本。

get_docx_text()中,这一行抓取所有文本:

all_text = '\n'.join(n.text for n in tree.findall('.//w:t', _NS_DICT))

字符串:'.//w:t' 是一个 XPATH 表达式,告诉模块 select Word 文档的所有 t(文本)节点。然后列表理解连接所有文本。

get_docx_text() 返回文本后,您可以应用正则表达式,逐行迭代它,或进行任何您需要做的事情。示例 re 表达式抓取所有带括号的短语。


链接

Fuse 文件系统:https://github.com/libfuse/libfuse

zip-fuse 手册页:https://linux.die.net/man/1/fuse-zip

MacOS 保险丝:https://osxfuse.github.io/

ImDisk (Windows): http://www.ltr-data.se/opencode.html/#ImDisk

内存驱动软件列表:https://en.wikipedia.org/wiki/List_of_RAM_drive_software

MS docx 文件格式:https://wiki.fileformat.com/word-processing/docx/

xml.ElementTree 文档:https://docs.python.org/3/library/xml.etree.elementtree.html?highlight=xml%20etree#module-xml.etree.ElementTree

XPATH:https://docs.python.org/3/library/xml.etree.elementtree.html?highlight=xml%20etree#elementtree-xpath

XML 示例借鉴了一些想法:https://etienned.github.io/posts/extract-text-from-word-docx-simply/