前回の記事(徹底解説】「小説家になろう」をNarou.rb, AozoraEpub3で整形してKindleで読む(Mac))で、Narou.rbとAozoraEpub3を駆使して「小説家になろう」の小説を美しい縦書きEPUBで読む環境を構築したが、今日久しぶりに新しい小説のダウンロードを試したところ、エラーでダウンロードができなくなっていた。
[ERROR] 小説が削除されているか非公開な可能性があります
[ERROR] https://ncode.syosetu.com/n9669bk/ の目次データが取得出来ませんでした
ブラウザでアクセスすると普通に掲載されているのに、なぜ取得できないのか?
自分で原因調査するのは大変なので、AIに原因調査をしてもらった。使用したのは、私が今月に入ってから導入して使っている「Google Antigravity」というコーディングAI。コーディングAIでは「Claude Code」が一番有名だが、課金額が嵩みがちという評判を聞いて、無料で使い始めることができるAntigravityを私は選んだ。
無料で使用できる上限にはすぐに到達してしまって、「Google AI Pro:月額2,900円」のプランを契約した。このプランでも、「Claude」や「Gemini Pro」などの強力なAIはすぐに上限に達して使えなくなってしまうが、「Gemini Flash」は私くらいの用途であればほぼ無尽蔵に使えるのでこの料金の範囲内で十分やりくりできる。
1. 前回のブログとフォルダをAIに丸投げしてスタート
まずここまでの状況をAntigravityに教えるために、作業フォルダである narou_wk をプロジェクトフォルダとして開き、さらに前回のブログの内容を記載したファイルを渡して「このフォルダの構築について書いた自分のブログ記事があるからまずはこれを読んで」と伝えてチャットをスタートした。
これだけで、Antigravityはこれまでの経緯を完璧に理解してくれる。
2. エラーの原因を特定
つづいて「『小説家になろう』のWebサイトでは当該作品のページがブラウザで見えるのにNarou.rbでダウンロードができない。原因を調べて。」と依頼した。
時折Antigravityからの実行許可に答えながら待つこと数分、原因の特定とコードの修正までが自動的に完了した。
判明した原因は、なろう側が実施した以下の2段階のリニューアルだった。
- 目次ページのリニューアル(2024年):
各話のリンクが
p-eplist という新しいクラス名で管理されるようになったため、古いバージョンのNarou.rb(3.9.0)では目次そのものが読み取れなくなっていた。
- 作品情報ページの仕様変更(2025年3月):
さらにタイトルやあらすじ等が
p-infotop-... クラスに変わり、タイトルのHTMLに改行コードが挟まれる仕様に変更されたため、古い正規表現ではマッチしなくなっていた。
Narou.rbを最新にアップデートすることで1つ目の問題には対応できるが、Narou.rb公式(開発元)によるアップデートは2024年9月にリリースされたバージョン 3.9.1 を最後に、新しいバージョンは公開されておらず、開発が停止状態のため、2つ目の問題には対応できない。
GitHub上を見ると、この2025年3月に発生したエラー(Issue #445)に対して、有志がすでに修正用のソースコード(Pull Request #446)を開発元に提出してくれている。しかし、管理者がその変更を取り込んで(マージして)新バージョンとしてリリースしていないため、我々一般ユーザーが普通に gem install しただけでは、この作品情報ページのパースエラーを回避することができない。
つまり、最新の 3.9.1 にアップデートした上で、有志が残してくれたパッチ(YAMLの正規表現修正)を自分で手動適用する(ハックする)しかないのが現状なのだ。
3. 具体的な変更箇所(diff)の説明
今回、AI(Antigravity)に手動で適用してもらったYAMLファイルの修正(パッチ)の具体的な内容がこれだ。
主に、なろうが2025年3月に実施した「作品情報ページ」のクラス名変更(p-infotop-... への移行)と、タイトルタグの直後に改行が挟み込まれるようになった変化に対応している。
# 小説情報からパースするための設定
novel_info_url: \\k<top_url>/novelview/infotop/ncode/\\k<ncode>/
# タイトル
-t: <h1><a href=".+?">(?<title>.+?)</a></h1>
+t: |-
+ <h1 class="p-infotop-title">
+ <a href=".+?">(?<title>.+?)</a>
+ </h1>
# novel_type 小説種別
-nt: <span id="noveltype(?:.*?)">(?<novel_type>.+?)</span>
+nt: <span class="p-infotop-type__type(?:.*?)">(?<novel_type>.+?)</span>
novel_type_string:
連載中: 1
完結済: 3
短編: 2
# general_all_no 掲載話数
-ga: </span>全(?<general_all_no>\d+)部
+ga: <span class="p-infotop-type__allep">全(?<general_all_no>\d+)エピソード
# story あらすじ
-s: <td class="ex">(?<story>.+?)</td>
+s: <dt class="p-infotop-data__title">あらすじ</dt>
+ <dd class="p-infotop-data__value">(?<story>.+?)</dd>
# general_firstup 初回掲載日
gf: |-
- <th>掲載日</th>
- <td>(?<general_firstup>.+?)</td>
+ <dt class="p-infotop-data__title">掲載日</dt>
+ <dd class="p-infotop-data__value">(?<general_firstup>.+?)</dd>
この修正を、一般用の ncode.syosetu.com.yaml とR18用の novel18.syosetu.com.yaml の両方に適用した。さらに、R18用では掲載サイト名の動的取得部分(sitename:)も同様のクラス名へと書き換えが必要だった。
これらすべての修正作業を、AIが裏でファイルを特定して自律的に実行してくれた。
なお、動作確認もAIが勝手に実施してくれた。
4. まとめと修正コード全文
今回のように、自分自身も状況を忘れてしまった過去の作業の続きを再開するとき、AIがあると瞬時に状況を把握して続きをやってくれるので助かる。
もしAIが無くて自力でGoogle検索を駆使した調査とコードの修正をしていたら、今回の件でおそらく週末を何回か潰していたと思うので、月額2900円を払う価値は十分にあると感じる。
最後に、今回修正を施した最新(3.9.1ベース)のYAML設定ファイルの全文を掲載しておく。同じエラーで困っている人は、これをそのまま貼り付ければ動作するはずだ。
一般作品用: ncode.syosetu.com.yaml
name: &name 小説家になろう
domain: ncode.syosetu.com
top_url: https://\\k<domain>
url: https?://\\k<domain>/(?<ncode>n\d+[a-z]+)
encoding: UTF-8
confirm_over18: no
append_title_to_folder_name: yes
title_strip_pattern: null
cookie: over18=yes
sitename: *name
version: 2.2
toc_url: https://\\k<domain>/\\k<ncode>/
subtitles: |-
(?:<div class="p-eplist__chapter-title">(?<chapter>.+?)</div>
)?<div class="p-eplist__sublist">
<a href="(?<href>/.+?/(?<index>\d+?)/)" class="p-eplist__subtitle">
(?<subtitle>.+?)
</a>
<div class="p-eplist__update">
(?:<span class="p-eplist__favep">.+?</span></span>)?(?<subdate>.+?)
(?:<span title="(?<subupdate>.+?) 改稿">(<u>改</u>)</span>
)?</div>
</div>
next_toc: <a href="/(?<next_page>[^"]+)" class="c-pager__item c-pager__item--next">
next_url: https://\\k<domain>/\\k<next_page>
toc_page_max: <a href="/[^"]+p=(?<toc_page_max>\d+)" class="c-pager__item c-pager__item--last">
body_pattern: |-
<div class="js-novel-text p-novel__text">
(?<body>.+?)
?</div>
introduction_pattern: |-
<div class="js-novel-text p-novel__text p-novel__text--preface">
(?<introduction>.+?)
?</div>
postscript_pattern: |-
<div class="js-novel-text p-novel__text p-novel__text--afterword">
(?<postscript>.+?)
?</div>
illust_current_url: null
illust_grep_pattern: null
is_narou: true
narou_api_url: https://api.syosetu.com/novelapi/api/
novel_info_url: \\k<top_url>/novelview/infotop/ncode/\\k<ncode>/
t: |-
<h1 class="p-infotop-title">
<a href=".+?">(?<title>.+?)</a>
</h1>
nt: <span class="p-infotop-type__type(?:.*?)">(?<novel_type>.+?)</span>
novel_type_string:
連載中: 1
完結済: 3
短編: 2
ga: <span class="p-infotop-type__allep">全(?<general_all_no>\d+)エピソード
s: <dt class="p-infotop-data__title">あらすじ</dt>
<dd class="p-infotop-data__value">(?<story>.+?)</dd>
gf: |-
<dt class="p-infotop-data__title">掲載日</dt>
<dd class="p-infotop-data__value">(?<general_firstup>.+?)</dd>
nu: |-
<dt class="p-infotop-data__title">(?:最終更新日|最新掲載日|最終掲載日)</dt>
<dd class="p-infotop-data__value">(?:
)?(?<novelupdated_at>.+?)(?:
)?</dd>
gl: |-
<dt class="p-infotop-data__title">(?:最新掲載日|最終掲載日)</dt>
<dd class="p-infotop-data__value">(?<general_lastup>.+?)</dd>
w: |-
<dt class="p-infotop-data__title">作者名</dt>
<dd class="p-infotop-data__value">(?:<a href=".+?">)?(?<writer>.+?)(?:</a>)?.?</dd>
l: |-
<dt class="p-infotop-data__title">文字数</dt>
<dd class="p-infotop-data__value">(?<length>.+?)文字</dd>
R18作品用: novel18.syosetu.com.yaml
name: 小説家になろうR18
domain: novel18.syosetu.com
top_url: https://\\k<domain>
url: https?://\\k<domain>/(?<ncode>n\d+[a-z]+)
encoding: UTF-8
confirm_over18: yes
append_title_to_folder_name: yes
title_strip_pattern: null
cookie: over18=yes
sitename: |-
<dt class="p-infotop-data__title">掲載サイト</dt>
<dd class="p-infotop-data__value">(?<sitename>.+?)\(.+\)</dd>
version: 2.2
toc_url: https://\\k<domain>/\\k<ncode>/
subtitles: |-
(?:<div class="p-eplist__chapter-title">(?<chapter>.+?)</div>
)?<div class="p-eplist__sublist">
<a href="(?<href>/.+?/(?<index>\d+?)/)" class="p-eplist__subtitle">
(?<subtitle>.+?)
</a>
<div class="p-eplist__update">
(?:<span class="p-eplist__favep">.+?</span></span>)?(?<subdate>.+?)
(?:<span title="(?<subupdate>.+?) 改稿">(<u>改</u>)</span>
)?</div>
</div>
next_toc: <a href="/(?<next_page>[^"]+)" class="c-pager__item c-pager__item--next">
next_url: https://\\k<domain>/\\k<next_page>
toc_page_max: <a href="/[^"]+p=(?<toc_page_max>\d+)" class="c-pager__item c-pager__item--last">
body_pattern: |-
<div class="js-novel-text p-novel__text">
(?<body>.+?)
?</div>
introduction_pattern: |-
<div class="js-novel-text p-novel__text p-novel__text--preface">
(?<introduction>.+?)
?</div>
postscript_pattern: |-
<div class="js-novel-text p-novel__text p-novel__text--afterword">
(?<postscript>.+?)
?</div>
illust_current_url: null
illust_grep_pattern: null
is_narou: true
narou_api_url: https://api.syosetu.com/novel18api/api/
novel_info_url: \\k<top_url>/novelview/infotop/ncode/\\k<ncode>/
t: |-
<h1 class="p-infotop-title">
<a href=".+?">(?<title>.+?)</a>
</h1>
nt: <span class="p-infotop-type__type(?:.*?)">(?<novel_type>.+?)</span>
novel_type_string:
連載中: 1
完結済: 3
短編: 2
ga: <span class="p-infotop-type__allep">全(?<general_all_no>\d+)エピソード
s: <dt class="p-infotop-data__title">あらすじ</dt>
<dd class="p-infotop-data__value">(?<story>.+?)</dd>
gf: |-
<dt class="p-infotop-data__title">掲載日</dt>
<dd class="p-infotop-data__value">(?<general_firstup>.+?)</dd>
nu: |-
<dt class="p-infotop-data__title">(?:最終更新日|最新掲載日|最終掲載日)</dt>
<dd class="p-infotop-data__value">(?:
)?(?<novelupdated_at>.+?)(?:
)?</dd>
gl: |-
<dt class="p-infotop-data__title">(?:最新掲載日|最終掲載日)</dt>
<dd class="p-infotop-data__value">(?<general_lastup>.+?)</dd>
w: |-
<dt class="p-infotop-data__title">作者名</dt>
<dd class="p-infotop-data__value">(?:<a href=".+?">)?(?<writer>.+?)(?:</a>)?.?</dd>
l: |-
<dt class="p-infotop-data__title">文字数</dt>
<dd class="p-infotop-data__value">(?<length>.+?)文字</dd>