Baixar e descompactar um arquivo .zip sem gravar no disco

86

Consegui fazer meu primeiro script Python funcionar, o qual baixa uma lista de arquivos .ZIP de uma URL e, em seguida, extrai os arquivos ZIP e os grava no disco.

Agora não consigo dar o próximo passo.

Meu objetivo principal é baixar e extrair o arquivo zip e passar o conteúdo (dados CSV) por meio de um fluxo TCP. Eu preferiria não gravar nenhum dos arquivos compactados ou extraídos no disco se eu pudesse me safar.

Aqui está meu script atual que funciona, mas infelizmente tem que gravar os arquivos no 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" ))
user714415
fonte
3
O formato ZIP não foi projetado para ser transmitido. Ele usa rodapés, o que significa que você precisa do final do arquivo para descobrir onde as coisas pertencem a ele, o que significa que você precisa ter o arquivo inteiro antes de fazer qualquer coisa com um subconjunto dele.
Charles Duffy

Respostas:

66

Minha sugestão seria usar um StringIOobjeto. Eles emulam arquivos, mas residem na memória. Então você poderia fazer algo assim:

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

Ou mais simplesmente (desculpas a Vishal):

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

No Python 3, use BytesIO em vez de StringIO:

import zipfile
from io import BytesIO

filebytes = BytesIO(get_zip_data())
myzipfile = zipfile.ZipFile(filebytes)
for name in myzipfile.namelist():
    [ ... ]
remetente
fonte
"O objeto StringIO pode aceitar strings Unicode ou de 8 bits" Isso não significa que se o número de bytes que você espera gravar não for congruente com 0 mod 8, você lançará uma exceção ou gravará dados incorretos?
ninjagecko
1
De forma alguma - por que você só conseguiria gravar 8 bytes por vez? Por outro lado, quando você escreve menos de 8 bits por vez?
remetente
@ninjagecko: Você parece temer um problema se o número de bytes que se espera que sejam gravados não seja um múltiplo de 8. Isso não é derivado da declaração sobre StringIO e é totalmente sem base. O problema com StringIO é quando o usuário mistura unicode objetos com strobjetos que não são decodificáveis ​​pela codificação padrão do sistema (que normalmente é ascii).
John Machin,
1
Pequeno comentário sobre o código acima: ao ler vários arquivos do .zip, certifique-se de ler os dados um por um, porque chamar zipfile.open duas vezes removerá a referência no primeiro.
scippie,
15
Observe que a partir de Python 3 você tem que usarfrom io import StringIO
Jorge Leitao
81

Abaixo está um snippet de código que usei para buscar o arquivo csv compactado, por favor, dê uma olhada:

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

Aqui fileestá uma string. Para obter a string real que deseja passar, você pode usar zipfile.namelist(). Por exemplo,

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
fonte
27

Eu gostaria de oferecer uma versão atualizada do Python 3 da excelente resposta de Vishal, que estava usando Python 2, junto com algumas explicações sobre as adaptações / mudanças, que podem ter sido mencionadas.

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)

Mudanças necessárias:

  • Não há StringIOmódulo no Python 3 (ele foi movido para io.StringIO). Em vez disso, eu uso io.BytesIO] 2 , porque estaremos lidando com um bytestream - Docs , também este tópico .
  • urlopen:

Nota:

  • Em Python 3, as linhas de saída impressa ficará assim: b'some text'. Isso é esperado, pois não são strings - lembre-se, estamos lendo um bytestream. Dê uma olhada na excelente resposta de Dan04 .

Algumas pequenas alterações que fiz:

  • Eu uso em with ... asvez de de zipfile = ...acordo com o Docs .
  • O script agora usa .namelist()para percorrer todos os arquivos no zip e imprimir seu conteúdo.
  • Mudei a criação do ZipFileobjeto para a withinstrução, embora não tenha certeza se isso é melhor.
  • Eu adicionei (e comentei) uma opção para gravar o bytestream no arquivo (por arquivo no zip), em resposta ao comentário de NumenorForLife; adiciona "unzipped_and_read_"ao início do nome do arquivo e uma ".file"extensão (prefiro não usar ".txt"para arquivos com bytes). O recuo do código, é claro, precisa ser ajustado se você quiser usá-lo.
    • É preciso ter cuidado aqui - porque temos uma string de bytes, usamos o modo binário "wb"; Tenho a sensação de que escrever binário abre uma lata de vermes de qualquer maneira ...
  • Estou usando um arquivo de exemplo, o arquivo de texto UN / LOCODE :

O que eu não fiz:

  • NumenorForLife perguntou sobre como salvar o zip no disco. Não tenho certeza do que ele quis dizer com isso - baixar o arquivo zip? Essa é uma tarefa diferente; veja a excelente resposta de Oleh Prypin .

Aqui está uma maneira:

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
fonte
Se você deseja gravar todos os arquivos no disco, a maneira mais fácil é usar my_zip_file.extractall ('my_target') `ao invés de fazer um loop. Mas isso é ótimo!
MCMZL
você pode me ajudar com esta questão: stackoverflow.com/questions/62417455/…
Harshit Kakkar
18

escrever em um arquivo temporário que reside na RAM

Acontece que o tempfilemódulo ( http://docs.python.org/library/tempfile.html ) tem exatamente a coisa:

tempfile.SpooledTemporaryFile ([max_size = 0 [, modo = 'w + b' [, bufsize = -1 [, sufixo = '' [, prefixo = 'tmp' [, dir = Nenhum]]]]]])

Esta função opera exatamente como TemporaryFile (), exceto que os dados são colocados em spool na memória até que o tamanho do arquivo exceda max_size, ou até que o método fileno () do arquivo seja chamado, momento em que o conteúdo é gravado no disco e a operação prossegue como com TemporaryFile ().

O arquivo resultante tem um método adicional, rollover (), que faz com que o arquivo passe para um arquivo em disco, independentemente de seu tamanho.

O objeto retornado é um objeto semelhante a um arquivo cujo atributo _file é um objeto StringIO ou um objeto de arquivo verdadeiro, dependendo se rollover () foi chamado. Este objeto semelhante a um arquivo pode ser usado em uma instrução with, assim como um arquivo normal.

Novo na versão 2.6.

ou se você for preguiçoso e tiver um tmpfs montado /tmpno Linux, você pode simplesmente fazer um arquivo lá, mas você mesmo tem que deletar e lidar com a nomenclatura

ninjagecko
fonte
3
+1 - não sabia sobre SpooledTemporaryFile. Minha inclinação ainda seria usar StringIO explicitamente, mas é bom saber disso.
remetente de
16

Eu gostaria de adicionar minha resposta 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
fonte
14

Somando-se a outras respostas usando solicitações :

 # 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 obter mais detalhes de funções para, por exemplo, extractall () que extrai o conteúdo de um arquivo zip que mais tarde pode ser usado com open .

Akson
fonte
Para ler seu CSV, faça:with f.open(f.namelist()[0], 'r') as g: df = pd.read_csv(g)
Corey Levinson
3

O exemplo de Vishal, por maior que seja, confunde quando se trata do nome do arquivo, e não vejo o mérito de redefinir 'zipfile'.

Aqui está meu exemplo que baixa um zip que contém alguns arquivos, um dos quais é um arquivo csv que posteriormente li em um DataFrame do 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, eu uso Python 2.7.13)

Esta é a solução exata que funcionou para mim. Acabei de ajustar um pouco para a versão Python 3 removendo StringIO e adicionando biblioteca IO

Versão 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
fonte
1

Não era óbvio na resposta de Vishal qual deveria ser o nome do arquivo nos casos em que não havia arquivo no disco. Modifiquei sua resposta para funcionar sem modificações para a maioria das necessidades.

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
lavrador
fonte
Esta é uma resposta do Python 2.
Boris
0

Use o zipfilemódulo. Para extrair um arquivo de um URL, você precisará envolver o resultado de uma urlopenchamada em um BytesIOobjeto. Isso ocorre porque o resultado de uma solicitação da web retornada por urlopennão suporta a busca:

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 você já fez o download do arquivo localmente, não é necessário BytesIO, basta abri-lo em modo binário e passar ZipFilediretamente para :

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

Mais uma vez, observe que você precisa opendo arquivo no modo binary ( 'rb') , não como texto ou você obterá um zipfile.BadZipFile: File is not a zip fileerro.

É uma boa prática usar todas essas coisas como gerenciadores de contexto com a withinstrução, para que sejam fechadas corretamente.

Boris
fonte
0

Todas essas respostas parecem volumosas e longas. Use solicitações para encurtar o código, por exemplo:

import requests, zipfile, io
r = requests.get(zip_file_url)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall("/path/to/directory")
Alex
fonte