BeautifulSoupを使って画像ブログから画像をダウンロードする

2016年4月1日金曜日

python

t f B! P L
あのブログにもこのブログにも対応とか無理なので「画像が並んでて、その画像をクリックすると画像ファイルに飛ぶタイプのブログの1ページ」に限定します。ブログの具体例はありません。
例外とかステータスコードが200じゃない時とか全く考慮してないです。
実行環境
  • Windows10 64bit
  • python 3.5.1 (多分64bit)
tl;dr。長い記事読んでられないと思うのはおかしなことではないです。
  1. pip install requests
  2. pip install beautifulsoup4
  3. hoge.pyみたいなファイルを作ってコードをコピペしてpython hoge.py


コード

# -*- coding: utf-8 -*-

import re
import time
import os
import sys
import datetime

import requests
from bs4 import BeautifulSoup


def download_images(url):
    res = requests.get(url)
    soup = BeautifulSoup(res.text.encode(res.encoding), 'html.parser')
    image_items = soup.findAll('a', href=re.compile('(\.jpg|\.png)$'))
    image_urls = [item.get('href') for item in image_items]

    now = datetime.datetime.today()
    dirname = '{}{:0>2}{:0>2}{:0>2}{:0>2}{:0>6}'\
              .format(now.year, now.month, now.day,
                      now.minute, now.second, now.microsecond)
    os.mkdir(dirname)

    first_url = None
    isdup = False
    for i, url in enumerate(image_urls):
        image_data = requests.get(url).content  # バイナリデータ
        _, ext = os.path.splitext(url)
        if first_url is None:
            first_url = url
        elif first_url == url:
            isdup = True
        with open('{0}/{1:0>3}{2}'.format(dirname, i, ext), 'wb') as f:
            filename = '{:0>3}{}'.format(i, ext)
            print('downloading ' + filename)
            f.write(image_data)
        time.sleep(1)
    if isdup:
        print('delete 000' + ext)
        os.remove(dirname + '/000' + ext)


if __name__ == '__main__':
    if len(sys.argv) == 2:
        download_images(sys.argv[1])
    else:
        print('arguments error')

つかったライブラリについて

requests HTTPのリクエストに使います。urllibより圧倒的に使いやすいです。 BeautifulSoup4 HTMLの解析に使います。これがないと正規表現で頑張ることになります。

コードについて

目的に必要な部分と横着した部分があるので解説もどきを書きます。
# -*- coding: utf-8 -*-

import re
import time
import os
import sys
import datetime

import requests
from bs4 import BeautifulSoup
特にいうことはないと思います。
def download_images(url):
    res = requests.get(url)
    soup = BeautifulSoup(res.text.encode(res.encoding), 'html.parser')
    image_items = soup.findAll('a', href=re.compile('(\.jpg|\.png)$'))
    image_urls = [item.get('href') for item in image_items]
GETで取ってきてBeautifulSoupに渡します。res.textでHTMLが取れるのですが、エンコード指定しないと文字化けすることがあるらしいです。今回必要だったかどうかはわかりません。
parserの指定がないと動かないのでhtml.parserを指定します。多分これが一番標準だと思います。
soup.findAll('a')でHTMLのaタグをすべて取ります。キーワード引数にre.compileを渡してほしい部分だけ取れるので便利です。
そしてhrefの中身だけを取ります。これが欲しい画像のurlのリストです。
参考にしました
BeautifulSoupを使ってスクレイピングをしてみる
now = datetime.datetime.today()
dirname = '{}{:0>2}{:0>2}{:0>2}{:0>2}{:0>6}'\
          .format(now.year, now.month, now.day,
                  now.minute, now.second, now.microsecond)
os.mkdir(dirname)
取ってきた画像をぶち込むためのディレクトリを作ります。適切な名前が思いつかないのでタイムスタンプにしました。ミリ秒まで使っているので「このディレクトリはすでにあるよ」みたいなエラーは出ないと信じています。
formatを使って4月は04みたいに0で詰め表現してます。あとpep8にしたがって1行79字以下を目指すと改行はこうなると思います。
first_url = None
isdup = False
初期値。重複チェックに使います。
for i, url in enumerate(image_urls):
    image_data = requests.get(url).content  # バイナリデータ
    _, ext = os.path.splitext(url)
画像の名前は出てきた順に連番で付けたいのでenumerateを使いました。
3日前に1章だけ読んだEffective Pythonに
for i in range(len(lst)):
hoge = lst[i]
とかやってはならぬと書いてありました。私もそう思います。
os.path.splitextは.を含めた拡張子とそれ以外に文字列を分解します。使わない部分は_でいいですね。
    if first_url is None:
        first_url = url
    elif first_url == url:
        isdup = True
画像ブログの中には画像群のどれか一つのコピーを最初にもってくるところがあるのでそこだけ対処します。
最初の画像のurlと同じものが出てくるかどうかで処理してます。urlが違うとなるとhashlibとか使うことになりますがそこまではやってません。
    with open('{0}/{1:0>3}{2}'.format(dirname, i, ext), 'wb') as f:
        filename = '{:0>3}{}'.format(i, ext)
        print('downloading ' + filename)
        f.write(image_data)
取ってきた画像のバイナリを保存してます。よく見たらfilenameはwithの前に書いてopenにも使うべきですね。
あとprint文で現在の状況を出力するのはいいとして、既にrequests.getは終わっているのだからdownloadingは微妙に違う気がしますがここまで書いて気づいたのでそのまま放置です。
    time.sleep(1)
サーバーに対する優しさ。多分ブログ1ページずつならsleepなくても怒られないと思います。
if isdup:
    print('delete 000' + ext)
    os.remove(dirname + '/000' + ext)
重複画像の削除。
if __name__ == '__main__':
    if len(sys.argv) == 2:
        download_images(sys.argv[1])
    else:
        print('arguments error')
引数の数だけ確認してます。

なんで今回一つ一つコメント書いているのか

ちょっと前に「便利なライブラリを使ってこう書いてこうやるとできます。簡単。」みたいな記事はよくない、何を考えてそのコードを書いたか書くべき。みたいな話をどこかで見たから書いた。確かにググッて見つけたけどなんかよくわからんので保留にして他のサイト探しに行くみたいなことはある。
実際に色々書いてみた結果、
a = 3 # aに3を代入
を見ている感がところどころある。
適切な書き方ではないからなのか、そもそも必要ないのか。コレガワカラナイ。

終わりに

HTMLのスクレイピングはプログラミングの中では格段に面白い部類だと思います。
完成するたびに何かが楽になる感がよいです。あとイケア効果。

QooQ