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>

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

ダウンロードできないサイト 対策
✕ すべてのサイト 対策1:プロキシを指定する
△ 特定のサイトだけ 対策2:リクエストのヘッダー情報を書き換える
対策3:リトライを実行する
対策4:requests-htmlを利用する

またその他のケースとして、
大きなファイルのダウンロードに失敗してしまう場合」、「いつの間にかSSLエラーが出るようになった場合」があります。これらについても対策を説明していますので、併せて参考にしてください。

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

簡単なリソースのダウンロードや指定したステータスコードを返してくれる「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": "http://proxy.example.co.jp:8080",
}
r = requests.get("https://www.kantei.go.jp", proxies=proxies_dic)
print(r.status_code)

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

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

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

対策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

対策4:requests-htmlを利用する

Yahoo!やTwitterなどにある頻繁に更新されるWebページでは、requestsでダウンロードは出来るのに、そこから要素をうまく読み取れないことがあります。この問題は、requests-htmlライブラリを利用すると解決できる場合があります。以下のページで詳しく説明しています。

no image
スクレイピングの定番の方法と言えば「requests + BeautifulSoup」の組み合わせです。一般的はWebページであれば、大抵はスクレイピングできます。 しかし、この方法で読み取れな…

その他

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

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

大きなファイルのダウンロードに失敗してしまう場合は、以下のように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)

いつの間にかSSLエラーが出るようになった場合

今まで問題なく通信できていたのに、突然ssl.SSLErrorrequests.exceptions.SSLErrorが出るようになることがあります。その場合は、SSL通信に使用している証明書が古くなってしまった可能性があるので、コマンドプロンプトで以下のコマンドを実行して「certifi 」というライブラリを更新してください。

> py -m pip install -U certifi

# 環境により適宜以下のコマンドを使用
> python -m pip install -U certifi
> pip3 install -U certifi

requestsはバージョン2.16より前までは、MozillaのCA証明書(Certificate Authority certificate)のセットをバンドルしていました。そのため、requestsを更新しないとCA証明書も更新されませんでした。現在ではcertifiライブラリのCA証明書を使用しているので、certifiだけを更新すれば新しいCA証明書を使うことができます。

なお、以下のドキュメントでも説明されているように、セキュリティの面からもcertifiはなるべく頻繁に更新するようにしてください。

https://docs.python-requests.org/en/master/user/advanced/