なろう作品をSend to Kindleする(ツール不要)

背景

小説家になろう」の作品をKindleの電子インクペーパーで読みたい。以前は便利なWebサービスがあったが、つかえなくなって久しい。代替ツールはあるが、インストールに手間がかかるようだ。

Kindleは、mobiやepubのような電子書籍専用のファイル形式のほかに、textやhtmlにも対応している。よって、複数話をひとつのhtmlファイルに結合するだけで、Kindleにテキストを表示することはできる。この際、縦書きではなくてもよかろう!挿絵もいらない。

やったこと

以下のRubyスクリプトを任意のディレクトリに保存して、

$ ruby narou2zip.rb [ncode]

のようにコマンドラインで実行すると、zipファイルができる。このファイルを手動でSend to Kindleすればよい。

ただし、もし未インストールのときは、一度だけ事前にNokogiriとRubyzipのインストールが必要。

$ gem install nokogiri
$ gem install rubyzip

require 'open-uri'
require 'nokogiri'
require 'zip'


def scrape(url)
  opt = {}
  opt['Cookie'] = "over18=no"
  html = open(url, opt) do |io|
    charset = io.charset
    io.read
  end
  
  return Nokogiri::HTML.parse(html, nil, charset)
end

#
def puts_novel(io, doc)
  results = doc.xpath('//div[@id="novel_p"]')
  unless results.length == 0
    io.puts results.inner_html.gsub(/<p id="L?\d+">/, '<p>')
    io.puts '<hr>' 
  end
  
  results = doc.xpath('//div[@id="novel_honbun"]')
  io.puts results.inner_html.gsub(/<p id="L?\d+">/, '<p>')
  
  results = doc.xpath('//div[@id="novel_a"]')
  unless results.length == 0
    io.puts '<hr>' 
    io.puts results.inner_html.gsub(/<p id="L?\d+">/, '<p>')
  end
end

#
ncode = (ARGV[0].nil?) ? 'n6316bn' : ARGV[0]
doc = scrape('https://ncode.syosetu.com/'+ncode+'/')

title = doc.xpath('//p[@class="novel_title"]').inner_text
writer = doc.xpath('//div[@class="novel_writername"]').inner_text.gsub(/[\r\n]/,"")
chapters= []
episodes = []

results = doc.xpath('//dl[@class="novel_sublist2"]')
results.each do |r|
  chapter_title = r.xpath('preceding-sibling::div[@class="chapter_title"][1]').inner_text
  subtitle = r.xpath('descendant::a').inner_text
  link = r.xpath('descendant::a/@href').inner_text
  
  chapters.push chapter_title unless chapters.last == chapter_title
  episodes.push [chapters.length-1, subtitle, link]
end

#io = File.new(File.basename(title) + '.html', 'w')
io = Zip::OutputStream.open(ncode+'.zip')
io.put_next_entry(File.basename(title) + '.html') 

io.puts '<!DOCTYPE html>'
io.puts '<html>'
io.puts '<head>'
io.puts '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
io.puts '<title>'+title+'</title>'
io.puts '</head>'
io.puts '<body>'
io.puts '<h1>'+title+'</h1>'
io.puts writer

if chapters.length > 1
  io.puts '<h2>目次</h2>'
  chapters.each_index {|i| io.puts '<a href="#Ch_'+i.to_s+'">'+chapters[i]+'</a><br>'}
elsif episodes.length > 1
  io.puts '<h2>目次</h2>'
  episodes.each {|e| io.puts '<a href="#Ep'+e[2].gsub("/", "_")+'">'+e[1]+'</a><br>'}
else
  io.puts '<hr>'
end

if episodes.length == 0
  puts_novel(io, doc)
else
  temp= nil
  episodes.each do |e|
    STDERR.puts e.join(' ')
    
    unless temp == e[0]
      io.puts '<h2 id="Ch_'+e[0].to_s+'">'+chapters[e[0]]+'</h2>'
      temp = e[0]
    end
    
    io.puts '<h3 id="Ep'+e[2].gsub('/', '_')+'">'+e[1]+'</h3>'
    
    puts_novel(io, scrape('https://ncode.syosetu.com'+e[2]))
    sleep 3
  end
end

io.puts '</body>'
io.puts '</html>'
io.close

きづいたこと

  • 拡張子を".html"に変更するだけでは、html形式として識別されない。「<!DOCTYPE html>」や「<html></html>」などの最低限の構成要素を含めるべき。
    • 「<!DOCTYPE html>」がないと、markdown形式に認識されるらしい。なぜならば、単語の中に"*"(半角アスタリスク)があると、以降がすべて斜体で表示されていたため。
  • ページ内リンク(<a href="#sample">...</a>と<h2 id="sample">...</h2>のセット)がつかえるので、目次もかんたんに作れる。name属性も可。
  • ルビタグ(<ruby>...</ruby>)はつかえる。
  • アイテム名には、ファイル名が表示される。titleタグ(<title>...</title>)やdc:titleでは表示されない。
  • 著者名は、metaタグ(<meta name="Author" content="...">)やdc:creatorタグでは表示されない。
  • 改行コードは、macOS/UNIX(LF)でもOK。
  • CSSによる縦書き表示(writing-mode: vertical-rl;や-webkit-writing-mode: vertical-rl;)はできない。
  • 挿絵の画像は、表示できない。
  • MacOSでは、ファイル名を右クリックして圧縮したものをSend to Kindleすると、Amazonからエラーメールが返ってくる。zipコマンドで圧縮しないとだめ。
  • 短編の場合とそうでない場合との両方に対応する必要がある。
  • R18作品のときは、Cookieに"over18=yes"を指定する。

つぎやること

著者名が自動で入力できるようにしたい。