[Web小説を管理するアプリ] No. 03-01 : 小説家になろうのブックマークを抽出する

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

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

準備

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

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

また、secret_info.jsonに小説家になろうのユーザー情報を入力してください

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

}

プログラム

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

小説家になろうの小説情報は静的なHTMLなので、小説情報に書かれていることはBeautifulsoupで取得します。

また、ブックマークのデータ(栞の話数)はログインする必要があるため、Seleniumで取得します。

import

importはselenium、beautifulsoupの使うものと、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
from bs4 import BeautifulSoup
import requests
import time

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://syosetu.com/login/input/')

        user_info = secret_info['plattform_users']['narou']

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

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

        login_button = browser.find_element(by=By.ID, value='mainsubmit')
        login_button.submit()


        user_name = browser.find_element(by=By.CLASS_NAME, value='p-up-header-pc__username').text
        print(f'login : {user_name}')

        browser.get('https://syosetu.com/favnovelmain/list/')

        category_elem = browser.find_element(by=By.CLASS_NAME, value='p-up-bookmark-category__select')
        select = Select(category_elem)

        categories = [option.text for option in select.options]
        novel_list = []
        for i, category in enumerate(categories):
            category_elem = browser.find_element(by=By.CLASS_NAME, value='p-up-bookmark-category__select')
            select = Select(category_elem)

            select.select_by_index(i)
            print(f'bookmark category : {category}')

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

小説家になろうのブックマークは、カテゴリーで分けられているので、抽出が少し面倒ですね。

また、ここではget_list(browser)という関数を呼び出します。

get_list

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

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

ループのブレイク条件は、同じ小説をリストに入れた場合です。next_pageボタンの状態は正しく取得できていないようです。

小説の情報はget_novel_info(url)によって取得できます。また、作者の情報や栞の情報をブックマークから取得して、付け足します。

def get_list(browser):
    ret = []
    check = set()
    looping = True
    while looping:
        bookmark_items = browser.find_elements(by=By.CLASS_NAME, value='p-up-bookmark-item')
        for item in bookmark_items:
            novel_title_elem = item.find_element(by=By.CLASS_NAME, value='p-up-bookmark-item__title')
            #print('  ', novel_title_elem.text)
            novel_auther_elem = item.find_element(by=By.CLASS_NAME, value='p-up-bookmark-item__author')

            url = novel_title_elem.find_element(by=By.TAG_NAME, value='a').get_attribute('href')
            info = get_novel_info(url)
            info['auther'] = novel_auther_elem.text
            info['auther_url'] = novel_auther_elem.find_element(by=By.TAG_NAME, value='a').get_attribute('href')
            if info['state'] != '短編':
                episode_info_text= item.find_element(by=By.CLASS_NAME, value='p-up-bookmark-item__button').text
                episode_info_texts = re.split('\n| ', episode_info_text)
                if 'ep' in episode_info_texts[0]:
                    info['siori_ep'] = episode_info_texts[0].replace('ep.', '')

            if info['url'] in check:
                looping = False
                break
            
            check.add(info['url'])
            ret.append(info)
            print(info)
            time.sleep(1)

        next_button_ = browser.find_elements(by=By.CLASS_NAME, value='p-icon--angle-right')
        if len(next_button_) == 0:
            looping = False
        else:
            if next_button_[0].is_enabled()==False:
                looping = False
            next_button_[0].click()
        print('click next page')
    return ret

短編小説は栞情報がないなどの条件を踏まえて、データを整形しています。

get_novel_info

get_novel_infoでは、urlから小説情報を抽出します。

この処理をSeleniumでやらずにbeautifulsoupでやるのは、処理速度が早いためです。また、urlを指定してデータを抽出できるようにすることで、モジュールとしての機能性を高めておきます。

また、urlから小説IDを抜き出して、小説情報のページへアクセスしました。これは、小説情報urlと小説目次のurlの両方に対応できるようにしたかったためです。

def get_novel_info(url):
    novel_id = url.split('/')[-1]
    if novel_id == '':
        novel_id = url.split('/')[-2]
    print(novel_id)
    detail_url = f'https://ncode.syosetu.com/novelview/infotop/ncode/{novel_id}/'
    headers = requests.utils.default_headers()
    headers.update({'User-Agent': 'My User Agent 1.0'})
    response = requests.get(detail_url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')
    print(detail_url)

#
    info = {
        'url':f'https://ncode.syosetu.com/{novel_id}/',
        'site_type':'narou',
        'site_id':novel_id,
        'state':None,
        'title':soup.title.text[:-len('[作品情報]')],

        'auther_name':None,
        'auther_url':None,

        'novel_update_date':None,
        'siori_ep':None,
        'least_ep':None,
        
        'summary':None,
        'series':None,
        'series_url':None,
        'tags':None,
        'genre':None,

        'novel_init_date':None,
        'string_num':None,
        'thoughts_num':None,
        'review_num':None,
        'bookmark_num':None,
        'overall_eval_point':None,
        'eval_point':None
    }

    texts = soup.find('div', id='pre_info').text.split()[0].split('全')
    info['state'] = texts[0]
    if len(texts) != 1:
        info['least_ep'] = int(texts[1].replace(',', '')[:-len('エピソード')])

    table = soup.find('table', id='noveltable1')
    td_dict = {th.text : td for th, td in zip(table.find_all('th'), table.find_all('td'))}

    if 'あらすじ' in td_dict:
        info['summary'] = td_dict['あらすじ'].text.strip()
    if 'シリーズ' in td_dict:
        info['series'] = td_dict['シリーズ'].text.strip()
        info['series_url'] = td_dict['シリーズ'].find('a').get('href')
    if '作者名' in td_dict:
        info['auther_name'] = td_dict['作者名'].text.strip()
        if td_dict['作者名'].find('a') != None:
            info['auther_url'] = td_dict['作者名'].find('a').get('href')
    if 'キーワード' in td_dict:
        tags= td_dict['キーワード'].text.strip().replace('\xa0', ' ').split(' ')
        info['tags'] = [tag.strip() for tag in tags]
    if 'ジャンル' in td_dict:
        info['genre'] = td_dict['ジャンル'].text.strip()

    table = soup.find('table', id='noveltable2')
    td_dict = {th.text : td.text.strip().split('\n')[0].replace(',', '') for th, td in zip(table.find_all('th'), table.find_all('td'))}
    if '掲載日' in td_dict:
        info['novel_init_date'] = datetime.strptime(td_dict['掲載日'], '%Y年 %m月%d日 %H時%M分')
    if '最新掲載日' in td_dict:
        info['novel_update_date'] = datetime.strptime(td_dict['最新掲載日'], '%Y年 %m月%d日 %H時%M分')
    if '最終掲載日' in td_dict:
        info['novel_update_date'] = datetime.strptime(td_dict['最終掲載日'], '%Y年 %m月%d日 %H時%M分')
    if '感想' in td_dict:
        info['thoughts_num'] = int(td_dict['感想'][:-1])
    if 'レビュー' in td_dict:
        info['review_num'] = int(td_dict['レビュー'][:-1])
    if 'ブックマーク登録' in td_dict:
        info['bookmark_num'] = int(td_dict['ブックマーク登録'][:-1])
    if '総合評価' in td_dict and '非公開' not in  td_dict['総合評価']:
        info['overall_eval_point'] = int(td_dict['総合評価'][:-2])
    if '評価ポイント' in td_dict and '非公開' not in  td_dict['評価ポイント']:
        info['eval_point'] =  int(td_dict['評価ポイント'][:-2])
    if '文字数' in td_dict:
        info['string_num'] =  int(td_dict['文字数'][:-2])
    return info

小説データが表データで表現されているので、ラベルと内容の辞書を作ります。

なろうの小説情報の表データには、非公開などの理由で行が抜けている事があるので、キーを確認しながらデータを抽出していきます。

実行

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

cd scraping_modules
python get_narou_bookmarks.py

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

その後、各カテゴリについて、ブックマークの読み込みが始まります。

login : xxx
bookmark category : カテゴリ1(284)
...
{'state': '連載', 'title': '復讐を果たして死んだけど転生したので今度こそ幸せになる', 'url': 'https://ncode.syosetu.com/n8258fm/', 'auther': 'クロッチ', 'auther_url': 'https://mypage.syosetu.com/413310', 'update_date': datetime.datetime(2021, 9, 24, 17, 0), 'siori_ep': '116', 'least_ep': '177'}
{'state': '連載', 'title': '【書籍化】日常ではさえないただのおっさん、本当は地上最強の戦神【完結】', 'url': 'https://ncode.syosetu.com/n3740ei/', 'auther': '相野仁', 'auther_url': 'https://mypage.syosetu.com/231817', 'update_date': datetime.datetime(2021, 2, 10, 18, 11), 'siori_ep': '30', 'least_ep': '141'}
{'state': '連載', 'title': '俺にはこの暗がりが心地よかった', 'url': 'https://ncode.syosetu.com/n7820go/', 'auther': '星崎崑', 'auther_url': 'https://mypage.syosetu.com/183419', 'update_date': datetime.datetime(2023, 5, 2, 21, 25), 'siori_ep': '119', 'least_ep': '230'}
...

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

total novel : 896

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

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


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