How to extract a file within a folder within a zip?

evamvid picture evamvid · Mar 4, 2014 · Viewed 7.7k times · Source

I need to extract a file called Preview.pdf from a folder called QuickLooks inside of a zip file.

Right now my code looks a little like this:

with ZipFile(newName, 'r') as newName:
        newName.extract(\QuickLooks\Preview.pdf)
        newName.close()

(In this case, newName has been set equal to the full path to the zip).

It's important to note that the backslash is correct in this case because I'm on Windows.

The code doesn't work; here's the error it gives:

Traceback (most recent call last):
  File "C:\Users\Asit\Documents\Evam\Python_Scripts\pageszip.py", line 18, in <module>
    ZF.extract("""QuickLooks\Preview.pdf""")
  File "C:\Python33\lib\zipfile.py", line 1019, in extract
    member = self.getinfo(member)
  File "C:\Python33\lib\zipfile.py", line 905, in getinfo
    'There is no item named %r in the archive' % name)
KeyError: "There is no item named 'QuickLook/Preview.pdf' in the archive"

I'm running the Python script from inside Notepad++, and taking the output from its console.

How can I accomplish this?

Alternatively, how could I extract the whole QuickLooks folder, move out Preview.pdf, and then delete the folder and the rest of it's contents?

Just for context, here's the rest of the script. It's a script to get a PDF of a .pages file. I know there are bonified converters out there; I'm just doing this as an excercise with some sort of real-world application.

import os.path
import zipfile
from zipfile import *
import sys

file = raw_input('Enter the full path to the .pages file in question. Please note that file and directory names cannot contain any spaces.')
dir = os.path.abspath(os.path.join(file, os.pardir))
fileName, fileExtension = os.path.splitext(file)
if fileExtension == ".pages":
    os.chdir(dir)
    print (dir)
    fileExtension = ".zip"
    os.rename (file, fileName + ".zip")
    newName = fileName + ".zip"  #for debugging purposes
    print (newName) #for debugging purposes
    with ZipFile(newName, 'w') as ZF:
        print("I'm about to list names!")
        print(ZF.namelist()) #for debugging purposes
        ZF.extract("QuickLook/Preview.pdf")
    os.rename('Preview.pdf', fileName + '.pdf')
    finalPDF = fileName + ".pdf"
    print ("Check out the PDF! It's located at" + dir +  finalPDF + ".")
else:
    print ("Sorry, this is not a valid .pages file.")
    sys.exit

I'm not sure if the import of Zipfile is redundant; I read on another SO post that it was better to use from zipfile import * than import zipfile. I wasn't sure, so I used both. =)

EDIT: I've changed the code to reflect the changes suggested by Blckknght.

Answer

martineau picture martineau · Mar 4, 2014

Here's something that seems to work. There were several issues with your code. As I mentioned in a comment, the zipfile must be opened with mode 'r' in order to read it. Another is that zip archive member names always use forward slash / characters in their path names as separators (see section 4.4.17.1 of the PKZIP Application Note). It's important to be aware that there's no way to extract a nested archive member to a different subdirectory with Python's currentzipfilemodule. You can control the root directory, but nothing below it (i.e. any subfolders within the zip).

Lastly, since it's not necessary to rename the .pages file to .zip — the filename you passZipFile() can have any extension — I removed all that from the code. However, to overcome the limitation on extracting members to a different subdirectory, I had to add code to first extract the target member to a temporary directory, and then copy that to the final destination. Afterwards, of course, this temporary folder needs to deleted. So I'm not sure the net result is much simpler...

import os.path
import shutil
import sys
import tempfile
from zipfile import ZipFile

PREVIEW_PATH = 'QuickLooks/Preview.pdf'  # archive member path
pages_file = input('Enter the path to the .pages file in question: ')
#pages_file = r'C:\Stack Overflow\extract_test.pages'  # hardcode for testing
pages_file = os.path.abspath(pages_file)
filename, file_extension = os.path.splitext(pages_file)
if file_extension == ".pages":
    tempdir = tempfile.gettempdir()
    temp_filename = os.path.join(tempdir, PREVIEW_PATH)
    with ZipFile(pages_file, 'r') as zipfile:
        zipfile.extract(PREVIEW_PATH, tempdir)
    if not os.path.isfile(temp_filename):  # extract failure?
        sys.exit('unable to extract {} from {}'.format(PREVIEW_PATH, pages_file))
    final_PDF = filename + '.pdf'
    shutil.copy2(temp_filename, final_PDF)  # copy and rename extracted file
    # delete the temporary subdirectory created (along with pdf file in it)
    shutil.rmtree(os.path.join(tempdir, os.path.split(PREVIEW_PATH)[0]))
    print('Check out the PDF! It\'s located at "{}".'.format(final_PDF))
    #view_file(final_PDF)  # see Bonus below
else:
    sys.exit('Sorry, that isn\'t a .pages file.')

Bonus: If you'd like to actually view the final pdf file from the script, you can add the following function and use it on the final pdf created (assuming you have a PDF viewer application installed on your system):

import subprocess
def view_file(filepath):
    subprocess.Popen(filepath, shell=True).wait()