1. 程式人生 > >python aiohttp簡易使用教程

python aiohttp簡易使用教程

0. 前言

本文翻譯自aiohttp的官方文件,如有紕漏,歡迎指出。

aiohttp分為伺服器端和客戶端,本文只介紹客戶端。

另外我已經對 aiohttp 和 asyncio進行了封裝,可以參考我的 github 地址:

https://github.com/web-trump/ahttp

由於上下文的緣故,請求程式碼必須在一個非同步的函式中進行:

async def fn():

pass


1. aiohttp安裝


pip3 install aiohttp



1.1. 基本請求用法




  
  1. async with aiohttp. get( 'https://github.com') as r:
  2. await r.text()

    其中r.text(), 可以在括號中指定解碼方式,編碼方式,例如

    await resp.text(encoding='windows-1251')
       

      或者也可以選擇不編碼,適合讀取影象等,是無法編碼的

      await resp.read()
         



      2.發起一個session請求


      首先是匯入aiohttp模組:

      import aiohttp
      然後我們試著獲取一個web原始碼,這裡以GitHub的公共Time-line頁面為例:

      async with aiohttp.ClientSession() as session:
          async with session.get('https://api.github.com/events'
      ) as resp: print(resp.status) print(await resp.text())
      上面的程式碼中,我們建立了一個 ClientSession 物件命名為session,然後通過session的get方法得到一個 ClientResponse 物件,命名為resp,get方法中傳入了一個必須的引數url,就是要獲得原始碼的http url。至此便通過協程完成了一個非同步IO的get請求。

      有get請求當然有post請求,並且post請求也是一個協程:

      session.post('http://httpbin.org/post', data=b'data')
      用法和get是一樣的,區別是post需要一個額外的引數data,即是需要post的資料。

      除了get和post請求外,其他http的操作方法也是一樣的:

      session.put('http://httpbin.org/put', data=b'data')
      session.delete('http://httpbin.org/delete')
      session.head('http://httpbin.org/get')
      session.options('http://httpbin.org/get')
      session.patch('http://httpbin.org/patch', data=b'data')
      小記:

      不要為每次的連線都建立一次session,一般情況下只需要建立一個session,然後使用這個session執行所有的請求。

      每個session物件,內部包含了一個連線池,並且將會保持連線和連線複用(預設開啟)可以加快整體的效能。


      3.在URL中傳遞引數


      我們經常需要通過 get 在url中傳遞一些引數,引數將會作為url問號後面的一部分發給伺服器。在aiohttp的請求中,允許以dict的形式來表示問號後的引數。舉個例子,如果你想傳遞 key1=value1   key2=value2 到 httpbin.org/get 你可以使用下面的程式碼:

      params = {'key1': 'value1', 'key2': 'value2'}
      async with session.get('http://httpbin.org/get',
                             params=params) as resp:
                             assert resp.url == 'http://httpbin.org/get?key2=value2&key1=value1'
      可以看到,程式碼正確的執行了,說明引數被正確的傳遞了進去。不管是一個引數兩個引數,還是更多的引數,都可以通過這種方式來傳遞。除了這種方式之外,還有另外一個,使用一個 list 來傳遞(這種方式可以傳遞一些特殊的引數,例如下面兩個key是相等的也可以正確傳遞):

      params = [('key', 'value1'), ('key', 'value2')]
      async with session.get('http://httpbin.org/get',
                             params=params) as r:
          assert r.url == 'http://httpbin.org/get?key=value2&key=value1'
      除了上面兩種,我們也可以直接通過傳遞字串作為引數來傳遞,但是需要注意,通過字串傳遞的特殊字元不會被編碼:

      async with session.get('http://httpbin.org/get',
                             params='key=value+1') as r:
              assert r.url == 'http://httpbin.org/get?key=value+1'


      4.響應的內容


      還是以GitHub的公共Time-line頁面為例,我們可以獲得頁面響應的內容:

      async with session.get('https://api.github.com/events') as resp:
          print(await resp.text())
      執行之後,會打印出類似於如下的內容:

      '[{"created_at":"2015-06-12T14:06:22Z","public":true,"actor":{...
      resp的text方法,會自動將伺服器端返回的內容進行解碼--decode,當然我們也可以自定義編碼方式:

      await resp.text(encoding='gb2312')
      除了text方法可以返回解碼後的內容外,我們也可以得到型別是位元組的內容:

      print(await resp.read())
      執行的結果是:

      b'[{"created_at":"2015-06-12T14:06:22Z","public":true,"actor":{...

      gzip和deflate轉換編碼已經為你自動解碼。

      小記:

      text(),read()方法是把整個響應體讀入記憶體,如果你是獲取大量的資料,請考慮使用”位元組流“(streaming response


      5.特殊響應內容:json


      如果我們獲取的頁面的響應內容是json,aiohttp內建了更好的方法來處理json:

      async with session.get('https://api.github.com/events') as resp:
          print(await resp.json())
      如果因為某種原因而導致resp.json()解析json失敗,例如返回不是json字串等等,那麼resp.json()將丟擲一個錯誤,也可以給json()方法指定一個解碼方式:

      print(await resp.json(
      encoding='gb2312'
      )) 或者傳遞一個函式進去:

      print(await resp.json( lambda(x:x.replace('a','b')) ))


      6.以位元組流的方式讀取響應內容


      雖然json(),text(),read()很方便的能把響應的資料讀入到記憶體,但是我們仍然應該謹慎的使用它們,因為它們是把整個的響應體全部讀入了記憶體。即使你只是想下載幾個位元組大小的檔案,但這些方法卻將在記憶體中載入所有的資料。所以我們可以通過控制位元組數來控制讀入記憶體的響應內容:

      async with session.get('https://api.github.com/events') as resp:
          await resp.content.read(10) #讀取前10個位元組
      一般地,我們應該使用以下的模式來把讀取的位元組流儲存到檔案中:

      with open(filename, 'wb') as fd:
          while True:
              chunk = await resp.content.read(chunk_size)
              if not chunk:
                  break
              fd.write(chunk)


      7.自定義請求頭


      如果你想新增請求頭,可以像get新增引數那樣以dict的形式,作為get或者post的引數進行請求:

      import json
      url = 'https://api.github.com/some/endpoint'
      payload = {'some': 'data'}
      headers = {'content-type': 'application/json'}
      

    await session.post(url,
    data=json.dumps(payload),
    headers=headers)


    8.自定義Cookie


    給伺服器傳送cookie,可以通過給 ClientSession 傳遞一個cookie引數:

    url = 'http://httpbin.org/cookies'
    cookies = {'cookies_are': 'working'}
    async with ClientSession(cookies=cookies) as session:
        async with session.get(url) as resp:
            assert await resp.json() == {
               "cookies": {"cookies_are": "working"}}

    可直接訪問連結 “httpbin.org/cookies”檢視當前cookie,訪問session中的cookie請見第10節。


    9.post資料的幾種方式


    (1)模擬表單post資料

    payload = {'key1': 'value1', 'key2': 'value2'}
    async with session.post('http://httpbin.org/post',
                            data=payload) as resp:
        print(await resp.text())

    注意:data=dict的方式post的資料將被轉碼,和form提交資料是一樣的作用,如果你不想被轉碼,可以直接以字串的形式 data=str 提交,這樣就不會被轉碼。

    (2)post json

    import json
    url = 'https://api.github.com/some/endpoint'
    payload = {'some': 'data'}
    

    async with session.post(url, data=json.dumps(payload)) as resp:

    其實json.dumps(payload)返回的也是一個字串,只不過這個字串可以被識別為json格式

    (3)post 小檔案

    url = 'http://httpbin.org/post'
    files = {'file': open('report.xls', 'rb')}
    

    await session.post(url, data=files)
    可以設定好檔名和content-type:

    url = 'http://httpbin.org/post'
    data = FormData()
    data.add_field('file',
                   open('report.xls', 'rb'),
                   filename='report.xls',
                   content_type='application/vnd.ms-excel')
    

    await session.post(url, data=data)
    如果將檔案物件設定為資料引數,aiohttp將自動以位元組流的形式傳送給伺服器。

    (4)post 大檔案

    aiohttp支援多種型別的檔案以流媒體的形式上傳,所以我們可以在檔案未讀入記憶體的情況下發送大檔案。

    @aiohttp.streamer
    def file_sender(writer, file_name=None):
        with open(file_name, 'rb') as f:
            chunk = f.read(2**16)
            while chunk:
                yield from writer.write(chunk)
                chunk = f.read(2**16)
    

    # Then you can use file_sender as a data provider:

    async with session.post(http://httpbin.org/post,
    data=file_sender(file_name=‘huge_file’)) as resp:
    print(await resp.text())
    同時我們可以從一個url獲取檔案後,直接post給另一個url,並計算hash值:

    async def feed_stream(resp, stream):
        h = hashlib.sha256()
    

    resp = session.get(http://httpbin.org/post)
    stream = StreamReader()
    loop.create_task(session.post(http://httpbin.org/post, data=stream))

    file_hash = await feed_stream(resp, stream)
    因為響應內容型別是StreamReader,所以可以把get和post連線起來,同時進行post和get:

    r = await session.get('http://python.org')
    await session.post('http://httpbin.org/post',
                       data=r.content)
    (5)post預壓縮資料

    在通過aiohttp傳送前就已經壓縮的資料, 呼叫壓縮函式的函式名(通常是deflate 或 zlib)作為content-encoding的值:

    async def my_coroutine(session, headers, my_data):
        data = zlib.compress(my_data)
        headers = {'Content-Encoding': 'deflate'}
        async with session.post('http://httpbin.org/post',
                                data=data,
                                headers=headers)
            pass


    10.keep-alive, 連線池,共享cookie


    ClientSession 用於在多個連線之間共享cookie:

    async with aiohttp.ClientSession() as session:
        await session.get(
            'http://httpbin.org/cookies/set?my_cookie=my_value')
        filtered = session.cookie_jar.filter_cookies('http://httpbin.org')
        assert filtered['my_cookie'].value == 'my_value'
        async with session.get('http://httpbin.org/cookies') as r:
            json_body = await r.json()
            assert json_body['cookies']['my_cookie'] == 'my_value'
    也可以為所有的連線設定共同的請求頭:

    async with aiohttp.ClientSession(
        headers={"Authorization": "Basic bG9naW46cGFzcw=="}) as session:
        async with session.get("http://httpbin.org/headers") as r:
            json_body = await r.json()
            assert json_body['headers']['Authorization'] == \
                'Basic bG9naW46cGFzcw=='
    ClientSession 還支援 keep-alive連線和連線池(connection pooling)


    11.cookie安全性


    預設ClientSession使用的是嚴格模式的 aiohttp.CookieJar. RFC 2109,明確的禁止接受url和ip地址產生的cookie,只能接受 DNS 解析IP產生的cookie。可以通過設定aiohttp.CookieJar 的 unsafe=True 來配置:

    jar = aiohttp.CookieJar(unsafe=True)
    session = aiohttp.ClientSession(cookie_jar=jar)


    12.控制同時連線的數量(連線池)


    也可以理解為同時請求的數量,為了限制同時開啟的連線數量,我們可以將限制引數傳遞給聯結器:

    conn = aiohttp.TCPConnector(limit=30)#同時最大進行連線的連線數為30,預設是100,limit=0的時候是無限制
    限制同時開啟限制同時開啟連線到同一端點的數量((host, port, is_ssl) 三的倍數),可以通過設定 limit_per_host 引數:

    conn = aiohttp.TCPConnector(limit_per_host=30)#預設是0


    13.自定義域名解析


    我們可以指定域名伺服器的 IP 對我們提供的get或post的url進行解析:

    from aiohttp.resolver import AsyncResolver
    

    resolver = AsyncResolver(nameservers=[“8.8.8.8”, “8.8.4.4”])
    conn = aiohttp.TCPConnector(resolver=resolver)


    14.設定代理


    aiohttp支援使用代理來訪問網頁:

    async with aiohttp.ClientSession() as session:
        async with session.get("http://python.org",
                               proxy="http://some.proxy.com") as resp:
            print(resp.status)
    當然也支援需要授權的頁面:
    async with aiohttp.ClientSession() as session:
        proxy_auth = aiohttp.BasicAuth('user', 'pass')
        async with session.get("http://python.org",
                               proxy="http://some.proxy.com",
                               proxy_auth=proxy_auth) as resp:
            print(resp.status)
    或者通過這種方式來驗證授權:
    session.get("http://python.org",
                proxy="http://user:[email protected]")


    15.響應狀態碼 response status code


    可以通過 resp.status來檢查狀態碼是不是200:

    async with session.get('http://httpbin.org/get') as resp:
        assert resp.status == 200


    16.響應頭


    我們可以直接使用 resp.headers 來檢視響應頭,得到的值型別是一個dict:

    >>> resp.headers
    {'ACCESS-CONTROL-ALLOW-ORIGIN': '*',
     'CONTENT-TYPE': 'application/json',
     'DATE': 'Tue, 15 Jul 2014 16:49:51 GMT',
     'SERVER': 'gunicorn/18.0',
     'CONTENT-LENGTH': '331',
     'CONNECTION': 'keep-alive'}
    或者我們可以檢視原生的響應頭:

    >>> resp.raw_headers
    ((b'SERVER', b'nginx'),
     (b'DATE', b'Sat, 09 Jan 2016 20:28:40 GMT'),
     (b'CONTENT-TYPE', b'text/html; charset=utf-8'),
     (b'CONTENT-LENGTH', b'12150'),
     (b'CONNECTION', b'keep-alive'))


    17.檢視cookie


    url = 'http://example.com/some/cookie/setting/url'
    async with session.get(url) as resp:
        print(resp.cookies)


    18.重定向的響應頭


    如果一個請求被重定向了,我們依然可以檢視被重定向之前的響應頭資訊:

    >>> resp = await session.get('http://example.com/some/redirect/')
    >>> resp
    <ClientResponse(http://example.com/some/other/url/) [200]>
    >>> resp.history
    (<ClientResponse(http://example.com/some/redirect/) [301]>,)


    19.超時處理


    預設的IO操作都有5分鐘的響應時間 我們可以通過 timeout 進行重寫:

    async with session.get('https://github.com', timeout=60) as r:
        ...
    如果 timeout=None 或者 timeout=0 將不進行超時檢查,也就是不限時長