PythonでWeb上の画像などのファイルをダウンロード(個別・一括)

Modified: | Tags: Python, 自動化, スクレイピング

Pythonで、Web上のファイル(画像やZIP、PDFなど)のURLを指定してダウンロード・保存する方法について説明する。

URLを指定してファイルをダウンロード

個別のファイルのURLを指定してダウンロードするのは標準ライブラリのみで実現できる。追加のインストールは必要ない。

サードパーティライブラリRequestsを使う方法については以下の記事を参照。

urllib.request.urlretrieve()

最も簡単なのはurllib.request.urlretrieve()を使う方法。

第一引数にURL、第二引数に保存先のパスを指定する。保存先のパスは絶対パスでも相対パスでもよい。

import urllib.request

url = 'https://www.python.org/static/img/python-logo.png'
dst_path = 'data/temp/py-logo.png'

urllib.request.urlretrieve(url, dst_path)

なお、urllib.request.urlretrieve()はPython2のurllibモジュールから移植された関数で、将来的に廃止される可能性がある。

レガシーインターフェースを避けたい場合は、次に紹介するurllib.request.urlopen()を使う。

urllib.request.urlopen()とopen()を利用

urllib.request.urlopen()で、URLを開いてデータをバイト列(bytes)で取得できる。

取得したデータを組み込み関数open()でバイナリとして書き込み、保存する。第二引数mode'wb'とすればよい。wが書き込み、bがバイナリの意味。

def download_file(url, dst_path):
    with urllib.request.urlopen(url) as web_file:
        with open(dst_path, 'wb') as local_file:
            local_file.write(web_file.read())

url = 'https://www.python.org/static/img/python-logo.png'
dst_path = 'data/temp/py-logo.png'

download_file(url, dst_path)

ネストしたwith文はカンマ区切りで一度に書けるので、以下のように書くことも可能。

def download_file(url, dst_path):
    with urllib.request.urlopen(url) as web_file, open(dst_path, 'wb') as local_file:
        local_file.write(web_file.read())

download_file(url, dst_path)

保存先のディレクトリを指定、URLのファイル名で保存

保存先のディレクトリを指定して、URLのファイル名で保存するには、以下のようにする。ここでは上で定義した関数を使っているが、urllib.request.urlretrieve()でも同様。

import os

def download_file_to_dir(url, dst_dir):
    download_file(url, os.path.join(dst_dir, os.path.basename(url)))

url = 'https://www.python.org/static/img/python-logo.png'
dst_dir = 'data/temp'

download_file_to_dir(url, dst_dir)

os.path.basename()でURLからファイル名を抽出し、os.path.join()で指定したディレクトリと結合して保存先のパスを生成している。パス文字列の操作についての詳細は以下の記事を参照。

例外処理

urllib.request.urlretrieve()urllib.request.urlopen()では、存在しないURLを指定するとHTTPErrorが送出される。

url_error = 'https://www.python.org/static/img/python-logo-abc.png'
dst_path = 'data/temp/py-logo.png'

# download_file(url_error, dst_path)
# HTTPError: HTTP Error 404: Not Found

これを捕捉するには、urllib.errorをインポートして使う。

import urllib.error

try:
    download_file(url_error, dst_path)
except urllib.error.HTTPError as e:
    print(e)
# HTTP Error 404: Not Found

urllib.error.HTTPErrorurllib.error.URLErrorのサブクラスなので、urllib.error.URLErrorを指定してもよい。

ローカルでの保存時の例外も捕捉したい場合は、exceptにタプルを指定する。例えば存在しないディレクトリを含むパスを指定するとFileNotFoundErrorとなる。

url = 'https://www.python.org/static/img/python-logo.png'
dst_path_error = 'data/not_exist/py-logo.png'

try:
    download_file(url, dst_path_error)
except (urllib.error.HTTPError, FileNotFoundError) as e:
    print(e)
# [Errno 2] No such file or directory: 'data/not_exist/py-logo.png'

例外処理の詳細は以下の記事を参照。

URL指定の注意点

これまでの例は画像ファイルをダウンロード・保存したが、ただ単にWeb上のファイルを開いてそのままローカルに保存しているだけなので、その他の種類のファイルでも同じ関数が使える。

url_zip = 'https://github.com/nkmk/python-snippets/raw/master/notebook/data/src/sample_header.csv.zip'
download_file_to_dir(url_zip, dst_dir)

url_pdf = 'https://github.com/nkmk/python-snippets/raw/master/notebook/data/src/pdf/sample1.pdf'
download_file_to_dir(url_pdf, dst_dir)

ただし、指定するURLはファイルそのものへのリンクでなければならない。

例えば、GitHubのレポジトリのファイルの場合、以下のURLは拡張子がpdfだが実際はhtmlのページ。このURLを指定すると、htmlのソースがダウンロードされてしまう。

ファイルの実体(raw file)へのリンクは以下のURL。ファイルをダウンロード・保存したい場合はこちらを指定する必要がある。

また、ユーザーエージェントやリファラなどでアクセスが制限されていてダウンロードできない場合もある。すべてのファイルのダウンロードを保証するものではない。

ユーザーエージェントなどリクエストヘッダの変更・追加はRequestsを使うと簡単。

複数のURLからファイルを一括ダウンロード

複数のURLからファイルをダウンロードしたい場合は、各URLに対して個別にurllib.request.urlretrieve()や上述のurllib.request.urlopen()を使った関数を呼び出せばよい。

例えばURLのリストがあれば、forループで回せばよい。仮のURLリストのため、ここでは関数呼び出しはコメントアウトしている。

import time

url_list = ['url_0', 'url_1', 'url_2', 'url_3', 'url_4']
dst_dir = 'data/temp'
sleep_time_sec = 1

for url in url_list:
#     download_file_to_dir(url, dst_dir)
    time.sleep(sleep_time_sec)

サーバーに負担をかけないように、ファイルを1つダウンロードするごとにtime.sleep()で待機させている。単位は秒なので、上の例では1秒ずつスリープ。timeモジュールをインポートして使う。

Webページの画像のURLを抽出し、一括ダウンロード

ファイルを一括ダウンロードする具体的な例として、Webページ内の画像の一括ダウンロードを考える。

連番になっている場合

ダウンロードしたい画像のURLが単純な連番になっている場合は簡単。連番に限らず何らかの規則性があれば、後述のBeautiful Soupなどでスクレイピングをするより、規則に従ってURLを生成するほうが楽。

dst_dir = 'data/temp'
sleep_time_sec = 1

for i in range(5):
    url = f'https://example.com/dir/base_{i:03}.jpg'
    print(url)
#     download_file_to_dir(url, dst_dir)
    time.sleep(sleep_time_sec)
# https://example.com/dir/base_000.jpg
# https://example.com/dir/base_001.jpg
# https://example.com/dir/base_002.jpg
# https://example.com/dir/base_003.jpg
# https://example.com/dir/base_004.jpg

上の例では{i:03}で3桁の0埋め連番としている。0埋めの必要が無い場合は{i}、3桁ではなく5桁の場合は{i:05}とすればよい。f文字列については以下の記事を参照。

Beautiful Soupで抽出

Webページの画像URLを一括で抽出するには、スクレイピングのライブラリであるBeautiful Soupを使う方法もある。

以下の例では、Yahoo!ニュースのトピックスのサムネイル画像のURLを抽出している。ユーザーエージェントによってレスポンスが異なるため、Chromeのユーザーエージェントを指定している。

from bs4 import BeautifulSoup

url = 'https://news.yahoo.co.jp/list/'
ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\
     'AppleWebKit/537.36 (KHTML, like Gecko) '\
     'Chrome/116.0.0.0 Safari/537.36'

req = urllib.request.Request(url, headers={'User-Agent': ua})
html = urllib.request.urlopen(req)

soup = BeautifulSoup(html, "html.parser")

dst_dir = 'data/temp/yahoo'
sleep_time_sec = 1

for img in soup.find(class_='newsFeed_list').find_all('img'):
    url = img.get('src')
    download_file_to_dir(url, dst_dir)
    time.sleep(sleep_time_sec)

Webページによって構成は異なるが、基本的には以下のような流れになる。

  • ダウンロードしたい画像が含まれるブロックのclassidを指定して、その中の<img>タグのオブジェクトを抽出
    • soup.find(class_='newsFeed_list').find_all('img')の部分
    • soup.find_all('img')とすると、ページ内のすべての<img>タグが抽出される(ロゴなどの不要な画像も含まれる)
  • <img>タグのsrc要素やdata-src要素から画像のURLを取得
    • img.get('src')の部分

なお、上のサンプルコードはあくまでも一例であり動作を保証するものではない。Yahoo!のページ構成が変更されると動かなくなる可能性があるので注意。

関連カテゴリー

関連記事