本記事は次の連載の一部です。
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を幾つかのブロックで解説します。
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の流れは次のとおりです。
- 小説家になろうにログインする
- ブックマークページへ行く
- カテゴリーリストを選ぶ(全部選ぶまでループ)
- カテゴリーの小説リストを抽出する
- 全部の小説リストを作成する
- 各小説の小説情報から、小説のタグやあらすじなどを抽出する
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)}')
for info in novel_list:
info = get_novel_detail(browser, info)
pprint(info, sort_dicts=False)
return novel_list
小説家になろうのブックマークは、カテゴリーで分けられているので、抽出が少し面倒ですね。
また、この関数では次の2つの関数を呼び出します。
- get_list(browser)
- get_novel_detail(browser, info)
それぞれの関数についても、解説します。
get_list
get_listは、表示されているブラウザの小説リストを整形してリストにいれる関数です。
また、next_pageのボタンが押せる場合は押して、ループします。
ループのブレイク条件は、同じ小説をリストに入れた場合です。next_pageボタンの状態は正しく取得できていないようです。
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')
novel_titles = novel_title_elem.text.split(' ')
update_date_str = item.find_element(by=By.CLASS_NAME, value='p-up-bookmark-item__date').text
info = {
'state':novel_titles[0],
'title':' '.join( novel_titles[1:]),
'url':novel_title_elem.find_element(by=By.TAG_NAME, value='a').get_attribute('href'),
'auther': novel_auther_elem.text,
'auther_url':novel_auther_elem.find_element(by=By.TAG_NAME, value='a').get_attribute('href'),
'update_date':datetime.strptime(update_date_str, '最新掲載日:%Y年%m月%d日 %H時%M分'),
'siori_ep':None,
'least_ep':None
}
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.', '')
info['least_ep'] = episode_info_texts[-1].replace('ep.', '')
if info['url'] in check:
looping = False
break
check.add(info['url'])
ret.append(info)
print(info)
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
この関数で取ってくる小説のデータは、次のとおりです
- 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):
novel_id = info['url'].split('/')[-2]
print(info['url'])
browser.get(f'https://ncode.syosetu.com/novelview/infotop/ncode/{novel_id}/')
table1 = browser.find_element(by=By.ID, value='noveltable1')
td_list = table1.find_elements(by=By.TAG_NAME, value='td')
if len(td_list) == 4:
info['summary'] = td_list[0].text
info['series'] = None
info['series_url'] = None
info['tags'] = td_list[2].text.split(' ')
info['genre'] = td_list[3].text
if len(td_list) == 5:
info['summary'] = td_list[0].text
info['series'] = td_list[1].text
info['series_url'] = td_list[1].find_element(by=By.TAG_NAME, value='a').get_attribute('href')
info['tags'] = td_list[3].text.split(' ')
info['genre'] = td_list[4].text
table2 = browser.find_element(by=By.ID, value='noveltable2')
th_list = table2.find_elements(by=By.TAG_NAME, value='th')
td_list = table2.find_elements(by=By.TAG_NAME, value='td')
td_dict = {th.text : td.text.split('\n')[0].replace(',', '') for th, td in zip(th_list, td_list)}
info['init_date'] = None
if '掲載日' in td_dict:
info['init_date'] = datetime.strptime(td_dict['掲載日'], '%Y年 %m月%d日 %H時%M分')
info['thoughts_num'] = None
if '感想' in td_dict:
info['thoughts_num'] = int(td_dict['感想'][:-1])
info['review_num'] = None
if 'レビュー' in td_dict:
info['review_num'] = int(td_dict['レビュー'][:-1])
info['bookmark_num'] = None
if 'ブックマーク登録' in td_dict:
info['bookmark_num'] = int(td_dict['ブックマーク登録'][:-1])
info['overall_eval_point'] = None
if '総合評価' in td_dict:
info['overall_eval_point'] = int(td_dict['総合評価'][:-2])
info['eval_point'] = None
if '評価ポイント' in td_dict:
info['eval_point'] = int(td_dict['評価ポイント'][:-2])
info['string_num'] = None
if '文字数' in td_dict:
info['string_num'] = int(td_dict['文字数'][:-2])
return info
この関数で取ってくる小説のデータは、次のとおりです
- summary : 小説のあらすじ
- series : シリーズ名。シリーズではない場合はNone
- series_url : シリーズのURL
- tags : 小説に付けられているタグ
- genre : 小説ジャンル
- init_date : 初回掲載日
- thoughts_num : 感想の数。無い場合や、受け付けていない場合はNone
- review_num : レビューの数。無い場合や、受け付けていない場合はNone
- thoughts_num : 感想の数。無い場合や、受け付けていない場合はNone
- bookmark_num : ブックマーク登録の数。無い場合や、受け付けていない場合はNone
- overall_eval_point : 総合評価ポイント。無い場合や、非公開の場合はNone
- eval_point : 評価ポイント。無い場合や、非公開の場合はNone
- string_num : 文字数。無い場合や、受け付けていない場合はNone
実行
実行してみます。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
そして、各小説についての詳細情報の読み込みが始まります。
詳細情報を読み込むと、各小説について以下のログが流れます。
{'state': '連載',
'title': '噛ませ犬な中年冒険者は今日も頑張って生きてます。',
'url': 'https://ncode.syosetu.com/n0116di/',
'auther': '丘/丘野\u3000優',
'auther_url': 'https://mypage.syosetu.com/233968',
'update_date': datetime.datetime(2020, 5, 21, 12, 0),
'siori_ep': None,
'least_ep': '37',
'summary': '中年冒険者ゲオルグは今日もだらだらと依頼を受けようとしていた。\n'
'同じ冒険者と他愛もない話をし、情報を仕入れて、依頼をこなす。\n'
'そしてわずかな報酬をもらい、酒を飲む。\n'
'それがゲオルグの日常だった。\n'
'しかし、ある日、少女に話しかけたところから彼の日常は変化を始める。\n'
'ナンパと勘違いされ、少女の連れと思しき少年にぶん殴られてふっとんだベテラン冒険者、ゲオルグの冒険が始まる。',
'series': None,
'series_url': None,
'tags': ['R15', '', '噛ませ犬', '中年冒険者', '錬金術師', '彫金師', '剣と魔法', 'タイトル詐欺気味'],
'genre': 'ハイファンタジー〔ファンタジー〕',
'init_date': datetime.datetime(2016, 5, 25, 7, 58),
'thoughts_num': 411,
'review_num': 4,
'bookmark_num': 15219,
'overall_eval_point': 47295,
'eval_point': 16857,
'string_num': 176853}
最終的には、この辞書のリストが今回得られます。
あとは、各プラットフォームでも同様のことを行い、表記を合わせてからデータベースへと挿入します。