小説執筆とIT化
いつもは『マンガで分かるプログラミング用語辞典』を描いている、クロノス・クラウンの柳井です。昨年、松本清張賞の最終候補に残った私の小説が『裏切りのプログラム ハッカー探偵 鹿敷堂桂馬』として出版され、その続編『顔貌売人 ハッカー探偵 鹿敷堂桂馬』が2017年8月7日に出版されました。前作は、プログラマの心理に肉薄した騙し合いになっていましたが、今作はそれだけでなく、情報技術を駆使した、犯人とのチェイスを描いています。
デビューしてからはまだ1年ですが、小説を書いている期間自体は10年以上になります。そのあいだ様々なソフトを試したり、自作したりしてきました。そして今年の2月ぐらいに、そうした自作ツールを私以外の人でも使えるように『小説推敲補助ソフト「Novel Supporter」』を開発してリリースしました。その後も改良を続けています。
本稿では、この小説推敲補助ソフトの開発について、技術的な背景や、その処理の内容について書いていきます。
小説推敲補助ソフトとは
そもそも小説推敲補助ソフトというのは、聞き慣れないソフトウェアのジャンルだと思います。このジャンルのソフトは数が少ないです。また、Web上で動くCGIとして公開されていることが多いです。このジャンルのソフトウェアは、小説や文章を推敲する際に、問題箇所かもしれない場所を可視化するものです。
小説を書いている本人は、自分の原稿を客観的に見ることが難しいです。しかし、他人から見ると、目立って見えてしまうところが非常に多いです。たとえば同じ単語が近い範囲で連続している。文末が連続している。段落先頭が連続している。その他にも、漢字の比率が高くなりすぎて読みにくくなっていたり、一文が長すぎて読解が難しくなっているケースもあります。
そうした部分は、書いた本人は、すでに脳内に文章が入っているため、スルーしてしまうことが多いです。そうしたところを強調することで、推敲の際に注意すべき場所を可視化する。このジャンルのソフトウェアは、そうしたものです。この可視化は、大きく分けて2つに分類されます。
- 間違いの指摘。
- 改善点の指摘。
あからさまな間違いの指摘には、たとえば句点や読点の連続(「。。」「、、」)や、カギ括弧の閉じ忘れ、行頭字下げの抜けなどがあります。また、半角スペースの混入や、行末が尻切れトンボになっている場所の指摘もこの範疇に入るでしょう。
改善点の指摘には、こそあど言葉が多すぎる、同じ単語が近くに連続してありすぎる、文末が連続してリズムが悪くなっているといった表現上の問題が該当します。
指摘は100%正しく行なう必要はありません。8割ぐらいの精度で指摘すれば、あとは人間がどうするか判断してくれます。文章というのは正解がなく、意図的にルールを踏み外すことも多いためです。ただ、あまりにも無関係な指摘が多いと、人間の負荷が高くなりますので、程度問題です。
そこで小説推敲補助ソフトでは、機械的に指摘部分を可視化して、あとは人間に推敲を任せることにします。また、推敲以外のところでストレスをかけないように、操作性を高くする必要があります。
そうした観点で『小説推敲補助ソフト「Novel Supporter」』を作成して、新しい小説を書く度に改良して、現在もアップデートを続けています。こうしたソフトを開発することで、小説執筆のノウハウを言語化して、自分自身に把握させるというメリットもあります。
技術的背景
『小説推敲補助ソフト「Novel Supporter」』は、以下の実行環境やライブラリを使っています。そのリストを掲載したあとに、それらの紹介を簡単にしていきます。
- 実行環境
- UI用ライブラリ
- 日本語文字コード変換
- 形態素解析
実行環境
『小説推敲補助ソフト「Novel Supporter」』では、プログラミング言語はJavaScript、実行環境は「Electron」を採用しています。
なぜ、JavaScriptを利用しているかというと、元々ローカルで作っていたツール群が、WSH(Windows Script Host )で作成されていたからです。WSHは、Windows環境でスクリプト言語を利用できる技術です。WSHでは、VBScriptやJScript(JavaScriptとほぼ同じ)を使えます。
「Electron」は、JavaScriptのサーバーサイド実行環境「node.js」に、Webブラウザのレンダリングエンジン「WebKit」をドッキングしたものです。HTML、CSS、JavaScriptといったWebの技術で、ローカルアプリケーションを作れます。また、Windows、Linux、Mac用にリリースでき、マルチプラットフォームのアプリケーションを作成可能なメリットもあります。
「Electron」と似た実行環境に「NW.js」(旧node-webkit)もあります。私は普段、両方を開発に利用しているのですが、『小説推敲補助ソフト「Novel Supporter」』の開発では「Electron」を採用しました。
この「Electron」と「NW.js」は、大きく方向性が違います。
「Electron」は、「node.js」のプログラム(メインプロセス)から、UI環境としての「WebKit」を呼び出して、そこにユーザーとの対話処理(レンダラプロセス)を、HTMLやCSS、JavaScriptで記述します。そして、2つのプロセス間で通信を行いながらプログラムを書きます。煩雑と言えば煩雑なのですが、アプリケーション自体とUI部分を分けるという意味で、堅牢なプログラムを作ることができます。
「Electron」製のアプリケーションとして代表的なものは、「Atom」です。「Electron」は、「Atom」を作るためにGitHubによって開発されました。その他、「Slack」「Visual Studio Code」などがあります。
「NW.js」は、「WebKit」によるWebブラウザがメインです。そして、HTML内から「node.js」の機能を使い、ローカルファイルにアクセスしたり、OSの機能を利用したりします。そのため、Webサイト用のWebアプリケーションを作り、そこにファイル保存機能を加えたいといった用途の場合に、サクッとプログラムを完成させられます。簡単なツールを、書き殴りで作成したい場合にも向いています。
歴史としては「NW.js」の方が長いのですが、大がかりなプログラムを作る際には「Electron」の方が向いています。そのため最近では「NW.js」よりも「Electron」製のアプリケーションの方が多いです。これは、どちらがよいというものではなく、それぞれ向き不向きがあると考えた方がよいです。
UI用ライブラリ1 - jQuery
UI用ライブラリとしては、「jQuery」とそのプラグインを多く使っています。これは、SNSのような動的なコンテンツではなく、すでに書かれた小説という静的なコンテンツを扱うからです。
データが、サーバーから次々に流れてくることはない。そして、ボタンのクリックをトリガーにして、小説の強調表示を出力する。そうしたアプリケーションの特性から、「jQuery」とそのプラグインが向いていると判断しました。
UI用ライブラリ2 - Snap.svg
グラフ表示など一部の表現では、SVGを利用した方が利便性も高く、見た目もよいです。そのため「Snap.svg」でSVGを生成して出力しています。「Snap.svg」は、「jQuery」風に手軽にSVGを生成、操作できるライブラリです。動的処理にも対応しており、クリックによる操作や、アニメーションも行えます。
たとえば、以下のような簡単な命令で円を描いて、bodyにSVGを追加できます。「Snap.svg」のトップページで、コンソールを開いて、下記のプログラムを実行すると、Webページに円が追加されます。
var s = Snap(400, 400); s.circle(200, 200, 150); s.prependTo($('body'));
「Snap.svg」は、「Raphael」の後継ライブラリです。少し前にSVGを扱っていた人は、「Raphael」の名前をよく見ていたかもしれません。この2つのライブラリは作者が同じです。「Snap.svg」では、より簡便に、そして古い環境を切り捨てて開発されています。
日本語文字コード変換
私は、Windowsで小説を書いています。Windowsの標準的な文字コードは「Shift-JIS」です。しかし、Web技術の標準的な文字コードは「UTF-8」です。そのため、普通に読み込むと文字化けします。
そこで必要になるのが、文字コードの変換ライブラリです。『小説推敲補助ソフト「Novel Supporter」』では、「encoding.js」を利用しています。いくつか、この種のライブラリがあるのですが、私が使った範囲では「encoding.js」が使いやすかったので、こちらを利用しています。「encoding.js」は、node.jsからだけでなく、Webページからも利用できるので汎用性が高いです。
テキストファイルの読み込みでは、たとえば以下のようなプログラムを書いています。
// モジュールの読み込み var path = require('path'); var fs = require('fs'); var Encoding = require('encoding-japanese'); // (略) // テキスト読み込み 同期 mngTxt.read = function(p) { // パスの変換 p = this.pRslv(p); // 読み込み var bin = ""; try { bin = fs.readFileSync(p); } catch(e) { console.log('[Err] : ' + e); } // エンコードの判定と変換 var encoding = Encoding.detect(bin); var unicodeString = Encoding.convert(bin, { to: 'unicode', from: encoding, type: 'string' }); // 値を戻す return { txt: unicodeString, encoding: encoding, path: p }; };
形態素解析
JavaScriptでは「kurmoji」を使って、非常に手軽に形態素解析を行なえます。ただし「kurmoji」は、辞書ファイルだけで17MB近くあるので、Webページで読み込んで使うのは困難です。使う場合は、ローカル環境かサーバー環境での実行になるでしょう。
使い方は非常に簡単で、下のようなコードを書くことで、形態素解析をした結果(オブジェクトの配列)を得られます。300~400KB程度の小説なら、瞬時に結果を出せます。
この「kurmoji」を使うことで、文章を品詞に分解して見ていくことができます。ただ実際には、「文末の音の確認」「段落先頭の1文字の確認」というような、品詞に分解するまでもない確認事項も多く、形態素解析を全ての場所で使っているわけではありません。
(function() { var kuromoji = require('kuromoji'); var kuromojiTokenizer = null; kuromoji .builder({dicPath: 'node_modules/kuromoji/dict'}) .build(function (err, tokenizer) { kuromojiTokenizer = tokenizer; }); var tokenize = function(text) { var tokens = kuromojiTokenizer.tokenize(text); console.log(JSON.stringify(tokens, null, ' ')); }; window.tokenize = tokenize; })();
tokenize('安藤は、素っ頓狂な声を上げた。');
[ { "word_id": 326230, "word_type": "KNOWN", "word_position": 1, "surface_form": "安藤", "pos": "名詞", "pos_detail_1": "固有名詞", "pos_detail_2": "組織", "pos_detail_3": "*", "conjugated_type": "*", "conjugated_form": "*", "basic_form": "安藤", "reading": "アンドウ", "pronunciation": "アンドー" }, { "word_id": 93010, "word_type": "KNOWN", "word_position": 3, "surface_form": "は", "pos": "助詞", "pos_detail_1": "係助詞", "pos_detail_2": "*", "pos_detail_3": "*", "conjugated_type": "*", "conjugated_form": "*", "basic_form": "は", "reading": "ハ", "pronunciation": "ワ" }, { "word_id": 51340, "word_type": "KNOWN", "word_position": 4, "surface_form": "、", "pos": "名詞", "pos_detail_1": "数", "pos_detail_2": "*", "pos_detail_3": "*", "conjugated_type": "*", "conjugated_form": "*", "basic_form": "、", "reading": "、", "pronunciation": "、" }, { "word_id": 181510, "word_type": "KNOWN", "word_position": 5, "surface_form": "素っ頓狂", "pos": "名詞", "pos_detail_1": "形容動詞語幹", "pos_detail_2": "*", "pos_detail_3": "*", "conjugated_type": "*", "conjugated_form": "*", "basic_form": "素っ頓狂", "reading": "スットンキョウ", "pronunciation": "スットンキョー" }, { "word_id": 23730, "word_type": "KNOWN", "word_position": 9, "surface_form": "な", "pos": "助動詞", "pos_detail_1": "*", "pos_detail_2": "*", "pos_detail_3": "*", "conjugated_type": "特殊・ダ", "conjugated_form": "体言接続", "basic_form": "だ", "reading": "ナ", "pronunciation": "ナ" }, { "word_id": 2199180, "word_type": "KNOWN", "word_position": 10, "surface_form": "声", "pos": "名詞", "pos_detail_1": "一般", "pos_detail_2": "*", "pos_detail_3": "*", "conjugated_type": "*", "conjugated_form": "*", "basic_form": "声", "reading": "コエ", "pronunciation": "コエ" }, { "word_id": 92880, "word_type": "KNOWN", "word_position": 11, "surface_form": "を", "pos": "助詞", "pos_detail_1": "格助詞", "pos_detail_2": "一般", "pos_detail_3": "*", "conjugated_type": "*", "conjugated_form": "*", "basic_form": "を", "reading": "ヲ", "pronunciation": "ヲ" }, { "word_id": 3893790, "word_type": "KNOWN", "word_position": 12, "surface_form": "上げ", "pos": "動詞", "pos_detail_1": "自立", "pos_detail_2": "*", "pos_detail_3": "*", "conjugated_type": "一段", "conjugated_form": "連用形", "basic_form": "上げる", "reading": "アゲ", "pronunciation": "アゲ" }, { "word_id": 23430, "word_type": "KNOWN", "word_position": 14, "surface_form": "た", "pos": "助動詞", "pos_detail_1": "*", "pos_detail_2": "*", "pos_detail_3": "*", "conjugated_type": "特殊・タ", "conjugated_form": "基本形", "basic_form": "た", "reading": "タ", "pronunciation": "タ" }, { "word_id": 90940, "word_type": "KNOWN", "word_position": 15, "surface_form": "。", "pos": "記号", "pos_detail_1": "句点", "pos_detail_2": "*", "pos_detail_3": "*", "conjugated_type": "*", "conjugated_form": "*", "basic_form": "。", "reading": "。", "pronunciation": "。" } ]