Python requests - 打印整个 HTTP 请求(raw)?

351

在使用 requests 模块 时,有没有办法打印原始的 HTTP 请求?

我不仅想要头部信息,还想要请求行、头部信息和内容的输出。是否可以查看最终从 HTTP 请求构建的内容?


5
好问题。从源代码来看,似乎没有办法获取已准备请求的原始内容,只有在发送时才会序列化。这似乎是一个不错的功能。 - Tim Pierce
好的,你也可以启动Wireshark并以那种方式查看它。 - RickyA
@qwrrty,将其作为“requests”功能集成可能会很困难,因为这意味着需要重写/绕过“urllib3”和“httplib”。请参见下面的堆栈跟踪。 - loopbackbee
2
这对我有用 - https://dev59.com/4mkv5IYBdhLWcg3wdQdT - Ajay
Ajay提供的日志记录答案确实是一种非常有用的方法。 - rschwieb
9个回答

321
自从v1.2.3版本以来,Requests添加了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 - 低级类

123
如果您使用简单的response = requests.post(...)(或requests.getrequests.put等)方法,实际上可以通过response.request获得PreparedResponse。如果在接收响应之前不需要访问原始HTTP数据,则可以节省手动操作requests.Requestrequests.Session的工作。 - Gershom Maes
5
你提到的HTTP协议版本号是指URL后面的部分,比如"HTTP/1.1"。使用你提供的漂亮打印工具无法找到它。 - Sajuuk
1
已更新为使用CRLF,因为这是RFC 2616所要求的,并且对于非常严格的解析器可能会成为一个问题。 - nimish
1
@ntg 好的,那么我们都知道你输入了 pretty_print_POST(req) 而不是实际上应该输入的 (pretty_print_POST(prepared))。它需要一个 PreparedRequest,而不是一个 Request。 - rschwieb
@Noumenon 这对我无效。唯一有效的方法是使用 from urllib.parse import unquote,然后对 req.body 进行解码。https://www.urldecoder.io/python/ - undefined
显示剩余9条评论

216
import requests

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

Response 对象具有一个 .request 属性,它是被发送的 PreparedRequest 对象。


请注意,在重定向的情况下,response.request实际上将包含已发送的最后一个PreparedRequest。可以使用https://httpbin.org/redirect/3进行尝试。要真正挖掘出响应对象中的原始请求,您必须查看response.history - Jeyekomon
3
如果请求抛出异常,这个操作将无法完成。 - gre_gor

67
使用requests_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

3
然而,这似乎并不会在发送请求之前将其丢弃。 - Dobes Vandermeer
1
dump_all似乎无法正常工作,因为我从调用中得到“TypeError:cannot concatenate'str'and'UUID'objects”的错误。 - rtaft
@rtaft:请在他们的Github存储库中报告此问题:https://github.com/sigmavirus24/requests-toolbelt/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen - Emil Stenström
它打印带有>和<符号的转储,它们是实际请求的一部分吗? - Jay
@DobesVandermeer,有一个未记录的私有函数 _dump_request_data 可能会解决问题。https://github.com/requests/toolbelt/blob/master/requests_toolbelt/utils/dump.py#L57 - Christian Reall-Fluharty
1
@Jay 看起来它们是为了外观而在实际请求/响应之前添加的。(https://github.com/requests/toolbelt/blob/master/requests_toolbelt/utils/dump.py#L103) 并且可以通过传递 request_prefix=b'{some_request_prefix}', response_prefix=b'{some_response_prefix}' 到 dump_all 来指定。(https://github.com/requests/toolbelt/blob/master/requests_toolbelt/utils/dump.py#L161) - Christian Reall-Fluharty

50

注意:本回答适用于旧版本的requests,当时该功能尚未添加。较新版本已经原生支持此功能(请参阅其他回答)

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 到达套接字。

由于没有钩子来实现您想要的内容,作为最后的手段,您可以 monkey patch 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倍)?因此,如果我进行第三个请求,它将打印3倍,依此类推...有什么办法只获取一次输出吗?提前致谢。 - opstalj
1
@opstalj,您不应该多次调用“patch_send”,只需在导入“httplib”后调用一次即可。 - loopbackbee
顺便问一下,你是怎么获取堆栈跟踪的?是通过代码追踪还是有什么诀窍? - huggie
1
@huggie 没有什么诀窍,只需要耐心、手动步进和阅读文件。 - loopbackbee
对于Python 3,请使用http.client替换httplib。 - Brett Elliot
@loopbackbee,我喜欢这个解决方案,可以查看正在调用的URL,以便稍后使用requests_mocker进行模拟。如果你有一个“patch_receive”来查看响应,那就太酷了! - Brett Elliot

23

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


1
这是最好的现代方法。 - Mikko Ohtamaa
非常好的解决方案,只使用了内置模块! - Gruber

8

这里有一段代码,它可以使用响应头来实现同样的功能:

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

我花了很长时间寻找这个,所以我将它留在这里,如果有人需要的话。


5

使用以下代码获取表示原始HTTP数据包的字符串,而无需发送它:

AntonioHerraizS的答案的分支(根据评论中所述缺少HTTP版本)

import requests


def get_raw_request(request):
    request = request.prepare() if isinstance(request, requests.Request) else request
    headers = '\r\n'.join(f'{k}: {v}' for k, v in request.headers.items())
    body = '' if request.body is None else request.body.decode() if isinstance(request.body, bytes) else request.body
    return f'{request.method} {request.path_url} HTTP/1.1\r\n{headers}\r\n\r\n{body}'


headers = {'User-Agent': 'Test'}
request = requests.Request('POST', 'https://stackoverflow.com', headers=headers, json={"hello": "world"})
raw_request = get_raw_request(request)
print(raw_request)

结果:

POST / HTTP/1.1
User-Agent: Test
Content-Length: 18
Content-Type: application/json

{"hello": "world"}

Can also print the request in the response object

r = requests.get('https://stackoverflow.com')
raw_request = get_raw_request(r.request)
print(raw_request)

3
我使用以下函数来格式化请求。它类似于@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

test_print.py 内容:

import logging
import pytest
import requests
from requests_toolbelt.utils import dump


def print_raw_http(response):
    data = dump.dump_all(response, request_prefix=b'', response_prefix=b'')
    return '\n' * 2 + data.decode('utf-8')

@pytest.fixture
def logger():
    log = logging.getLogger()
    log.addHandler(logging.StreamHandler())
    log.setLevel(logging.DEBUG)
    return log

def test_print_response(logger):
    session = requests.Session()
    response = session.get('http://127.0.0.1:5000/')
    assert response.status_code == 300, logger.warning(print_raw_http(response))

hello.py内容:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

运行:

 $ python -m flask hello.py
 $ python -m pytest test_print.py

标准输出:

------------------------------ Captured log call ------------------------------
DEBUG    urllib3.connectionpool:connectionpool.py:225 Starting new HTTP connection (1): 127.0.0.1:5000
DEBUG    urllib3.connectionpool:connectionpool.py:437 http://127.0.0.1:5000 "GET / HTTP/1.1" 200 13
WARNING  root:test_print_raw_response.py:25 

GET / HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive


HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 13
Server: Werkzeug/1.0.1 Python/3.6.8
Date: Thu, 24 Sep 2020 21:00:54 GMT

Hello, World!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接