Requests: 无法将未压缩的内容作为类文件对象读取

创建于 2012-02-29  ·  44评论  ·  资料来源: psf/requests

根据文档,可以通过三种方式读取响应的内容: .text.content.raw 。 前两个考虑传输编码并在生成内存结果时自动解压缩流。 但是,特别是对于结果较大的情况,目前还没有简单的方法以类文件对象的形式获取解压结果,例如将其直接传递到 XML 或 Json 解析器中。

从一个旨在使 HTTP 请求用户友好的库的角度来看,为什么用户必须关心像 Web 服务器和库之间内部协商的流的压缩类型这样低级别的东西? 毕竟,如果它默认接受这样的流,那是图书馆的“错误”。 从这个角度来看, .raw流对我来说有点太原始了。

也许像.stream这样的第四个属性可能会提供更好的抽象级别?

最有用的评论

我已经解释了为什么这是一个设计错误而不是功能请求:现有的 API 使用了错误的抽象并将连接的协商细节泄漏到用户空间中,这些细节受远程站点的支配,因此,用户不应该得关心。 这使得当前的原始流读取支持难以使用。 本质上,这是修复损坏的功能的请求,而不是新功能的请求。

所有44条评论

Response.iter_content

嗯,不,那是一个迭代器。 我要求一个类似文件的对象,即文档处理器可以直接读取的对象。

iter_content创建一个类似文件的对象会非常简单

感谢您的快速回复,顺便说一句。

我同意。 尽管如此, requests提供此功能会更容易。 我的观点是,对于大多数想要从流中读取的用例来说, .raw是错误的抽象级别,因为它公开了传输级别的详细信息。

就我个人而言,我没有看到在 HTTP 请求的结果上逐行甚至逐块迭代的主要用例,但我看到了几个主要用例,将其解析为类文件对象,特别是响应格式需要文档解析器,例如 HTML、XML、Json 等。

另请注意,编写包装类文件对象的迭代器比编写包装迭代器的类文件对象要容易得多。

我想出了以下代码。 它处理所有必要的情况,但我发现它相当复杂。 这就是为什么我说我想要这样的东西作为图书馆的一部分。 用户不应该自己弄清楚这一点。

我认为 requests 的 models.py 中的代码在这里使用了错误的抽象。 它应该在_before_ 以它的迭代机制开始解压原始流,而不是在迭代过程中。 从类文件到迭代器只是为了回到类文件只是愚蠢的。 单个 API 转换就足够了,而且大多数用户无论如何都不会关心内容迭代器。

class FileLikeDecompressor(object):
    """
    File-like object that wraps and decompresses an HTTP stream transparently.
    """
    def __init__(self, stream, mode='gzip'):
        self.stream = stream
        zlib_mode = 16 + zlib.MAX_WBITS if mode == 'gzip' else -zlib.MAX_WBITS  # magic
        self.dec = zlib.decompressobj(zlib_mode)
        self.data = ''

    def read(self, n=None):
        if self.dec is None:
            return '' # all done
        if n is None:
            data = self.data + self.dec.decompress(self.stream.read())
            self.data = self.dec = None
            return data
        while len(self.data) < n:
            new_data = self.stream.read(n)
            self.data += self.dec.decompress(new_data)
            if not new_data:
                self.dec = None
                break
        if self.data:
            data, self.data = self.data[:n], self.data[n:]
            return data
        return ''

def decompressed(response):
    """
    Return a file-like object that represents the uncompressed HTTP response data.
    For compressed HTTP responses, wraps the stream in a FileLikeDecompressor.
    """
    stream = response.raw
    mode = response.headers.get('content-encoding')
    if mode in ('gzip', 'deflate'):
        return FileLikeDecompressor(stream, mode)
    return stream

为什么不按照建议从content_iter构建类似文件的对象。 这可能看起来像:

class FileLikeFromIter(object):
    def __init__(self, content_iter):
        self.iter = content_iter
        self.data = ''

    def __iter__(self):
        return self.iter

    def read(self, n=None):
        if n is None:
            return self.data + '\n'.join(l for l in self.iter)
        else:
            while len(self.data) < n:
                try:
                    self.data = '\n'.join((self.data, self.iter.next()))
                except StopIteration:
                    break
            result, self.data = self.data[:n], self.data[n:]
            return result

您可能想再次阅读我的评论,特别是我发布的代码之前的段落。

是的,但是这个解决方案仍然比在第二个地方进行解压缩更干净(并且 IMO 更容易),因为这已经内置在请求中。

但总的来说,我同意你的看法, r.file (或类似的东西)比r.raw有更多的用例。 所以我也希望看到这也包含在请求中。 @kennethreitz

“response.stream”对我来说听起来是个好名字。

这就是 response.raw 的用途:)

这也是我看到的时候直觉上的想法。 但后来我意识到 response.raw 被破坏了,因为它暴露了用户不应该关心的底层传输层的内部细节。

他们应该需要的唯一方法是raw.read

嗯,是的 - 除了 raw.read() 的行为取决于客户端和服务器之间的内部协商。 它有时会返回预期的数据,有时会返回裸压缩的字节。

基本上, response.raw是一个很好的功能,大多数用户会很乐意忽略它,而一些高级用户可能会觉得有用,而独立于压缩的response.stream是大多数流媒体用户会使用的功能想。

+1

+1

这个设计错误会被修复吗?

不确定这种方式有多正确或有效,但对我来说,以下方法有效

>>> import lxml  # a parser that scorns encoding
>>> unicode_response_string = response.text
>>> lxml.etree.XML(bytes(bytearray(unicode_response_string, encoding='utf-8')))  # provided unicode() means utf-8
<Element html at 0x105364870>

@kernc :这是一件奇怪的事情。 response.content已经是一个字节串,所以你在这里做的是用 Python 选择的任何编解码器解码内容,然后将其重新编码为 utf-8。

这_不是_一个错误,它绝对不是您建议的错误。 如果你真的需要一个类似文件的对象,我推荐 StringIO 和 BytesIO。

@Lukasa是正确的。 content应该总是一个字节串(在 Python 3 中它是一个显式的字节串;在 Python 2 str == bytes)。 唯一不是字节串的项目是text

@kennethreitz有这方面的消息吗? 这是一个非常严重的设计错误,最好尽早解决。 为解决它而编写的代码越多,每个人的成本就越高。

这不是设计错误,它只是一个功能请求。 由于请求具有功能冻结,我认为这不会很快出现在请求中(如果有的话)......

我不认为重新声明一个长期存在的设计错误是“缺失的功能”
让它轻松消失。 听说作者在考虑
使“请求”成为 Python stdlib 的一部分。 那将是一个很好的
有机会解决这个问题。

听说作者在考虑
使“请求”成为 Python stdlib 的一部分。

不是真的: http ://docs.python-requests.org/en/latest/dev/philosophy/#standard -library

这不是错误,而是功能请求。 Requests 没有做错任何事情,它只是没有做一些可选的事情。 这就是特征的定义。

此外,为 stdlib 做准备正是 Requests 处于功能冻结状态的原因。 一旦请求在标准库中,就很难及时修复错误。 因此,如果添加新功能会增加错误或回归行为,则在下一个次要版本之前无法修复 stdlib 中的版本。 那会很糟糕。

马克·施莱奇,2013 年 3 月 19 日 08:41:

听说作者在考虑
使“请求”成为 Python stdlib 的一部分。

不是真的: http ://docs.python-requests.org/en/latest/dev/philosophy/#standard -library

我在这里阅读:

http://python-notes.boredomandlaziness.org/en/latest/conferences/pyconus2013/20130313-language-summit.html

斯蒂芬

我已经解释了为什么这是一个设计错误而不是功能请求:现有的 API 使用了错误的抽象并将连接的协商细节泄漏到用户空间中,这些细节受远程站点的支配,因此,用户不应该得关心。 这使得当前的原始流读取支持难以使用。 本质上,这是修复损坏的功能的请求,而不是新功能的请求。

让我清楚地总结一下。 错误在于原始流读取功能的任何实际使用都必须重新实现库的一部分,特别是整个条件流解压缩部分,因为一旦客户端允许压缩,没有它该功能将毫无用处。 我们在这里谈论的是已经存在的代码,在“请求”中——它只是用在了错误的地方。 它应该在原始阅读级别之下使用,而不是在它之上,因为客户端无法控制服务器是否接受接受标头。 压缩应该是连接的透明协商细节,而不是伤害启用相关标头的任何用户的东西。

我想不出客户端会对压缩流感兴趣的任何用例,特别是如果它无法预测流是否真的会被压缩,因为服务器可以愉快地忽略客户端的愿望。 这是一个纯粹的谈判细节。 这就是原始流读取使用错误抽象的原因,因为它更喜欢极不可能的用例而不是最常见的用例。

我能。 例如,如果您正在下载一个基于文本的大型文件并希望对其进行压缩,该怎么办? 我可以使用名为No way to save original-compressed data to disk的新“设计错误”来跟进此更改。

这个想法是故意陈腐和愚蠢的,但我试图说明一个观点,那就是:Requests 没有义务为每个人提供他们想要的确切交互机制。 事实上,这样做会直接违背 Requests 的主要目标,即 API 的简单性。 有一个很长很长的 _long_ 请求更改列表,这些更改被反对,因为它们使 API 复杂化,即使它们添加了有用的功能。 Requests 的目的不是为所有用例替换 urllib2,而是旨在简化最常见的情况。

在这种情况下,Requests 假设大多数用户不想要类似文件的对象,因此提出以下交互:

  • Response.textResponse.content :您想要一次性获得所有数据。
  • Response.iter_lines()Response.iter_content() :您不希望一次性获得所有数据。
  • Response.raw :您对其他两个选项不满意,所以自己做。

选择这些是因为它们绝大多数代表了请求的常见用途。 您曾说过“无论如何大多数用户都不会关心内容迭代器”和“ response.stream是大多数流媒体用户想要的功能”。 这个项目的经验让我不同意:很多人使用内容迭代器,但没有多少人迫切想要类似文件的对象。

最后一点:如果压缩应该是连接的透明协商细节,那么您应该针对 urllib3 提出适当的错误,它处理我们的连接逻辑。

很抱歉您觉得 Requests 不适合您的用例。

我明白你的观点, response.raw在当前的实现中被破坏了,甚至部分同意这一点(你至少应该能够在不解析标头的情况下获得压缩细节)。

但是,您的建议仍然是功能请求...

@卢卡萨
我真的不明白针对 urllib3 提交错误将如何修复请求的 API,至少不是全部。

我同意你的“用例”是虚构的。 正如我所说,如果客户端不能积极控制服务器端的压缩(并且它禁用它,但不能可靠地启用它),那么依靠它能够将压缩文件保存到磁盘是,好吧,不是那么有趣.

@施拉马尔
我同意它可以这样阅读。 我向您保证,我对解决此问题的任何方法都满意。 如果需要打开新票才能到达那里,那就这样吧。

如果需要打开新票才能到达那里,那就这样吧。

我仍然认为由于功能冻结,Kenneth 会拒绝这一点。

我可以解决这个问题

  1. iter_content包装
  2. 解析头文件并在适当情况下解压response.raw

两种解决方案都在上面的评论中,后者是您发布的。 为什么会出现这样的问题,这不会直接出现在请求中?

让我们在这里 100% 清楚:当它处于功能冻结状态时,基本上没有机会进入请求。 没有任何问题,API 并不完美满足您的需求。 因为没有东西坏掉,唯一重要的是肯尼斯是否想要它。 请求不是民主,而是一人一票。 肯尼斯是那个人,他有投票权。 肯尼斯在 8 个月前关闭了这个问题,所以很明显他不想要它。

我真的不明白针对 urllib3 提交错误将如何修复请求的 API,至少不是全部。

修补 urllib3 以始终返回未压缩的文件对象应该可以自行解决此问题(并不是说这是一个好主意)。

哦,这是解决方案 3(未经测试):

response.raw.read = functools.partial(response.raw.read, decode_content=True)

https://github.com/shazow/urllib3/blob/master/urllib3/response.py#L112

有趣 - 我不知道现在存在。 当然,这使得包装功能更容易。

虽然,这真的有效吗? 即解压器是有状态的还是增量的? 例如,第二次调用 read(123) 将不再返回 gzip 文件的有效开头。

虽然,这真的有效吗? 即解压器是有状态的还是增量的?

哦,好像不是。 我没有阅读文档字符串。

但是,这是我的建议:

  1. 修补 urllib3 以便HTTPResponse.readamtdecode_content同时使用。
  2. 使 HTTPResponse._decode_content 成为公共成员(因此您可以执行response.raw.decode_content = True而不是修补read方法)。
  3. 使用decode_content=True中的iter_content decode_content=True完全删除请求中的解压

@Lukasa我认为这不会违反功能冻结,对吗?

@schlamar :原则上,当然可以。 只要 API 保持不变,内部更改_应该_就可以了,我会在这一点上 +1。 但是,请记住,我不是 BDFL,=)

请求中的stream_decompress无论如何都被破坏了:#1249

+1

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

JimHokanson picture JimHokanson  ·  3评论

jakul picture jakul  ·  3评论

cnicodeme picture cnicodeme  ·  3评论

jake491 picture jake491  ·  3评论

Matt3o12 picture Matt3o12  ·  3评论