Descargar y descomprimir un archivo .zip sin escribir en el disco

85

Me las arreglé para hacer funcionar mi primer script de Python que descarga una lista de archivos .ZIP de una URL y luego procede a extraer los archivos ZIP y los escribe en el disco.

Ahora no puedo dar el siguiente paso.

Mi objetivo principal es descargar y extraer el archivo zip y pasar el contenido (datos CSV) a través de una secuencia TCP. Preferiría no escribir ninguno de los archivos zip o extraídos en el disco si pudiera salirme con la mía.

Aquí está mi script actual que funciona pero desafortunadamente tiene que escribir los archivos en el disco.

import urllib, urllister
import zipfile
import urllib2
import os
import time
import pickle

# check for extraction directories existence
if not os.path.isdir('downloaded'):
    os.makedirs('downloaded')

if not os.path.isdir('extracted'):
    os.makedirs('extracted')

# open logfile for downloaded data and save to local variable
if os.path.isfile('downloaded.pickle'):
    downloadedLog = pickle.load(open('downloaded.pickle'))
else:
    downloadedLog = {'key':'value'}

# remove entries older than 5 days (to maintain speed)

# path of zip files
zipFileURL = "http://www.thewebserver.com/that/contains/a/directory/of/zip/files"

# retrieve list of URLs from the webservers
usock = urllib.urlopen(zipFileURL)
parser = urllister.URLLister()
parser.feed(usock.read())
usock.close()
parser.close()

# only parse urls
for url in parser.urls: 
    if "PUBLIC_P5MIN" in url:

        # download the file
        downloadURL = zipFileURL + url
        outputFilename = "downloaded/" + url

        # check if file already exists on disk
        if url in downloadedLog or os.path.isfile(outputFilename):
            print "Skipping " + downloadURL
            continue

        print "Downloading ",downloadURL
        response = urllib2.urlopen(downloadURL)
        zippedData = response.read()

        # save data to disk
        print "Saving to ",outputFilename
        output = open(outputFilename,'wb')
        output.write(zippedData)
        output.close()

        # extract the data
        zfobj = zipfile.ZipFile(outputFilename)
        for name in zfobj.namelist():
            uncompressed = zfobj.read(name)

            # save uncompressed data to disk
            outputFilename = "extracted/" + name
            print "Saving extracted file to ",outputFilename
            output = open(outputFilename,'wb')
            output.write(uncompressed)
            output.close()

            # send data via tcp stream

            # file successfully downloaded and extracted store into local log and filesystem log
            downloadedLog[url] = time.time();
            pickle.dump(downloadedLog, open('downloaded.pickle', "wb" ))
usuario714415
fuente
3
El formato ZIP no está diseñado para ser transmitido. Utiliza pies de página, lo que significa que necesita el final del archivo para averiguar dónde pertenecen las cosas dentro de él, lo que significa que debe tener el archivo completo antes de poder hacer algo con un subconjunto del mismo.
Charles Duffy

Respuestas:

65

Mi sugerencia sería utilizar un StringIOobjeto. Emulan archivos, pero residen en la memoria. Entonces podrías hacer algo como esto:

# get_zip_data() gets a zip archive containing 'foo.txt', reading 'hey, foo'

import zipfile
from StringIO import StringIO

zipdata = StringIO()
zipdata.write(get_zip_data())
myzipfile = zipfile.ZipFile(zipdata)
foofile = myzipfile.open('foo.txt')
print foofile.read()

# output: "hey, foo"

O más simplemente (disculpas a Vishal):

myzipfile = zipfile.ZipFile(StringIO(get_zip_data()))
for name in myzipfile.namelist():
    [ ... ]

En Python 3 use BytesIO en lugar de StringIO:

import zipfile
from io import BytesIO

filebytes = BytesIO(get_zip_data())
myzipfile = zipfile.ZipFile(filebytes)
for name in myzipfile.namelist():
    [ ... ]
remitente
fuente
"El objeto StringIO puede aceptar cadenas Unicode o de 8 bits" ¿No significa esto que si el número de bytes que espera escribir no es congruente con 0 mod 8, lanzará una excepción o escribirá datos incorrectos?
ninjagecko
1
En absoluto, ¿por qué solo podría escribir 8 bytes a la vez? Por el contrario, ¿cuándo escribe menos de 8 bits a la vez?
senderle
@ninjagecko: Parece temer un problema si el número de bytes que se espera escribir no es un múltiplo de 8. Eso no se deriva de la declaración sobre StringIO y no tiene ningún fundamento. El problema con StringIO es cuando el usuario mezcla unicode objetos con strobjetos que no son decodificables por la codificación predeterminada del sistema (que es típicamente ascii).
John Machin
1
Pequeño comentario sobre el código anterior: cuando lea varios archivos del .zip, asegúrese de leer los datos uno por uno, porque llamar a zipfile.open dos veces eliminará la referencia en el primero.
scippie
15
Tenga en cuenta que a partir de Python 3 debe usarfrom io import StringIO
Jorge Leitao
81

A continuación se muestra un fragmento de código que utilicé para recuperar el archivo csv comprimido, por favor, eche un vistazo:

Python 2 :

from StringIO import StringIO
from zipfile import ZipFile
from urllib import urlopen

resp = urlopen("http://www.test.com/file.zip")
zipfile = ZipFile(StringIO(resp.read()))
for line in zipfile.open(file).readlines():
    print line

Python 3 :

from io import BytesIO
from zipfile import ZipFile
from urllib.request import urlopen
# or: requests.get(url).content

resp = urlopen("http://www.test.com/file.zip")
zipfile = ZipFile(BytesIO(resp.read()))
for line in zipfile.open(file).readlines():
    print(line.decode('utf-8'))

Aquí filehay una cadena. Para obtener la cadena real que desea pasar, puede usar zipfile.namelist(). Por ejemplo,

resp = urlopen('http://mlg.ucd.ie/files/datasets/bbc.zip')
zipfile = ZipFile(BytesIO(resp.read()))
zipfile.namelist()
# ['bbc.classes', 'bbc.docs', 'bbc.mtx', 'bbc.terms']
Vishal
fuente
26

Me gustaría ofrecer una versión actualizada de Python 3 de la excelente respuesta de Vishal, que estaba usando Python 2, junto con una explicación de las adaptaciones / cambios, que puede que ya se hayan mencionado.

from io import BytesIO
from zipfile import ZipFile
import urllib.request
    
url = urllib.request.urlopen("http://www.unece.org/fileadmin/DAM/cefact/locode/loc162txt.zip")

with ZipFile(BytesIO(url.read())) as my_zip_file:
    for contained_file in my_zip_file.namelist():
        # with open(("unzipped_and_read_" + contained_file + ".file"), "wb") as output:
        for line in my_zip_file.open(contained_file).readlines():
            print(line)
            # output.write(line)

Cambios necesarios:

  • No hay ningún StringIOmódulo en Python 3 (se ha movido a io.StringIO). En su lugar, uso io.BytesIO] 2 , porque manejaremos un bytestream: Docs , también este hilo .
  • urlopen:

Nota:

  • En Python 3, las líneas de salida impresos se verá así: b'some text'. Es de esperar, ya que no son cadenas; recuerde, estamos leyendo un bytestream. Eche un vistazo a la excelente respuesta de Dan04 .

Algunos cambios menores que hice:

  • Yo uso en with ... aslugar de zipfile = ...según los Docs .
  • El script ahora usa .namelist()para recorrer todos los archivos en el zip e imprimir su contenido.
  • Moví la creación del ZipFileobjeto a la withdeclaración, aunque no estoy seguro si eso es mejor.
  • Agregué (y comenté) una opción para escribir la corriente de bytes en el archivo (por archivo en el zip), en respuesta al comentario de NumenorForLife; se agrega "unzipped_and_read_"al comienzo del nombre del archivo y una ".file"extensión (prefiero no usar ".txt"para archivos con cadenas de bytes). La sangría del código, por supuesto, deberá ajustarse si desea utilizarlo.
    • Debe tener cuidado aquí - debido a que tenemos una cadena de bytes, usamos el modo binario, entonces "wb"; Tengo la sensación de que escribir binario abre una lata de gusanos de todos modos ...
  • Estoy usando un archivo de ejemplo, el archivo de texto UN / LOCODE :

Lo que no hice:

  • NumenorForLife preguntó acerca de guardar el zip en el disco. No estoy seguro de lo que quiso decir con eso: ¿descargar el archivo zip? Esa es una tarea diferente; vea la excelente respuesta de Oleh Prypin .

He aquí una forma:

import urllib.request
import shutil

with urllib.request.urlopen("http://www.unece.org/fileadmin/DAM/cefact/locode/2015-2_UNLOCODE_SecretariatNotes.pdf") as response, open("downloaded_file.pdf", 'w') as out_file:
    shutil.copyfileobj(response, out_file)
Zubo
fuente
Si desea escribir todos los archivos en el disco, la forma más fácil es usar my_zip_file.extractall ('my_target') `en lugar de bucle. ¡Pero eso es genial!
MCMZL
¿Pueden ayudarme con esta pregunta ?: stackoverflow.com/questions/62417455/…
Harshit Kakkar
18

escribir en un archivo temporal que reside en la RAM

resulta que el tempfilemódulo ( http://docs.python.org/library/tempfile.html ) tiene exactamente lo siguiente:

tempfile.SpooledTemporaryFile ([max_size = 0 [, mode = 'w + b' [, bufsize = -1 [, suffix = '' [, prefix = 'tmp' [, dir = None]]]]]])

Esta función opera exactamente como lo hace TemporaryFile (), excepto que los datos se ponen en cola en la memoria hasta que el tamaño del archivo excede max_size, o hasta que se llama al método fileno () del archivo, momento en el que el contenido se escribe en el disco y la operación continúa como con TemporaryFile ().

El archivo resultante tiene un método adicional, rollover (), que hace que el archivo se transfiera a un archivo en el disco independientemente de su tamaño.

El objeto devuelto es un objeto similar a un archivo cuyo atributo _file es un objeto StringIO o un objeto de archivo verdadero, dependiendo de si se ha llamado a rollover (). Este objeto similar a un archivo se puede usar en una declaración with, al igual que un archivo normal.

Nuevo en la versión 2.6.

o si es vago y tiene un tmpfs montado /tmpen Linux, puede simplemente crear un archivo allí, pero debe eliminarlo usted mismo y ocuparse de los nombres

ninjagecko
fuente
3
+1: no sabía sobre SpooledTemporaryFile. Mi inclinación aún sería usar StringIO explícitamente, pero es bueno saberlo.
senderle
16

Me gustaría agregar mi respuesta de Python3 para completar:

from io import BytesIO
from zipfile import ZipFile
import requests

def get_zip(file_url):
    url = requests.get(file_url)
    zipfile = ZipFile(BytesIO(url.content))
    zip_names = zipfile.namelist()
    if len(zip_names) == 1:
        file_name = zip_names.pop()
        extracted_file = zipfile.open(file_name)
        return extracted_file
    return [zipfile.open(file_name) for file_name in zip_names]
lababidi
fuente
14

Agregando a las otras respuestas usando solicitudes :

 # download from web

 import requests
 url = 'http://mlg.ucd.ie/files/datasets/bbc.zip'
 content = requests.get(url)

 # unzip the content
 from io import BytesIO
 from zipfile import ZipFile
 f = ZipFile(BytesIO(content.content))
 print(f.namelist())

 # outputs ['bbc.classes', 'bbc.docs', 'bbc.mtx', 'bbc.terms']

Use help (f) para obtener más detalles de las funciones, por ejemplo, extractall () que extrae el contenido en un archivo zip que luego se puede usar con open .

Akson
fuente
Para leer su CSV, haga lo siguiente:with f.open(f.namelist()[0], 'r') as g: df = pd.read_csv(g)
Corey Levinson
3

El ejemplo de Vishal, por muy bueno que sea, confunde cuando se trata del nombre del archivo, y no veo el mérito de redefinir 'zipfile'.

Aquí está mi ejemplo que descarga un zip que contiene algunos archivos, uno de los cuales es un archivo csv que luego leo en un DataFrame de pandas:

from StringIO import StringIO
from zipfile import ZipFile
from urllib import urlopen
import pandas

url = urlopen("https://www.federalreserve.gov/apps/mdrm/pdf/MDRM.zip")
zf = ZipFile(StringIO(url.read()))
for item in zf.namelist():
    print("File in zip: "+  item)
# find the first matching csv file in the zip:
match = [s for s in zf.namelist() if ".csv" in s][0]
# the first line of the file contains a string - that line shall de ignored, hence skiprows
df = pandas.read_csv(zf.open(match), low_memory=False, skiprows=[0])

(Nota, yo uso Python 2.7.13)

Esta es la solución exacta que funcionó para mí. Simplemente lo modifiqué un poco para la versión de Python 3 eliminando StringIO y agregando la biblioteca IO

Versión de Python 3

from io import BytesIO
from zipfile import ZipFile
import pandas
import requests

url = "https://www.nseindia.com/content/indices/mcwb_jun19.zip"
content = requests.get(url)
zf = ZipFile(BytesIO(content.content))

for item in zf.namelist():
    print("File in zip: "+  item)

# find the first matching csv file in the zip:
match = [s for s in zf.namelist() if ".csv" in s][0]
# the first line of the file contains a string - that line shall de     ignored, hence skiprows
df = pandas.read_csv(zf.open(match), low_memory=False, skiprows=[0])
Martien Lubberink
fuente
1

No era obvio en la respuesta de Vishal cuál se suponía que era el nombre del archivo en los casos en los que no hay ningún archivo en el disco. Modifiqué su respuesta para que funcione sin modificaciones para la mayoría de las necesidades.

from StringIO import StringIO
from zipfile import ZipFile
from urllib import urlopen

def unzip_string(zipped_string):
    unzipped_string = ''
    zipfile = ZipFile(StringIO(zipped_string))
    for name in zipfile.namelist():
        unzipped_string += zipfile.open(name).read()
    return unzipped_string
arador
fuente
Esta es una respuesta de Python 2.
Boris
0

Utilice el zipfilemódulo. Para extraer un archivo de una URL, deberá envolver el resultado de una urlopenllamada en un BytesIOobjeto. Esto se debe a que el resultado de una solicitud web devuelta por urlopenno admite la búsqueda:

from urllib.request import urlopen

from io import BytesIO
from zipfile import ZipFile

zip_url = 'http://example.com/my_file.zip'

with urlopen(zip_url) as f:
    with BytesIO(f.read()) as b, ZipFile(b) as myzipfile:
        foofile = myzipfile.open('foo.txt')
        print(foofile.read())

Si ya tiene el archivo descargado localmente, no lo necesita BytesIO, simplemente ábralo en modo binario y pase ZipFiledirectamente a:

from zipfile import ZipFile

zip_filename = 'my_file.zip'

with open(zip_filename, 'rb') as f:
    with ZipFile(f) as myzipfile:
        foofile = myzipfile.open('foo.txt')
        print(foofile.read().decode('utf-8'))

Nuevamente, tenga en cuenta que tiene que openguardar el archivo en modo binario ( 'rb') , no como texto o obtendrá un zipfile.BadZipFile: File is not a zip fileerror.

Es una buena práctica usar todas estas cosas como administradores de contexto con la withdeclaración, para que se cierren correctamente.

Boris
fuente