2011-01-11 4 views
20

mam archive.zip z dwóch plików: hello.txt i world.txtnadpisania pliku w ziparchive

chcę zastąpić hello.txt plik z nową z tym kodem:

ale nie zastąpi plik, jakoś tworzy kolejną instancję z hello.txt - spójrz na zrzut ekranu Winzip:

alt text

Ponieważ nie ma czegoś takiego jak zipfile.remove(), jaki jest najlepszy sposób na rozwiązanie tego problemu?

+1

otwarta kwestia: https://bugs.python.org/issue6818 – denfromufa

Odpowiedz

26

Nie można tego zrobić przy pomocy modułu python zipfile. Musisz utworzyć nowy plik zip i ponownie skompresować wszystko z pierwszego pliku oraz nowego zmodyfikowanego pliku.

Poniżej znajduje się kod, aby to zrobić. Ale pamiętaj, że nie jest wydajny, ponieważ dekompresuje, a następnie rekompresuje wszystkie dane.

import tempfile 
import zipfile 
import shutil 
import os 

def remove_from_zip(zipfname, *filenames): 
    tempdir = tempfile.mkdtemp() 
    try: 
     tempname = os.path.join(tempdir, 'new.zip') 
     with zipfile.ZipFile(zipfname, 'r') as zipread: 
      with zipfile.ZipFile(tempname, 'w') as zipwrite: 
       for item in zipread.infolist(): 
        if item.filename not in filenames: 
         data = zipread.read(item.filename) 
         zipwrite.writestr(item, data) 
     shutil.move(tempname, zipfname) 
    finally: 
     shutil.rmtree(tempdir) 

Zastosowanie:

remove_from_zip('archive.zip', 'hello.txt') 
with zipfile.ZipFile('archive.zip', 'a') as z: 
    z.write('hello.txt') 
+0

Tak, nie ma skutecznego sposobu na nadpisywanie plików w ogóle? Może kolejny moduł zip? W każdym razie, dzięki za to – nukl

+0

@ cru3l: Dokładnie to mówię w mojej odpowiedzi. – nosklo

+0

Możesz zadzwonić do zewnętrznego narzędzia zip. Można również utworzyć własny interfejs do biblioteki zip. – Apalala

10

Opierając się na odpowiedź nosklo użytkownika. UpdateableZipFile Klasa dziedzicząca z ZipFile, obsługuje ten sam interfejs, ale dodaje możliwość nadpisywania plików (poprzez writestr lub write) i usuwania plików.

import os 
import shutil 
import tempfile 
from zipfile import ZipFile, ZIP_STORED, ZipInfo 


class UpdateableZipFile(ZipFile): 
    """ 
    Add delete (via remove_file) and update (via writestr and write methods) 
    To enable update features use UpdateableZipFile with the 'with statement', 
    Upon __exit__ (if updates were applied) a new zip file will override the exiting one with the updates 
    """ 

    class DeleteMarker(object): 
     pass 

    def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False): 
     # Init base 
     super(UpdateableZipFile, self).__init__(file, mode=mode, 
               compression=compression, 
               allowZip64=allowZip64) 
     # track file to override in zip 
     self._replace = {} 
     # Whether the with statement was called 
     self._allow_updates = False 

    def writestr(self, zinfo_or_arcname, bytes, compress_type=None): 
     if isinstance(zinfo_or_arcname, ZipInfo): 
      name = zinfo_or_arcname.filename 
     else: 
      name = zinfo_or_arcname 
     # If the file exits, and needs to be overridden, 
     # mark the entry, and create a temp-file for it 
     # we allow this only if the with statement is used 
     if self._allow_updates and name in self.namelist(): 
      temp_file = self._replace[name] = self._replace.get(name, 
                   tempfile.TemporaryFile()) 
      temp_file.write(bytes) 
     # Otherwise just act normally 
     else: 
      super(UpdateableZipFile, self).writestr(zinfo_or_arcname, 
                bytes, compress_type=compress_type) 

    def write(self, filename, arcname=None, compress_type=None): 
     arcname = arcname or filename 
     # If the file exits, and needs to be overridden, 
     # mark the entry, and create a temp-file for it 
     # we allow this only if the with statement is used 
     if self._allow_updates and arcname in self.namelist(): 
      temp_file = self._replace[arcname] = self._replace.get(arcname, 
                    tempfile.TemporaryFile()) 
      with open(filename, "rb") as source: 
       shutil.copyfileobj(source, temp_file) 
     # Otherwise just act normally 
     else: 
      super(UpdateableZipFile, self).write(filename, 
               arcname=arcname, compress_type=compress_type) 

    def __enter__(self): 
     # Allow updates 
     self._allow_updates = True 
     return self 

    def __exit__(self, exc_type, exc_val, exc_tb): 
     # call base to close zip file, organically 
     try: 
      super(UpdateableZipFile, self).__exit__(exc_type, exc_val, exc_tb) 
      if len(self._replace) > 0: 
       self._rebuild_zip() 
     finally: 
      # In case rebuild zip failed, 
      # be sure to still release all the temp files 
      self._close_all_temp_files() 
      self._allow_updates = False 

    def _close_all_temp_files(self): 
     for temp_file in self._replace.itervalues(): 
      if hasattr(temp_file, 'close'): 
       temp_file.close() 

    def remove_file(self, path): 
     self._replace[path] = self.DeleteMarker() 

    def _rebuild_zip(self): 
     tempdir = tempfile.mkdtemp() 
     try: 
      temp_zip_path = os.path.join(tempdir, 'new.zip') 
      with ZipFile(self.filename, 'r') as zip_read: 
       # Create new zip with assigned properties 
       with ZipFile(temp_zip_path, 'w', compression=self.compression, 
          allowZip64=self._allowZip64) as zip_write: 
        for item in zip_read.infolist(): 
         # Check if the file should be replaced/or deleted 
         replacement = self._replace.get(item.filename, None) 
         # If marked for deletion, do not copy file to new zipfile 
         if isinstance(replacement, self.DeleteMarker): 
          del self._replace[item.filename] 
          continue 
         # If marked for replacement, copy temp_file, instead of old file 
         elif replacement is not None: 
          del self._replace[item.filename] 
          # Write replacement to archive, 
          # and then close it (deleting the temp file) 
          replacement.seek(0) 
          data = replacement.read() 
          replacement.close() 
         else: 
          data = zip_read.read(item.filename) 
         zip_write.writestr(item, data) 
      # Override the archive with the updated one 
      shutil.move(temp_zip_path, self.filename) 
     finally: 
      shutil.rmtree(tempdir) 

przykład użycia:

with UpdateableZipFile("C:\Temp\Test2.docx", "a") as o: 
    # Overwrite a file with a string 
    o.writestr("word/document.xml", "Some data") 
    # exclude an exiting file from the zip 
    o.remove_file("word/fontTable.xml") 
    # Write a new file (with no conflict) to the zp 
    o.writestr("new_file", "more data") 
    # Overwrite a file with a file 
    o.write("C:\Temp\example.png", "word/settings.xml") 
+0

Kiedy używam klasy UpdateableZipFile, tak jak w twoim przykładzie, zgłaszane są dwa wyjątki: 'File" /home/jezor/Code/updateable_zipfile.py ", wiersz 38, w writestr temp_file.write (bytes) TypeError: a bytes- wymagany jest obiekt, nie "str" ​​i "Plik" /home/jezor/Code/updateable_zipfile.py ", wiersz 77, w _close_all_temp_files dla temp_file w self._replace.itervalues ​​(): AttributeError: obiekt" dict "nie ma atrybut "itervalues". Czy mógłbyś zaktualizować swoją odpowiedź? Używam Pythona 3.4 i ta klasa jest najlepszym rozwiązaniem [to pytanie] (http://stackoverflow.com/a/37956562/5922757). – Jezor

+1

@Jezor. Próbujesz uruchomić kod Py2 w Py3, stąd błąd. Zmień "itervalues" na "wartości". –

+1

Nie miałem pewności co do kodu, biorąc pod uwagę niską liczbę pobrań, ale działa bardzo dobrze, wydaje się dobrze zakodowane i spełnia wymagania tego i kilka powtórzeń. – VectorVictor