Download e decompressione di un file .zip senza scrivere su disco


86

Sono riuscito a far funzionare il mio primo script Python che scarica un elenco di file .ZIP da un URL, quindi procede all'estrazione dei file ZIP e li scrive su disco.

Ora non riesco a raggiungere il prossimo passo.

Il mio obiettivo principale è scaricare ed estrarre il file zip e passare il contenuto (dati CSV) tramite un flusso TCP. Preferirei non scrivere effettivamente nessuno dei file zip o estratti su disco se potessi farla franca.

Ecco il mio script attuale che funziona ma sfortunatamente deve scrivere i file su 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" ))

3
Il formato ZIP non è progettato per essere trasmesso in streaming. Utilizza i piè di pagina, il che significa che hai bisogno della fine del file per capire dove appartengono le cose al suo interno, il che significa che devi avere l'intero file prima di poter fare qualsiasi cosa con un sottoinsieme di esso.
Charles Duffy

Risposte:


66

Il mio suggerimento sarebbe quello di utilizzare un StringIOoggetto. Emulano i file, ma risiedono nella memoria. Quindi potresti fare qualcosa del genere:

# 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 più semplicemente (scuse a Vishal):

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

In Python 3 usa BytesIO invece di StringIO:

import zipfile
from io import BytesIO

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

"L'oggetto StringIO può accettare stringhe Unicode o 8 bit" Non significa che se il numero di byte che ci si aspetta di scrivere non è congruente con 0 mod 8, si genererà un'eccezione o si scriveranno dati errati?
ninjagecko

1
Niente affatto - perché dovresti essere in grado di scrivere solo 8 byte alla volta? Al contrario, quando scrivi mai meno di 8 bit alla volta?
mittente

@ninjagecko: Sembri temere un problema se il numero di byte che dovrebbero essere scritti non è un multiplo di 8. Questo non è derivabile dall'affermazione su StringIO ed è abbastanza infondato. Il problema con StringIO è quando l'utente mescola unicode oggetti con stroggetti che non sono decodificabili dalla codifica predefinita del sistema (che è tipicamente ascii).
John Machin

1
Piccolo commento sul codice sopra: quando leggi più file dal .zip, assicurati di leggere i dati uno per uno, perché chiamando zipfile.open due volte rimuoverà il riferimento nella prima.
scippie

15
Nota che a partire da Python 3 devi usarefrom io import StringIO
Jorge Leitao

81

Di seguito è riportato uno snippet di codice che ho usato per recuperare il file CSV zippato, dai un'occhiata:

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'))

Ecco fileuna stringa. Per ottenere la stringa effettiva che desideri passare, puoi utilizzare zipfile.namelist(). Per esempio,

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']

27

Vorrei offrire una versione aggiornata di Python 3 dell'eccellente risposta di Vishal, che utilizzava Python 2, insieme ad alcune spiegazioni degli adattamenti / modifiche, che potrebbero essere già state menzionate.

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)

Modifiche necessarie:

  • Non c'è alcun StringIOmodulo in Python 3 (è stato spostato in io.StringIO). Invece, io uso io.BytesIO] 2 , perché gestiremo un bytestream - Docs , anche questo thread .
  • urlopen:

Nota:

  • In Python 3, le linee di uscita a stampa sarà simile così: b'some text'. Questo è previsto, poiché non sono stringhe: ricorda, stiamo leggendo un bytestream. Dai un'occhiata all'eccellente risposta di Dan04 .

Alcune piccole modifiche che ho apportato:

  • Uso with ... asinvece che zipfile = ...secondo i documenti .
  • Lo script ora utilizza .namelist()per scorrere tutti i file nello zip e stamparne il contenuto.
  • Ho spostato la creazione ZipFiledell'oggetto withnell'istruzione, anche se non sono sicuro che sia meglio.
  • Ho aggiunto (e commentato) un'opzione per scrivere il bytestream su file (per file nello zip), in risposta al commento di NumenorForLife; si aggiunge "unzipped_and_read_"all'inizio del nome del file e di ".file"un'estensione (preferisco non usarlo ".txt"per i file con bytestrings). Il rientro del codice, ovviamente, dovrà essere modificato se si desidera utilizzarlo.
    • Bisogna stare attenti qui - poiché abbiamo una stringa di byte, usiamo la modalità binaria, quindi "wb"; Ho la sensazione che scrivere binario apra comunque un barattolo di vermi ...
  • Sto usando un file di esempio, l' archivio di testo UN / LOCODE :

Cosa non ho fatto:

  • NumenorForLife ha chiesto di salvare lo zip su disco. Non sono sicuro di cosa volesse dire: scaricare il file zip? Questo è un compito diverso; vedi l'eccellente risposta di Oleh Prypin .

Ecco un modo:

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)

Se vuoi scrivere tutti i file su disco, il modo più semplice è usare my_zip_file.extractall ('my_target') `invece di eseguire il loop. Ma è fantastico!
MCMZL

Potete per favore aiutarmi con questa domanda: stackoverflow.com/questions/62417455/...
Harshit Kakkar

18

scrivere in un file temporaneo che risiede nella RAM

si scopre che il tempfilemodulo ( http://docs.python.org/library/tempfile.html ) ha proprio la cosa:

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

Questa funzione opera esattamente come TemporaryFile (), tranne per il fatto che i dati vengono inseriti in spool in memoria fino a quando la dimensione del file supera max_size, o fino a quando non viene chiamato il metodo fileno () del file, a quel punto i contenuti vengono scritti su disco e l'operazione procede come con TemporaryFile ().

Il file risultante ha un metodo aggiuntivo, rollover (), che fa sì che il file venga spostato su un file su disco indipendentemente dalle sue dimensioni.

L'oggetto restituito è un oggetto simile a file il cui attributo _file è un oggetto StringIO o un vero oggetto file, a seconda che sia stato chiamato rollover (). Questo oggetto simile a un file può essere utilizzato in un'istruzione with, proprio come un normale file.

Novità nella versione 2.6.0

o se sei pigro e hai un tmpfs montato /tmpsu Linux, puoi semplicemente creare un file lì, ma devi eliminarlo tu stesso e occuparti della denominazione


3
+1 - non sapevo di SpooledTemporaryFile. La mia inclinazione sarebbe ancora quella di utilizzare StringIO esplicitamente, ma è bene saperlo.
senderle

16

Vorrei aggiungere la mia risposta Python3 per completezza:

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]

14

Aggiungendo alle altre risposte utilizzando le richieste :

 # 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']

Usa help (f) per ottenere maggiori dettagli sulle funzioni per es. Extractall () che estrae il contenuto in un file zip che in seguito può essere usato con open .


Per leggere il tuo CSV, fai:with f.open(f.namelist()[0], 'r') as g: df = pd.read_csv(g)
Corey Levinson

3

L'esempio di Vishal, per quanto ottimo, confonde quando si tratta del nome del file e non vedo il merito di ridefinire 'zipfile'.

Ecco il mio esempio che scarica uno zip che contiene alcuni file, uno dei quali è un file csv che ho successivamente letto in un DataFrame panda:

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, io uso Python 2.7.13)

Questa è la soluzione esatta che ha funzionato per me. L'ho appena modificato un po 'per la versione di Python 3 rimuovendo StringIO e aggiungendo la libreria IO

Versione 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])

1

Non era ovvio nella risposta di Vishal quale doveva essere il nome del file nei casi in cui non ci fosse alcun file sul disco. Ho modificato la sua risposta per funzionare senza modifiche per la maggior parte delle esigenze.

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

Questa è una risposta di Python 2.
Boris

0

Usa il zipfilemodulo. Per estrarre un file da un URL, dovrai racchiudere il risultato di una urlopenchiamata in un BytesIOoggetto. Questo perché il risultato di una richiesta web restituita da urlopennon supporta la ricerca di:

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())

Se hai già il file scaricato in locale, non ti serve BytesIO, basta aprirlo in modalità binaria e passare ZipFiledirettamente 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'))

Di nuovo, nota che devi openil file in modalità binary ( 'rb') , non come testo o otterrai un filezipfile.BadZipFile: File is not a zip file errore.

È buona norma utilizzare tutte queste cose come gestori di contesto con l' withistruzione, in modo che vengano chiuse correttamente.


0

Tutte queste risposte sembrano ingombranti e lunghe. Utilizza le richieste per abbreviare il codice, ad esempio:

import requests, zipfile, io
r = requests.get(zip_file_url)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall("/path/to/directory")
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.