本記事は次の連載の一部です。
No. 03-02では、ハーメルンに登録されているユーザーのブックマーク情報を抽出し、小説のリストを生成します。
準備
ディレクトリ構成は次のとおりです。
.
├── .devcontainer
├── scraping_modules
│ └── get_hameln_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_hameln_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.org/')
login_page_button = browser.find_element(by=By.XPATH, value="//input[@value='ログインページへ']")
login_page_button.click()
user_info = secret_info['plattform_users']['hameln']
input_id_elem = browser.find_element(by=By.NAME, value='id')
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.XPATH, value="//input[@value='ログイン']")
login_button.submit()
login_state = browser.find_element(by=By.CLASS_NAME, value='spotlight').text
print(f'login state : {login_state}')
browser.get('https://syosetu.org/?mode=favo')
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
ハーメルンのブックマークは、なろうと違ってカテゴリーに分かれていないので、抽出手順は少し楽になります。
また、この関数では次の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='section3')
for item in bookmark_items:
info = {
'state':None,
'title':None,
'url':None,
'auther': None,
'auther_url':None,
'update_date':None,
'siori_ep':None,
'least_ep':None
}
novel_elem = item.find_element(by=By.TAG_NAME, value='h3')
a_list = novel_elem.find_elements(by=By.TAG_NAME, value='a')
info['title'] = a_list[0].text
info['url'] = a_list[0].get_attribute('href')
if len(a_list) >= 2:
info['auther'] = a_list[1].text
info['auther_url'] = a_list[1].get_attribute('href')
else:
s = novel_elem.text.find('(作者:') + len('(作者:')
e = novel_elem.text.rfind(')')
info['auther'] = novel_elem.text[s:e]
p_list = item.find_elements(by=By.TAG_NAME, value='p')
ep_and_date_info_list = p_list[1].text.split(' ')
ep_text = ep_and_date_info_list[0].replace('話', '')
if '/' in ep_text:
siori_ep, least_ep = ep_text.split('/')
info['siori_ep'] = siori_ep.replace('栞', '')
info['least_ep'] = least_ep.replace('最新', '')
else:
info['least_ep'] = ep_text.replace('最新', '')
date_text = ep_and_date_info_list[1].strip()
info['update_date'] = datetime.strptime(date_text, '%Y年%m月%d日(%a) %H:%M')
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='paging')
li_list = paging_elem.find_elements(by=By.TAG_NAME, value='li')
next_button = li_list[-2]
next_button.click()
print('click next page')
return ret
この関数で取ってくる小説のデータは、次のとおりです
- state : 小説の状態。「連載中」「完結済」「短編」の3つを取りえます。
- title : 小説のタイトル
- url : 小説のURL
- auther : 作者の(ブックマークに記載されている)名前
- auther_url : 作者のマイページのURL
- update_date : 最終更新日
- siori_ep : 栞を挟んでいる話数。栞を挟んでいない場合はNone
- least_ep : 最新話数。小説の話数でもある。
ハーメルンでは作者のURLが非公開の場合があるので、そこに注意します。
また、日付をpythonのdatetimeに変換する場合、localeの設定をしないと、(金)などの曜日が変換できないので、注意します。
get_novel_detail
get_novel_detailでは、get_listで得られなかった小説の情報を補完します。
各小説の小説情報のページへアクセスするため、処理速度は低下します。
def get_novel_detail(browser, info):
novel_id = info['url'].split('/')[-2]
browser.get(f'https://syosetu.org/?mode=ss_detail&nid={novel_id}')
tables = browser.find_elements(by=By.CLASS_NAME, value='table1')
td_list = tables[0].find_elements(by=By.TAG_NAME, value='td')
info['summary'] = td_list[9].text
info['series'] = None
info['series_url'] = None
info['genre'] = td_list[5].text
info['tags'] = td_list[11].text.split(' ') + td_list[13].text.split(' ')
td_list = tables[1].find_elements(by=By.TAG_NAME, value='td')
info['init_date'] = datetime.strptime(td_list[1].text, '%Y年%m月%d日(%a) %H:%M')
state = td_list[3].text.split(' ')[0]
if '短編' in state:
info['state'] = '短編'
elif '完結' in state:
info['state'] = '完結'
else:
info['state'] = '連載中'
info['thoughts_num'] = int(td_list[17].text[:-1].replace(',', ''))
info['review_num'] = None
info['bookmark_num'] = int(td_list[11].text[:-1].replace(',', ''))
info['overall_eval_point'] = int(td_list[23].text[:-2].replace(',', ''))
info['eval_point'] = None
info['string_num'] = int(td_list[15].text[:-2].replace(',', ''))
return info
この関数で取ってくる小説のデータは、次のとおりです
- summary : 小説のあらすじ
- series : シリーズ名。ハーメルンではNone
- series_url : ハーメルンではNone
- tags : 小説に付けられているタグ
- genre : 小説ジャンル。二次小説の場合とオリジナル小説で、区分が違うので注意
- init_date : 初回掲載日
- thoughts_num : 感想の数。無い場合や、受け付けていない場合はNone
- review_num : レビューの数。ハーメルンのレビュー機能は、小説単位で管理されていないのでNone
- bookmark_num : ブックマーク登録の数。無い場合や、受け付けていない場合はNone
- overall_eval_point : 総合評価ポイント。無い場合や、非公開の場合はNone
- eval_point : 評価ポイント。ハーメルンではNone
- string_num : 文字数。無い場合や、受け付けていない場合はNone
実行
実行してみます。main文は、同じディレクトリで実行されることを想定しているので、ディレクトリを移動します。
cd scraping_modules
python get_hameln_bookmarks.py
実行すると、ログインが正しくされていれば、ログインステータスが表示されます。
その後、ブックマークの読み込みが始まります。
login state : ログイン中
...
{'state': None, 'title': '神の装飾品店', 'url': 'https://syosetu.org/novel/336063/', 'auther': 'Actueater', 'auther_url': None, 'update_date': datetime.datetime(2024, 6, 10, 12, 0), 'siori_ep': '11', 'least_ep': '80'}
{'state': None, 'title': '【書籍化決定】TSクソビッチ少女は寝取られたい', 'url': 'https://syosetu.org/novel/291727/', 'auther': '二本目海老天マン', 'auther_url': 'https://syosetu.org/user/304423/', 'update_date': datetime.datetime(2024, 6, 10, 7, 1), 'siori_ep': None, 'least_ep': '136'}
{'state': None, 'title': 'ディストピアゲーに転生したら行政側だった件について', 'url': 'https://syosetu.org/novel/283626/', 'auther': '我等の優雅なりし様を見るや?', 'auther_url': 'https://syosetu.org/user/366126/', 'update_date': datetime.datetime(2024, 6, 10, 4, 27), 'siori_ep': None, 'least_ep': '33'}
...
すべてのブックマークが読み込み終わると、全ブックマーク数が表示されます。
total novel : 224
そして、各小説についての詳細情報の読み込みが始まります。
詳細情報を読み込むと、各小説について以下のログが流れます。
{'state': '連載中',
'title': '顔だけは良い妹が何故かバーチャルアイドルをやっているらしい',
'url': 'https://syosetu.org/novel/325018/',
'auther': 'hikari kawa',
'auther_url': 'https://syosetu.org/user/277563/',
'update_date': datetime.datetime(2024, 6, 10, 18, 0),
'siori_ep': None,
'least_ep': '70',
'summary': '主人公は僕、綾野礼。我が家に住み着いているのは妖精の如き美少女、通称エリちゃん。我が妹。持ち前の美貌と不遜かつ繊細な性格によって見事に現実世界からハブられてしまった彼女は今日も元気に不登校。何もかも与えられているというのに何もしない。そんな妹の事を僕は嫌いだった。しかしそんな生活も両親の出張と共に変わり始める。きっかけは一本の電話。『その子、バーチャルアイドルなの』と、母から告げられた非現実的な一言。そして社会性の無いエリちゃんにつきあう形で彼女の仕事について知っていき、徐々に嫌いだった妹を見直していくのだが――。',
'series': None,
'series_url': None,
'genre': '現代 / コメディ',
'tags': ['Vtuber',
'バーチャルアイドル',
'コメディ',
'深夜ラジオ',
'ラブコメ',
'ヤバい女',
'シリアル',
'R-15'],
'init_date': datetime.datetime(2023, 9, 4, 20, 57),
'thoughts_num': 417,
'review_num': None,
'bookmark_num': 5506,
'overall_eval_point': 13322,
'eval_point': None,
'string_num': 254591}
最終的には、この辞書のリストが今回得られます。
あとは、各プラットフォームの表記を合わせてからデータベースへと挿入します。