Скачивание и разархивирование файла .zip без записи на диск

86

Мне удалось заставить работать мой первый скрипт Python, который загружает список файлов .ZIP из URL-адреса, а затем переходит к извлечению файлов ZIP и записывает их на диск.

Сейчас я не могу сделать следующий шаг.

Моя основная цель - загрузить и извлечь zip-файл и передать его содержимое (данные CSV) через поток TCP. Я бы предпочел не записывать на диск какие-либо zip-архивы или извлеченные файлы, если бы это сошло мне с рук.

Вот мой текущий сценарий, который работает, но, к сожалению, должен записывать файлы на диск.

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
источник
3
Формат ZIP не предназначен для потоковой передачи. Он использует нижние колонтитулы, что означает, что вам нужен конец файла, чтобы определить, где что-то внутри него, а это означает, что вам нужно иметь весь файл, прежде чем вы сможете что-либо делать с его подмножеством.
Чарльз Даффи

Ответы:

66

Я предлагаю использовать StringIOобъект. Они имитируют файлы, но находятся в памяти. Итак, вы могли бы сделать что-то вроде этого:

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

Или проще (извинения перед Вишалом):

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

В Python 3 используйте BytesIO вместо StringIO:

import zipfile
from io import BytesIO

filebytes = BytesIO(get_zip_data())
myzipfile = zipfile.ZipFile(filebytes)
for name in myzipfile.namelist():
    [ ... ]
отправитель
источник
«Объект StringIO может принимать либо Unicode, либо 8-битные строки» Не означает ли это, что если количество байтов, которое вы ожидаете записать, не совпадает с 0 по модулю 8, то вы либо вызовете исключение, либо запишете неверные данные?
ninjagecko
1
Вовсе нет - почему вы можете записывать только 8 байтов за раз? И наоборот, когда вы когда-нибудь записываете меньше 8 бит за раз?
senderle
@ninjagecko: Похоже, вы опасаетесь проблемы, если количество байтов, которые должны быть записаны, не кратно 8. Это не выводится из утверждения о StringIO и совершенно необоснованно. Проблема с StringIO заключается в том, что пользователь смешивает unicode объекты с strобъектами, которые не могут быть декодированы системной кодировкой по умолчанию (которая обычно такова ascii).
Джон Мачин
1
Небольшой комментарий к приведенному выше коду: когда вы читаете несколько файлов из .zip, убедитесь, что вы считываете данные один за другим, потому что вызов zipfile.open два раза удалит ссылку в первом.
scippie 06
15
Обратите внимание, что начиная с Python 3 вы должны использоватьfrom io import StringIO
Хорхе Лейтао
81

Ниже приведен фрагмент кода, который я использовал для получения заархивированного CSV-файла, пожалуйста, посмотрите:

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

Вот fileстрока. Чтобы получить фактическую строку, которую вы хотите передать, вы можете использовать zipfile.namelist(). Например,

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
источник
27

Я хотел бы предложить обновленную версию Python 3 отличного ответа Вишала, в котором использовался Python 2, вместе с некоторым объяснением адаптаций / изменений, которые, возможно, уже упоминались.

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)

Необходимые изменения:

  • В StringIOPython 3 нет модуля (он был перемещен io.StringIO). Вместо этого я использую io.BytesIO] 2 , потому что мы будем обрабатывать байтовый поток - Docs , а также этот поток .
  • urlopen:

Заметка:

  • В Python 3, отпечатанные выходные линии будут выглядеть так: b'some text'. Это ожидается, поскольку они не являются строками - помните, мы читаем байтовый поток. Взгляните на отличный ответ Dan04 .

Я сделал несколько небольших изменений:

  • Использую with ... asвместо zipfile = ...согласно Документам .
  • Сценарий теперь использует .namelist()для циклического перебора всех файлов в zip-архиве и печати их содержимого.
  • Я переместил создание ZipFileобъекта в withоператор, хотя не уверен, что это лучше.
  • Я добавил (и закомментировал) возможность записывать байтовый поток в файл (для каждого файла в zip-архиве) в ответ на комментарий NumenorForLife; он добавляет "unzipped_and_read_"в начало имени файла и ".file"расширение (я предпочитаю не использовать ".txt"для файлов со строками байтов). Отступы в коде, конечно, нужно будет отрегулировать, если вы хотите его использовать.
    • Здесь нужно быть осторожным - поскольку у нас есть байтовая строка, мы используем двоичный режим, поэтому "wb"; У меня такое чувство, что написание двоичного кода в любом случае открывает банку червей ...
  • Я использую файл примера, текстовый архив UN / LOCODE :

Чего я не делал:

  • NumenorForLife спросил о сохранении zip-архива на диск. Я не уверен, что он имел в виду - скачивание zip-файла? Это другая задача; см . отличный ответ Олега Припина .

Вот способ:

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)
Зубо
источник
Если вы хотите записать все файлы на диск, более простой способ - использовать my_zip_file.extractall ('my_target') `вместо цикла. Но это здорово!
MCMZL 02
не могли бы вы помочь мне с этим вопросом: stackoverflow.com/questions/62417455/…
Harshit Kakkar
18

записать во временный файл, который находится в ОЗУ

оказывается, что в tempfileмодуле ( http://docs.python.org/library/tempfile.html ) есть как раз то, что нужно:

tempfile.SpooledTemporaryFile ([max_size = 0 [, mode = 'w + b' [, bufsize = -1 [, суффикс = '' [, prefix = 'tmp' [, dir = None]]]]]])

Эта функция работает точно так же, как TemporaryFile (), за исключением того, что данные буферизируются в памяти до тех пор, пока размер файла не превысит max_size или пока не будет вызван метод файла fileno (), после чего содержимое записывается на диск, и операция продолжается, как с TemporaryFile. ().

У результирующего файла есть один дополнительный метод, rollover (), который вызывает перенос файла в файл на диске независимо от его размера.

Возвращаемый объект является файловым объектом, атрибут _file которого является либо объектом StringIO, либо истинным файловым объектом, в зависимости от того, был ли вызван rollover (). Этот файловый объект может использоваться в операторе with, как обычный файл.

Новое в версии 2.6.

или если вы ленивы и у вас установлен tmpfs /tmpв Linux, вы можете просто создать там файл, но вам придется удалить его самостоятельно и разобраться с именами

ниндзягеко
источник
3
+1 - не знал о SpooledTemporaryFile. Я по-прежнему склонен использовать StringIO явно, но это полезно знать.
senderle
16

Я хотел бы добавить свой ответ Python3 для полноты:

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

Добавление к другим ответам с помощью запросов :

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

Используйте help (f) для получения дополнительных сведений о функциях, например, extractall (), которая извлекает содержимое из zip-файла, которое позже можно использовать с open .

Аксон
источник
Чтобы прочитать ваш CSV, выполните:with f.open(f.namelist()[0], 'r') as g: df = pd.read_csv(g)
Кори Левинсон
3

Пример Вишала, каким бы великолепным он ни был, сбивает с толку, когда дело доходит до имени файла, и я не вижу достоинств переопределения «zipfile».

Вот мой пример, который загружает zip-архив, содержащий некоторые файлы, один из которых является файлом csv, который я впоследствии прочитал в pandas 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])

(Обратите внимание, я использую Python 2.7.13)

Это точное решение, которое сработало для меня. Я просто немного подправил его для версии Python 3, удалив StringIO и добавив библиотеку ввода-вывода.

Версия 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

В ответе Вишала не было очевидно, каким должно быть имя файла в тех случаях, когда на диске нет файла. Я изменил его ответ, чтобы он работал без изменений для большинства нужд.

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
пахарь
источник
Это ответ Python 2.
Boris
0

Используйте zipfileмодуль. Чтобы извлечь файл из URL-адреса, вам необходимо обернуть результат urlopenвызова в BytesIOобъект. Это связано с тем, что результат веб-запроса, возвращенный urlopenне поддерживает поиск:

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

Если у вас уже есть файл, загруженный локально, вам не нужно BytesIO, просто откройте его в двоичном режиме и перейдите ZipFileнапрямую:

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

Опять же, обратите внимание, что вы должны openиспользовать файл в двоичном ( 'rb') режиме , а не в виде текста, иначе вы получите сообщение zipfile.BadZipFile: File is not a zip fileоб ошибке.

Хорошая практика - использовать все эти вещи в качестве менеджеров контекста с with оператором, чтобы они были закрыты должным образом.

Борис
источник
0

Все эти ответы кажутся громоздкими и длинными. Используйте запросы для сокращения кода, например:

import requests, zipfile, io
r = requests.get(zip_file_url)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall("/path/to/directory")
Alex
источник