将从网络服务器获取的解压文件写入磁盘
我可以获取一个文件,它的 content-encoding
是 gzip
。
这是不是意味着服务器存储的是压缩文件?那对于存储为压缩的 zip 或 7z 文件的情况也是这样吗?
如果是这样的话(假设 durl
是一个 zip 文件)
>>> durl = 'https://db.tt/Kq0byWzW'
>>> dresp = requests.get(durl, allow_redirects=True, stream=True)
>>> dresp.headers['content-encoding']
'gzip'
>>> r = requests.get(durl, stream=True)
>>> data = r.raw.read(decode_content=True)
但是数据出来却是空的,而我想要在提取这个 zip 文件到磁盘时直接获取数据!!
3 个回答
你需要区分一下 内容编码(不要和 传输编码搞混了)和 内容类型。
简单来说,content-type
是你想获取的资源的媒体类型(也就是实际的文件类型)。而 content-encoding
是在发送给客户端之前对数据进行的任何修改。
假设你想获取一个名为 "foo.txt" 的资源。它的内容类型可能是 text/plain
。除此之外,数据在传输过程中可能会被修改,这就是 content-encoding
。所以在这个例子中,你可以有一个内容类型为 text/plain
,同时内容编码为 gzip
。这意味着在服务器将文件发送出去之前,它会使用 gzip
对文件进行压缩。这样在网络上传输的只有压缩后的数据,而不是原始文件的字节(foo.txt
)。
客户端的工作就是根据这些头信息来处理数据。
现在,我不太确定 requests
或底层的 Python 库是否会这样做,但很可能它们会。如果没有,Python 自带一个默认的 gzip 库,所以你可以自己处理这个问题。
考虑到这些,来回答你的问题:不,拥有 gzip
的 "content-encoding" 并不意味着远程资源是一个压缩文件。包含这个信息的字段是 content-type
(根据你的问题,这个字段的值可能是 application/zip
或 application/x-7z-compressed
,具体取决于实际使用的压缩算法)。
如果你无法根据 content-type
字段确定真实的文件类型(例如,如果它是 application/octet-stream
),你可以将文件保存到磁盘,然后用十六进制编辑器打开它。对于 7z
文件,你应该能在某处看到字节序列 37 7a bc af 27 1c
,很可能在文件的开头或者文件末尾的112字节处。对于 gzip
文件,开头应该是 1f 8b
。
既然你在 content-encoding
字段中看到了 gzip
:如果你得到的是 7z
文件,你可以确定 requests
已经解析了 content-encoding
并为你正确解码了。如果你得到的是 gzip
文件,这可能有两种情况。要么是 requests
没有解码任何东西,要么文件确实是一个 gzip
文件,因为它可能是用 gzip
编码发送的。这就意味着它被压缩了两次。这听起来没有什么意义,但根据服务器的不同,这种情况可能还是会发生。
你可以简单地在控制台上运行 gunzip
来看看结果。
你需要从请求文件中获取内容才能进行写入。已经确认可以正常工作:
import requests
durl = 'https://db.tt/Kq0byWzW'
dresp = requests.get(durl, allow_redirects=True, stream=True)
dresp.headers['content-encoding']
file = open('test.html', 'w')
file.write(dresp.text)
首先,durl
不是一个压缩文件,它是一个 Dropbox 的登录页面。你看到的其实是用 gzip 编码发送的 HTML 文件。如果你用 gzip 解码从原始套接字获取的数据,你会得到 HTML 内容。所以使用原始数据只是为了隐藏你意外获取了一个不同的文件,而不是你想要的那个。
根据你在 这个链接 上问的问题:
有没有人知道如何直接将压缩文件写入磁盘并解压?
我理解你是想直接获取一个 zip 文件,并将其解压到一个目录,而不需要先存储它。要做到这一点,你需要使用 这个链接 中的内容。
不过,这里有个问题,来自请求的响应实际上是不能随机访问的,而 zipfile 需要这种随机访问(它的第一步就是要找到文件的末尾,以确定文件的长度)。
为了解决这个问题,你需要将响应包装成一个类似文件的对象。我个人建议使用 tempfile.SpooledTemporaryFile
,并设置一个最大大小。这样,如果文件比你预期的要大,你的代码就会自动切换到将内容写入磁盘。
import requests
import tempfile
import zipfile
KB = 1<<10
MB = 1<<20
url = '...' # Set url to the download link.
resp = requests.get(url, stream=True)
with tmp as tempfile.SpooledTemporaryFile(max_size=500*MB):
for chunk in resp.iter_content(4*KB):
tmp.write(chunk)
archive = zipfile.ZipFile(tmp)
archive.extractall(path)
使用 io.BytesIO
的相同代码:
resp = requests.get(url, stream=True)
tmp = io.BytesIO()
for chunk in resp.iter_content(4*KB):
tmp.write(chunk)
archive = zipfile.ZipFile(tmp)
archive.extractall(path)