未来はあまりに遠いし、おれはもう待てない

SF小説やプログラミングの話題を中心とするフジ・ナカハラのブログ

Web小説のリリース自動化 2018

私は自分の書いた小説を以下のフォーマットで発表している。

原稿をこれらの形にするにあたって、その変換や公開・更新の作業をできるかぎり自動化した。 この記事では、その実現方法について説明する。

はじめに

動機

なぜ小説をいくつものフォーマットで発表する必要があるのだろうか。これにはふたつの理由がある。

第一に、作品を発表する場所が増えれば増えるほど、人の目に触れる可能性が高くなる。 特に、小説投稿サイトには小説を読むことを目的とした人が集まっている。

第二に、特定のプラットフォームに依存しなくて済む。 たとえば、小説投稿サイトには、突然規約が変わったり、サービスが終了したりするリスクがある。 そうしたリスクは分散させておいたほうがよい。

しかし、作品を発表する場所が増えれば増えるほど、運用が大変になる。作品の変換やアップロードに時間を取られれば、肝心の作品を書く時間が減ってしまう。 また、運用に失敗すると、場所によって内容が違い、どれが正しいかわからないという事態にもなりかねない。 逆に、更新作業が面倒だからという理由で誤字脱字等を放置するようになれば、それこそ本末転倒だ。

運用作業を自動化してしまえば、そうしたことに頭を悩ませる必要がなくなる。

方針

自動化の理想は、原稿を書いたり修正したりするだけで、はじめに挙げたすべてのフォーマットで小説が公開・更新される状態である。

この記事では、それに限りなく近いものとして「作品をGitリポジトリとしたとき、origin/master を更新するとすべてのリリース作業が自動的に行われる」という状態を目指す。 これはソフトウェアの世界でいう「継続的デプロイ」の一形態である。

前提知識

すでにGitという言葉が出てきたが、この記事では以下のツールやサービスについて説明しない。 それぞれを詳細に知っている必要はないが、どのようなものか分かっている前提で話を進める。

自動化に向けた原稿の書き方

リリースを自動化するには、原稿をデータとして扱いやすくする必要がある。 この節では、そのためのファイルフォーマットとディレクトリ構成について述べる。

拡張マークダウン

原稿は拡張マークダウンで書く。 Web小説は最終的にHTMLに変換される必要があり、マークダウンはHTMLと相性がよい。 何より、マークダウンの記法はシンプルで、文章を書くことに集中できる。

日本語の文章には青空文庫の注記法という選択肢もあるが、これは採用しなかった。 パーサーなど、この記法をサポートするツールがマークダウンに比べて少ないというのが主な理由だ。 組版に関する情報を原稿に含めたくないということもある。

拡張マークダウンと書いたが、ルビを表現する独自記法をもつ点が標準的なマークダウンと異なる。 これは、でんでんマークダウンのルビ記法と同じである。

{漢字|かん|じ} # => <ruby>漢<rt>かん</rt>字<rt>じ</rt></ruby>

小説投稿サイトで一般的な |《》 を用いたルビ記法(|漢《かん》|字《じ》)を採用しなかったのは、サイトによって仕様がまちまちで、標準的かつ優れたものがなさそうだったからだ。 また、記法の開始と終了が明確な方が機械的に扱いやすいということ、漢《かん》字《じ》 よりは {漢字|かん|じ} の方が読みやすく感じたということもある。

ディレクトリ構成

小説リポジトリには、原稿と一緒に設定資料やメタデータも含めるべきだろう。 また、Web小説は連載形式が一般的であり、ファイルも1話ずつに分割したい。

そこで、次のようなディレクトリ構成をとるようにした。

$ tree -N --dirsfirst

├── _data
│   ├── characters.yml
│   └── terms.yml
├── _drafts
│   ├── 鳴らない、電話.md
│   └── 雨、逃げ出した後.md
├── _posts
│   ├── 1995-10-04-使徒、襲来.md
│   └── 1995-10-11-見知らぬ、天井.md
└── _config.yml

_data には、設定資料を置く。 私の場合、登場人物の設定を書いた characters.yml と、作品内の用語をまとめた temrs.yml なんかを用意することが多い。 その他、作品に関するデータはこのディレクトリに入れる。

_drafts には、下書きを置く。 書きかけの原稿やあとで使えそうな表現など、リリースにはまだ含めないものが対象だ。

_posts には、決定稿を置く。 ファイルは1話ずつに分け、名前を YEAD-MONTH-DAY-title.md の形にする。 こうしておけば、ファイルが発表した順番通りに並ぶ。 タイトルもあるので、ファイル名を見るだけで内容を連想することができる。

_config.yml には作品全体のメタデータを書く。 作品のタイトルや著者名などがこれにあたる。 あとで詳しく説明するが、Webサイトや電子書籍のビルド設定なんかもここに書く。

# _config.yml
title: サンプル
author: フジ・ナカハラ
description: 「Web小説のリリース自動化」記事用サンプル

原稿の変換とリリース自動化

小説のデータ構造が決まったところで、各フォーマットへの変換とリリース自動化の方法を説明していく。

Webサイト

Webサイトへの変換にはJekyllを使う。 JekyllはRuby製の静的サイトジェネレータである。 ブログに特化しており、マークダウンの変換を標準で備えている。

先述のディレクトリ構成をしていれば、以下のコマンドを実行するだけで _posts の中身をHTMLに変換できる。

$ gem install jekyll
$ jekyll build

しかし、これだけではルビ記法に対応しておらず、また、Webサイトとしてもまだ不十分だ。

ルビ記法に対応するため、マークダウンコンバーターにFujiMarkdownを用いるようにする。 プロジェクトのルートに Gemfile を導入し、_config.yml でFujiMarkdownを使うよう設定する。

# Gemfile
gem 'jekyll', '~> 3.8'
gem 'jekyll-fuji_markdown', group: :jekyll_plugins
# _config.yml
markdown: FujiMarkdown

FujiMarkdownは、CommonMarkerによるマークダウンのパース前後にRubyによる処理を加える。 ルビ記法は、マークダウン中にHTMLが使えることを利用して、前処理でルビ記法をHTMLに置き換えることにより実現している。 つまり、シンタックスシュガー的な実装になっている。

次に、Webサイトとしての体裁を整え、小説として読みやすい見た目にするため、jekyll-theme-fujiをJekyllテーマとして用いる。 Jekyllテーマは、HTMLテンプレートやスタイルシートを含むもので、サイトテンプレートのような役割を果たす。

# Gemfile
gem 'jekyll-theme-fuji', git: 'https://github.com/fuji-nakahara/jekyll-theme-fuji.git'
# _config.yml
theme: jekyll-theme-fuji
defaults:
  - scope:
      path: ""
    values:
      layout: novel # デフォルトのレイアウトを `novel` にする

jekyll-theme-fujiには novel というレイアウトがあり、これを用いると小説用のスタイルが当てられる。 このスタイルで特徴的なのは、<em> 要素が斜体にならず圏点が振られるようになる点と、<hr> 要素が水平線ではなく空行を表現する点だろう。 他に、フォントを明朝体にしたり、行間を大きめにしたり、コンテンツが読みやすい横幅を超えないようにしたりしている。

$line-height: 1.8;

.novel {
  max-width: 690px; // 読みやすい横幅を超えないようにする
  margin: 0 auto;
  font-family: 'Noto Serif JP', serif; // 明朝体のWebフォントを使う
  line-height: $line-height;

  p {
    margin: 0;
  }

  hr { // 空行を表現する
    border: 0;
    margin: 0;
    height: 1.0rem * $line-height;
  }

  em { // 斜体にはせず圏点を振る
    font-style: normal;
    text-emphasis-style: filled dot;
    -webkit-text-emphasis-style: filled dot;
  }
}

また、トップページの役割を果たす home というレイアウトもあるので、次の内容を書いた index.md というファイルをプロジェクトのルートに置く。

---
layout: home
---

これで、以下のコマンドを実行すれば小説用にスタイリングされたWebサイトを作成できるようになった。

$ bundle exec jekyll build

小説投稿サイト

カクヨムと小説家になろうへの投稿には、jekyll-deploy-shosetsuというJekyllプラグインを使う。

# Gemfile
gem 'jekyll-deploy-shosetsu', group: :jekyll_plugins

また、このプラグインはChromeDriverを利用するので、あらかじめダウンロードしてインストールしておく。

このプラグインを導入すると、deply-kakuyomudeploy-narou という2つのサブコマンドが jekyll コマンドに追加される。 これらのコマンドを使うには、事前にカクヨムのwork_idと小説家になろうのNコードを取得する必要がある(この操作は自動化していない)。 カクヨムのwork_idとは、小説ページのURL kakuyomu.jp/works/XXXXXXXX の部分の数字である。 work_idとNコードは _config.yml に書いておく。

# _config.yml
kakuyomu:
  work_id: 1177354054885765919
narou:
  ncode: N7531FC

すると、以下のコマンドを叩くだけで、カクヨムと小説家になろうそれぞれへの投稿が完了する。 --future オプションをつければ予約投稿もできる。

$ bundle exec jekyll deploy-kakuyomu --email KAKUYOMU_EMAIL --password KAKUYOMU_PASSWORD
$ bundle exec jekyll deploy-narou --id NAROU_ID --password NAROU_PASSWORD

投稿に成功すると、各原稿ファイル先頭にURLが追記される。 先頭にURLが存在するファイルでは、deploy-kakuyomu, depoy-narou コマンドを実行したとき、新規投稿ではなく既存の投稿の更新が行われるようになる。

---
kakuyomu:
  url: https://kakuyomu.jp/works/1177354054885765919/episodes/1177354054887464835
narou:
  url: https://syosetu.com/usernoveldatamanage/top/ncode/1327399/noveldataid/11173996/
---

これらのコマンドの裏側では、マークダウンの変換とブラウザ操作が行われている。

FujiMarkdownは、HTMLへの変換だけでなくカクヨム記法や小説家になろうのルビ記法への変換も行うことができる。 jekyll-deploy-shosetsuでは、FujiMarkdownのこの機能を使って、原稿をそれぞれのサイトに適した形に変換している。

require 'fuji_markdown'

FujiMarkdown.render('*圏点*と{Ruby|ルビ}', :KAKUYOMU) # => "《《圏点》》と|Ruby《ルビ》"
FujiMarkdown.render('*圏点*と{Ruby|ルビ}', :NAROU) # => "|圏《・》|点《・》と|Ruby《ルビ》"

また、ブラウザ操作の自動化には、Seleniumを利用している。 Seleniumは、主にWebアプリケーションのE2Eテストの自動化に用いられるものだが、ブラウザ作業の自動化にも用いることができる。 ただ、jekyll-deploy-shosetuではSeleniumを直接使わず、それぞれのサイトの操作をラップしたKakuyomuAgentNarouAgentというライブラリを通して利用している。

カクヨムのログイン操作はselenium-webdriver gemを使うと次のように書ける。

require 'selenium-webdriver'

driver = Selenium::WebDriver.for(:chrome)

driver.get('https://kakuyomu.jp/login')
driver.find_element(name: 'email_address').send_keys('KAKUYOMU_EMAIL') 
driver.find_element(name: 'password').send_keys('KAKUYOMU_PASSWORD')
driver.find_element(xpath: '//button[text()="ログイン"]').click

KakuyomuAgentを使えば、Webページの構造を意識することなく同じ操作を行える。

require 'kakuyomu_agent'

agent = KakuyomuAgent.new

agent.login!(email: 'KAKUYOMU_EMAIL', password: 'KAKUYOMU_PASSWORD')

電子書籍

電子書籍の作成には、jekyll-build-ebookを用いる。

gem 'jekyll-build-ebook', group: :jekyll_plugins

このJekyllプラグインを入れると、build-ebook というサブコマンドが追加される。 EPUBには言語の情報が必須なので、_config.yml に以下の内容を加える必要がある。

# _config.yml
language: ja_JP

すると、次のコマンドを叩くだけで、_ebook ディレクトリにEPUBファイルが作成される。 Kindle用にMOBIファイルも作成したければ、--kindle オプションをつける。

$ bundle exec jekyll build-ebook --kindle

jekyll-build-ebookは、ebook というレイアウトがあれば、それを使って電子書籍用のHTMLをレンダリングする。

jekyll-theme-fujiにはebook レイアウトが存在し、縦書きのスタイルを適用するようになっている。 ただ、デフォルト設定のままでは左から右へページをめくるので、これを右から左に変更する必要がある。 これは page_progression_directionrtl (right-to-left)にしてやればよい。

# _config.yml
ebook:
  page_progression_direction: rtl

このコマンドの裏側では、gepubというEPUBジェネレータを使っている。 また、MOBIファイルの作成には、KindleGenをRubyから利用できるようにしたKindlegen gemを利用している。

継続的デプロイ

ここまでで、いくつかのコマンドを叩けば、原稿をWebサイトや電子書籍にしたり、各種小説投稿サイトに投稿したりできるようになった。 あとは、Webサイトと電子書籍の置き場所を用意し、origin/master の更新をフックにデプロイを行うだけである。 ここで、origin はGitHubにホストしたリポジトリとする。

Webサイトの置き場所にはGitHub Pagesを用いる。 リポジトリの設定から、gh-pages というブランチのファイルをホストするようにする。

電子書籍はGitHub Releasesを使って公開する。 GitHub ReleasesはGitのtagに紐づけてバイナリを提供できるGitHubの機能である。

これらの継続的デプロイの実現には、Travis CIを使う。 .travis.yml は次のようになる。 環境変数はよしなに設定する。

sudo: required
language: ruby

addons:
  chrome: stable

env:
  global:
    - JEKYLL_ENV=production

script:
  - bundle exec jekyll build
  - bundle exec jekyll build-ebook --kindle

before_deploy:
  # Chromedriverをインストールする
  - export CHROMEDRIVER_VERSION=`curl -s http://chromedriver.storage.googleapis.com/LATEST_RELEASE`
  - curl -L -O "http://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip"
  - unzip chromedriver_linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin

deploy:
  - provider: pages
    github-token: $GITHUB_TOKEN
    skip-cleanup: true
    local-dir: _site
    target_branch: gh-pages
    on:
      branch: master
  - provide: script
    script:
      - bundle exec jekyll deploy-kakuyomu --email $KAKUYOMU_EMAIL --password $KAKUYOMU_PASSWORD
      - bundle exec jekyll deploy-narou --id $NAROU_ID --password $NAROU_PASSWORD
    on:
      branch: master
  - provider: releases
    api_key: $GITHUB_TOKEN
    skip_cleanup: true
    file:
      - _ebook/サンプル.epub
      - _ebook/サンプル.mobi
    on:
      tags: true

ただし、この .travis.yml には1つの妥協と1つの問題がある。

まず、origin/master の更新をフックにするというのが当初の方針だったが、電子書籍のデプロイは on.tags: true になっており、master ブランチではなくtagにフックするようになっている。 before_deploy でtagをつくることもできるが、毎ビルドごとに電子書籍をリリースする必要はないと思ったのでやらなかった。

問題は、deploy-kakuyomu, deploy-narou コマンドが新規投稿後ファイルにURLを追記する点にある。 現状の .travis.yml では、このファイル変更は捨てられる。 すると、次のビルドでも、更新ではなく新規投稿が行われてしまう。 これを防止するには、after_deploy でファイル変更をコミットし、プルリクエストを出す必要がある。 ただ、プルリクエスト作成のスクリプトを書くのが面倒だったのでそこまではやっていない。 実際の運用では、deploy-kakuyomu, deploy-narou コマンドはTravis CIではなく、手元で実行するようにしている。

おわりに

以上が、私の行なっているWeb小説のリリース自動化である。

小説を書き始めた5月頃から、約半年かけてこの環境を整えた。 その過程でつくった7つのライブラリは、すべてOSSとしてGitHubに公開している。 とはいえ、今のところ自分が使うことしか考えていないので、他人が使えるかはわからない。

その7つのうち4つはJekyllプラグインであり、かなりJekyllに依存したやり方になっている。 が、別にJekyllがイケてると思ってそうしているわけではない。 Jekyllが採用しているテンプレート言語のLiquidは、ERBやHamlに比べるとかなり使いにくい。 Assetsの扱いも素朴で、近年のJavaScriptの変化を取り込めていない。 しかし、Jekyllは拡張が容易であり、また、Ruby言語は文字列の扱いに優れている。 枯れた技術だからこそ、安定して運用できるというメリットもある。

これらの自動化技術を使って、現在までに4つの作品を運用している。 作品はすべて https://fuji-nakahara.page/ からたどることができる。