【问题标题】:Python requests - print entire http request (raw)?Python请求 - 打印整个http请求(原始)?
【发布时间】:2014-01-06 15:45:25
【问题描述】:

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

我不只想要标题,我想要请求行、标题和内容打印输出。是否可以看到最终由 HTTP 请求构造的内容?

【问题讨论】:

  • 这是个好问题。从源码看,似乎没有任何方法可以获取准备好的请求的原始内容,并且只有在发送时才会序列化。这似乎是一个很好的功能。
  • 好吧,你也可以启动 wireshark 看看。
  • @qwrrty 很难将其集成为requests 功能,因为这意味着重写/绕过urllib3 和httplib。请参阅下面的堆栈跟踪

标签: python http python-requests


【解决方案1】:

Since v1.2.3Requests 添加了 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)

这些链接指向可用的最新文档,因此它们的内容可能会有所变化: Advanced - Prepared requests 和 API - Lower level classes

【讨论】:

  • 这似乎是have been added on 2.0.0,但不是1.2.3
  • @goncalopp 我在 1.2.3 的文档中看到了它,但我没有看代码。如果您可以确认它在 2.0.0 之前不存在,我会更改它以避免混淆。
  • 如果使用简单的response = requests.post(...)(或requests.get或requests.put等)方法,实际上可以通过response.request得到PreparedResponse。如果您不需要在收到响应之前访问原始http数据,它可以节省手动操作requests.Request和requests.Session的工作。
  • url后面的HTTP协议版本部分呢?像“HTTP/1.1”?使用漂亮的打印机打印时找不到。
  • 更新为使用 CRLF,因为这是 RFC 2616 的要求,对于非常严格的解析器来说可能是个问题
【解决方案2】:
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 property,这是发送的原始 PreparedRequest 对象。

【讨论】:

    【解决方案3】:

    一个更好的主意是使用 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
    

    【讨论】:

    【解决方案4】:

    注意:此答案已过时。较新版本的requests 支持直接获取请求内容,如AntonioHerraizS's answer 文档。

    不可能从requests 中获取请求的true 原始内容,因为它只处理更高级别的对象,例如headers 和方法类型。 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 倍)?所以,如果我做第三个请求,它会打印 3 次,依此类推......知道如何只获得一次输出吗?提前致谢。
    • @opstalj 你不应该多次调用patch_send,只调用一次,在导入httplib之后
    • 顺便说一句,你是如何获得堆栈跟踪的?是通过追踪代码完成的还是有什么技巧?
    • @huggie 没有技巧,只是耐心,手动步进和读取文件
    【解决方案5】:

    requests 支持所谓的event hooks(从 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。

    【讨论】:

    • 这是最好的现代方法。
    【解决方案6】:

    这里是一个代码,它是相同的,但带有响应头:

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

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

    【讨论】:

      【解决方案7】:

      我使用以下函数来格式化请求。就像 @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
      

      【讨论】:

        【解决方案8】:

        A fork of @AntonioHerraizS answer(如 cmets 中所述,缺少 HTTP 版本)


        使用此代码获取表示原始 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"}
        

        ?也可以在响应对象中打印请求

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

        【讨论】:

          【解决方案9】:

          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!
          

          【讨论】:

            猜你喜欢
            • 2012-03-28
            • 2014-10-17
            • 2014-10-07
            • 2021-12-23
            • 2012-10-26
            • 1970-01-01
            • 2021-01-20
            • 1970-01-01
            • 2013-03-19
            相关资源
            最近更新 更多