问题 覆盖以前提取的文件而不是创建新文件


有一些库用于通过Python提取存档文件,例如gzip,zipfile库,rarfile,tarfile,patool等。我发现其中一个库(patool)特别有用,因为它具有交叉格式功能它可以提取几乎任何类型的存档,包括最流行的存档,如ZIP,GZIP,TAR和RAR。

要使用patool提取存档文件,它就像这样简单:

patoolib.extract_archive( "Archive.zip",outdir="Folder1")

在哪里 "Archive.zip" 是存档文件的路径和 "Folder1" 是存储解压缩文件的目录的路径。

提取工作正常。问题是,如果我再次为完全相同的存档文件运行相同的代码,则相同的提取文件将存储在同一文件夹中,但名称略有不同(第一次运行时为filename,第二次为filename1,文件名为11)第三等等。

如果目录中已经存在同名文件,我需要代码来覆盖提取的文件。

这个 extract_archive 函数看起来很小 - 它只有这两个参数,a verbosity 参数,和 program 参数,指定要用于提取存档的程序。

编辑: Nizam Mohamed的回答记录了这一点 extract_archive 函数实际上是覆盖输出。我发现这部分是正确的 - 该函数会覆盖ZIP文件,但不会覆盖我所追求的GZ文件。对于GZ文件,该函数仍会生成新文件。

编辑 Padraic Cunningham的回答建议使用 主要来源 。因此,我下载了该代码,并用链接中的脚本替换了旧的patool库脚本。结果如下:

os.listdir()
Out[11]: ['a.gz']

patoolib.extract_archive("a.gz",verbosity=1,outdir=".")
patool: Extracting a.gz ...
patool: ... a.gz extracted to `.'.
Out[12]: '.'

patoolib.extract_archive("a.gz",verbosity=1,outdir=".")
patool: Extracting a.gz ...
patool: ... a.gz extracted to `.'.
Out[13]: '.'

patoolib.extract_archive("a.gz",verbosity=1,outdir=".")
patool: Extracting a.gz ...
patool: ... a.gz extracted to `.'.
Out[14]: '.'

os.listdir()
Out[15]: ['a', 'a.gz', 'a1', 'a2']

所以,再次, extract_archive 函数每次执行时都会创建新文件。该文件存档在 a.gz 有一个不同的名字 a 其实。


2371
2018-04-14 15:45


起源

@nathanhayfield,如何在不先提取文件的情况下知道文件的名称? - multigoodverse
好的,我的错误 - nathan hayfield
patool (1.7) 覆盖。 - Nizam Mohamed
@NizamMohamed你确定吗?我有patoolib版本1.7,它不会覆盖。 - multigoodverse


答案:


如您所述,patoolib旨在成为通用归档工具。

可以使用patool创建,提取,测试,列出,比较,搜索和重新打包各种存档类型。 patool的优点是处理存档文件的简单性,而无需记住无数的程序和选项。

通用提取行为与特定提取行为

这里的问题是 extract_archive 没有公开广泛修改存档工具的基础默认行为的能力。

对于.zip扩展名,patoolib将使用解压缩。通过将-o作为选项传递给命令行界面,可以获得所需的提取存档的行为。即 unzip -o ... 但是,这是解压缩的特定命令行选项,并且每个归档实用程序都会更改。

例如,tar提供了覆盖选项,但没有缩短的命令行等同于zip。即 tar --overwrite 但 tar -o 没有预期的效果。

要解决此问题,您可以向作者发出功能请求,或使用替代库。不幸的是,patoolib的咒语需要扩展所有提取实用程序函数,然后实现底层提取器自己的覆盖命令选项。

示例对patoolib的更改

patoolib.programs.unzip

def extract_zip (archive, compression, cmd, verbosity, outdir, overwrite=False):
    """Extract a ZIP archive."""
    cmdlist = [cmd]
    if verbosity > 1:
        cmdlist.append('-v')
    if overwrite:
        cmdlist.append('-o')
    cmdlist.extend(['--', archive, '-d', outdir])
    return cmdlist

patoolib.programs.tar

def extract_tar (archive, compression, cmd, verbosity, outdir, overwrite=False):
    """Extract a TAR archive."""
    cmdlist = [cmd, '--extract']
    if overwrite:
        cmdlist.append('--overwrite')
    add_tar_opts(cmdlist, compression, verbosity)
    cmdlist.extend(["--file", archive, '--directory', outdir])
    return cmdlist

更新每个程序并不是一个微不足道的变化,每个程序都是不同的!

猴子修补覆盖行为

所以你决定不改进patoolib源代码......我们可以改写它的行为 extract_archive 最初查找现有目录,删除它,然后调用原始目录 extract_archive

您可以在模块中包含此代码,如果许多模块需要它,也许坚持下去 __init__.py

import os
import patoolib
from shutil import rmtree


def overwrite_then_extract_archive(archive, verbosity=0, outdir=None, program=None):
    if outdir:
        if os.path.exists(outdir):
            shutil.rmtree(outdir)
    patoolib.extract_archive(archive, verbosity, outdir, program)

patoolib.extract_archive = overwrite_then_extract_archive

现在我们打电话的时候 extract_archive() 我们有的功能 overwrite_then_extract_archive()


4
2018-04-16 18:49



我不熟悉猴子修补或改进库源代码。你的意思是,如果有人改进了源代码,我可以立即访问并下载更新后的库的改进版本吗? - multigoodverse
理想情况下,您可以自己为库提供必要的更改。 (如果您不想这样做,那完全可以理解!)如果您在使用之前包含最后一个代码段 extract_archive 函数,它将为您提供所需的覆盖行为。看最后一行 patoolib.extract_archive = overwrite_then_extract_archive 它使用覆盖行为修补先前的行为。 - Matt Davidson
我想我只是添加一个关于猴子修补性质的一般答案的参考: stackoverflow.com/questions/5626193/what-is-monkey-patch - OYRM
由于存在一些潜在的破坏性后果,我认为值得指出的是,删除整个输出目录就像在拟议的猴子补丁实现中所做的那样,并不完全相同,因为它也会删除其中没有的任何文件归档的一部分,与覆盖其中的内容并不完全相同。 - martineau


如果该功能不存在,则需要添加该功能。这方面的一个例子是用你自己的函数包装函数:

import os
from shutil import rmtree

def overwriting_extract_archive(zippath, outpath, **kwargs): 
    if os.path.exists(outpath):
        shutil.rmtree(outpath)
    patoolib.extract_archive(zippath, outdir=outpath, **kwargs)

如果你想逐个文件检查并将新输出与现有输出合并,那当然会成为一个更复杂的问题,但如果它就像你描述的那样(第二次运行它),这应该有效。


2
2018-04-16 18:44



我同意。代码是在 github.com/wummel/patool/blob/... 我模糊地推测,也许你可以修补后端程序的命令行参数数据库,为你关心的每种格式添加“--overwrite”选项(首先提供这种设施) 。无论如何,OP描述的行为似乎都不在Python代码中。 - tripleee
@tripleee这样的东西可能会起作用,但monkeypatching被认为比包装函数更少'Pythonic'。我倾向于认为更简单也更好,修改库可以回来后再咬你。 - a p
@tripleee我认为这是由这个函数引起的: github.com/wummel/patool/blob/... - 1.618


使用 主要来源 如果使用outdir传递目录,它将覆盖 包含 .gz文件:

from patoolib import extract_archive

extract_archive("foo.tar.gz",verbosity=1,outdir=".")

你会看见:

patool: ... /pathto/.foo.tar.gz extracted to `.'.

它不会覆盖的唯一方法是,如果你没有传递一个第二次提取的目录:

 ...foo.tar.gz extracted to `foo-1.0.2.tar1' ...(local file exists).

从bash运行,7z每次都要求确认覆盖:

In [9]: ls
foo.gz

In [10]: from patoolib import extract_archive

In [11]: extract_archive("foo.gz",verbosity=1,outdir=".")
patool: Extracting foo.gz ...
patool: running /usr/bin/7z e -o. -- foo.gz

7-Zip [64] 9.20  Copyright (c) 1999-2010 Igor Pavlov  2010-11-18
p7zip Version 9.20 (locale=en_IE.UTF-8,Utf16=on,HugeFiles=on,4 CPUs)

Processing archive: foo.gz

Extracting  foo

Everything is Ok

Size:       12
Compressed: 36
patool: ... foo.gz extracted to `.'.
Out[11]: '.'

In [12]: extract_archive("foo.gz",verbosity=1,outdir=".")
patool: Extracting foo.gz ...
patool: running /usr/bin/7z e -o. -- foo.gz

7-Zip [64] 9.20  Copyright (c) 1999-2010 Igor Pavlov  2010-11-18
p7zip Version 9.20 (locale=en_IE.UTF-8,Utf16=on,HugeFiles=on,4 CPUs)

Processing archive: foo.gz

file ./foo
already exists. Overwrite with 
foo?
(Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? y
Extracting  foo

Everything is Ok

Size:       12
Compressed: 36
patool: ... foo.gz extracted to `.'.
Out[12]: '.'

In [13]: extract_archive("foo.gz",verbosity=1,outdir=".")
patool: Extracting foo.gz ...
patool: running /usr/bin/7z e -o. -- foo.gz

7-Zip [64] 9.20  Copyright (c) 1999-2010 Igor Pavlov  2010-11-18
p7zip Version 9.20 (locale=en_IE.UTF-8,Utf16=on,HugeFiles=on,4 CPUs)

Processing archive: foo.gz

file ./foo
already exists. Overwrite with 
foo?
(Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? y
Extracting  foo

Everything is Ok

Size:       12
Compressed: 36
patool: ... foo.gz extracted to `.'.
Out[13]: '.'

In [14]: ls
foo  foo.gz

提取tar.gz文件:

In [1]: from patoolib import extract_archive

In [2]: for x in range(4):
            extract_archive("/home/padraic/Downloads/pycrypto-2.0.1.tar.gz",verbosity=1,outdir=".")
   ...:     
patool: Extracting /home/padraic/Downloads/pycrypto-2.0.1.tar.gz ...
patool: running /bin/tar --extract -z --file /home/padraic/Downloads/pycrypto-2.0.1.tar.gz --directory .
patool: ... /home/padraic/Downloads/pycrypto-2.0.1.tar.gz extracted to `.'.
patool: Extracting /home/padraic/Downloads/pycrypto-2.0.1.tar.gz ...
patool: running /bin/tar --extract -z --file /home/padraic/Downloads/pycrypto-2.0.1.tar.gz --directory .
patool: ... /home/padraic/Downloads/pycrypto-2.0.1.tar.gz extracted to `.'.
patool: Extracting /home/padraic/Downloads/pycrypto-2.0.1.tar.gz ...
patool: running /bin/tar --extract -z --file /home/padraic/Downloads/pycrypto-2.0.1.tar.gz --directory .
patool: ... /home/padraic/Downloads/pycrypto-2.0.1.tar.gz extracted to `.'.
patool: Extracting /home/padraic/Downloads/pycrypto-2.0.1.tar.gz ...
patool: running /bin/tar --extract -z --file /home/padraic/Downloads/pycrypto-2.0.1.tar.gz --directory .
patool: ... /home/padraic/Downloads/pycrypto-2.0.1.tar.gz extracted to `.'.

In [3]: ls
pycrypto-2.0.1/

同样所有都被覆盖,我能看到的唯一解释是无论应用程序被调用以解压缩你的 .gz 默认情况下,文件不会覆盖或提示,但每次稍微更改名称时都会创建新文件。


2
2018-04-22 14:03



我下载了主源代码并用新文件替换了旧的patool库文件,但仍然没有覆盖。有关详细信息,请参阅我在问题中添加的最后一个编辑。 - multigoodverse
@ArditS。,os和你在哪里运行它? - Padraic Cunningham
@ArditS。什么应用程序解压缩.gz文件? - Padraic Cunningham
我正在使用Windows。根据应用程序,我不知道如何获取该信息,但我可以告诉我打开GZ文件的默认Windows应用程序是WinRar。 - multigoodverse


如果提取失败,则在提取存档时覆盖现有文件可能会使目标目录处于不一致状态。

如果提取失败,在提取之前删除目标目录可能会导致文件丢失。

我认为最好的方法是,提取到临时目录并同步到目标目录。

对于此解决方案,模块 dirsync 是必须的。但 dirsync 只有当 mtime 和 ctime 默认情况下更新,而不是文件大小。

import os
import sys
from shutil import rmtree
from patoolib import extract_archive
from dirsync import sync

archive = ''
dst_dir = ''

try:
    tmp_dir = extract_archive(archive)
except Exception as e:
    print('extract_archive error {}'.format(e))
    sys.exit(1)
else:
    try:
        sync(tmp_dir,dst_dir,'sync',options=['modtime'])
    except Exception as e:
        print('updating {} from {} failed, error {}'.format(dst_dir,tmp_dir,e))
        sys.exit(1)
    else:
        sys.exit(0)
finally:
   if os.path.exists(tmp_dir):
       rmtree(tmp_dir)

2
2018-04-21 20:56



我明白了 - 因为您使用了ZIP文件,所以它正在与您合作。如果传递GZ文件,则该函数不会覆盖。这是一个很好的发现,但它仍然没有解决问题。 - multigoodverse


似乎我找到了解决每次创建新文件的问题 extract_archive 的方法 patool 库已执行。 需要强调的是,该方法能够覆盖/跳过先前为其他存档扩展提取的文件,但不能覆盖Gun Zipped文件。

我注意到任何Gun Zipped文件时(.gz)被提取,提取的文件与存档具有相同的名称,但没有任何扩展名。为了更好地说明,如果您更改名称 X.gz 至 Y.gz,然后提取存档,提取的文件将具有名称“Y”。 因此,我能够实现一个简单的条件:

import os,patoolib
if "name" not in os.listdir():
    patoolib.extract_archive("name.gz",outdir="C:\")

这似乎解决了我的问题。


1
2018-04-27 11:26