なろう作品をKindleで読めるePub形式に変換

つかいかた

1. この記事の下部にあるRubyスクリプトを、任意のディレクトリに"narou2epub.rb"というファイル名で保存する。

2. 以下のようにコマンドラインで実行すると、epubファイルができる。[ncode]には、なろう作品のものを指定する。

$ ruby narou2epub.rb [ncode]

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

$ gem install nokogiri
$ gem install rubyzip

3. できたファイルをメールに添付し、Kindle端末のメールアドレスに送信する。

Rubyスクリプト

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

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

def replace(text)
  text.gsub!(/<p id="L?\d+">/, '<p>')
  text.gsub!('<br>', '<br />')
  text.gsub!(/<img .*?>/, '(画像削除)')
  text.gsub!(/<a .*?>/, '')
  text.gsub!("</a>", '')
  return text
end

def puts_novel(io, doc)
  results = doc.xpath('//div[@id="novel_p"]')
  unless results.length == 0
    io.puts replace(results.inner_html)
    io.puts '<hr />' 
  end
  
  results = doc.xpath('//div[@id="novel_honbun"]')
  io.puts replace(results.inner_html)
  
  results = doc.xpath('//div[@id="novel_a"]')
  unless results.length == 0
    io.puts '<hr />' 
    io.puts replace(results.inner_html)
  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
creator = doc.xpath('//div[@class="novel_writername"]').inner_text.gsub(/[\r\n]/,"").gsub(/作者:/,"")
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 = Zip::OutputStream.open(File.basename(title)+'.epub')

io.put_next_entry('mimetype')
io.puts 'application/epub+zip'

io.put_next_entry('META-INF/container.xml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="content.opf" media-type="application/oebps-package+xml" />
  </rootfiles>
</container>
EOS

io.put_next_entry('content.opf')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<package version="3.0" xmlns="http://www.idpf.org/2007/opf" xml:lang="ja">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:title>#{title}</dc:title>
    <dc:creator>#{creator}</dc:creator>
    <dc:language>ja</dc:language>
  </metadata>
  <manifest>
    <item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav" />
    <item id="title" href="title.xhtml" media-type="application/xhtml+xml" />
    <item id="main" href="main.xhtml" media-type="application/xhtml+xml" />
    <item id="style" href="style.css" media-type="text/css" />
  </manifest>
  <spine page-progression-direction="rtl">
    <itemref idref="title" />
    <itemref idref="toc" />
    <itemref idref="main" />
  </spine>
</package>
EOS

io.put_next_entry('toc.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>目次</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
  <nav epub:type="toc">
EOS

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

io.puts <<"EOS"
  </nav>
</body>
</html>
EOS

io.put_next_entry('style.css')
io.puts <<"EOS"
html {
-webkit-writing-mode: vertical-rl;
-epub-writing-mode: vertical-rl;
writing-mode: vertical-rl;
}
nav ol { list-style: none; }
EOS

io.put_next_entry('title.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>表題</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<h1>#{title}</h1>
#{creator}
</body>
</html>
EOS

io.put_next_entry('main.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>本文</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
EOS

if episodes.length == 0
  io.puts '<section>'
  puts_novel(io, doc)
  io.puts '</section>'
else
  temp= nil
  episodes.each do |e|
    STDERR.puts e.join(' ')
    
    io.puts '<section>'
    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]))
    io.puts '</section>'
    
    sleep 3
  end
end

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

io.close

今後の課題

  • 画像は表示できない。

メモ

  • 目次はulタグではなく、olタグでなければならない。代わりにCSSでlist-style: none;指定する。
  • Apple Booksはnavプロパティなしだと壊れていると判定する。一方、Kindleは表示できる。
  • 本文中にimgタグが含まれると、kindle端末が表紙と誤解してしまうため、画像を削除している。