Requests: нет возможности читать несжатый контент как файловый объект

Созданный на 29 февр. 2012  ·  44Комментарии  ·  Источник: psf/requests

Согласно документации, есть три способа прочитать содержимое ответа: .text , .content и .raw . Первые два учитывают кодирование передачи и автоматически распаковывают поток при получении результата в памяти. Однако, особенно для случая, когда результат большой, в настоящее время нет простого способа получить распакованный результат в форме файлового объекта, например, передать его прямо в синтаксический анализатор XML или Json.

С точки зрения библиотеки, которая стремится сделать HTTP-запросы удобными для пользователя, почему пользователь должен заботиться о чем-то столь же низкоуровневом, как тип сжатия потока, который был внутренне согласован между веб-сервером и библиотекой? В конце концов, это «вина» библиотеки, если она по умолчанию принимает такой поток. В этом свете поток .raw на мой вкус слишком сырой.

Может быть, четвертое свойство, такое как .stream могло бы обеспечить лучший уровень абстракции?

Самый полезный комментарий

Я уже объяснял, почему это ошибка дизайна, а не запрос функции: существующий API использует неправильную абстракцию и пропускает детали согласования соединения в пространство пользователя, которые находятся во власти удаленного сайта, и, таким образом, пользователь не должен нужно заботиться. Это затрудняет использование текущей поддержки чтения необработанного потока. По сути, это запрос на исправление неисправной функции, а не запрос на добавление новой функции.

Все 44 Комментарий

Response.iter_content

Эээ, нет, это итератор. Я просил о файловом объекте, то есть о том, что обработчики документов могут читать напрямую.

Было бы довольно просто создать объект в виде файла с помощью iter_content

Кстати, спасибо за быстрый ответ.

Я согласен. Тем не менее, requests было бы еще проще реализовать эту функцию. Я хочу сказать, что .raw - это неправильный уровень абстракции для большинства случаев использования, которые хотят читать из потока, потому что он раскрывает детали уровня передачи.

Лично я не вижу основного варианта использования для итерации построчно или даже фрагмент за фрагментом результата HTTP-запроса, но я вижу несколько основных вариантов использования для синтаксического анализа его как файлового объекта, в частности форматов ответов. которые требуют парсера документов, такого как HTML, XML, Json и т. д.

Также обратите внимание, что гораздо проще написать итератор, который обертывает файловый объект, чем файловый объект, который обертывает итератор.

Я придумал следующий код. Он обрабатывает все необходимые дела, но я считаю это довольно сложным. Вот почему я сказал, что хочу что-то подобное как часть библиотеки. Пользователи не должны разбираться в этом сами.

Я думаю, что код в файле models.py запросов использует здесь неправильную абстракцию. Он должен распаковывать необработанный поток _перед_ запуском его итерационного механизма, а не во время итерации. Переход от файлового к итератору просто для того, чтобы вернуться к файловому, просто глупо. Одного преобразования 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.

@ Лукаса прав. content всегда должна быть байтовой строкой (в Python 3 это явная байтовая строка; в Python 2 str == bytes). Единственный элемент, который не является байтовой строкой, - это text .

@kennethreitz есть новости по этому

Это не ошибка дизайна, это просто запрос функции. И поскольку у запросов есть замораживание функций, я предполагаю, что это не попадет в запросы в ближайшее время (если вообще) ...

Я не думаю, что объявить давнюю ошибку дизайна "недостающей функцией"
заставляет это уйти так легко. Я слышал, что автор думает о
выполнение "запросов" частью библиотеки Python stdlib. Это было бы хорошо
возможность исправить это.

Я слышал, что автор думает о
выполнение "запросов" частью библиотеки Python stdlib.

Не совсем: http://docs.python-requests.org/en/latest/dev/philosophy/#standard -library

Это не ошибка, это запрос функции. Запросы не делают ничего плохого, они просто не делают чего-то необязательного. Это само определение функции.

Кроме того, именно подготовка к stdlib является причиной заморозки функций Requests. Как только запросы помещаются в stdlib, становится очень сложно своевременно исправлять ошибки. В результате, если добавление новой функции приводит к появлению ошибок или ухудшению поведения, версия в stdlib не может быть исправлена ​​до следующего второстепенного выпуска. Это было бы плохо.

Марк Шлайх, 19.03.2013 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 использует неправильную абстракцию и пропускает детали согласования соединения в пространство пользователя, которые находятся во власти удаленного сайта, и, таким образом, пользователь не должен нужно заботиться. Это затрудняет использование текущей поддержки чтения необработанного потока. По сути, это запрос на исправление неисправной функции, а не запрос на добавление новой функции.

Позвольте мне резюмировать это чисто. Ошибка заключается в том, что при любом реальном использовании функции чтения необработанного потока придется повторно реализовать часть библиотеки, в частности, всю часть условной декомпрессии потока, потому что без нее эта функция бесполезна, как только клиент разрешит сжатие. Здесь мы говорим о коде, который уже есть, в «запросах» - он просто используется не в том месте. Его следует использовать ниже уровня необработанного чтения, а не выше, потому что клиент не может контролировать, соблюдает ли сервер заголовок принятия или нет. Сжатие должно быть прозрачной деталью согласования соединения, а не чем-то, что может повредить любому пользователю, включившему соответствующий заголовок.

Я не могу придумать ни одного варианта использования, когда клиент был бы заинтересован в сжатом потоке, особенно если он не может предсказать, будет ли поток действительно сжат или нет, поскольку сервер может с радостью проигнорировать желание клиента. Это чистая переговорная деталь. Вот почему чтение сырого потока использует неправильную абстракцию, предпочитая крайне маловероятный вариант использования наиболее распространенному.

Я могу. Например, что, если вы загружаете большой текстовый файл и хотите сохранить его сжатым? Я мог бы дополнить это изменение новой «дизайнерской ошибкой» под названием « Невозможно сохранить изначально сжатые данные на диск» .

Эта идея намеренно банальна и глупа, но я пытаюсь проиллюстрировать одну мысль: запросы не обязаны предлагать каждому именно тот механизм взаимодействия, который они желают. Фактически, это будет напрямую противоречить основной цели Requests, а именно простоте API. Существует длинный-длинный _длинный_ список предлагаемых изменений в Запросы, против которых были возражения, поскольку они усложняют API, хотя и добавляют полезные функции. Запросы не нацелены на замену urllib2 для всех случаев использования, они нацелены на упрощение наиболее распространенных случаев.

В этом случае Requests предполагает, что большинству пользователей не нужны файловые объекты, и поэтому предлагает следующие взаимодействия:

  • Response.text и Response.content : вам нужны все данные за один раз.
  • Response.iter_lines() и Response.iter_content() : вам не нужны все данные за один раз.
  • Response.raw : Вам не нравятся два других варианта, поэтому сделайте это самостоятельно.

Они были выбраны потому, что они в подавляющем большинстве случаев представляют собой распространенное использование запросов. Вы сказали, что « большинство пользователей все равно не заботятся об итераторах контента » и « response.stream - это функция, которая нужна большинству пользователей потоковой передачи ». Опыт работы с этим проектом заставляет меня не согласиться: очень многие люди используют итераторы контента, и немногие отчаянно хотят файловые объекты.

И еще один заключительный момент: если сжатие должно быть прозрачной деталью согласования соединения, тогда вы должны поднять соответствующую ошибку для urllib3, который обрабатывает нашу логику соединения.

Мне очень жаль, что вы считаете, что запросы не подходят для вашего варианта использования.

Я понимаю, что response.raw работает в текущей реализации и даже частично согласен с этим (вы, по крайней мере, должны иметь возможность получать детали сжатия без анализа заголовков).

Однако ваше предложение по-прежнему представляет собой функцию запросов ...

@Lukasa
Я действительно не понимаю, как регистрация ошибки в urllib3 может исправить API запросов, по крайней мере, не сама по себе.

И я согласен с тем, что ваш «вариант использования» надуман. Как я уже сказал, если клиент не может положительно контролировать сжатие на стороне сервера (и он отключает его, но не включает надежно), поэтому полагаться на него, чтобы сохранить сжатый файл на диск, не так уж и интересно .

@schlamar
Я согласен с тем, что это можно читать как таковое. Уверяю вас, что меня устраивает все, что решает эту проблему. Если для того, чтобы попасть туда, требуется открытие нового билета, пусть будет так.

Если для того, чтобы попасть туда, требуется открытие нового билета, пусть будет так.

Я все еще думаю, что Кеннет отвергнет это из-за замораживания функции.

Меня устраивает все, что решает эту проблему

  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.read работал с amt и decode_content .
  2. Сделайте HTTPResponse._decode_content публичным членом (чтобы вы могли сделать response.raw.decode_content = True вместо исправления метода read ).
  3. Полностью отказаться от декомпрессии в запросах, используя decode_content=True в iter_content

@Lukasa Я думаю, это не нарушит заморозку функций, верно?

@schlamar : В принципе да. Пока API остается неизменным, внутренние изменения _должны_ выполняться, и я бы получил +1 за это. Однако учтите, что я не БДФЛ, =)

stream_decompress в запросах все равно не работает: # 1249

+1

Была ли эта страница полезной?
0 / 5 - 0 рейтинги