Python-запити - друкувати весь http-запит (необроблений)?


197

Чи використовуєте requestsмодуль , чи є можливість друкувати необроблений запит HTTP?

Я не хочу лише заголовків, я хочу запит, розсилки заголовків та вмісту. Чи можливо побачити, що в кінцевому підсумку побудовано з HTTP-запиту?


9
@ RickyA він запитує про зміст запиту, а не про відповідь
goncalopp

2
Це гарне запитання. З огляду на джерело, схоже, немає способу отримати вихідний вміст підготовленого запиту, і він серіалізується лише після його надсилання. Здається, це було б хорошою особливістю.
Тім Пірс

Ну, ви також можете запустити проводку і побачити це саме так.
RickyA

@qwrrty важко було б інтегрувати це як requestsособливість, оскільки це означатиме перезапис / обхід urllib3і httplib. Дивіться слід стека нижче
goncalopp

Це працювало для мене - stackoverflow.com/questions/10588644/…
Ajay

Відповіді:


215

Оскільки запити v1.2.3 додали об’єкт PreparedRequest . Згідно з документацією, "вона містить точні байти, які будуть відправлені на сервер".

Можна скористатися цим, щоб красиво надрукувати запит, як-от так:

import requests

req = requests.Request('POST','http://stackoverflow.com',headers={'X-Custom':'Test'},data='a=1&b=2')
prepared = req.prepare()

def pretty_print_POST(req):
    """
    At this point it is completely built and ready
    to be fired; it is "prepared".

    However pay attention at the formatting used in 
    this function because it is programmed to be pretty 
    printed and may differ from the actual request.
    """
    print('{}\n{}\r\n{}\r\n\r\n{}'.format(
        '-----------START-----------',
        req.method + ' ' + req.url,
        '\r\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
        req.body,
    ))

pretty_print_POST(prepared)

яка виробляє:

-----------START-----------
POST http://stackoverflow.com/
Content-Length: 7
X-Custom: Test

a=1&b=2

Тоді ви можете надіслати фактичний запит за допомогою цього:

s = requests.Session()
s.send(prepared)

Ці посилання стосуються останньої наявної документації, тому вони можуть змінюватись у змісті: Розширені - Підготовлені запити та API - Класи нижчого рівня


2
Це набагато надійніше, ніж мій метод виправлення мавп. Оновлення requestsпросте, тому я думаю, що це має стати прийнятою відповіддю
goncalopp

69
Якщо ви використовуєте простий response = requests.post(...)(або requests.getчи requests.put, і т.д.) методи, ви можете отримати PreparedResponseнаскрізний response.request. Це може зберегти роботу ручного маніпулювання requests.Requestта requests.Session, якщо вам не потрібно отримувати доступ до необроблених даних http, перш ніж отримати відповідь.
Гершом

2
Хороша відповідь. Однак, можливо, ви хочете оновити, що розрив рядків у HTTP повинен бути \ r \ n не лише \ n.
ltc

3
як щодо частини версії протоколу HTTP відразу після URL-адреси? як "HTTP / 1.1"? що не знайдено при роздрукуванні за допомогою гарного принтера.
Саджук

1
Оновлено для використання CRLF, оскільки це вимагає RFC 2616, і це може бути проблемою для дуже суворих аналізаторів
nimish

56
import requests
response = requests.post('http://httpbin.org/post', data={'key1':'value1'})
print(response.request.body)
print(response.request.headers)

Я використовую запити версії 2.18.4 та Python 3


44

Примітка: ця відповідь застаріла. Новіші версії requests підтримки безпосередньо отримують вміст запиту, як документи відповідей AntonioHerraizS .

Неможливо отримати справжній вихідний вміст запиту requests, оскільки він стосується лише об'єктів вищого рівня, таких як заголовки та тип методу . requestsвикористовує urllib3для надсилання запитів, але urllib3 також не обробляє необроблені дані - він використовує httplib. Ось представницький слід стека запиту:

-> r= requests.get("http://google.com")
  /usr/local/lib/python2.7/dist-packages/requests/api.py(55)get()
-> return request('get', url, **kwargs)
  /usr/local/lib/python2.7/dist-packages/requests/api.py(44)request()
-> return session.request(method=method, url=url, **kwargs)
  /usr/local/lib/python2.7/dist-packages/requests/sessions.py(382)request()
-> resp = self.send(prep, **send_kwargs)
  /usr/local/lib/python2.7/dist-packages/requests/sessions.py(485)send()
-> r = adapter.send(request, **kwargs)
  /usr/local/lib/python2.7/dist-packages/requests/adapters.py(324)send()
-> timeout=timeout
  /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/connectionpool.py(478)urlopen()
-> body=body, headers=headers)
  /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/connectionpool.py(285)_make_request()
-> conn.request(method, url, **httplib_request_kw)
  /usr/lib/python2.7/httplib.py(958)request()
-> self._send_request(method, url, body, headers)

Усередині httplibмеханізму ми можемо бачити HTTPConnection._send_requestопосередковано використання HTTPConnection._send_output, яке, нарешті, створює необроблений запит та тіло (якщо воно існує) та використовує HTTPConnection.sendдля надсилання їх окремо. sendнарешті досягає розетки.

Оскільки немає гачків для того, щоб робити те, що ви хочете, в крайньому випадку ви можете попросити мавпи, httplibщоб отримати вміст. Це неміцне рішення, і вам може знадобитися адаптувати його, якщо воно httplibбуде змінено. Якщо ви маєте намір поширювати програмне забезпечення за допомогою цього рішення, ви можете розглянути можливість упаковки httplibзамість системи, що легко, оскільки це чистий модуль python.

На жаль, без зайвих помилок, рішення:

import requests
import httplib

def patch_send():
    old_send= httplib.HTTPConnection.send
    def new_send( self, data ):
        print data
        return old_send(self, data) #return is not necessary, but never hurts, in case the library is changed
    httplib.HTTPConnection.send= new_send

patch_send()
requests.get("http://www.python.org")

який дає вихід:

GET / HTTP/1.1
Host: www.python.org
Accept-Encoding: gzip, deflate, compress
Accept: */*
User-Agent: python-requests/2.1.0 CPython/2.7.3 Linux/3.2.0-23-generic-pae

Привіт, goncalopp, якщо я дзвоню процедуру patch_send () вдруге (після 2-го запиту), вона друкує дані вдвічі (так що в 2 рази перевищує вихід, як ви показали вище)? Отже, якби я зробив 3-й запит, він надрукував би його в 3 рази і так далі ... Будь-яка ідея, як отримати лише вихід один раз? Заздалегідь спасибі.
opstalj

@opstalj вам не потрібно дзвонити patch_sendкілька разів, після імпортуhttplib
goncalopp

40

Ще кращою ідеєю є використання бібліотеки request_toolbelt, яка може скидати і запити, і відповіді як рядки для друку на консоль. Він обробляє всі складні справи з файлами та кодуваннями, з якими вищезгадане рішення не вдається добре.

Це так просто, як це:

import requests
from requests_toolbelt.utils import dump

resp = requests.get('https://httpbin.org/redirect/5')
data = dump.dump_all(resp)
print(data.decode('utf-8'))

Джерело: https://toolbelt.readthedocs.org/en/latest/dumputils.html

Ви можете просто встановити його, ввівши:

pip install requests_toolbelt

2
Схоже, це не скидає запит, не надсилаючи його.
Dobes Vandermeer

1
dump_all не працює належним чином, оскільки я отримую "TypeError: не можна об'єднати" str "та" UUID 'об'єкти "під час виклику.
rtaft

@rtaft: Повідомте про це як про помилку в їх сховищі github: github.com/sigmavirus24/requests-toolbelt/…
Emil Stenström

Він друкує дамп із знаками> та <, вони є частиною фактичного запиту?
Джей

1
@Jay Схоже, вони є попередньо створеними фактичним запитом / відповіддю для появи ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… ) і можуть бути визначені, передаючи request_prefix = b '{some_request_prefix}', response_prefix = b '{some_response_prefix}' to dump_all ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… )
Christian Reall-Fluharty

7

Ось код, який робить те саме, але з заголовками відповідей:

import socket
def patch_requests():
    old_readline = socket._fileobject.readline
    if not hasattr(old_readline, 'patched'):
        def new_readline(self, size=-1):
            res = old_readline(self, size)
            print res,
            return res
        new_readline.patched = True
        socket._fileobject.readline = new_readline
patch_requests()

Я витратив багато часу на пошуки цього, тому залишаю його тут, якщо комусь потрібно.


4

Я використовую наступну функцію для форматування запитів. Це як @AntonioHerraizS, за винятком того, що він також добре друкує JSON-об'єкти в тілі, і він позначає всі частини запиту.

format_json = functools.partial(json.dumps, indent=2, sort_keys=True)
indent = functools.partial(textwrap.indent, prefix='  ')

def format_prepared_request(req):
    """Pretty-format 'requests.PreparedRequest'

    Example:
        res = requests.post(...)
        print(format_prepared_request(res.request))

        req = requests.Request(...)
        req = req.prepare()
        print(format_prepared_request(res.request))
    """
    headers = '\n'.join(f'{k}: {v}' for k, v in req.headers.items())
    content_type = req.headers.get('Content-Type', '')
    if 'application/json' in content_type:
        try:
            body = format_json(json.loads(req.body))
        except json.JSONDecodeError:
            body = req.body
    else:
        body = req.body
    s = textwrap.dedent("""
    REQUEST
    =======
    endpoint: {method} {url}
    headers:
    {headers}
    body:
    {body}
    =======
    """).strip()
    s = s.format(
        method=req.method,
        url=req.url,
        headers=indent(headers),
        body=indent(body),
    )
    return s

І у мене є аналогічна функція форматування відповіді:

def format_response(resp):
    """Pretty-format 'requests.Response'"""
    headers = '\n'.join(f'{k}: {v}' for k, v in resp.headers.items())
    content_type = resp.headers.get('Content-Type', '')
    if 'application/json' in content_type:
        try:
            body = format_json(resp.json())
        except json.JSONDecodeError:
            body = resp.text
    else:
        body = resp.text
    s = textwrap.dedent("""
    RESPONSE
    ========
    status_code: {status_code}
    headers:
    {headers}
    body:
    {body}
    ========
    """).strip()

    s = s.format(
        status_code=resp.status_code,
        headers=indent(headers),
        body=indent(body),
    )
    return s

1

requestsпідтримує так звані гачки подій (станом на 2.23 насправді є лише responseгачок). Гак може використовуватися для запиту для друку повних даних пари-відповіді на запит, включаючи ефективну URL-адресу, заголовки та тіла, наприклад:

import textwrap
import requests

def print_roundtrip(response, *args, **kwargs):
    format_headers = lambda d: '\n'.join(f'{k}: {v}' for k, v in d.items())
    print(textwrap.dedent('''
        ---------------- request ----------------
        {req.method} {req.url}
        {reqhdrs}

        {req.body}
        ---------------- response ----------------
        {res.status_code} {res.reason} {res.url}
        {reshdrs}

        {res.text}
    ''').format(
        req=response.request, 
        res=response, 
        reqhdrs=format_headers(response.request.headers), 
        reshdrs=format_headers(response.headers), 
    ))

requests.get('https://httpbin.org/', hooks={'response': print_roundtrip})

Запуск друкує:

---------------- request ----------------
GET https://httpbin.org/
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

None
---------------- response ----------------
200 OK https://httpbin.org/
Date: Thu, 14 May 2020 17:16:13 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 9593
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

<!DOCTYPE html>
<html lang="en">
...
</html>

Ви можете змінити , res.textщоб res.contentякщо відповідь є двійковим.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.