【Ruby】Nokogiriでカクヨムの作品をダウンロード

#前置き
[**カクヨム**](https://kakuyomu.jp/)というサイトがあります。これは[小説家になろう](https://syosetu.com/)とか[ノベルアップ+](https://novelup.plus/)とか[ハーメルン](https://syosetu.org/)のような[^1]所謂「**小説投稿サイト**」の一つです。
私は今月にコミカライズが連載開始予定の名作・[**超世界転生エグゾドライブ　-激闘！異世界全日本大会編-**](https://kakuyomu.jp/works/1177354054884850859)をオフライン保存して読もうと思ったのですが、カクヨムの公式は小説家になろうのように第三者向けにダウンロードの手段を用意してません。
そこで、作品ページを直接スクレイピングして保存することでダウンロードを実現したので、それに使ったプログラムをメモついでに紹介します。
#注意
- Rubyは殆ど初めて触るので、至らない点も多々あると思われます
- まずないと思いますがこのプログラムを使う場合、自己責任でお願いします。
- 保存したデータは私的利用に留めましょう。他人への譲渡・販売は違法です。

#準備
[この記事](https://qiita.com/kumamonmaster/items/9bb2aadde56c956fdc9f)等を参考に、Nokogiriをインストールしておいてください。

#作る
##入力を受け取る
特段延べるべきこともなく、普通に受け取ります。

```ruby:Scrape.rb
require 'nokogiri'
require 'open-uri'

puts "目次のURLを下さい"
get = gets.chomp
if ! /https:\/\/kakuyomu.jp\/works\/\d{19}/ === get #入力された文字列がURLの型に合うかを正規表現でチェック
    while ! /https:\/\/kakuyomu.jp\/works\/\d{19}/ === get
        puts "目次のURLを下さい"
    end
end
```
もうちょっと短縮できたと思うんですが、どうもエラーが連続しまして…
##目次を取得する
カクヨムのURLに関するシステムは、こういう局面においては少しばかり面倒臭いものです。
仮にこれが小説家になろうとかハーメルンとかだったら、ユニークIDは各作品に1つずつ割り振られる他には存在しないため、URLから直接ダウンロードを開始できたでしょう。

```text:小説家になろうにおける話数の増え方(例：「シャングリラ・フロンティア～クソゲーハンター、神ゲーに挑まんとす～」)
第一話：https://ncode.syosetu.com/n6169dz/1/
第二話：https://ncode.syosetu.com/n6169dz/2/
第三話：https://ncode.syosetu.com/n6169dz/3/
第四話：https://ncode.syosetu.com/n6169dz/4/
第五話：https://ncode.syosetu.com/n6169dz/5/
```

ですが、カクヨムやノベルアップ+だとそうはいきません。恐らくエピソードの挿入との兼ね合いだと思われるんですが、こいつらは**「各作品」**とはまた別に**「各エピソード」**にもユニークIDを設定しているんです。

```text:カクヨムにおける話数の増え方(例：「超世界転生エグゾドライブ　-激闘！異世界全日本大会編-」)
第一話：https://kakuyomu.jp/works/1177354054884850859/episodes/1177354054884851015
第二話：https://kakuyomu.jp/works/1177354054884850859/episodes/1177354054884853763
第三話：https://kakuyomu.jp/works/1177354054884850859/episodes/1177354054884862727
第四話：https://kakuyomu.jp/works/1177354054884850859/episodes/1177354054884878794
第五話：https://kakuyomu.jp/works/1177354054884850859/episodes/1177354054884886288
```
そういうわけで、カクヨムでは小説家になろうみたいに作品のユニークIDの後ろにつける数字を1ずつ増やしていくだけではダメでして、先に**目次を取得**する必要があります。
ということで、次のようなコードでぱぱっと取得します。

```ruby:Scrape.rb
require 'nokogiri'
require 'open-uri'
#(略)
doc = Nokogiri::HTML(URI.open(get))
doc.xpath("//a[@class='widget-toc-episode-episodeTitle']").each do |url|
    urls.push(url[:href]) #配列「urls」に目次を格納
end
```
##ディレクトリ周りのあれこれ
プログラムと同じ階層にテキストデータを保存するのはぐちゃぐちゃになる未来が目に見えている選択肢なので、フォルダを新しく作ってその中にダウンロードしたデータを格納しましょう。

```Ruby:Scrape.rb
require 'nokogiri'
#(略)
if ! Dir::exist?("Kakuyomu_DL_Data") #既にフォルダを作成済みか判定
    Dir::mkdir("Kakuyomu_DL_Data")
end
```
そして、作ったフォルダ「Kakuyomu_DL_Data」の下に更にフォルダを作ります。カクヨムのタイトルは被ることもあったと思うので、万が一のフォルダ名の重複を避けるために変化をつけるようにしています。

```Ruby:Scrape.rb
#(略)
Dir_Path = "Kakuyomu_DL_Data/" + doc.xpath("//h1[@id='workTitle']").inner_text #先ほど読み込んだ目次から作品タイトルを取得
if Dir.exist?(Dir_Path)
    count = 1
    while Dir.exist?("#{Dir_Path}(#{count})") do
        count += 1
    end
    Dir_Path << "(#{count})"
end
Dir::mkdir(Dir_Path)
```
##各エピソードにアクセスしてダウンロード
いよいよ本文のダウンロードのフェーズです。先ほど作った作品専用フォルダの下に、テキストデータとして一話ずつ作品を保存します。

```ruby:Scrape.rb
#(略)
count = 0
size = urls.size
urls.each do |url|
    count += 1
    doc = Nokogiri::HTML(URI.open("https://kakuyomu.jp" + url))
    File.open("#{Dir_Path}/#{count}.txt","w") do |text|
        text.puts doc.xpath("//header[@id='contentMain-header']").inner_text #章名とエピソード名
        text.puts doc.xpath("//div[@class='widget-episodeBody js-episode-body']").inner_text#本文
    end
    puts "保存中…(#{count}/#{size})"
    sleep 1 #スクレイピングをするときは、しっかり間隔を置こう！
end
puts "完了！"
```
ここまでに挙げたコードを連結すれば、カクヨムの目次を投げると全話保存するプログラムができます。

#感想
やっぱりRubyはScratchではとてもできないことが色々できますね……
ところでもうちょっとコードが短くできるのでは？(診断メーカー脳)
#参考サイト等
- [Ruby + Nokogiriでスクレイピング](https://qiita.com/kumamonmaster/items/9bb2aadde56c956fdc9f)
- [Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath](https://blog.takuros.net/entry/2014/04/15/070434)

[^1]: 例として挙げるサイト群の選定について疑問をお持ちの方もいらっしゃるでしょうが、適当に挙げただけですので見逃していただけますと幸いです