Żądania w Pythonie - wydrukuj całe żądanie HTTP (surowe)?

197

Czy podczas korzystania z requestsmodułu można wydrukować nieprzetworzone żądanie HTTP?

Nie chcę tylko nagłówków, chcę wiersz żądania, nagłówki i wydruk zawartości. Czy można zobaczyć, co ostatecznie skonstruowane jest z żądania HTTP?

huggie
źródło
9
@RickyA pyta o treść prośby, a nie o odpowiedź
goncalopp
2
To dobre pytanie. Patrząc na źródło, nie wydaje się, że istnieje sposób na uzyskanie surowej zawartości przygotowanego żądania, a jest ono serializowane tylko wtedy, gdy jest wysyłane. Wygląda na to, że byłaby to dobra funkcja.
Tim Pierce,
Cóż, możesz również uruchomić wireshark i zobaczyć to w ten sposób.
RickyA,
@qwrrty trudno byłoby zintegrować to jako requestsfunkcję, ponieważ oznaczałoby to przepisywanie / pomijanie urllib3i httplib. Zobacz ślad stosu poniżej
goncalopp,
To zadziałało dla mnie - stackoverflow.com/questions/10588644/…
Ajay

Odpowiedzi:

213

Od Żądań 1.2.3 dodano obiekt PreparedRequest. Zgodnie z dokumentacją „zawiera dokładne bajty, które zostaną wysłane na serwer”.

Można to wykorzystać do wydrukowania żądania, na przykład:

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)

który produkuje:

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

a=1&b=2

Następnie możesz wysłać faktyczne żądanie za pomocą:

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

Te linki prowadzą do najnowszej dostępnej dokumentacji, więc mogą ulec zmianie w treści: Zaawansowane - Przygotowane żądania i API - Klasy niższego poziomu

AntonioHerraizS
źródło
2
Jest to o wiele bardziej niezawodne niż moja metoda łatania małp. Aktualizacja requestsjest prosta, więc myślę, że powinna to być akceptowana odpowiedź
goncalopp
69
Jeśli używasz prostego response = requests.post(...)(lub requests.getczy requests.put, etc) metody, można rzeczywiście uzyskać PreparedResponsethrough response.request. Może zaoszczędzić pracy ręcznego manipulowania requests.Requesti requests.Session, jeśli nie musisz uzyskiwać dostępu do surowych danych http przed otrzymaniem odpowiedzi.
Gershom
2
Dobra odpowiedź. Jedną rzeczą, którą możesz chcieć zaktualizować, jest to, że podziały linii w HTTP powinny być \ r \ n nie tylko \ n.
ltc
3
co z częścią wersji protokołu HTTP zaraz po adresie URL? jak „HTTP / 1.1”? którego nie można znaleźć podczas drukowania za pomocą pięknej drukarki.
Sajuuk
1
Zaktualizowano, aby używać CRLF, ponieważ tego właśnie wymaga RFC 2616, i może to być problem dla bardzo ścisłych parserów
nimish
55
import requests
response = requests.post('http://httpbin.org/post', data={'key1':'value1'})
print(response.request.body)
print(response.request.headers)

Korzystam z żądań w wersji 2.18.4 i Python 3

Payman
źródło
44

Uwaga: ta odpowiedź jest nieaktualna. Nowsze wersje requests wsparcia pobierają bezpośrednio treść żądania, jako dokumenty odpowiedzi AntonioHerraizS .

Nie można uzyskać prawdziwej surowej treści żądania requests, ponieważ dotyczy ona tylko obiektów wyższego poziomu, takich jak nagłówki i typ metody . requestsużywa urllib3do wysyłania żądań, ale urllib3 także nie zajmuje się surowymi danymi - używa httplib. Oto reprezentatywny ślad stosu żądania:

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

Wewnątrz httplibmaszyny widzimy HTTPConnection._send_requestpośrednio zastosowania HTTPConnection._send_output, które ostatecznie tworzą surowe żądanie i treść (jeśli istnieje), i wykorzystuje HTTPConnection.sendje do wysłania osobno. sendw końcu dociera do gniazda.

Ponieważ nie ma żadnych haczyków do robienia tego, co chcesz, w ostateczności możesz małpować łatkę, httplibaby uzyskać zawartość. Jest to delikatne rozwiązanie i może być konieczne dostosowanie go, jeśli httplibzostanie zmienione. Jeśli zamierzasz dystrybuować oprogramowanie za pomocą tego rozwiązania, możesz rozważyć pakowanie httplibzamiast korzystania z systemu, co jest łatwe, ponieważ jest to czysty moduł python.

Niestety, bez zbędnych ceregieli rozwiązanie:

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

co daje wynik:

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
źródło
Cześć goncalopp, jeśli wywołam procedurę patch_send () drugi raz (po drugim żądaniu), to drukuje dane dwa razy (czyli 2x razy wyjście, jak pokazano powyżej)? Tak więc, gdybym zrobił trzecie żądanie, wydrukowałoby je 3 razy i tak dalej ... Masz pomysł, jak uzyskać tylko jeden wynik? Z góry dziękuję.
opstalj
@opstalj nie powinieneś dzwonić patch_sendwiele razy, tylko raz, po zaimportowaniuhttplib
goncalopp
40

Jeszcze lepszym pomysłem jest użycie biblioteki Request_toolbelt, która może zrzucić zarówno żądania, jak i odpowiedzi jako ciągi do wydrukowania na konsoli. Obsługuje wszystkie trudne sprawy z plikami i kodowaniami, które powyższe rozwiązanie nie obsługuje dobrze.

To takie proste:

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

Źródło: https://toolbelt.readthedocs.org/en/latest/dumputils.html

Możesz go po prostu zainstalować, wpisując:

pip install requests_toolbelt
Emil Stenström
źródło
2
Wydaje się jednak, że nie odrzuca żądania bez jego wysłania.
Dobes Vandermeer
1
dump_all nie działa poprawnie, ponieważ otrzymuję „TypeError: nie można połączyć obiektów„ str ”i„ UUID ”z wywołania.
rtaft,
@rtaft: Zgłoś to jako błąd w swoim repozytorium github: github.com/sigmavirus24/requests-toolbelt/...
Emil Stenström
Wyświetla zrzut ze znakami> i <, czy są one częścią rzeczywistego żądania?
Jay
1
@Jay Wygląda na to, że są one poprzedzone rzeczywistym żądaniem / odpowiedzią na pojawienie się ( github.com/requests/toolbelt/blob/master/requests_toolbelt/... ) i można je określić, przekazując request_prefix = b '{some_request_prefix}', response_prefix = b '{some_response_prefix}' do dump_all ( github.com/requests/toolbelt/blob/master/requests_toolbelt/… )
Christian Reall-Fluharty
7

Oto kod, który robi to samo, ale z nagłówkami odpowiedzi:

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

Spędziłem dużo czasu na szukaniu tego, więc zostawiam to tutaj, jeśli ktoś potrzebuje.

denself
źródło
4

Korzystam z następującej funkcji do formatowania żądań. To jest jak @AntonioHerraizS, tyle że ładnie wydrukuje również obiekty JSON w ciele i oznaczy wszystkie części żądania.

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

Mam podobną funkcję do sformatowania odpowiedzi:

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
Ben
źródło
1

requestsobsługuje tak zwane haki zdarzeń (od 2.23 tak naprawdę jest tylko responsehak). Haka można użyć na żądanie, aby wydrukować pełne dane pary żądanie-odpowiedź, w tym efektywny adres URL, nagłówki i treści, takie jak:

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

Uruchamianie drukuje:

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

Może chcesz zmienić res.text, aby res.contentjeśli odpowiedź jest binarny.

saaj
źródło