Pobieranie i rozpakowywanie pliku .zip bez zapisywania na dysku

85

Udało mi się uruchomić mój pierwszy skrypt Pythona, który pobiera listę plików ZIP z adresu URL, a następnie przystępuje do wyodrębniania plików ZIP i zapisuje je na dysku.

Nie mogę teraz osiągnąć następnego kroku.

Moim głównym celem jest pobranie i wyodrębnienie pliku zip i przekazanie zawartości (dane CSV) przez strumień TCP. Wolałbym nie zapisywać żadnych spakowanych lub wyodrębnionych plików na dysk, gdybym mógł sobie z tym poradzić.

Oto mój aktualny skrypt, który działa, ale niestety musi zapisać pliki na dysku.

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
źródło
3
Format ZIP nie jest przeznaczony do przesyłania strumieniowego. Używa stopek, co oznacza, że ​​potrzebujesz końca pliku, aby dowiedzieć się, gdzie należy do niego rzeczy, co oznacza, że ​​musisz mieć cały plik, zanim będziesz mógł cokolwiek zrobić z jego podzbiorem.
Charles Duffy

Odpowiedzi:

65

Moją sugestią byłoby użycie StringIOprzedmiotu. Emulują pliki, ale znajdują się w pamięci. Możesz więc zrobić coś takiego:

# 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"

Lub prościej (przepraszam Vishal):

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

W Pythonie 3 użyj BytesIO zamiast StringIO:

import zipfile
from io import BytesIO

filebytes = BytesIO(get_zip_data())
myzipfile = zipfile.ZipFile(filebytes)
for name in myzipfile.namelist():
    [ ... ]
nadawca
źródło
„Obiekt StringIO może akceptować ciągi Unicode lub 8-bitowe” Czy nie oznacza to, że jeśli liczba bajtów, które spodziewasz się zapisać, nie jest zgodna z 0 mod 8, to albo zgłosisz wyjątek, albo napiszesz niepoprawne dane?
ninjagecko
1
Wcale nie - dlaczego miałbyś jednorazowo zapisywać tylko 8 bajtów? I odwrotnie, kiedy zdarza się, że piszesz mniej niż 8 bitów na raz?
senderle
@ninjagecko: Wydaje się, że obawiasz się problemu, jeśli oczekiwana liczba bajtów do zapisania nie jest wielokrotnością 8. Nie da się tego wyprowadzić ze stwierdzenia o StringIO i jest całkowicie bezpodstawne. Problem z StringIO polega na tym, że użytkownik miesza unicode obiekty z strobiektami, których nie można dekodować za pomocą domyślnego kodowania systemu (co jest typowe ascii).
John Machin
1
Mały komentarz do powyższego kodu: kiedy czytasz wiele plików z .zip, upewnij się, że czytasz dane jeden po drugim, ponieważ dwukrotne wywołanie zipfile.open usunie odwołanie w pierwszym.
scippie
15
Zauważ, że od Pythona 3 musisz użyćfrom io import StringIO
Jorge Leitao
81

Poniżej znajduje się fragment kodu, którego użyłem do pobrania spakowanego pliku CSV, spójrz:

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

Oto filesznurek. Aby uzyskać rzeczywisty ciąg, który chcesz przekazać, możesz użyć zipfile.namelist(). Na przykład,

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
źródło
26

Chciałbym zaoferować zaktualizowaną wersję doskonałej odpowiedzi Vishal w Pythonie 3, która korzystała z Pythona 2, wraz z wyjaśnieniem adaptacji / zmian, o których być może już wspomniano.

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)

Potrzebne zmiany:

  • W StringIOPythonie 3 nie ma modułu (został przeniesiony io.StringIO). Zamiast tego używam io.BytesIO] 2 , ponieważ będziemy obsługiwać bytestream - Docs , także ten wątek .
  • urlopen:

Uwaga:

  • W Pythonie 3, drukowane linie wyjściowe będzie wyglądać tak: b'some text'. Jest to oczekiwane, ponieważ nie są to ciągi znaków - pamiętaj, czytamy bajtestream. Spójrz na doskonałą odpowiedź Dan04 .

Kilka drobnych zmian, które wprowadziłem:

  • Używam with ... aszamiast zipfile = ...zgodnie z Docs .
  • Skrypt używa teraz .namelist()do przeglądania wszystkich plików w archiwum zip i drukowania ich zawartości.
  • Przeniosłem tworzenie ZipFileobiektu do withinstrukcji, chociaż nie jestem pewien, czy to lepiej.
  • Dodałem (i wykomentowałem) opcję zapisywania strumienia bajtów do pliku (na plik w zipie), w odpowiedzi na komentarz NumenorForLife; dodaje "unzipped_and_read_"na początku nazwy pliku i ".file"rozszerzenia (wolę nie używać ".txt"dla plików z bajtestami). Wcięcie kodu będzie oczywiście wymagało dostosowania, jeśli chcesz go użyć.
    • Trzeba tutaj uważać - ponieważ mamy ciąg bajtów, używamy trybu binarnego, więc "wb"; Mam wrażenie, że pisanie plików binarnych i tak otwiera puszkę robaków ...
  • Używam przykładowego pliku, archiwum tekstowego UN / LOCODE :

Czego nie zrobiłem:

Oto sposób:

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
źródło
Jeśli chcesz zapisać wszystkie pliki na dysku, łatwiejszym sposobem jest użycie my_zip_file.extractall ('my_target') `zamiast zapętlania. Ale to świetnie!
MCMZL
czy możesz mi pomóc z tym pytaniem: stackoverflow.com/questions/62417455/…
Harshit Kakkar
18

zapisz do pliku tymczasowego, który znajduje się w pamięci RAM

okazuje się, że tempfilemoduł ( http://docs.python.org/library/tempfile.html ) ma właśnie to:

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

Ta funkcja działa dokładnie tak samo, jak robi to TemporaryFile (), z wyjątkiem tego, że dane są buforowane w pamięci, dopóki rozmiar pliku nie przekroczy max_size lub do momentu wywołania metody fileno () pliku, w którym to momencie zawartość jest zapisywana na dysk i operacja przebiega jak w przypadku TemporaryFile ().

Wynikowy plik ma jedną dodatkową metodę, rollover (), która powoduje przeniesienie pliku do pliku na dysku niezależnie od jego rozmiaru.

Zwrócony obiekt to obiekt plikopodobny, którego atrybut _file jest albo obiektem StringIO, albo prawdziwym obiektem plikowym, w zależności od tego, czy wywołano funkcję rollover (). Ten obiekt przypominający plik może być użyty w instrukcji with, tak jak zwykły plik.

Nowość w wersji 2.6.

lub jeśli jesteś leniwy i masz zamontowany tmpfs /tmpw systemie Linux, możesz po prostu utworzyć tam plik, ale musisz go usunąć samodzielnie i zająć się nazewnictwem

ninjagecko
źródło
3
+1 - nie wiedziałem o SpooledTemporaryFile. Nadal wolałbym używać StringIO w sposób jawny, ale dobrze to wiedzieć.
nadawca
16

Chciałbym dodać moją odpowiedź w Pythonie3 dla kompletności:

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
źródło
14

Dodając do innych odpowiedzi za pomocą zapytań :

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

Skorzystaj z pomocy (f), aby uzyskać więcej szczegółów na temat funkcji, np. Extractall (), która wyodrębnia zawartość w pliku zip, którego później można używać z open .

Akson
źródło
Aby przeczytać swój plik CSV, zrób:with f.open(f.namelist()[0], 'r') as g: df = pd.read_csv(g)
Corey Levinson
3

Przykład Vishala, jakkolwiek świetny, wprowadza zamieszanie, jeśli chodzi o nazwę pliku, i nie widzę korzyści z redefinicji „zipfile”.

Oto mój przykład, który pobiera plik zip zawierający niektóre pliki, z których jeden jest plikiem csv, który następnie wczytuję do pandy DataFrame:

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

(Uwaga, używam Pythona 2.7.13)

To jest dokładne rozwiązanie, które u mnie zadziałało. Po prostu poprawiłem go trochę dla wersji Python 3, usuwając StringIO i dodając bibliotekę IO

Wersja Pythona 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
źródło
1

W odpowiedzi Vishala nie było oczywiste, jaka powinna być nazwa pliku w przypadkach, gdy na dysku nie ma pliku. Zmodyfikowałem jego odpowiedź, aby działała bez modyfikacji dla większości potrzeb.

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
oracz
źródło
To jest odpowiedź Pythona 2.
Boris
0

Użyj zipfilemodułu. Aby wyodrębnić plik z adresu URL, musisz zawrzeć wynik urlopenwywołania w BytesIOobiekcie. Dzieje się tak, ponieważ wynik żądania internetowego zwróconego przez urlopennie obsługuje wyszukiwania:

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

Jeśli masz już plik pobrany lokalnie, nie potrzebujesz BytesIO, po prostu otwórz go w trybie binarnym i przejdź ZipFilebezpośrednio do :

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

Ponownie zwróć uwagę, że openplik musi być w trybie binary ( 'rb') , a nie jako tekst, w przeciwnym razie pojawi się zipfile.BadZipFile: File is not a zip filebłąd.

Dobrą praktyką jest używanie wszystkich tych rzeczy jako menedżerów kontekstu z withoświadczeniem, aby zostały poprawnie zamknięte.

Boris
źródło