Herunterladen und Entpacken einer ZIP-Datei ohne Schreiben auf die Festplatte

83

Ich habe es geschafft, mein erstes Python-Skript zum Laufen zu bringen, das eine Liste von ZIP-Dateien von einer URL herunterlädt und dann die ZIP-Dateien extrahiert und auf die Festplatte schreibt.

Ich bin jetzt ratlos, den nächsten Schritt zu erreichen.

Mein primäres Ziel ist es, die Zip-Datei herunterzuladen und zu extrahieren und den Inhalt (CSV-Daten) über einen TCP-Stream zu übergeben. Ich würde es vorziehen, keine der Zip- oder extrahierten Dateien auf die Festplatte zu schreiben, wenn ich damit durchkommen könnte.

Hier ist mein aktuelles Skript, das funktioniert, aber leider die Dateien auf die Festplatte schreiben muss.

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" ))
user714415
quelle
3
Das ZIP-Format kann nicht gestreamt werden. Es werden Fußzeilen verwendet, dh Sie benötigen das Ende der Datei, um herauszufinden, wo sich die Dinge befinden. Dies bedeutet, dass Sie die gesamte Datei haben müssen, bevor Sie mit einer Teilmenge davon etwas tun können.
Charles Duffy

Antworten:

65

Mein Vorschlag wäre, ein StringIOObjekt zu verwenden. Sie emulieren Dateien, befinden sich jedoch im Speicher. Sie könnten also so etwas tun:

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

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"

Oder einfacher (Entschuldigung an Vishal):

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

Verwenden Sie in Python 3 BytesIO anstelle von StringIO:

filebytes = io.BytesIO(get_zip_data())
myzipfile = zipfile.ZipFile(filebytes)
for name in myzipfile.namelist():
    [ ... ]
senderle
quelle
"Das StringIO-Objekt kann entweder Unicode- oder 8-Bit-Zeichenfolgen akzeptieren." Bedeutet dies nicht, dass Sie entweder eine Ausnahme auslösen oder falsche Daten schreiben, wenn die Anzahl der zu schreibenden Bytes nicht mit 0 mod 8 übereinstimmt?
Ninjagecko
1
Überhaupt nicht - warum sollten Sie nur 8 Bytes gleichzeitig schreiben können? Umgekehrt, wann schreiben Sie jemals weniger als 8 Bits gleichzeitig?
senderle
@ninjagecko: Sie scheinen ein Problem zu befürchten, wenn die Anzahl der zu schreibenden Bytes kein Vielfaches von 8 ist. Das lässt sich nicht aus der Aussage über StringIO ableiten und ist ziemlich unbegründet. Das Problem mit StringIO besteht darin, dass der Benutzer Objekte mit Objekten mischt , die durch die Standardcodierung des Systems (die normalerweise vorhanden ist ) nicht decodierbar sind . unicodestrascii
John Machin
1
Kleiner Kommentar zum obigen Code: Wenn Sie mehrere Dateien aus der ZIP-Datei lesen, stellen Sie sicher, dass Sie die Daten einzeln auslesen, da durch zweimaliges Aufrufen von zipfile.open die Referenz in der ersten entfernt wird.
Scippie
15
Beachten Sie, dass Sie ab Python 3 verwenden müssenfrom io import StringIO
Jorge Leitao
80

Unten ist ein Codefragment, das ich zum Abrufen einer gezippten CSV-Datei verwendet habe. Schauen Sie sich das an:

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

Hier fileist eine Zeichenfolge. Um die tatsächliche Zeichenfolge zu erhalten, die Sie übergeben möchten, können Sie verwenden zipfile.namelist(). Zum Beispiel,

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
quelle
25

Ich möchte eine aktualisierte Python 3-Version von Vishals ausgezeichneter Antwort anbieten, die Python 2 verwendet, zusammen mit einigen Erläuterungen zu den Anpassungen / Änderungen, die möglicherweise bereits erwähnt wurden.

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)

Notwendige Änderungen:

Hinweis:

  • In Python 3 sehen die gedruckten Ausgabezeilen folgendermaßen aus : b'some text'. Dies wird erwartet, da es sich nicht um Zeichenfolgen handelt. Denken Sie daran, wir lesen einen Bytestream. Schauen Sie sich die hervorragende Antwort von Dan04 an .

Ein paar kleine Änderungen, die ich vorgenommen habe:

  • Ich benutze with ... asstatt zipfile = ...nach den Docs .
  • Das Skript verwendet jetzt .namelist(), um alle Dateien in der Zip-Datei zu durchlaufen und deren Inhalt zu drucken.
  • Ich habe die Erstellung des ZipFileObjekts in die withAnweisung verschoben , obwohl ich nicht sicher bin, ob das besser ist.
  • Als Antwort auf den Kommentar von NumenorForLife habe ich eine Option hinzugefügt (und auskommentiert), um den Bytestream in eine Datei (pro Datei in der Zip-Datei) zu schreiben. Es fügt "unzipped_and_read_"den Anfang des Dateinamens und eine ".file"Erweiterung hinzu (ich bevorzuge es, nicht ".txt"für Dateien mit Bytestrings zu verwenden). Der Einzug des Codes muss natürlich angepasst werden, wenn Sie ihn verwenden möchten.
    • Hier muss man vorsichtig sein - da wir eine Byte-Zeichenfolge haben, verwenden wir den Binärmodus "wb". Ich habe das Gefühl, dass das Schreiben von Binärdateien sowieso eine Dose Würmer öffnet ...
  • Ich verwende eine Beispieldatei, das UN / LOCODE-Textarchiv :

Was ich nicht getan habe:

  • NumenorForLife fragte nach dem Speichern der Zip-Datei auf der Festplatte. Ich bin mir nicht sicher, was er damit gemeint hat - die Zip-Datei herunterladen? Das ist eine andere Aufgabe; siehe Oleh Prypins ausgezeichnete Antwort .

Hier ist ein Weg:

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
quelle
Wenn Sie alle Dateien auf die Festplatte schreiben möchten, ist es einfacher, my_zip_file.extractall ('my_target') anstelle einer Schleife zu verwenden. Aber das ist großartig!
MCMZL
Kannst
?:
18

Schreiben Sie in eine temporäre Datei, die sich im RAM befindet

Es stellt sich heraus, dass das tempfileModul ( http://docs.python.org/library/tempfile.html ) genau das Richtige hat:

tempfile.SpooledTemporaryFile ([max_size = 0 [, mode = 'w + b' [, bufsize = -1 [, Suffix = '' [, Präfix = 'tmp' [, dir = None]]]]])

Diese Funktion funktioniert genau wie TemporaryFile (), außer dass Daten im Speicher gespoolt werden, bis die Dateigröße max_size überschreitet oder bis die fileno () -Methode der Datei aufgerufen wird. Zu diesem Zeitpunkt wird der Inhalt auf die Festplatte geschrieben und der Vorgang wird wie bei TemporaryFile fortgesetzt ().

Die resultierende Datei verfügt über eine zusätzliche Methode, rollover (), mit der die Datei unabhängig von ihrer Größe in eine Datei auf der Festplatte übertragen wird.

Das zurückgegebene Objekt ist ein dateiähnliches Objekt, dessen _file-Attribut entweder ein StringIO-Objekt oder ein echtes Dateiobjekt ist, je nachdem, ob rollover () aufgerufen wurde. Dieses dateiähnliche Objekt kann wie eine normale Datei in einer with-Anweisung verwendet werden.

Neu in Version 2.6.

oder wenn Sie faul sind und ein tmpfs /tmpunter Linux gemountet haben , können Sie dort einfach eine Datei erstellen, diese aber selbst löschen und sich mit der Benennung befassen

Ninjagecko
quelle
3
+1 - wusste nichts über SpooledTemporaryFile. Meine Neigung wäre immer noch, StringIO explizit zu verwenden, aber das ist gut zu wissen.
senderle
16

Der Vollständigkeit halber möchte ich meine Python3-Antwort hinzufügen:

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
quelle
13

Hinzufügen zu den anderen Antworten mithilfe von Anfragen :

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

Verwenden Sie die Hilfe (f) , um weitere Funktionsdetails zu erhalten, z. B. extractall (), das den Inhalt in einer Zip-Datei extrahiert, die später mit open verwendet werden kann .

Akson
quelle
Um Ihre CSV zu lesen, gehen Sie wie folgt vor:with f.open(f.namelist()[0], 'r') as g: df = pd.read_csv(g)
Corey Levinson
3

Vishals Beispiel, so großartig es auch sein mag, verwirrt, wenn es um den Dateinamen geht, und ich sehe keinen Vorteil darin, 'zipfile' neu zu definieren.

Hier ist mein Beispiel, das eine Zip-Datei herunterlädt, die einige Dateien enthält. Eine davon ist eine CSV-Datei, die ich anschließend in einen Pandas-DataFrame einlese:

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

(Hinweis, ich verwende Python 2.7.13)

Dies ist genau die Lösung, die für mich funktioniert hat. Ich habe es nur ein wenig für die Python 3-Version optimiert, indem ich StringIO entfernt und eine E / A-Bibliothek hinzugefügt habe

Python 3 Version

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
quelle
1

In Vishals Antwort war nicht ersichtlich, wie der Dateiname lauten sollte, wenn sich keine Datei auf der Festplatte befindet. Ich habe seine Antwort so geändert, dass sie für die meisten Anforderungen ohne Änderung funktioniert.

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
Pflüger
quelle
Dies ist eine Python 2-Antwort.
Boris
0

Verwenden Sie das zipfileModul. Um eine Datei aus einer URL zu extrahieren, müssen Sie das Ergebnis eines urlopenAufrufs in ein BytesIOObjekt einschließen. Dies liegt daran, dass das Ergebnis einer von zurückgegebenen urlopenWebanforderung die Suche nicht unterstützt:

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

Wenn Sie die Datei bereits lokal heruntergeladen haben, brauchen Sie sie nicht. BytesIOÖffnen Sie sie einfach im Binärmodus und übergeben Sie sie ZipFiledirekt an:

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

Beachten Sie erneut, dass Sie opendie Datei im binären ( 'rb') Modus verwenden müssen , nicht als Text, da sonst eine zipfile.BadZipFile: File is not a zip fileFehlermeldung angezeigt wird.

Es ist empfehlenswert, all diese Dinge als Kontextmanager für die withAnweisung zu verwenden, damit sie ordnungsgemäß geschlossen werden.

Boris
quelle