python requests由淺入深


前言

平時我做用python做接口測試的話,首選的就是大名鼎鼎的requests庫。requests名氣之大,無人不曉,以其優雅的封裝、簡明的使用方式讓人都無法自拔。使用python進行接口自動化測試,爬蟲,那么requests是你絕對繞不開的第三方庫。

那我今天就在這篇博文中由淺入深的入門一下requests庫。本人也是班門弄斧,如有說的不正確的地方,請不惜吝嗇的指出來。

前幾天我使用django編寫了一個用戶管理的API,詳細的可以看看使用django開發restful接口這篇博文。對於requests庫的一個使用介紹呢,就使用這個用戶管理接口來進行吧。而且后續我會locust性能測試03-參數化的文章也會通過這個用戶管理接口來編寫。

安裝

requests庫是一個開源的庫,我們可以在GitHub中隨時獲取源代碼。——github地址

同時requests也提供了不錯的官方文檔https://requests.readthedocs.io/zh_CN/latest/

通過官方的倉庫我們可以了解到。目前最新的版本支持的是python版本是2.7 3.5 3.6 3.7 3.8

所以在安裝之前一定要確認一下自己的python版本是否能夠支持。

python安裝庫非常的簡單,我們直接通過pip來安裝requests庫。

 pip install requests

安裝好之后我們先來測試一下。

首先我們進入ipython的交互環境。打開一下百度的網頁的。

In [1]: import requests

In [2]: r = requests.get('https://www.baidu.com')

In [3]: r.text
Out[3]: '<!DOCTYPE html>\r\n<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>ç\x99¾åº¦ä¸\x80ä¸\x8bï¼\x8cä½\xa0å°±ç\x9f¥é\x81\x93</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div cl
ass=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=ç\x99¾åº¦ä¸\x80ä¸\x8b class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>æ\x96°é\x97»</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao1 23</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>å\x9c°å\x9b¾</a> <a href=http://v.baidu.com name=tj_trvide o class=mnav>è§\x86é¢\x91</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>è´´å\x90§</a> <noscript> <a hr
ef=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>ç\x99»å½\x95</a> </noscript> <script>document.write(\'<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=\'+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ \'" name="tj_login" class="lb">ç\x99»å½\x95</a>\');\r\n                </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">æ\x9b´å¤\x9a产å\x93\x81</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <
p id=lh> <a href=http://home.baidu.com>å\x85³äº\x8eç\x99¾åº¦</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使ç\x94¨ç\x99¾åº¦å\x89\x8då¿\x85读</a>&nbsp; <a href =http://jianyi.baidu.com/ class=cp-feedback>æ\x84\x8fè§\x81å\x8f\x8dé¦\x88</a>&nbsp;京ICPè¯\x81030173å\x8f·&nbsp; <im
g src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>\r\n'

可以看到我們的通過導入的requests庫,僅僅三行代碼,就通過get方法成功的返回了百度首頁的數據。使用起來十分方便。

啟動用戶管理項目

在開始之前我們先把我們之前編寫的用戶管理API啟動一下。

好了成功啟動了我們開始把。

GET請求

用戶管理接口一共有三種表現形式。我們分別使用requests來請求試試。還是進入ipython的交互環境。

  • /user/

    In [85]: r = requests.get('http://127.0.0.1:8000/user/')
    
    In [86]: r.text
    Out[86]: '{"code": 200, "message": "success", "data": [{"id": 3, "name": "\\u968f\\u98ce\\u6325\\u624b", "sex": 1, "idcard": "123", "email": "1084502012@qq.com", "address": "123", "company": "123", "created_time": "2020-11-11T14:33:12.869Z"}]}'
    

    可以看到我們的這個是什么成功返回了,但是格式好像不太正確,被單引號圈起來了,因為text返回的是一個文本字符串的格式。

    但是我們的接口返回的是一個json格式的。

    所以我們需要獲取json數據。

    In [87]: r.json()
    Out[87]:
    {'code': 200,
     'message': 'success',
     'data': [{'id': 3,
       'name': '隨風揮手',
       'sex': 1,
       'idcard': '123',
       'email': '1084502012@qq.com',
       'address': '123',
       'company': '123',
       'created_time': '2020-11-11T14:33:12.869Z'}]}
    

    可以看到這樣就成功的返回了json格式的數據。

  • /user/1/

    In [10]: r = requests.get('http://127.0.0.1:8000/user/3/')
    
    In [11]: r.json()
    Out[11]:
    {'code': 200,
     'message': 'success',
     'data': {'id': 3,
      'name': '隨風揮手',
      'sex': 1,
      'idcard': '123',
      'email': '1084502012@qq.com',
      'address': '123',
      'company': '123'}}
    
  • /user/?name=隨風

    那么這個參數是怎么去訪問呢。get方法是可以在后面通過?key=value&key2=value2的一個方式來進行請求的,那么我們先來看看這樣的一個方式怎么樣來進行的呢。

    In [12]: r = requests.get('http://127.0.0.1:8000/user/?name=隨風')
    In [13]: r.json()
    Out[13]:
    {'code': 200,
     'message': 'success',
     'data': [{'id': 3,
       'name': '隨風揮手',
       'sex': 1,
       'idcard': '123',
       'email': '1084502012@qq.com',
       'address': '123',
       'company': '123',
       'created_time': '2020-11-11T14:33:12.869Z'}]}
    

    可以看到直接把參數附加在請求地址的后面也可以進行訪問。

    那有沒有更加簡便的方法呢。在requests庫中提供了一個更加簡單的方法。

    首先我們閱讀一下requests庫的源碼。下圖。可以看到有一個params參數,通過這個參數呢。

    def get(url, params=None, **kwargs):
        r"""Sends a GET request.
    
        :param url: URL for the new :class:`Request` object.
        :param params: (optional) Dictionary, list of tuples or bytes to send
            in the query string for the :class:`Request`.
        :param \*\*kwargs: Optional arguments that ``request`` takes.
        :return: :class:`Response <Response>` object
      :rtype: requests.Response
        """
    
        kwargs.setdefault('allow_redirects', True)
        return request('get', url, params=params, **kwargs)
    

    那我們來試試這種方式把。

    In [17]: payload = {'name':'隨風'}
    
    In [18]: r = requests.get('http://127.0.0.1:8000/user/',params=payload)
    
    In [19]: r.json()
    Out[19]:
    {'code': 200,
     'message': 'success',
     'data': [{'id': 3,
       'name': '隨風揮手',
       'sex': 1,
       'idcard': '123',
       'email': '1084502012@qq.com',
       'address': '123',
       'company': '123',
       'created_time': '2020-11-11T14:33:12.869Z'}]}
    

    可以看到這種方式更簡潔清晰明了。推薦使用這種方式。

    當然同時也支持一個鍵多個值的同時查找,需要在鍵值對中指定值為列表的方式。

    In [20]: payload = {'name':['隨','風']}
    
    In [21]: r = requests.get('http://127.0.0.1:8000/user/',params=payload)
    
    In [22]: r.json()
    Out[22]:
    {'code': 200,
     'message': 'success',
     'data': [{'id': 3,
       'name': '隨風揮手',
       'sex': 1,
       'idcard': '123',
       'email': '1084502012@qq.com',
       'address': '123',
       'company': '123',
       'created_time': '2020-11-11T14:33:12.869Z'}]}
    

POST請求

post請求一般都是提交一個表單,在requests的post請求中提供了兩個參數。看下源碼吧。

def post(url, data=None, json=None, **kwargs):
    r"""Sends a POST request.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) json data to send in the body of the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return request('post', url, data=data, json=json, **kwargs)

分別是datajson,這兩個都是傳遞參數的。其中data傳的是表單參數,而json傳遞的是content_type=application/json的數據類型的。所以我們先來試試吧。先試一下json參數的post請求。

In [32]: data = {'name':'隨風揮手11','sex':1,'idcard':'1234567','email':'1084502012@qq.com','address':'123','company'
    ...: :'123'}

In [33]: r = requests.post('http://127.0.0.1:8000/user/',json=data)

In [34]: r.json()
Out[34]:
{'code': 201,
 'message': 'created',
 'data': {'id': 4,
  'name': '隨風揮手11',
  'sex': 1,
  'idcard': '1234567',
  'email': '1084502012@qq.com',
  'address': '123',
  'company': '123'}}

可以看到使用json參數成功的創建了一個新的用戶。然后我們使用data參數來試試。

image-20201111230921077

可以看到報錯了,因為我編寫的接口不支持非json格式數據的提交。這個報錯太長了,所以我這里直接截圖了。

那么我們怎么使用這個data參數呢。我手里面也沒有合適的示例,直接用官方的演示吧。

In [40]: payload = {'key1': 'value1', 'key2': 'value2'}

In [41]: r = requests.post("http://httpbin.org/post", data=payload)

In [42]: r.text
Out[42]: '{\n  "args": {}, \n  "data": "", \n  "files": {}, \n  "form": {\n    "key1": "value1", \n    "key2": "value2"\n  }, \n  "headers": {\n    "Accept": "*/*", \n    "Accept-Encoding": "gzip, deflate", \n    "Content-Length": "23", \n    "Content-Type": "application/x-www-form-urlencoded", \n    "Host": "httpbin.org", \n    "User-Agent": "python-requests/2.24.0", \n    "X-Amzn-Trace-Id": "Root=1-5fabff48-70ea1f742434542615593b4b"\n  }, \n  "json": null, \n  "origin": "39.144.1.194", \n  "url": "http://httpbin.org/post"\n}\n'

這樣通過data參數就成功提交了一個表單參數。

PUT請求

def put(url, data=None, **kwargs):
    r"""Sends a PUT request.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) json data to send in the body of the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return request('put', url, data=data, **kwargs)

然后我們試一下put請求得一個方式。

In [51]: r = requests.put('http://127.0.0.1:8000/user/4/',json=data)

In [52]: r.json()
Out[52]:
{'code': 200,
 'message': 'updated',
 'data': {'id': 4,
  'name': '隨風揮手11111',
  'sex': 1,
  'idcard': '1234567',
  'email': '1084502012@qq.com',
  'address': '123',
  'company': '123'}}

可以看到數據成功更新了。

DELETE方法

delete方法源碼

def delete(url, **kwargs):
    r"""Sends a DELETE request.

    :param url: URL for the new :class:`Request` object.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return request('delete', url, **kwargs)

刪除剛才創建的數據試試。

In [63]: r = requests.delete('http://127.0.0.1:8000/user/5/')

In [64]: r.status_code
Out[64]: 204

因為delete是刪除方法,所以我們只需要查看一下它的狀態碼是204即可。至此我們的四種請求方式就說完了。

requests響應

在代碼里面我們使用的r.textr.json()都是requests提供的響應方法。

我們結合官方文檔在這里總結一下常用的響應結果吧。

方法 描述
r.text Requests 會自動解碼來自服務器的內容。大多數 unicode 字符集都能被無縫地解碼。
r.json() 返回json數據,如果數據不符合json格式規范,則會拋出異常
r.encoding 查看返回的數據使用了什么編碼
r.content 使用字節的方式返回數據,通常用於圖片等
r.status_code 返回服務器響應的狀態碼,如200,400,500
r.raw 返回響應的原始內容
r.headers 返回服務器的響應頭
r.url 返回請求的地址數據
r.history 返回請求的歷史記錄

這些都是requests庫響應內容提供的一些方法。

Session請求

session意思就是會話,在很多的場景中都是存在依賴關系的,比如我之前些的接口測試框架中智學網的接口就是我需要先登錄才可以訪問其他的接口,那么我必須保持這登錄結果傳到下一個請求中,在這里舉這個例子可能不是很恰當,因為這個示例的token是通過請求體傳遞的。
所以我們來看看官方的一個示例把。

import requests
s = requests.Session()
 
r1 = s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
print(r1.text)
#out: '{"cookies": {"sessioncookie": "123456789"}}'
 
r2 = s.get("http://httpbin.org/cookies")
print(r2.text)
#out '{"cookies": {"sessioncookie": "123456789"}}'

沒有使用session會話保持的方式:

import requests
r1 = requests.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
print(r1.text)
#out: '{"cookies": {"sessioncookie": "123456789"}}'

r2 = requests.get("http://httpbin.org/cookies")
print(r2.text)
#out '{"cookies": {}}'

由於手頭沒有示例,所以只能繼續先拿官方的示例來敷衍一下了。。。如果誰有好的網站可以分享一下。

為什么Session類可以保持會話

那么為什么session方法能夠保持會話呢,而requests普通的getpost等方法不能保持會話呢。那我們就得來探究下requests的內部是怎么實現的。回顧上文的get方法源碼,我們發現get方法內部調用的是request方法,看看這個方法里面實現了什么呢。

def request(method, url, **kwargs):
    """Constructs and sends a :class:`Request <Request>`.

    :param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
    :param url: URL for the new :class:`Request` object.
    :param params: (optional) Dictionary, list of tuples or bytes to send
        in the query string for the :class:`Request`.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
    :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
    :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
    :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
        ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
        or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string
        defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
        to add for the file.
    :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
    :param timeout: (optional) How many seconds to wait for the server to send data
        before giving up, as a float, or a :ref:`(connect timeout, read
        timeout) <timeouts>` tuple.
    :type timeout: float or tuple
    :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.
    :type allow_redirects: bool
    :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
    :param verify: (optional) Either a boolean, in which case it controls whether we verify
            the server's TLS certificate, or a string, in which case it must be a path
            to a CA bundle to use. Defaults to ``True``.
    :param stream: (optional) if ``False``, the response content will be immediately downloaded.
    :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response

    Usage::

      >>> import requests
      >>> req = requests.request('GET', 'https://httpbin.org/get')
      >>> req
      <Response [200]>
    """

    # By using the 'with' statement we are sure the session is closed, thus we
    # avoid leaving sockets open which can trigger a ResourceWarning in some
    # cases, and look like a memory leak in others.
    with sessions.Session() as session:
        return session.request(method=method, url=url, **kwargs)

我們發現request方法的內部也是使用了Session類的對象來進行請求的,說明他也是可以支持會話保持的,但是他使用了with關鍵字,在python基礎中with關鍵字的作用就是,處理事務后自動關閉,比如我們的讀寫文件。

with open('test.txt') as f:
    print(f.read())

我們都知道在使用with關鍵字后,文件with下面的內容被執行完畢后,文件會被自動關閉的,那么同樣的在這個request方法也是一樣的效果,在這次請求結束后,會話被自動關閉,說明其使用了with處理之后喪失了會話保持的能力。

那么我們在來看看Session類的內部是怎么實現with上下文的。

要想把一個類通過with關鍵字調用,那么類的內部,一定有__init__,__enter__,__exit__,這三個方法。

打開Session的源碼,我們一起看看。

class Session(SessionRedirectMixin):
    """A Requests session.

    Provides cookie persistence, connection-pooling, and configuration.

    Basic Usage::

      >>> import requests
      >>> s = requests.Session()
      >>> s.get('https://httpbin.org/get')
      <Response [200]>

    Or as a context manager::

      >>> with requests.Session() as s:
      ...     s.get('https://httpbin.org/get')
      <Response [200]>
    """

    __attrs__ = [
        'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify',
        'cert', 'adapters', 'stream', 'trust_env',
        'max_redirects',
    ]

    def __init__(self):

        #: A case-insensitive dictionary of headers to be sent on each
        #: :class:`Request <Request>` sent from this
        #: :class:`Session <Session>`.
        self.headers = default_headers()

        #: Default Authentication tuple or object to attach to
        #: :class:`Request <Request>`.
        self.auth = None

        #: Dictionary mapping protocol or protocol and host to the URL of the proxy
        #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to
        #: be used on each :class:`Request <Request>`.
        self.proxies = {}

        #: Event-handling hooks.
        self.hooks = default_hooks()

        #: Dictionary of querystring data to attach to each
        #: :class:`Request <Request>`. The dictionary values may be lists for
        #: representing multivalued query parameters.
        self.params = {}

        #: Stream response content default.
        self.stream = False

        #: SSL Verification default.
        self.verify = True

        #: SSL client certificate default, if String, path to ssl client
        #: cert file (.pem). If Tuple, ('cert', 'key') pair.
        self.cert = None

        #: Maximum number of redirects allowed. If the request exceeds this
        #: limit, a :class:`TooManyRedirects` exception is raised.
        #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is
        #: 30.
        self.max_redirects = DEFAULT_REDIRECT_LIMIT

        #: Trust environment settings for proxy configuration, default
        #: authentication and similar.
        self.trust_env = True

        #: A CookieJar containing all currently outstanding cookies set on this
        #: session. By default it is a
        #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but
        #: may be any other ``cookielib.CookieJar`` compatible object.
        self.cookies = cookiejar_from_dict({})

        # Default connection adapters.
        self.adapters = OrderedDict()
        self.mount('https://', HTTPAdapter())
        self.mount('http://', HTTPAdapter())

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

這個里面就實現了一套with關鍵字操作,可以看到的__exit__就實現了清理的操作。這樣每次請求之后的會話記錄就被清除了。

所以我們要想保持會話記錄,可以直接使用Session進行請求,這樣在不關閉python解釋器的情況下每次請求的會話會一直被保持。

至於close里面的實現,目前我也只能看一個大概,里面的整體邏輯還是挺復雜的,為了不誤人子弟,這里我就不再講解了。大佬們可以自己去看看。

封裝requests

requests庫已經很好用了,但是我們在這個基礎上還是要進行一層封裝,為什么要封裝呢。

首先,封裝可以更好的去集成到測試框架中,減少我們的代碼數量,並且我們只要要通過封裝的類實例化一次session會話,不用擔心會話重復的問題。在者我們可以和測試框架的其他模塊進行耦合,所以在這種前提下,封裝requests必不可少,即使他已經足夠好用了。

以下是我的一個封裝示例。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import urllib3
import requests

urllib3.disable_warnings()

http_method_names = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']


class HttpRequest(object):
    """requests方法二次封裝"""

    def __init__(self):
        self.timer = None
        self.timeout = 30.0
        self.session = requests.session()

    def request(self, method: str, url: str, **kwargs):
        """發送請求"""
        method = method.lower()

        def dispatch(method, *args, **kwargs):
            if method in http_method_names:
                handler = getattr(self.session, method)
                return handler(*args, **kwargs)
            else:
                raise AttributeError("請求方法不正確!")

        response = dispatch(method, url, **kwargs)
        self.timer = response.elapsed.total_seconds()
        return response

    def __call__(self, *args, **kwargs):
        """發送request請求"""
        return self.request(*args, **kwargs)


req = HttpRequest()

if __name__ == '__main__':
    r = req("get", url="https://www.baidu.com")
    print(r.text)
    print(req.timer)

運行一下:

image-20201112230753490

requests的封裝方式仁者見仁智者見智,每個人都有不一樣的思想,不能因為看到了別人的封裝形式就限制住了自己的思維,一定要有自己的想法,可以集百家之長處,但不可渾然無己見。所以在使用這個庫的時候希望我們都能在工作或學習中發揮出他的妙用。

最后就到這里吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM