とりあえず車を楽しむ

【Python】はてなブログの記事に使用している画像を一括バックアップしてみる

※記事内のリンクには広告が含まれる場合があります。

f:id:humidasu_1:20210814230827p:plain

はてなブログでは記事の本文を一括でバックアップ機能はあるのですが、画像データをバックアップする機能はないようです。 

私のブログでは記事内に画像を多く使用していて、画像がないと意味が伝わらなくなる記事も多いです。はてなのサーバーに何かしらの災害が起きた場合、画像データが全て紛失してしまう可能性もあります。ブログ記事の一つ一つは資産ともいえるので、万が一そういうことが起きると非常に困りますね。

ただ、バックアップしようにも当ブログも200記事を超えたので、全ての画像を1記事ずつ保存していくのは途方もない作業です・・・

そこで、昨年末から勉強し始めたプログラミング言語のPythonではてなブログの全記事の画像を一括でダウンロードしてバックアップとして保存するコードを書いてみました。

Python初学者なので、書き方がおかしかったり、無駄に冗長であったりするかもしれませんが、備忘録代わりに記載したいと思います。

 

前提条件

まず、バックアップにはプログラミング言語のPython を使用しますので、ある程度のプログラミングの知識がないと難しいと思います。

seleniumでブラウザの操作をしますが、ブラウザはGoogle Chromeを使用します。

今回バックアップする対象となるのは、はてなフォトライフにアップロードされている画像を記事本文に貼り付けている場合のみです。例えば、Googleフォトの画像を記事内に貼り付けている場合などは、今回のコードではその画像はバックアップされません。

バックアップのイメージ

f:id:humidasu_1:20210814011440p:plain

 画像バックアップのイメージとしては、上記のような感じです。

記事タイトルの名前のフォルダーを作成し、その中に記事本文に使用されている画像を使われている順に1.jpg、2.jpg、3jpg~という名前を付けて保存します。(商品リンクの画像や関連記事の画像などは除きます。)

例えば、全部で200記事ある場合は、200記事分のフォルダーを作成し、各フォルダーに記事に使用されている画像が格納されることになります。

記事数225、画像約2500枚、画像の合計容量約300MBの当ブログの場合、バックアップに掛かった所要時間は約10分程でした。

 

コード全体像

※赤字の斜体で記入している部分のURL,ID,パスワード等は各自置き換えてください。
import
requests from bs4 import BeautifulSoup import os import re import sys from selenium import webdriver from selenium.webdriver.common.keys import Keys from time import sleep driver_path = "chromedriverのパス" url = "はてなブログ「記事の管理」ページのURL"
#はてなブログの「記事の管理」ページを開く driver = webdriver.Chrome(executable_path=driver_path) driver.get(url) driver.implicitly_wait(10)
#ログインIDとパスワードを入力する login_id = driver.find_element_by_id("login-name") login_id.send_keys("はてなブログのID"+Keys.ENTER) login_pass = driver.find_element_by_class_name("password") login_pass.send_keys("パスワード"+Keys.ENTER) #ログインボタンをクリックする driver.find_element_by_id("login-button").click() #全ての記事が表示されるまで次のページをクリックし続ける while True: next_page =driver.find_element_by_css_selector(".btn.load-next-entries.js-load-next-page") try: next_page.click() sleep(1)#連続クリックすると失敗することがあるので1秒待機させる except: break
#全ての記事タイトルのリストを作成 entry_titles = driver.find_elements_by_css_selector("tr.tr-hover > td:nth-of-type(1) a") entry_titles_list = [entry_title.text for entry_title in entry_titles ] #全ての記事URLのリストを作成 entry_urls = driver.find_elements_by_css_selector("tr.tr-hover > td:nth-of-type(7) > a") entry_urls_list = [entry_url.get_attribute("href") for entry_url in entry_urls ] #{記事タイトル:記事URL}の辞書を作成する entry_dict = dict(zip(entry_titles_list, entry_urls_list)) #合計記事数を取得する total_entries =len(entry_dict) #記事タイトルのフォルダを作成&記事内のクラス名hatena-fotolifeのimg要素を取得 for i,entry_title in enumerate(entry_titles_list,start=1): sys.stdout.write(f"\r{i}/{total_entries}記事の処理をしています。")#処理状況の把握のため #\/:*?<>|はフォルダ名に使用できないのでアンダーバー(_)に変換する new_entry_title = re.sub(r'[\\|/|:|*|?|"|<|>|\|]', '_', entry_title).strip() backup_folder = fr"C:\Users\ユーザー名\Documents\はてなブログバックアップ\写真\{new_entry_title}" #既にbackup_folderが存在する場合は以下の処理をスキップする(新規追加された記事のみをバックアップするため) if os.path.exists(backup_folder): continue os.makedirs(backup_folder,exist_ok=True) #記事URLにアクセスし、クラス名hatena-fotolifeのimg要素を取得 res = requests.get(entry_dict[entry_title]) soup = BeautifulSoup(res.text,"html.parser") img_tags = soup.select("img.hatena-fotolife") #取得した画像のURLに一つ一つアクセスし、画像を保存する for j,img_tag in enumerate(img_tags,start=1): res_img = requests.get(img_tag["src"]) with open(backup_folder+"\\"+str(j)+".jpg","wb") as f: f.write(res_img.content) print("処理が完了しました。")

おおまかな流れは以下の通りです。

  1. はてなブログの記事の管理ページにアクセスし、全記事のタイトルとURLを取得
  2. バックアップ画像を保存するフォルダを作成
  3. 取得した全記事のURLに一つ一つアクセスし、画像URLをダウンロードする

 

コードの説明

はてなブログの記事の管理ページにアクセスし、全記事のタイトルとURLを取得

driver_path = "chromedriverのパス"
url = "はてなブログ「記事の管理」ページのURL"
#はてなブログの「記事の管理」ページを開く driver = webdriver.Chrome(executable_path=driver_path) driver.get(url) driver.implicitly_wait(10)
#ログインIDとパスワードを入力する login_id = driver.find_element_by_id("login-name") login_id.send_keys("はてなブログのID"+Keys.ENTER) login_pass = driver.find_element_by_class_name("password") login_pass.send_keys("パスワード"+Keys.ENTER) #ログインボタンをクリックする driver.find_element_by_id("login-button").click()

変数driver_pathには、ChromeDriverのパスを記入、変数urlにははてなブログ記事の管理画面のURLが入ります。記事の管理画面とは下記の記事の公開、下書きなどが確認できる画面のことです。

それからseleniumでGoogle Chromeを立ち上げてはてなブログの記事の管理ページを開きます。まず最初にログイン画面が出てくるので自分のログインID、パスワードを入力してログインボタンをクリックします。

f:id:humidasu_1:20210814183722p:plain

 

#全ての記事が表示されるまで次のページをクリックし続ける
while True:
    next_page =driver.find_element_by_css_selector(".btn.load-next-entries.js-load-next-page")
    try:
        next_page.click()
        sleep(1)#連続クリックすると失敗することがあるので1秒待機させる
    except:
        break

 まずは全記事分のタイトルとURLを取得する必要がありますが、それらは記事の管理画面から取得できます。

f:id:humidasu_1:20210814185432p:plain

htmlを確認すると、矢印で指した箇所からそれぞれ記事タイトルと記事URLが取得できます。

ただ、一度に表示されるのは20記事分なので、それ以上表示するためには下の方にある「次のページ」ボタンをクリックする必要があります。この「次のページ」ボタンを全ての記事が表示されるまでwhile文でループさせ、クリックし続けます。

間髪入れず連続クリックすると上手くいかないことがあるので、クリックするごとにsleepで一秒待機させます。

全記事が表示されると「次のページ」ボタンが消えて、それ以上クリックしようとすると"element not interactable"というエラーが発生するので、例外処理でbreakを使いループから抜けます。

#全ての記事タイトルのリストを作成
entry_titles = driver.find_elements_by_css_selector("tr.tr-hover > td:nth-of-type(1) a")
entry_titles_list = [entry_title.text for entry_title in entry_titles ]
#全ての記事URLのリストを作成
entry_urls = driver.find_elements_by_css_selector("tr.tr-hover > td:nth-of-type(7) > a") 
entry_urls_list =  [entry_url.get_attribute("href") for entry_url in entry_urls ]

全記事のタイトルとURLを取得して、それぞれをリストに格納します。

find_elements_by_css_selectorの引数に"tr.tr-hover > td:nth-of-type(1) a"を指定することで記事タイトルが入ったa要素が取得でき、.textで記事タイトルを抽出します。

同じく"tr.tr-hover > td:nth-of-type(7) > a"を指定することで記事URLが入ったa要素が取得でき、.get_attribute("href")で記事URLを抽出します。

リストは内包表記を使ってシンプルに作成しています。

#{記事タイトル:記事URL}の辞書を作成する
entry_dict = dict(zip(entry_titles_list, entry_urls_list))
#合計記事数を取得する
total_entries =len(entry_dict)

全記事分のタイトルのリストと全記事分のURLのリストを辞書の形に変換します。

{記事タイトル:記事URL, 記事タイトル:記事URL, 記事タイトル:記事URL,・・・ }という形にしておきたいので、zip()とdict()を使って辞書entry_dictを生成します。

また、後で使うのでlen(entry_dict)で合計記事数を変数total_entriesに入れておきます。

バックアップ画像を保存するフォルダを作成

#記事タイトルのフォルダを作成&記事内のクラス名hatena-fotolifeのimg要素を取得
for i,entry_title in enumerate(entry_titles_list,start=1):
    sys.stdout.write(f"\r{i}/{total_entries}記事の処理をしています。")#処理状況の把握のため
    
    #\/:*?<>|はフォルダ名に使用できないのでアンダーバー(_)に変換する
    new_entry_title = re.sub(r'[\\|/|:|*|?|"|<|>|\|]', '_', entry_title).strip()

    backup_folder = fr"C:\Users\ユーザー名\Documents\はてなブログバックアップ\写真\{new_entry_title}"
    #既にbackup_folderが存在する場合は以下の処理をスキップする(新規追加された記事のみをバックアップするため)
    if os.path.exists(backup_folder):
        continue
    os.makedirs(backup_folder,exist_ok=True)

 フォルダ名には各記事のタイトルを付けるので、リストentry_titlesから記事タイトルを一つ一つ取り出して、フォルダを作成します。

2行目にsys.stdout.write(f"\r{i}/{total_entries}記事の処理をしています。")と記載していますが、これは処理状況を確認するためなので、なくても問題ありません。

f:id:humidasu_1:20210814230523p:plain

処理中なのか止まっているのか不安になるので、このように何記事目を処理しているのかを表示させています。

ここで先ほどの合計記事数の変数total_entriesと何記事目かを出力するためfor文でenumerate関数を使用しています。

また、記事タイトルに半角記号の「\ / : * ? < |」が入っている場合、それらはフォルダ名には使用できないので、正規表現モジュールのreを使ってアンダーバー("_")に変換しておきます。

バックアップフォルダのパスを作成して、os.makedirs関数を使って各記事タイトル名のフォルダを作成します。

os.makedirsの一つ上のif文では、過去にバックアップしたことがあり、再びバックアップする場合に、全ての記事の画像をダウンロードするのは非効率なので、既にバックアップフォルダがある場合(=過去にバックアップ済み)は、それ以降の処理をスキップします。

要するに、一度でもバックアップをしたことがある場合は新規追加された記事のみ画像のバックアップ作業をします。

取得した全記事のURLに一つ一つアクセスし、画像URLをダウンロードする

    #記事URLにアクセスし、クラス名hatena-fotolifeのimg要素を取得
    res = requests.get(entry_dict[entry_title])
    soup = BeautifulSoup(res.text,"html.parser")
    img_tags = soup.select("img.hatena-fotolife")

 辞書を利用して記事タイトルに対応するURLにrequests.get()でアクセスし、取得した情報を変数resに格納します。それからres.textをbeautifulsoupで解析します。

はてなブログの記事本文に使用されている画像にはhatena-fotolifeというクラス名が付いているので、クラス名hatena-fotolifeのimg要素を全て取得し変数img_tagsに格納します。

    #取得した画像のURLに一つ一つアクセスし、画像を保存する
    for j,img_tag in enumerate(img_tags,start=1):
        res_img = requests.get(img_tag["src"])
        
        with open(backup_folder+"\\"+str(j)+".jpg","wb") as f:
            f.write(res_img.content)

for文でimg_tagsを回します。画像には順番に1.jpg、2.jpg、3.jpg~と名前を付けていくので、enumerate関数でインデックス番号を取り出しておきます。デフォルトだと0から始まるので引数にstart=1と指定しておきます。

 imgタグのsrc属性に画像URLが記述されているので、その画像URLの情報をrequests.getで取得し、jpgファイルとして保存します。

保存先は先ほど作成した記事タイトルの名前が付いたフォルダです。

for文で全記事分繰り返すので、これで各記事の画像のダウンロードが完了します。

まとめ

Pythonを使用してはてなブログの各記事の画像を一括でダウンロードしました。

バックアップやはてなから他のブログサービスに移転する際などにも活用できるかと思います。

 

'