WebサーバーからHTMLやPDFのファイルをダウンロードするには、本講座ではrequestsモジュールを利用しています。通常は以下のようにrequests.get()を用いて簡単にダウンロードできます。

>>> import requests
>>> r = requests.get("https://httpbin.org/html")
>>> print(r.text)
<html>
  <head>
  </head>
  <body>
      <h1>Herman Melville - Moby-Dick</h1>
(中略)
 </body>
</html>

しかし、なぜかダウンロードに失敗する場合があります。いくつかの考えるられるケースについて対策を挙げてみましたので、ぜひ参考にしてください。

テストにとても便利なサイト

簡単なリソースのダウンロードや指定したステータスコードを返してくれる「httpbin.org」というとても便利なサイトがあります。本記事でもこのサイトを活用しています。

httpbin.org

ダウンロードに失敗した時の症状

接続自体ができない

回線が繋がらない等の理由で、インターネット回線を介して相手のWebサーバーに接続できない場合は、以下のように複数のエラーが発生します。

>>> import requests
>>> r = requests.get("https://httpbin.org/html")Traceback (most recent call last):
  (中略)
socket.gaierror: [Errno 11001] getaddrinfo failed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  (中略)
urllib3.exceptions.NewConnectionError: : Failed to establish a new connection: [Errno 11001] getaddrinfo failed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  (中略)
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='httpbin.org', port=443): Max retries exceeded with url: /headers (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 11001] getaddrinfo failed'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  (中略)
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='httpbin.org', port=443): Max retries exceeded with url: /headers (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 11001] getaddrinfo failed'))

接続はできるがダウンロードに失敗する

接続できるのにダウンロードに失敗する場合は、requests.get()は通常エラーを表示しません。以下のように空の中身を返すだけです。

>>> r = requests.get("https://httpbin.org/status/503")
>>> r.text
''
>>> r.content
b''

ダウンロードに成功したかどうかはステータスコードを確認します。200であれば成功です。

>>> r.status_code
503
>>> is_ok = r.status_code == 200
>>> is_ok
False

失敗したらエラーを発生させたい場合は、raise_for_status()メソッドを実行します。

>>> r.raise_for_status()Traceback (most recent call last):
  (中略)
requests.exceptions.HTTPError: 503 Server Error: SERVICE UNAVAILABLE for url: https://httpbin.org/status/503
エラーのステータスコード

エラーになるステータスコードは、400番台(クライアントエラー)、500番台(サーバーエラー)です。それぞれ代表的なステータスコードは以下の通りです。

代表的なエラーのステータスコード
ステータスコード 説明
400 Bad Request リクエストに問題がある
401 Unauthorized 認証されていないためアクセスできない
403 Forbidden 許可されていないリクエスト
403 Forbidden 許可されていないリクエスト
404 Not Found リクエストされたリソースは存在しない
408 Request Timeout 制限時間内にリクエストを処理できなかった
500 Internal Server Error サーバー内部で予期しないエラーが発生した
503 Service Unavailable サーバーが一時的にリクエストを処理できない状態

対策1:プロキシを指定する

講座テキストにある弊社のサンプルページでもダウンロード出来ずプロキシサーバーのある社内環境からアクセスしている場合は、まずこの対策方法を試してください。

プロキシーを通過させるためにアドレスを指定します。方法には以下の2通りがあります。

1. get()の引数で指定する方法

以下のようにrequests.get()の引数に、proxies=でプロキシのアドレスを辞書で指定します。

import requests
proxies_dic = {
  "http": "http://proxy.example.co.jp:8080",
  "https": "https://proxy.example.co.jp:8080",
}
requests.get("https://www.kantei.go.jp", proxies=proxies_dic)

2. OSの環境変数に指定する方法

OSの環境変数は、コマンドプロンプトでsetコマンドを使う方法とPythonのコードでosモジュールを使うのと2通りで可能です。

コマンドプロンプトで指定する場合
> set HTTP_PROXY=http://proxy.example.co.jp:8080
> set HTTPS_PROXY=https://proxy.example.co.jp:8080
Pythonコードで環境変数を指定する場合
import os
os.environ["HTTP_PROXY"] = "http://proxy.example.co.jp:8080"
os.environ["HTTPS_PROXY"] = "https://proxy.example.co.jp:8080"

requestsの公式ドキュメントでは以下のページで説明されています。

https://requests.kennethreitz.org/en/master/user/advanced/#proxies

認証が必要なプロキシサーバーの場合

認証が必要な場合は以下のようにユーザー名(user)とパスワード(pass)を指定します。

"http://user:pass@proxy.example.co.jp:8080"
"https://user:pass@proxy.example.co.jp:8080"

対策2:リクエストのヘッダー情報を書き換える

Webサーバーは様々な理由でアクセスを制限している場合があります。例えば、負荷軽減対策として、機械によるリクエストをブロックすることがあります。

その時に、リクエストを許可するかどうかを判断するのに、ヘッダーの情報がよく用いられます。ヘッダーとはリクエストに付随する情報で、どのようなクライアントを使用しているか、どこのリンクから辿ってきたのかなどが書き込まれます。

httpbin.orgのhttps://httpbin.org/headersにアクセスすると、以下のようにリクエストした時のヘッダーの内容をレスポンスで返してくれます。

>>> import requests
>>> r = requests.get("https://httpbin.org/headers")
>>> print(r.text)
{
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.22.0"
  }
}

ここで、「User-Agent」を見ると”python-requests/2.22.0″のようにPythonのrequestsをクライアントに使用してリクエストしていることがわかります。

一方、Chromeブラウザーからリクエストした場合は、”Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36″のようになります(環境により内容は変わります)。

対策方法

機械によるリクエストは「User-Agentがブラウザーのものかどうか」で判別するのが、シンプルな方法として用いられます。そこで、まずはUser-Agentを適当なブラウザーのものに書き換えてダウンロードを試してみてください。

以下のようにrequests.get()の引数でheaders=に書き換えるヘッダー情報を辞書で渡します。

>>> import requests
>>> headers_dic = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"}
>>> r = requests.get("https://httpbin.org/headers", headers=headers_dic)
>>> print(r.text)
{
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
  }
}

対策3:リトライを実行する

500番台は一時的なサーバーエラーであることが多いので、一度失敗してもリトライするとダウンロードに成功することがよくあります

対策方法

以下のようなリトライ用のget_retry()関数を作成してリクエストを実行してみてください。以下の例では、ステータスコードが「500、502、503」のいずれかの場合に「3回まで」リトライします。リトライの間はtime.sleep(2)により「2秒間」待機します。

import requests
import time

def get_retry(url, retry_times, errs):
    for t in range(retry_times + 1):
        r = requests.get(url)
        if t < retry_times:
            if r.status_code in errs:
                time.sleep(2)
                continue
        return r

res = get_retry("https://httpbin.org/status/503", 3, [500, 502, 503])
print(res.status_code)

requestsモジュールのアダプターを用いた方法

requests.adapters.HTTPAdapterを用いて、最大リトライ回数(max_retries)を指定することもできます。詳しくは以下のページを参照してください。

https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request

その他

まだ他にも様々な原因でダウンロードに失敗する場合があります。

大きなファイルのダウンロード

大きなファイルのダウンロードに失敗してしまう場合は、以下のようにstream=Trueを指定してみてください。その場合は、requests.get()with文で実行して終了したらclose()が呼び出されるようにしてください。

import requests

large_image_url = "https://httpbin.org/image/jpeg"

with requests.get(large_image_url, stream=True) as r:
    with open("large_image.jpg", 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024):
            if chunk:
                f.write(chunk)