【问题标题】:Downloading and unzipping a .zip file without writing to disk下载和解压缩 .zip 文件而不写入磁盘
【发布时间】:2022-01-18 10:02:33
【问题描述】:

我已经设法让我的第一个 python 脚本工作,它从 URL 下载 .ZIP 文件列表,然后继续提取 ZIP 文件并将它们写入磁盘。

我现在不知如何实现下一步。

我的主要目标是下载和解压缩 zip 文件并通过 TCP 流传递内容(CSV 数据)。如果可以的话,我宁愿不实际将任何 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" ))

【问题讨论】:

  • ZIP 格式不是为流式传输而设计的。它使用页脚,这意味着您需要文件的 end 来确定其中的内容,这意味着您需要拥有整个文件才能对其中的一个子集进行任何操作。

标签: python unzip


【解决方案1】:

写入驻留在 RAM 中的临时文件

原来tempfile 模块(http://docs.python.org/library/tempfile.html)就是这样:

tempfile.SpooledTemporaryFile([max_size=0[, 模式='w+b'[, bufsize=-1[, 后缀=''[, prefix='tmp'[, dir=None]]]]]])

这个 功能完全按照 TemporaryFile() 会,除了数据 假脱机在内存中,直到文件 size 超过 max_size,或者直到 文件的 fileno() 方法被调用,在 内容写在哪一点 磁盘和操作继续进行 临时文件()。

生成的文件有一个额外的 方法,rollover(),这会导致 转存到磁盘文件的文件 不管它的大小。

返回的对象是类文件 _file 属性为 StringIO 对象或真实文件 对象,取决于是否 rollover() 已被调用。这 类文件对象可以在 with 中使用 声明,就像一个普通的文件。

2.6 版中的新功能。

或者如果你很懒,并且你在 Linux 上有一个 tmpfs 挂载的/tmp,你可以在那里创建一个文件,但你必须自己删除它并处理命名

【讨论】:

  • +1 -- 不知道 SpooledTemporaryFile。我仍然倾向于明确地使用 StringIO,但这很高兴知道。
【解决方案2】:

我的建议是使用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"

或者更简单(向 Vishal 道歉):

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 mod 8 不一致,那么您将要么抛出异常或写入错误数据?
  • 一点也不——为什么一次只能写入 8 个字节?相反,你什么时候一次写入少于 8 位?
  • @ninjagecko:如果预期写入的字节数不是 8 的倍数,您似乎担心会出现问题。这不是从有关 StringIO 的陈述中推导出来的,而且毫无根据。 StringIO 的问题在于,当用户将unicode 对象与系统默认编码(通常为ascii)无法解码的str 对象混合
  • 对上面代码的小注释:当你从.zip中读取多个文件时,一定要一个一个地把数据读出来,因为调用zipfile.open两次会去掉里面的引用首先。
  • 请注意,从 Python 3 开始,您必须使用 from io import StringIO
【解决方案3】:

下面是我用来获取压缩csv文件的代码sn-p,请看一下:

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

【讨论】:

  • 请注意,对类实例和库使用相同的名称有点令人困惑 - 标识符 zipfile 在此代码中重新定义。
【解决方案4】:

在 Vishal 的回答中,在磁盘上没有文件的情况下文件名应该是什么并不明显。我已经修改了他的答案,无需修改即可满足大多数需求。

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 的答案。
【解决方案5】:

为了完整起见,我想添加我的 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))
    files = [zipfile.open(file_name) for file_name in zipfile.namelist()]
    return files.pop() if len(files) == 1 else files

        

【讨论】:

    【解决方案6】:

    我想提供 Vishal 出色答案的更新 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)
    

    必要的改变:

    • Python 3 中没有StringIO 模块(已移至io.StringIO)。相反,我使用io.BytesIO]2,因为我们将处理一个字节流——Docs,还有this thread
    • 网址打开:

    注意:

    • 在 Python 3 中,打印的输出行将如下所示:b'some text'。这是意料之中的,因为它们不是字符串 - 请记住,我们正在读取字节流。看看Dan04's excellent answer

    我做了一些小改动:

    • 根据the Docs,我使用with ... as而不是zipfile = ...
    • 脚本现在使用 .namelist() 循环浏览 zip 中的所有文件并打印其内容。
    • 我将 ZipFile 对象的创建移到了 with 语句中,尽管我不确定这是否更好。
    • 为了回应 NumenorForLife 的评论,我添加(并注释掉)将字节流写入文件(zip 中的每个文件)的选项;它将"unzipped_and_read_" 添加到文件名的开头和".file" 扩展名(我不喜欢将".txt" 用于带有字节串的文件)。当然,如果您想使用它,则需要调整代码的缩进。
      • 这里需要小心——因为我们有一个字节串,我们使用二进制模式,所以"wb";我有一种感觉,编写二进制文件无论如何都会打开一罐蠕虫......
    • 我正在使用一个示例文件,UN/LOCODE text archive:

    我没有做的事:

    • NumenorForLife 询问有关将 zip 保存到磁盘的问题。我不确定他的意思——下载 zip 文件?这是一项不同的任务。见Oleh Prypin's excellent answer

    这是一种方法:

    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')` 而不是循环。但这很棒!
    • 你能帮我解决这个问题吗:stackoverflow.com/questions/62417455/…
    【解决方案7】:

    Vishal 的示例,无论多么出色,在文件名方面都令人困惑,我看不出重新定义“zipfile”的好处。

    这是我下载包含一些文件的 zip 的示例,其中一个是我随后读入 pandas DataFrame 的 csv 文件:

    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)

    这是对我有用的确切解决方案。我只是通过删除 StringIO 并添加 IO 库对 Python 3 版本进行了一些调整

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

    【讨论】:

      【解决方案8】:

      使用请求添加到其他答案:

       # 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 文件中的内容,以后可以与 with open 一起使用。

      【讨论】:

      • 要阅读您的 CSV,请执行以下操作:with f.open(f.namelist()[0], 'r') as g: df = pd.read_csv(g)
      【解决方案9】:

      使用zipfile 模块。要从 URL 中提取文件,您需要将 urlopen 调用的结果包装在 BytesIO 对象中。这是因为urlopen返回的web请求结果不支持搜索:

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

      再次注意,您必须openbinary ('rb') mode 中的文件,而不是文本,否则您将收到zipfile.BadZipFile: File is not a zip file 错误。

      通过with 语句将所有这些东西用作上下文管理器是一种很好的做法,这样它们就会被正确关闭。

      【讨论】:

        【解决方案10】:

        所有这些答案都显得过于庞大和冗长。使用requests缩短代码,例如:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-01-22
          • 1970-01-01
          相关资源
          最近更新 更多