[Web小説を管理するアプリ] No. 03-03 : カクヨムのブックマークを抽出する

本記事は次の連載の一部です。

No. 03-03では、カクヨムに登録されているユーザーのブックマーク情報を抽出し、小説のリストを生成します。

準備

ディレクトリ構成は次のとおりです。

.
├── .devcontainer
├── scraping_modules
│   └── get_kakuyomu_bookmarks.py
└── secret_info.json

また、secret_info.jsonに小説家にハーメルンのユーザー情報を入力してください(hameln)

{
    "plattform_users":{
        "narou":{
            "id":"xxx",
            "pass":"xxx"
        },
        "hameln":{
            "id":"xxx",
            "pass":"xxx"
        },
        "kakuyomu":{
            "id":"xxx",
            "pass":"xxx"
        }
    }

}

プログラム

get_kakuyomu_bookmarks.pyを幾つかのブロックで解説します。

import

importはseleniumの使うものと、json、re、datetime、localeを入れました。pprintは表示確認用です

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
import json
import re
from datetime import datetime
import locale
locale.setlocale(locale.LC_TIME, 'ja_JP.UTF-8')
from pprint import pprint

main

このプログラムは他のプログラムのモジュールとして利用します。そのため、mainは動作確認用となっています。

mainでは、secret_infoを読み込んで、get_bookmarksを呼び出すだけです。

if __name__ == '__main__':
    with open('../secret_info.json', 'r') as f:
        secret_info = json.load(f)
    get_bookmarks(secret_info)

get_bookmarks

get_bookmarksの流れは次のとおりです。

  1. カクヨムにログインする
  2. フォローしている小説リストのページへ行く
  3. 小説リストを抽出する(次のページがあったら、それも抽出する)
  4. 全部の小説リストを作成する
  5. 各小説の小説情報から、小説のタグやあらすじなどを抽出する
def get_bookmarks(secret_info):
    with webdriver.Remote(
        command_executor="http://chrome:4444/wd/hub",
        options=webdriver.ChromeOptions()
    ) as browser:
        browser.implicitly_wait(10)
        browser.get('https://kakuyomu.jp/login')

        user_info = secret_info['plattform_users']['kakuyomu']

        input_id_elem = browser.find_element(by=By.NAME, value='email_address')
        input_id_elem.send_keys(user_info['id'])

        input_pass_elem = browser.find_element(by=By.NAME, value='password')
        input_pass_elem.send_keys(user_info['pass'])

        login_button = browser.find_element(by=By.CLASS_NAME, value="ui-button-blue")
        login_button.submit()

        mainheader = browser.find_element(by=By.ID, value='mainHeader').text
        print(f'login : {mainheader}')

        browser.get('https://kakuyomu.jp/users/nijis/following_works')

        novel_list = get_list(browser)
        print(f'total novel : {len(novel_list)}')

        for info in novel_list:
            info = get_novel_detail(browser, info)
            pprint(info, sort_dicts=False)
        return novel_list

カクヨムのHTML構造は、なろうやハーメルンと違って少し複雑で、名前付けなどもわかり易くないので、大変です。

また、この関数では次の2つの関数を呼び出します。

  • get_list(browser)
  • get_novel_detail(browser, info)

それぞれの関数についても、解説します。

get_list

get_listは、表示されているブラウザの小説リストを整形してリストにいれる関数です。

また、next_pageのボタンが押せる場合は押して、ループします。

ループのブレイク条件は、同じ小説をリストに入れた場合です。

def get_list(browser):
    ret = []
    check = set()
    looping = True
    while looping:
        bookmark_items = browser.find_elements(by=By.CLASS_NAME, value='WorkItem_center__a_Pvu')
        for item in bookmark_items:
            a_list = item.find_elements(by=By.TAG_NAME, value='a')
            other_info_elem = item.find_element(by=By.CLASS_NAME, value='WorkMeta_bg-lightBeige__lNnbx')
            texts = other_info_elem.text.split('\n')
            info = {
                'state':texts[2].split(' ')[0],
                'title':a_list[0].text,
                'url':a_list[0].get_attribute('href'),
                'auther': a_list[1].text,
                'auther_url':a_list[1].get_attribute('href'),
                'update_date':datetime.strptime(texts[4], '%Y年%m月%d日 %H:%M'),
                'siori_ep':None,
                'least_ep':texts[2].split(' ')[-1][:-1]
            }
            if info['url'] in check:
                looping = False
                break
            check.add(info['url'])
            ret.append(info)
            print(info)

        paging_elem = browser.find_element(by=By.CLASS_NAME, value='Pager_next__wQCbl')
        next_button_ = paging_elem.find_elements(by=By.TAG_NAME, value='a')
        if len(next_button_) > 0:
            next_button_[0].click()
            print('click next page')
    return ret

この関数で取ってくる小説のデータは、次のとおりです

  • state : 小説の状態。「連載中」「完結済」「短編」の3つを取りえます。
  • title : 小説のタイトル
  • url : 小説のURL
  • auther : 作者の名前
  • auther_url : 作者のマイページのURL
  • update_date : 最終更新日
  • siori_ep : ここではNone
  • least_ep : 最新話数。小説の話数でもある。

get_novel_detail

get_novel_detailでは、get_listで得られなかった小説の情報を補完します。

各小説の小説情報のページへアクセスするため、処理速度は低下します。

def get_novel_detail(browser, info):
    browser.get(info['url'])
    info['summary'] = None
    info['series'] = None
    info['series_url'] = None
    summary_elem_ = browser.find_elements(by=By.CLASS_NAME, value='CollapseTextWithKakuyomuLinks_collapseText__XSlmz')
    if len(summary_elem_) > 0:
        info['summary'] = summary_elem_[0].text

    tabs = browser.find_element(by=By.CLASS_NAME, value='Tab_ul__raAoj')
    buttons = tabs.find_elements(by=By.TAG_NAME, value='li')
    detail_button = buttons[-1]
    detail_button.click()

    detail_data_elem = browser.find_element(by=By.CLASS_NAME, value='WorkDetail_basicInformation__bpm_r')
    dd_list = detail_data_elem.find_elements(by=By.TAG_NAME, value='dd')
    dt_list = detail_data_elem.find_elements(by=By.TAG_NAME, value='dt')
    dd_dict = {dt.text:dd.text for dt, dd in zip(dt_list, dd_list)}

    info['init_date'] = datetime.strptime(dd_dict['公開日'], '%Y年%m月%d日 %H:%M')
    info['string_num'] = int(dd_dict['総文字数'][:-2].replace(',', ''))
    
    detail_data_list = browser.find_elements(by=By.CLASS_NAME, value='WorkDetail_link__Z6c7y')
    detail_dict = {item.text.split('\n')[0]:item.text.split('\n')[1] for item in detail_data_list}

    info['thoughts_num'] = None
    info['review_num'] = None
    info['bookmark_num'] = None
    info['overall_eval_point'] = None
    info['eval_point'] = None
    if '応援コメント' in detail_dict:
        info['thoughts_num'] = int(detail_dict['応援コメント'][:-1].replace(',', ''))
    if '小説のフォロワー' in detail_dict:
        info['bookmark_num'] = int(detail_dict['小説のフォロワー'][:-1].replace(',', ''))
    tags_list = browser.find_elements(by=By.CLASS_NAME, value='WorkDetail_buttonInner__BUHfg')
    info['genre'] = tags_list[0].text
    info['tags'] = [tag.text for tag in tags_list[1:]]
    return info

この関数で取ってくる小説のデータは、次のとおりです

  • summary : 小説のあらすじ
  • series : シリーズ名。カクヨムではNone
  • series_url : カクヨムではNone
  • tags : 小説に付けられているタグ
  • genre : 小説ジャンル。二次小説の場合とオリジナル小説で、区分が違うので注意
  • init_date : 初回掲載日
  • thoughts_num : 応援コメントの数。無い場合や、受け付けていない場合はNone
  • review_num : レビューの数。カクヨムではNone
  • bookmark_num : ブックマーク登録(フォロー)の数。
  • overall_eval_point : 総合評価ポイント。カクヨムではNone
  • eval_point : 評価ポイント。カクヨムではNone
  • string_num : 文字数。無い場合や、受け付けていない場合はNone

実行

実行してみます。main文は、同じディレクトリで実行されることを想定しているので、ディレクトリを移動します。

cd scraping_modules
python get_kakuyomu_bookmarks.py

実行すると、ログインが正しくされていれば、ユーザー名が表示されます。

その後、ブックマークの読み込みが始まります。

login : xxxさんのダッシュボード

...
{'state': '完結済', 'title': '文明崩壊後の世界を女の子をバイクの後ろに乗せて旅している', 'url': 'https://kakuyomu.jp/works/1177354054882431571', 'auther': '新木伸', 'auther_url': 'https://kakuyomu.jp/users/araki_shin', 'update_date': datetime.datetime(2017, 2, 9, 9, 44), 'siori_ep': None, 'least_ep': '33'}
{'state': '連載中', 'title': 'ひげを剃る。そして女子高生を拾う。', 'url': 'https://kakuyomu.jp/works/1177354054882739112', 'auther': 'しめさば', 'auther_url': 'https://kakuyomu.jp/users/smsb_create', 'update_date': datetime.datetime(2018, 8, 2, 15, 41), 'siori_ep': None, 'least_ep': '39'}
{'state': '連載中', 'title': 'つまり、雑念の沙鳥さん。', 'url': 'https://kakuyomu.jp/works/1177354054882979595', 'auther': '関根パン', 'auther_url': 'https://kakuyomu.jp/users/sekinepan', 'update_date': datetime.datetime(2023, 12, 25, 11, 6), 'siori_ep': None, 'least_ep': '68'}
...

すべてのブックマークが読み込み終わると、全ブックマーク数が表示されます。

total novel : 84

そして、各小説についての詳細情報の読み込みが始まります。

詳細情報を読み込むと、各小説について以下のログが流れます。

{'state': '連載中',
 'title': '無双ゲーに転生したと思ったら、どうやらここはハードな鬱ゲーだったらしい',
 'url': 'https://kakuyomu.jp/works/16818093074058384419',
 'auther': '久路途緑',
 'auther_url': 'https://kakuyomu.jp/users/kurotomidori',
 'update_date': datetime.datetime(2024, 6, 10, 20, 2),
 'siori_ep': None,
 'least_ep': '50',
 'summary': 'ゲームの世界に転生したとして、その元になったゲームのストーリーを知らなかった場合。\n'
            '\n'
            '勇者の死後という、魔族の支配が強まる世界で一人の外道が原作を無視して魔族狩りに執心してしまったのが、彼らの運の尽きであった。\n'
            '\n'
            '主人公(元男)の目的は「スキルの育成」。それには魔族の弱点である魔核こそが最もいい栄養。ならば人を狩る魔族を殺すことになんの躊躇いがあるだろうか。',
 'series': None,
 'series_url': None,
 'init_date': datetime.datetime(2024, 4, 1, 14, 41),
 'string_num': 148812,
 'thoughts_num': 939,
 'review_num': None,
 'bookmark_num': 14416,
 'overall_eval_point': None,
 'eval_point': None,
 'genre': '異世界ファンタジー',
 'tags': ['カクヨムオンリー', 'TS主人公', '前世男、今世女', '主人公の気質はチンピラ', '主人公最強', 'チート']}

最終的には、この辞書のリストが今回得られます。

あとは、各プラットフォームの表記を合わせてからデータベースへと挿入します。


タイトルとURLをコピーしました