kakuyomu.ipynb•26.7 kB
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "952fe792",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"import requests\n",
"from typing import Dict, List, Any\n",
"from bs4 import BeautifulSoup"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "8f04bed0",
"metadata": {},
"outputs": [],
"source": [
"# 作品一覧を文字列に変換\n",
"def works_to_string(data: Dict[str, Dict[str, Any]], works: List[str]) -> str:\n",
" output_lines: List[str] = []\n",
"\n",
" for work_id in works:\n",
" work = data[work_id]\n",
"\n",
" id_ = work.get(\"id\")\n",
" if id_:\n",
" output_lines.append(f\"ID: {id_}\")\n",
"\n",
" title = work.get(\"title\")\n",
" if title:\n",
" output_lines.append(f\"タイトル: {title}\")\n",
"\n",
" catchphrase = work.get(\"catchphrase\")\n",
" if catchphrase:\n",
" output_lines.append(f\"キャッチフレーズ: {catchphrase}\")\n",
"\n",
" tags = work.get(\"tagLabels\")\n",
" if tags:\n",
" tag_str = \", \".join(tags)\n",
" output_lines.append(f\"タグ: {tag_str}\")\n",
"\n",
" introduction = work.get(\"introduction\")\n",
" if introduction:\n",
" output_lines.append(\"イントロダクション:\\n```\\n\" + introduction + \"\\n```\")\n",
"\n",
" output_lines.append(\"\") # 区切りの空行\n",
"\n",
" result: str = \"\\n\".join(output_lines)\n",
"\n",
" return result\n",
"\n",
"\n",
"# エピソード一覧を文字列に変換\n",
"def episodes_to_string(data: Dict[str, Dict[str, Any]], episodes: List[str]) -> str:\n",
" output_lines: List[str] = []\n",
"\n",
" for episode_id in episodes:\n",
" episode = data[episode_id]\n",
"\n",
" id_ = episode.get(\"id\")\n",
" if id_:\n",
" output_lines.append(f\"ID: {id_}\")\n",
"\n",
" title = episode.get(\"title\")\n",
" if title:\n",
" output_lines.append(f\"タイトル: {title}\")\n",
"\n",
" publishedAt = episode.get(\"publishedAt\")\n",
" if publishedAt:\n",
" output_lines.append(f\"公開日: {publishedAt}\")\n",
"\n",
" output_lines.append(\"\") # 区切りの空行\n",
"\n",
" result: str = \"\\n\".join(output_lines)\n",
"\n",
" return result"
]
},
{
"cell_type": "markdown",
"id": "befa3e67",
"metadata": {},
"source": [
"# トップページ"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "b03d7334",
"metadata": {},
"outputs": [],
"source": [
"url = \"https://kakuyomu.jp/\"\n",
"res = requests.get(url)\n",
"soup = BeautifulSoup(res.text, \"html.parser\")\n",
"data = json.loads(soup.find(\"script\", id=\"__NEXT_DATA__\").string)[\"props\"][\"pageProps\"][\n",
" \"__APOLLO_STATE__\"\n",
"]\n",
"works = list(filter(lambda x: x.startswith(\"Work\"), data.keys()))"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "81a6dd59",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ID: 16817330650993330082\n",
"タイトル: 『聖女じゃない方』の私の異世界冒険は『勇者じゃない方』の君と一緒! ~あれ、私達って本当に『じゃない方』?~\n",
"キャッチフレーズ: じゃない方の「私」「僕」だけど、君のためにがんばる! #付き合ってない\n",
"\n",
"ID: 16816700426798996703\n",
"タイトル: 100日後に別れるかもしれないゲイカップル\n",
"キャッチフレーズ: 【書籍化作品】男女が別れるように、男同士だって別れるわけで。\n",
"\n",
"ID: 16817330652495155185\n",
"タイトル: 近畿地方のある場所について\n",
"キャッチフレーズ: 情報をお持ちの方はご連絡ください\n",
"\n",
"ID: 1177354054887510509\n",
"タイトル: ある魔女が死ぬまで -メグ・ラズベリーの余命一年-\n",
"キャッチフレーズ: これは余命一年の魔女が紡ぐ奇跡の物語\n",
"\n",
"ID: 1177354054896217570\n",
"タイトル: エースはまだ自分の限界を知らない[第一部+Ex+1.5]\n",
"キャッチフレーズ: [累計4000万PV突破]リアルではないリアリティ野球青春物\n",
"\n"
]
}
],
"source": [
"print(works_to_string(data, works[:5]))"
]
},
{
"cell_type": "markdown",
"id": "506cd860",
"metadata": {},
"source": [
"# 検索"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "a03e09fc",
"metadata": {},
"outputs": [],
"source": [
"url = \"https://kakuyomu.jp/search\"\n",
"\n",
"params = {\n",
" \"q\": \"転生\", # 検索キーワード\n",
" \"page\": \"1\", # ページ数\n",
" \"ex_q\": \"あいうえお\", # 除外キーワード スペースで複数指定可能: \"あいうえお さしすせそ\"\n",
" \"serial_status\": \"running\", # 連載状態 \"running\" か \"completed\"\n",
" # 作品ジャンル\n",
" # 複数から選べ、カンマ区切りで複数指定可能: \"fantasy,action\"\n",
" # 使えるワード:\n",
" # fantasy(異世界ファンタジー), action(現代ファンタジー), sf(SF), love_story(恋愛), romance(ラブコメ), drama(現代ドラマ), horror(ホラー)\n",
" # mystery(ミステリー), nonfiction(エッセイ・ノンフィクション), history(歴史・時代・伝奇), criticism(創作論・評論), others(詩・童話・その他), maho(まほうのiらんど), fan_fiction(二次創作)\n",
" \"genre_name\": \"fantasy\",\n",
" # **評価数(★の多さ)**\n",
" # \"n以上\"の場合: \"{n}-\", \"n以下\"の場合: \"-{n}\", \"任意設定\"の場合: \"custom\"\n",
" # \"custom\"の場合、{\"total_review_point_min\":\"5\", \"total_review_point_max\":\"10\"} のようなparamが付く。\n",
" \"total_review_point_range\": \"1000-\",\n",
" # **小説の長さ**\n",
" # \"n以上\"の場合: \"{n}-\", \"n以下\"の場合: \"-{n}\", \"任意設定\"の場合: \"custom\"\n",
" # \"custom\"の場合、{\"total_character_count_min\":\"1000\", \"total_character_count_max\":\"10000\"} のようなparamが付く。\n",
" \"total_character_count_range\": \"-20000\",\n",
" # **作品公開日**\n",
" # \"1days\", \"7days\", \"1months\", \"6months\", \"1years\", \"custom\" から選べる。\n",
" # \"custom\"の場合、{\"published_date_start\": \"2025-05-01\", \"published_date_end\"=\"2025-05-08\"} のようなparamが付く。\n",
" \"published_date_range\": \"6months\",\n",
" # **作品更新日**\n",
" # \"1days\", \"7days\", \"1months\", \"6months\", \"1years\", \"custom\" から選べる。\n",
" # \"custom\"の場合、{\"last_episode_published_date_start\": \"2025-05-01\", \"last_episode_published_date_end\"=\"2025-05-08\"} のようなparamが付く。\n",
" \"last_episode_published_date_range\": \"1days\",\n",
"}\n",
"\n",
"res = requests.get(url, params=params)\n",
"soup = BeautifulSoup(res.text, \"html.parser\")\n",
"data = json.loads(soup.find(\"script\", id=\"__NEXT_DATA__\").string)[\"props\"][\"pageProps\"][\n",
" \"__APOLLO_STATE__\"\n",
"]\n",
"works = list(filter(lambda x: x.startswith(\"Work:\"), data.keys()))"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "7c5b45e1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ID: 16817330659748108558\n",
"タイトル: 追放された器用貧乏、隠しボスと配信始めたら徐々に万能とバレ始める\n",
"キャッチフレーズ: 攻撃A魔力A敏捷Aの器用貧乏?⇒オールSの万能です【完結まで毎日更新】\n",
"タグ: ダンジョン, 配信, 器用貧乏, 主人公最強, 追放側も主人公好き, カクヨムオンリー, 完結まで毎日更新\n",
"イントロダクション:\n",
"```\n",
"主人公はジョブ勇者。最初は恵まれたジョブと思われていたが、突出した能力がなく、器用貧乏の勇者詐欺と評価されるようになる。そのことからパーティを追放される。さらに最後の情けと称し、隠しボス部屋に放り込まれる。\n",
"そこで出迎えるは美しきヴァンパイア。\n",
"やけくそ気味に戦っていると、なぜかヴァンパイアが配信用ドローンに興味を持つ。\n",
"「ハイシン……? 裏切り的なあれか?」\n",
"そうして、半分、気の迷いで隠しボスとの配信を始めたら……、\n",
"\n",
"最初はボスと配信をする物珍しさで興味を持たれるのだが……、\n",
"\n",
"【そもそもヴァンパイアと闘えてるのすごないか?】\n",
"【A級配信者を瞬殺? 偶然かな?】\n",
"【え、S級ボスまで……!?】\n",
"\n",
"気がつくとめっちゃ注目を集めてしまっていた。\n",
"\n",
"そして追放したパーティもどこか様子がおかしい。。。\n",
"※追放側がそんなに悪い奴じゃない作品になります。復讐をご期待して読み始めるとすっきりできませんのでご注意ください。\n",
"```\n",
"\n",
"ID: 16818622170366457987\n",
"タイトル: 【290万PV感謝】恋愛バトルゲームの主人公のクズ兄に転生したが死にたく無いので全力で努力します\n",
"キャッチフレーズ: 転生したら主人公のクズ兄!?死亡フラグ回避しながらサブヒロインと交流\n",
"タグ: 学園モノ, ゲーム内転生, メインヒロインは全力回避, ゲーム知識チート, 微ハーレム(メイン以外), ゲスクズ兄への転生, 異能バトル, 成り上がり\n",
"イントロダクション:\n",
"```\n",
"目が覚めるとそこは知らないベッドに知らない家具、自分の部屋とは違う別のだれかの部屋\n",
"部屋の中を物色してると、自分が別人になってる事に気付く\n",
"そう、ここは学園恋愛バトルゲーム魔都東京1999の世界だった\n",
"\n",
"俺の名前は南原譲二、都内にある中堅ソフト開発会社のエンジニアだ、この会社は今2代目の社長が経営を引き継ぎ業績は悪化の一途を辿ってる、メインの仕事は大手からの業務委託とデバック、そんな俺たちの会社は今深刻な人材不足に陥ってる\n",
"\n",
"営業が取って来る案件の量に対し、対応するエンジニアの数が圧倒的に足りてない\n",
"俺たちエンジニアは何時終わるとも知れない残業に追われ心身共に疲弊していた、しかも働き方改革の為とか言う理由で残業時間は44時間で強制的にカットされる\n",
"\n",
"ブラックもブラック超ブラック企業だ\n",
"\n",
"そんな従業員に見向きもしない新社長は今日も接待と称し若い女の子に居る店に遊びに行く・・・\n",
"\n",
"こんな会社辞めてしまいたいが、俺には先代社長達と共にゲーム制作に関わっていた、あのやり甲斐に満ちていた時期の会社を忘れずにいる\n",
"\n",
"俺たちが開発に関わった処女作、魔都東京1999を手掛けていたあの時期に\n",
"\n",
"そして今まさにその思い入れのゲームの中に転生している、本当であればこのゲームを愛する者として喜ばしいはずが俺の気持ちは最悪に沈んでいる\n",
"\n",
"転生したのが北野 城二かよ\n",
"\n",
"北野 城二は主人公の北野 尊の兄でモブクズの悪役だ、しかもゲーム序盤に婚約者でメインヒロインである宮下 藍瑠に婚約破棄され逆上した城二と主人公尊で決闘になり戦闘のチュートリアルとしてボコボコにされ家を追い出され露頭に迷い最終的に死亡するキャラだ\n",
"\n",
"ゲームプレイヤーにざまぁを味わって貰う為にだけ存在する悪役キャラ・・・・だが俺は死ぬつもりは無いゲーム内の城二には出来なかった「努力」に全力で取り組み、あてがわれた死亡フラグを回避して見せる\n",
"\n",
"城二の運命を握るのは、サブヒロイン・・・屋上のアリエル事 雨宮 真白、俺は学園の現人神と噂される流星眼の美少女と交流を深める為に行動する\n",
"\n",
"最初は自分の命を守る為に関わった真白と共に過ごす内に、城二の心境にも変化が起こる・・・そして自身の目指す先も\n",
"\n",
"逆に主人公である尊はクズであった兄の周りに人が集まり信頼関係を築いている事に焦りを覚える・・・\n",
"\n",
"モブクズの悪役と主人公の運命は複雑に交錯する\n",
"\n",
"\n",
"```\n",
"\n",
"ID: 16818622175251032937\n",
"タイトル: 現代日本でダンジョン生活!ハズレスキルで無双生活\n",
"キャッチフレーズ: ハズレスキル+神スキルは、最強!底辺から始まる成り上がり生活!\n",
"タグ: 現代ダンジョン, 探索者, ハーレム, スキル\n",
"イントロダクション:\n",
"```\n",
"ダンジョンが現れて3年、世間にダンジョンが浸透してきた昨今、派遣先で契約を打ち切られ無職になった暁アキラ25歳は、ダンジョンの恩恵を受けることが出来ず日々を過ごしていた。\n",
"\n",
"三度目の正直でダンジョン潜入の資格を得たアキラは人生一発逆転をかけてダンジョンに潜る\n",
"しかし得たスキルはハズレスキルと呼ばれるものだった。\n",
"\n",
"\n",
"小説家になろうでも連載中:https://ncode.syosetu.com/n8482iq/\n",
"\n",
"小説家になろうからいろいろ細部を変更してます。\n",
"\n",
"```\n",
"\n"
]
}
],
"source": [
"print(works_to_string(data, works[:3]))"
]
},
{
"cell_type": "markdown",
"id": "1ae97a36",
"metadata": {},
"source": [
"# 作品ページ\n",
"\n",
"## エピソード一覧"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "45f5ad6a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ID: 16817330652496032095\n",
"タイトル: 某月刊誌別冊 2017年7月発行掲載 短編「おかしな書き込み」\n",
"公開日: 2023-01-28T13:22:04Z\n",
"\n",
"ID: 16817330652501371395\n",
"タイトル: 某週刊誌 1989年3月14日号掲載「実録!奈良県行方不明少女に新事実か?」\n",
"公開日: 2023-01-29T15:42:40Z\n",
"\n",
"ID: 16817330652581796438\n",
"タイトル: 『近畿地方のある場所について』 1\n",
"公開日: 2023-01-30T12:48:00Z\n",
"\n",
"ID: 16817330652620327747\n",
"タイトル: 某月刊誌 2006年4月号掲載「林間学校集団ヒステリー事件の真相」\n",
"公開日: 2023-01-31T08:58:05Z\n",
"\n",
"ID: 16817330652696945685\n",
"タイトル: 某月刊誌 1993年8月号掲載 短編「まっしろさん」\n",
"公開日: 2023-02-02T07:33:39Z\n",
"\n",
"ID: 16817330652737203886\n",
"タイトル: ネット収集情報 1\n",
"公開日: 2023-02-03T09:18:04Z\n",
"\n",
"ID: 16817330652920924558\n",
"タイトル: 読者からの手紙 1\n",
"公開日: 2023-02-07T11:48:44Z\n",
"\n",
"ID: 16817330652927986674\n",
"タイトル: 『近畿地方のある場所について』 2\n",
"公開日: 2023-02-16T03:00:06Z\n",
"\n",
"ID: 16817330653284890008\n",
"タイトル: 某月刊誌 2009年8月号掲載 読者投稿欄\n",
"公開日: 2023-02-16T09:00:00Z\n",
"\n",
"ID: 16817330653315697853\n",
"タイトル: 某月刊誌 2015年2月号掲載 短編「賃貸物件」\n",
"公開日: 2023-02-16T12:21:48Z\n",
"\n"
]
}
],
"source": [
"work_id = \"16817330652495155185\"\n",
"url = f\"https://kakuyomu.jp/works/{work_id}\"\n",
"res = requests.get(url)\n",
"soup = BeautifulSoup(res.text, \"html.parser\")\n",
"data = json.loads(soup.find(\"script\", id=\"__NEXT_DATA__\").string)[\"props\"][\"pageProps\"][\n",
" \"__APOLLO_STATE__\"\n",
"]\n",
"episodes = list(filter(lambda x: x.startswith(\"Episode:\"), data.keys()))\n",
"print(episodes_to_string(data, episodes[:10]))"
]
},
{
"cell_type": "markdown",
"id": "b04fc924",
"metadata": {},
"source": [
"## エピソード取得"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "fc957983",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"都内在住の24歳会社員、Aさんは新卒でエンジニアとして入社したシステム会社の業務にも慣れ、刺激のない鬱々とした毎日を送っていたという。\n",
"趣味もなく、彼女もいないAさんがストレス解消にしたのはサイト巡りだった。\n",
"「恥ずかしい話なんですが、いわゆるアダルトサイトってやつですね。最近だと動画の無料転載をやってるようなサイトも多いじゃないですか? もちろん褒められたものではないと思うんですが。毎日寝る前にいくつかのそういうサイトを巡るのがほぼ日課みたいになってました」\n",
"そのなかでもひときわお気に入りのサイトがあったという。\n",
"「そのサイトは有名なレーベルの新作も転載していて、けっこうアクセスしてたと思います。ただ、ちょっと作りが独特で……。動画の再生枠の下って普通は他の動画へのオススメ枠になってることが多いんですけど、そのサイトはコメント欄があったんです」\n",
"これはエンジニアとしての俺の予想なんですけど、と前置きしてAさんは続ける。\n",
"「こういう界隈のサイトって法律のグレーゾーンで運用してることがほとんどなんで、いつ閉鎖してもおかしくない。そういった理由もあってか、とにかくサイト自体に手間をかけてないんですよね。具体的にいうと、別のサイトの枠組みを流用してることが多い。そのほうがイチから作らなくて済むから簡単なんですよ。だから、そのサイトのコメント欄も運営側が意図して作ったっていうよりかはたまたま流用元にあったっていうような印象を受けました。無断転載のアダルト動画を観て、コメント欄で交流するような変人もいませんしね」\n",
"その予想を裏付けるように、コメント欄にコメントが書き込まれることはほとんどなく、あったとしても『動画が途中で切れてるぞ』や『こんな動画じゃなくて新作動画をアップしろ』などといったほとんど文句といってもいいものがまれにみられるくらいで、運営からの返信コメントがあることもなく、活用されている様子はなかった。\n",
"ある日、いつものようにそのサイトへアクセスしたAさんは妙な書き込みを見つけた。\n",
"「その動画はお気に入りのレーベルで売り出し中の、新人のデビュー作でした。見つけたときはラッキーって感じだったんですが、観終わった後になんとなくスクロールしたらそのコメントが目に入ったんです」\n",
"『かわいい。うちへきませんか。』\n",
"「第一印象としては、インターネットの使い方をわかっていないおじいさんとかが書き込んだのかなと思ったのですが、なんだか妙な雰囲気がして心に引っかかっていました」\n",
"ひと月ほど経って、そのサイトで同じ女優の新作をAさんは見つけた。\n",
"「もう二作目出てるんだ。なんて思いながら観たんですが、そこでまた書き込みを見つけました」\n",
"『うちへきませんか。かきもありますよ。』\n",
"「直感的に、同じひとだなと思いました。もちろん、こんなところでコメントしても女の子本人が読むはずもないし、文章も意味不明だし気持ち悪いなって感じました」\n",
"その後も不定期に転載されるその女優の動画には似た文体のコメントが必ずと言っていいほど書き込まれていたという。それらは、ほとんど書き込みのないコメント欄でかなり目を引いた。\n",
"「俺もだんだん楽しみになってきて、その女の子の動画が投稿されるとコメントがついてないか確認するようになっていました」\n",
"そんなことを続けて数か月、また例の女優の出演作が転載されている動画が投稿された。そのコメント欄にはこう書き込まれていたという。\n",
"『お山にきませんか。かきもあります。』\n",
"その日は仕事で上司に叱られたこともあり、Aさんの頭に暗い好奇心が湧いたという。\n",
"「ちょっとからかってやろうかな程度の考えでした」\n",
"そのコメントの返信欄に書き込みをしてみたのだ。\n",
"『お山にきませんか。かきもあります。』\n",
"『いつもコメントありがとうございます! 〇〇(出演女優の名前)です。おうちはどこなんですか?』\n",
"書き込んだはいいものの、返信したことで一旦満足してしまい、その日を終えるとAさんはそんな書き込みをしたことも忘れてしまっていたという。\n",
"次にそれを思い出したのは、新しく投稿されたその女優の動画だった。コメント欄にはこうあった。\n",
"『なぜこない。まって居るずっと。』\n",
"「あわてて前回返信したコメントのある動画ページまでアクセスしたんですが、俺がいたずらでした返信に、さらに返信がついていたんです」\n",
"『お山にきませんか。かきもあります。』\n",
"『いつもコメントありがとうございます! 〇〇です。おうちはどこなんですか?』\n",
"『●●●●●-●●-●●●(実在住所のため伏せる)』\n",
"「番地まで全て書いてあったんです。さすがに驚きました。相手は本気なんだなって。同時に自分は本当にヤバい人に返信をしてしまったんだなとも思いました」\n",
"恐怖を感じながらも、ふとAさんは思いたち、地図アプリで検索をかけてみたという。\n",
"「別に目的があったわけではないんですが、こんなにヤバい人はどんな家に住んでるんだろうと思って」\n",
"表示された場所を見て、驚いた。\n",
"「家じゃなかったんですよ。神社でした。それも田舎の方にあるかなり古い神社。ストリートビューで見たら小高い山の上にある神社みたいで。麓の車道の脇道に古ぼけた鳥居が立っていて、そこから山の上の方に本殿へ続く階段が伸びていました。本殿も荒れてたし、廃墟になってたんじゃないかな」\n",
"「もうこれ以上深入りしたくなくて。それ以降そのサイトにはあまりアクセスしないようになりました。ただ、一度だけ何かのきっかけでアクセスしたことがあったんです。そのときもたまたまその女の子の動画がアップされていて……」\n",
"そこにはこうコメントされていた。\n",
"『こしいれせよ』\n"
]
}
],
"source": [
"work_id = \"16817330652495155185\"\n",
"episode_id = \"16817330652496032095\"\n",
"url = f\"https://kakuyomu.jp/works/{work_id}/episodes/{episode_id}\"\n",
"res = requests.get(url)\n",
"soup = BeautifulSoup(res.text, \"html.parser\")\n",
"\n",
"episode_body = soup.find(\"div\", class_=\"widget-episodeBody js-episode-body\")\n",
"\n",
"# class=\"blank\" を除いた <p> タグのテキストだけ抽出\n",
"paragraphs = [\n",
" p.get_text(strip=True)\n",
" for p in episode_body.find_all(\"p\")\n",
" if \"blank\" not in p.get(\"class\", [])\n",
"]\n",
"\n",
"episode = \"\\n\".join(paragraphs)\n",
"print(episode)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}