Pythonリクエストの高度な使用方法


PythonのHTTPライブラリrequests おそらく私のプログラムのすべての言語で私のお気に入りのHTTPユーティリティです.これは、シンプルで直感的で、ユビキタスのPythonコミュニティです.HTTPとのインタフェースのほとんどのプログラムは標準ライブラリからのリクエストかurllib 3を使用します.
簡単なAPIのため、すぐに要求を生産するのは簡単ですが、ライブラリはまた、高度なユースケースの拡張性を提供しています.あなたがAPI重いクライアントまたはウェブスクレーパーを書くならば、あなたはおそらくネットワーク失敗、役に立つデバッグ跡と構文上の砂糖に寛容を必要とします.
以下は、JSON APIを広範囲に使用しているウェブ・スクレーピング・ツールまたはプログラムを書くとき、私が要求において役に立つとわかる特徴の概要です.

目次

  • Request hooks
  • Setting base URLs
  • Setting default timeouts
  • Retry on failure
  • Debugging HTTP requests
  • Testing and mocking requests
  • Mimicking browser behaviors
  • リクエストフック


    サードパーティAPIを使用する場合、返される応答が実際に有効であることを確認します.リクエストは速記ヘルパーを提供するraise_for_status() これは、応答HTTPステータスコードが4 xxまたは5 xxでないと主張します、すなわち、要求がクライアントかサーバエラーをもたらさなかったということです.
    例えば
    response = requests.get('https://api.github.com/user/repos?page=1')
    # Assert that there were no errors
    response.raise_for_status()
    
    必要に応じてこれは繰り返し得ることができますraise_for_status() 呼び出しごとに.幸いにもリクエストライブラリは、コールバックを付けることができるフックのインターフェイスを提供しています
    リクエストプロセスのある部分について.
    我々は、フックを使用することができますraise_for_status() が各レスポンスオブジェクトに対して呼び出されます.
    # Create a custom requests object, modifying the global module throws an error
    http = requests.Session()
    
    assert_status_hook = lambda response, *args, **kwargs: response.raise_for_status()
    http.hooks["response"] = [assert_status_hook]
    
    http.get("https://api.github.com/user/repos?page=1")
    
    > HTTPError: 401 Client Error: Unauthorized for url: https://api.github.com/user/repos?page=1
    

    ベースURLの設定


    APIでホストされているAPIだけを使用しているとします.org. HTTPプロトコルごとにプロトコルとドメインを繰り返します.
    requests.get('https://api.org/list/')
    requests.get('https://api.org/list/3/item')
    
    あなた自身を使用していくつかの入力を保存することができますBaseUrlSession . これにより、HTTPクライアントの基本URLを指定し、リクエストのときにリソースパスを指定できます.
    from requests_toolbelt import sessions
    http = sessions.BaseUrlSession(base_url="https://api.org")
    http.get("/list")
    http.get("/list/item")
    
    注意requests toolbelt デフォルトのリクエストのインストールには含まれませんので、別途インストールする必要があります.

    デフォルトのタイムアウトの設定


    リクエストドキュメントrecommends すべてのプロダクションコードでタイムアウトを設定します.タイムアウトを設定するのを忘れてしまった場合は、あなたのアプリケーションをハングさせるかもしれません.
    requests.get('https://github.com/', timeout=0.001)
    
    しかし、これは繰り返しになり、誰かがタイムアウトを設定し、生産のプログラムを停止していることを認識したときに将来のテーブルの反転を引き起こす可能性があります.

    使用Transport Adapters すべてのHTTPコールのデフォルトタイムアウトを設定できます.これは、開発者がtimeout = 1パラメータを彼の個々の呼び出しに加えるのを忘れても、賢明なタイムアウトがセットされるのを確実にしますが、1回の呼び出しベースでオーバーライドを考慮に入れます.
    既定のタイムアウトを持つカスタムトランスポートアダプタの例ですthis Github comment . timeout引き数が与えられていない場合は、デフォルトのタイムアウトを確実にするためにHTTPクライアントとsend ()メソッドを構築するときにコンストラクタをオーバーライドしてデフォルトのタイムアウトを提供します.
    from requests.adapters import HTTPAdapter
    
    DEFAULT_TIMEOUT = 5 # seconds
    
    class TimeoutHTTPAdapter(HTTPAdapter):
        def __init__(self, *args, **kwargs):
            self.timeout = DEFAULT_TIMEOUT
            if "timeout" in kwargs:
                self.timeout = kwargs["timeout"]
                del kwargs["timeout"]
            super().__init__(*args, **kwargs)
    
        def send(self, request, **kwargs):
            timeout = kwargs.get("timeout")
            if timeout is None:
                kwargs["timeout"] = self.timeout
            return super().send(request, **kwargs)
    
    以下のように使えます:
    import requests
    
    http = requests.Session()
    
    # Mount it for both http and https usage
    adapter = TimeoutHTTPAdapter(timeout=2.5)
    http.mount("https://", adapter)
    http.mount("http://", adapter)
    
    # Use the default 2.5s timeout
    response = http.get("https://api.twilio.com/")
    
    # Override the timeout as usual for specific requests
    response = http.get("https://api.twilio.com/", timeout=10)
    

    失敗を再試行する


    ネットワーク接続は損失、混雑し、サーバーが失敗します.本当に堅牢なプログラムを作りたいならば、我々は失敗を説明して、再試行戦略を必要とします.
    あなたのHTTPクライアントに再試行戦略を追加簡単です.HttpAdapterを作成し、アダプターに戦略を渡します.
    from requests.adapters import HTTPAdapter
    from requests.packages.urllib3.util.retry import Retry
    
    retry_strategy = Retry(
        total=3,
        status_forcelist=[429, 500, 502, 503, 504],
        method_whitelist=["HEAD", "GET", "OPTIONS"]
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    http = requests.Session()
    http.mount("https://", adapter)
    http.mount("http://", adapter)
    
    response = http.get("https://en.wikipedia.org/w/api.php")
    
    デフォルトの再試行クラスは、saneのデフォルト値を提供しますが、非常に設定可能です.
    以下のパラメータはリクエストライブラリが使用するデフォルトパラメータを含みます.
    total=10
    
    再試行の合計数を試みます.失敗したリクエストまたはリダイレクトの数がこの数を超える場合、クライアントはurllib3.exceptions.MaxRetryError 例外.私は、私が働いているAPIに基づいて、このパラメタを変えます、しかし、通常、私は通常10より低くそれをセットしました.
    status_forcelist=[413, 429, 503]
    
    HTTPレスポンスコードを再試行します.サーバと逆プロキシがHTTP仕様に常に適合していないので、一般的なサーバエラー(500、502、503、504)で再試行したいと思います.
    method_whitelist=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]
    
    再試行するHTTPメソッド.デフォルトでは、POSTを除くすべてのHTTPメソッドが含まれています.POSTは新しい挿入を行うことができます.このパラメータをポストに含めるように変更します.APIのほとんどは、エラーコードを返さず、同じ呼び出しで挿入を実行しないからです.そして、彼らがするならば、あなたは多分バグ報告を出すべきです.
    backoff_factor=0
    
    これは面白いものです.これは、プロセスが失敗した要求の間にスリープ状態になるかを変更することができます.アルゴリズムは以下の通りです.
    {backoff factor} * (2 ** ({number of total retries} - 1))
    
    例えば、バックオフ係数が設定されている場合、
  • 1秒連続眠る0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256 .
  • 2秒-1, 2, 4, 8, 16, 32, 64, 128, 256, 512
  • 10秒5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560
  • 値は指数関数的に増加するretry strategies .
    この値はデフォルト0で、指数関数のバックオフは設定されず、再試行は直ちに実行されます.これを1に設定してください
    あなたのサーバ!
    再試行モジュールの完全な文書はhere .

    タイムアウトとリトライの組み合わせ


    HttpAdapterは同等ですので、再試行とタイムアウトを組み合わせることができます.
    retries = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])
    http.mount("https://", TimeoutHTTPAdapter(max_retries=retries))
    

    要求のデバッグ


    時々、要求は失敗します、そして、あなたは理由を理解することができません.要求と応答をログ出力すると、失敗に対する洞察を与えるかもしれません.これを行うには2つの方法があります.

    HTTPヘッダの出力


    0より大きいログレベルのログレベルを変更すると、レスポンスHTTPヘッダがログ出力されます.これは最も簡単なオプションですが、HTTPリクエストやレスポンス本文を見ることはできません.ロギングに適していないか、バイナリ内容を含む大きなボディーペイロードを返すAPIを扱うなら、それは役に立ちます.
    0より大きい値はデバッグログを有効にします.
    import requests
    import http
    
    http.client.HTTPConnection.debuglevel = 1
    
    requests.get("https://www.google.com/")
    
    # Output
    send: b'GET / HTTP/1.1\r\nHost: www.google.com\r\nUser-Agent: python-requests/2.22.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\n\r\n'
    reply: 'HTTP/1.1 200 OK\r\n'
    header: Date: Fri, 28 Feb 2020 12:13:26 GMT
    header: Expires: -1
    header: Cache-Control: private, max-age=0
    

    印刷すべて


    リクエストと応答のテキスト表現の両方を含むHTTP LifeCycle全体をログに記録する場合は、RequestsCount ToolBarからリクエストフックとダンプUtilsを使用できます.
    私は非常に大きな応答を返さない残りのベースのAPIに対処しているいつでもこのオプションを好む.
    import requests
    from requests_toolbelt.utils import dump
    
    def logging_hook(response, *args, **kwargs):
        data = dump.dump_all(response)
        print(data.decode('utf-8'))
    
    http = requests.Session()
    http.hooks["response"] = [logging_hook]
    
    http.get("https://api.openaq.org/v1/cities", params={"country": "BA"})
    
    # Output
    < GET /v1/cities?country=BA HTTP/1.1
    < Host: api.openaq.org
    
    > HTTP/1.1 200 OK
    > Content-Type: application/json; charset=utf-8
    > Transfer-Encoding: chunked
    > Connection: keep-alive
    >
    {
       "meta":{
          "name":"openaq-api",
          "license":"CC BY 4.0",
          "website":"https://docs.openaq.org/",
          "page":1,
          "limit":100,
          "found":1
       },
       "results":[
          {
             "country":"BA",
             "name":"Goražde",
             "city":"Goražde",
             "count":70797,
             "locations":1
          }
       ]
    }
    
    参照https://toolbelt.readthedocs.io/en/latest/dumputils.html

    テストとモッキング要求


    サードパーティAPIを使用して開発に痛みポイントを導入する-彼らはユニットテストに困難です.エンジニアSentry 開発中にモックスリクエストにライブラリを書くことでこの痛みの一部を緩和しました.
    HTTPレスポンスをサーバに送信する代わりにgetsentry/responses HTTPリクエストを傍受し、テスト中に追加した応答を返します.
    それは例でより実証されます.
    import unittest
    import requests
    import responses
    
    
    class TestAPI(unittest.TestCase):
        @responses.activate  # intercept HTTP calls within this method
        def test_simple(self):
            response_data = {
                    "id": "ch_1GH8so2eZvKYlo2CSMeAfRqt",
                    "object": "charge",
                    "customer": {"id": "cu_1GGwoc2eZvKYlo2CL2m31GRn", "object": "customer"},
                }
            # mock the Stripe API
            responses.add(
                responses.GET,
                "https://api.stripe.com/v1/charges",
                json=response_data,
            )
    
            response = requests.get("https://api.stripe.com/v1/charges")
            self.assertEqual(response.json(), response_data)
    
    mockked応答にマッチしないHTTPリクエストが作成された場合、ConnectionErrorがスローされます.
    class TestAPI(unittest.TestCase):
        @responses.activate
        def test_simple(self):
            responses.add(responses.GET, "https://api.stripe.com/v1/charges")
            response = requests.get("https://invalid-request.com")
    
    出力
    requests.exceptions.ConnectionError: Connection refused by Responses - the call doesn't match any registered mock.
    
    Request:
    - GET https://invalid-request.com/
    
    Available matches:
    - GET https://api.stripe.com/v1/charges
    

    ブラウザの動作を模倣する


    あなたが十分なウェブスクレーパーコードを書いたならば、あなたはブラウザーを使用しているか、プログラム的にサイトにアクセスしているならば、特定のウェブサイトが異なるHTMLを返す方法に気がつきます.時々、これは反掻き対策です、しかし、通常、サーバーはどのような内容が最高の装置(例えばデスクトップまたはモバイル)に合うかについて調べるために、ユーザーエージェントSnffffingに取り組みます.
    ブラウザの表示と同じ内容を返したい場合は、ユーザエージェントのヘッダリクエストをオーバーライドできます.
    import requests
    http = requests.Session()
    http.headers.update({
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"
    })