tech.guitarrapc.cóm

Technical updates

PowerShell 6.0 のImport-Csv に W3C 拡張ログ ファイル形式のサポートを追加 #2482 について調べてみた

これを調べていたのは本を書いていたときなので、そろそろ一年経つのですがお蔵入りの前に出しておきます。 PowerShell 6.0 において、W3C 拡張ログが Import-Csv で読み込み可能になったという内容でリリースノートが出ているのですがその内容について。

docs.microsoft.com

目次

TL;DR

Import-Csv がW3Cログに対応したといっても、それはIISログではなくMicrosoft Exchange のログ (W3Cログ形式)となる。 なんという期待を裏切る罠。

まとめおいておきます。

Import-Csv in PowerShell 6.0 supports w3c log format, if delimiter is `,`. https://github.com/PowerShell/PowerShell/pull/2482 · GitHub

W3C 拡張ログとは

W3C に定義があります。

www.w3.org

Introduction で用途がだいたいわかるので引用します。

Most Web servers offer the option to store logfiles in either the common log format or a proprietary format. The common log file format is supported by the majority of analysis tools but the information about each server transaction is fixed. In many cases it is desirable to record more information. Sites sensitive to personal data issues may wish to omit the recording of certain data. In addition ambiguities arise in analyzing the common log file format since field separator characters may in some cases occur within fields. The extended log file format is designed to meet the following needs:

* Permit control over the data recorded.
* Support needs of proxies, clients and servers in a common format
* Provide robust handling of character escaping issues
* Allow exchange of demographic data.
* Allow summary data to be expressed.

重要な箇所はFormat にあります、引用します。

Format
An extended log file contains a sequence of lines containing ASCII characters terminated by either the sequence LF or CRLF. Log file generators should follow the line termination convention for the platform on which they are executed. Analyzers should accept either form. Each line may contain either a directive or an entry.

Entries consist of a sequence of fields relating to a single HTTP transaction. Fields are separated by whitespace, the use of tab characters for this purpose is encouraged. If a field is unused in a particular entry dash "-" marks the omitted field. Directives record information about the logging process itself.

Lines beginning with the # character contain directives. The following directives are defined:

Version: <integer>.<integer>
The version of the extended log file format used. This draft defines version 1.0.
Fields: [<specifier>...]
Specifies the fields recorded in the log.
Software: string
Identifies the software which generated the log.
Start-Date: <date> <time>
The date and time at which the log was started.
End-Date:<date> <time>
The date and time at which the log was finished.
Date:<date> <time>
The date and time at which the entry was added.
Remark: <text>
Comment information. Data recorded in this field should be ignored by analysis tools.
The directives Version and Fields are required and should precede all entries in the log. The Fields directive specifies the data recorded in the fields of each entry.

Example
The following is an example file in the extended log format:

#Version: 1.0
#Date: 12-Jan-1996 00:00:00
#Fields: time cs-method cs-uri
00:34:23 GET /foo/bar.html
12:21:16 GET /foo/bar.html
12:45:52 GET /foo/bar.html
12:57:34 GET /foo/bar.html

ここで最も重要なのは、Fields are separated by whitespace という箇所で、いわゆるデリミター(Delimiter/区切り文字) が 空白スペースであるという記述です。Exampleもそのようになっています。

Import-Csv でIIS ログを読んでみる

リリースノートを見ただけだと、W3Cログ = IIS ログが読み込めるのかと思った方もいらっしゃるかもしれません。 IISログを試していましょう。

#Software: Microsoft Internet Information Services 7.5
#Version: 1.0
#Date: 2013-06-24 10:56:45
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken
2013-06-24 10:56:45 192.168.0.1 POST /xas/ - 80 - 222.222.222.222 Mozilla/5.0+(Windows+NT+6.1;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/27.0.1453.110+Safari/537.36 404 0 2 471

しかし次のようにデリミターは空白文字であり、Import-Csvでは区切りを認識できません。

PS> import-csv .\iis_log.log
 
 <#
 date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken
------------------------------------------------------------------------------------------------------------------------------------------
2013-06-24 10:56:45 192.168.0.1 POST /xas/ - 80 - 222.222.222.222 Mozilla/5.0+(Windows+NT+6.1;+WOW64)+AppleWebKit/537.36+(KHTML
 #>

-Delimiter に " " を指定すると読み込みエラーが起こることから、ログのデリミターを変えるしかなさそうです。 IISログに関して、デリミターを変更できるか IIS_SCHEMA.xml で調整を試みるとできないことが分かります。

<!-- just for logFile section -->
<!-- there's no attribute, value to change delimiter -->
      <element name="logFile">
        <attribute name="logExtFileFlags" type="flags" defaultValue="Date, Time, ClientIP, UserName, ServerIP, Method, UriStem, UriQuery, TimeTaken, HttpStatus, Win32Status, ServerPort, UserAgent, HttpSubStatus, Referer">
          <flag name="Date" value="1"/>
          <flag name="Time" value="2"/>
          <flag name="ClientIP" value="4"/>
          <flag name="UserName" value="8"/>
          <flag name="SiteName" value="16"/>
          <flag name="ComputerName" value="32"/>
          <flag name="ServerIP" value="64"/>
          <flag name="Method" value="128"/>
          <flag name="UriStem" value="256"/>
          <flag name="UriQuery" value="512"/>
          <flag name="HttpStatus" value="1024"/>
          <flag name="Win32Status" value="2048"/>
          <flag name="BytesSent" value="4096"/>
          <flag name="BytesRecv" value="8192"/>
          <flag name="TimeTaken" value="16384"/>
          <flag name="ServerPort" value="32768"/>
          <flag name="UserAgent" value="65536"/>
          <flag name="Cookie" value="131072"/>
          <flag name="Referer" value="262144"/>
          <flag name="ProtocolVersion" value="524288"/>
          <flag name="Host" value="1048576"/>
          <flag name="HttpSubStatus" value="2097152"/>
        </attribute>
        <attribute name="customLogPluginClsid" type="string" defaultValue=""/>
        <attribute name="logFormat" type="enum" defaultValue="W3C">
          <enum name="IIS" value="0"/>
          <enum name="NCSA" value="1"/>
          <enum name="W3C" value="2"/>
          <enum name="Custom" value="3"/>
        </attribute>
        <attribute name="logTargetW3C" type="flags" defaultValue="File">
          <flag name="File" value="1"/>
          <flag name="ETW" value="2"/>
        </attribute>
        <attribute name="directory" type="string" expanded="true" defaultValue="%SystemDrive%\inetpub\logs\LogFiles" validationType="nonEmptyString" />
        <attribute name="period" type="enum" defaultValue="Daily">
          <enum name="MaxSize" value="0"/>
          <enum name="Daily" value="1"/>
          <enum name="Weekly" value="2"/>
          <enum name="Monthly" value="3"/>
          <enum name="Hourly" value="4"/>
        </attribute>
        <attribute name="truncateSize" type="int64" defaultValue="20971520" validationType="integerRange" validationParameter="1048576,4294967295" />
        <attribute name="localTimeRollover" type="bool" defaultValue="false"/>
        <attribute name="enabled" type="bool" defaultValue="true" />
        <attribute name="logSiteId" type="bool" defaultValue="true" />
        <attribute name="flushByEntryCountW3CLog" type="uint" defaultValue="0" />
        <attribute name="maxLogLineLength" type="uint" validationType="integerRange" validationParameter="2,65536" defaultValue="65536" />
        <element name="customFields">
          <attribute name="maxCustomFieldLength" type="uint" validationType="integerRange" validationParameter="2,65536" defaultValue="4096" />
          <collection addElement="add" clearElement="clear">
            <attribute name="logFieldName" type="string" required="true" isUniqueKey="true" validationType="nonEmptyString" />
            <attribute name="sourceName" type="string" required="true" validationType="nonEmptyString" />
            <attribute name="sourceType" type="enum" required="true" >
              <enum name="RequestHeader" value="0"/>
              <enum name="ResponseHeader" value="1"/>
              <enum name="ServerVariable" value="2"/>
            </attribute>
          </collection>
        </element>
      </element>
      <element name="traceFailedRequestsLogging">
        <attribute name="enabled" type="bool" defaultValue="false" />
        <attribute name="directory" type="string" expanded="true" defaultValue="%SystemDrive%\inetpub\logs\FailedReqLogFiles"/>
        <attribute name="maxLogFiles" type="uint" defaultValue="50" validationType="integerRange" validationParameter="1,10000"/>
        <attribute name="maxLogFileSizeKB" type="uint" defaultValue="1024" validationType="integerRange" validationParameter="0,1048576"/>
        <attribute name="customActionsEnabled" type="bool" defaultValue="false"/>
      </element>

web.config でも変更できないのは、IIS10 でも変わっていません。

stackoverflow.com

https://docs.mendix.com/refguide5/review-log-files-ms-iis-serverdocs.mendix.com

Advanced Logging ならわんちゃん....?

forums.iis.net

Import-Csv でExchange ログを読んでみる

Import-Csv が対象にしているのは、Delimiter が カンマ , つまり、csv のフォーマットです。 W3C でCSV フォーマットのログ、パッとでませんでしたがあります、Microsoft Exchange です。

docs.microsoft.com

これは実際に、PowerShell チームが Import-Csv で W3C を読むときのテストデータからもわかります。

https://github.com/PowerShell/PowerShell/blob/master/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/TestImportCsv_W3C_ELF.csv

#Software: Microsoft Exchange Server
#Version: 15.0.0.0
#Log-type: Transport Connectivity Log
#Date: 2016-09-16T23:30:07.338Z
#Fields: Column1,Column2,Column 3
data1,1,A
data2,2,B
data3,3,C
data4,4,D

これを読んでみると、ヘッダ部分の # が無視されてデータが読み込まれたのが分かります。

PS> import-csv .\iis_log.csv
 
 # https://github.com/iSazonov/PowerShell/blob/0818b6c921c1970dc294669134266f878352891a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/TestImportCsv_W3C_ELF.csv
 <#
 Column1 Column2 Column 3
------- ------- --------
data1   1       A
data2   2       B
data3   3       C
data4   4       D
 #>

まとめ

Import-Csv で IISログの W3C ログは読めない、Exchange は読める。なるほど、Import-Csv ですからね。

Windows でエクスプローラーを使って .gitconfig ファイルを作る

Windows のエクスプローラーで .gitignore というファイル名を作ろうとするとエラーが出ます。

.gitconfig という名前のファイルを作ろうとするとエラー

これをコマンドラインを使わず作る簡単な方法を紹介します。

目次

. から始まる名前のファイルを作る時は末尾にも . を置く

エクスプローラーを拡張子が表示できるようにしている前提です。

File name extensions (ファイル拡張子) を有効にしておく

エクスプローラーで. から始まるファイルを作るときは、.作りたいファイル名. と末尾にも. を置いてみてください。

例えば、.gitconfig というファイルを作りたいなら .gitconfig. です。

.gitconfig を作るために .gitconfig. と入力する

すると、ファイル拡張子を変えてもいいか聞かれるのでYes (はい) を選択します。

ファイル拡張子の変更ダイアログが表示される

.gitconfig ファイルができました。

.gitconfig というファイルができる

ファイル種別をみると、GITCONFIG file となっており、察できます。

ファイル種別が .以降の名称

簡単と感じてもらえるといいですね。

コマンドラインで作る

bash でも、コマンドプロンプトでも、PowerShell でも C#でも Goでもなんでもok です。

touch をいれているなら touch で。 コマンドプロンプトなら、type nulcopy nul を使うと空のファイルが作れます。 PowerShell なら、Out-FileNew-Item でもいいでしょう。

gist.github.com

なんでもいいのでどれでもどうぞ。

まとめ

数年前に教えてくださったのが @mayuki 、ありがとうございます。

はてなブログを https 対応するためにmixed content を検知する MixedContentCheckerを作った

前回の記事でhttps 化の前段階として、はてなブログの全URLを取得しました。

tech.guitarrapc.com

https化を有効にすると、mixed content が出るようになるので有効にします。

あとは、https 化したページに httpコンテンツが混じっている時に起こる、mixed content 警告一覧を取得して直してみましょう。

なお、直すのは手作業です。

目次

TL;DR;

dockerでchrome ヘッドレスをSeleniumで動かして、logからmixed content があるか検知してログ出力する。

検知したページは、3パターンの修正のいずれかをちまちまかけていく。

環境

今回は、PowerShell と C# で実装しました。golang は実装中。

いずれの環境も、docker で ubuntu 18.04 環境で実行します。

  • PowerShell : mcr.microsoft.com/powershell:ubuntu-xenial
  • C# : microsoft/dotnet:2.2-runtime-bionic
  • golang : 未実装

処理の流れ

どの言語も変わらず、chrome のログから mixed content を取得します。

  • URLにchrome headlessで自動アクセスする
  • chrome driver を初期化
  • 指定したURLにアクセス
  • ログから mixed content をフィルター
  • 該当ログがあれば markdown テーブルフォーマットで出力

実装

リポジトリおいておきます。

github.com

実行方法は、READMEをみてください。

MixedContentChecker/README.md at master · guitarrapc/MixedContentChecker · GitHub

ローカル実行もできますが、Dockerで動かすことで、手元にSeleniumやChromeヘッドレスドライバーを用意したり、環境初期化で困ったりすることを割けます。

こういうのをローカルで動かす意味はあんまりないので、Dockerで動かすのがいいでしょう。

PowerShell

Dockerfileです。特に何も気にせず、粛々とchrome headless + chrome driver + selenium をいれます。

gist.github.com

前回のサイトマップから全URLを取得するスクリプトを呼び出しつつ、Chrome Driver + Selenium処理を書きます。

gist.github.com

ポイントは、chrome driverに渡した引数 "--no-sandbox" です。

C# では起こらないのですが、PowerShellからChrome ヘッドレスを実行するときは、--no-sandbox がないと実行できないようです。(はまった)

CSharp

Dockerfileです。先ほどと違い、C# のビルド時にselenium + chrome driverは入るので、chrome driverだけ入れます。

gist.github.com

先ほどのPowerShellと異なり、Parallel.Foreachdによる並列処理を用意しました。

gist.github.com

記事が480以上あるため、1つ1つにアクセスしていると終わりません。単純に1ページ5秒としても、2400sec (40min) かかります。実際は、OneDrive の写真埋め込みページなどで60secかかったりしていたのでもっとです。

おおよそCPUコア数で並列がいいのですが、Docker内部への割り当てしだいです。今回は、15並列で不安定になり10並列で安定したのでデフォルト値にしています。Docker実行時にパラーメータを変えたいので、環境変数から値を渡せるようにしています。*1

10並列で、10分ぐらいでおわるようになったのでだいたいこれぐらい感があります。

あとは、先ほど同様にSeleniumでchrome headlessを動かすだけです。

処理の全体はリポジトリをみてください。

MixedContentChecker/Program.cs at master · guitarrapc/MixedContentChecker · GitHub

Golang

[TBD]

修正

3つのパターンで修正します。

  • httpをhttps にする
  • 存在しないURLを消す
  • はてなフォトがembedded記法なら記事の保存しなおし
  • Google Web Master をhttpsで取り直し

http -httpsへの置き換え

心を無にしてやりました。 はてなブログにAPIで取得、保存しなおしはちょっと面倒感があります。

記事を全コピー、vscodeで置換、貼り付け直して保存です。サシミタンポポ

また、デザインページもhttp -> https が必要です。私の場合は、Google フォントと外部cssなどでしたが、ついでにptengine やzenback 当りが邪魔をしていたので外しました。zenbackははてなブログで関連記事機能があるので不要になってました。

github.com

存在しないURLを消す

特にOneDrive のimage埋め込みと2013ぐらいの古い記事でした。

OneDriveは、生imageがOneDriveにあったものははてなフォトにいったんおいています。OneDriveのimage埋め込みは、以前コメントでも指摘受けていたので、もう二度とつかわないでしょう。

存在しないURLは404で取得できるので消しました。

はてなフォト

はてなフォトは、生urlでなくはてな記法によるid埋め込みの場合、記事を保存しなおすことでhttps化されます。 粛々と記事を保存しなおします。サシミタンポポ

help.hatenablog.com

結果

1122 + 296 + 70 + 22 + 187 なので、1697件だったようです。

sample/logs においておきました。

MixedContentChecker/samples/logs at master · guitarrapc/MixedContentChecker · GitHub

手作業なら死んでました。修正は手作業なのでもうやりたくないです。

あと、セキュリティ警告でページ表示できない状況も直ったようです、すいませんでした。

まとめ

golang 書いてから上げようと思いましたが、やってすでに1週間立つのでとりあえず記事にしていくスタイルで。

*1:Docker実行だと、引数より環境変数の方が素直で扱いやすくて好みです。

はてなブログの全エントリーURLを取得する

このブログ、実はhttpのままです。 はてなブログをやめるか考えているのですが、いったんhttps対応を進めましょう。

困るのがmixed content なのですが、とっかかりとしてこのブログの全URLを取得します。

目次

sitemap の取得

全記事のURLを取得するときに考えるのが、Google Web Master でどうやってクローラーにヒントを出しているかです。ご存知の通り、こういう時に使うのがサイトマップです。

ということで、全記事のURL取得は安直にサイトマップから辿ればいいでしょう。

はてなブログのsitemap は、ブログURL + /sitemap.xml でpagenation付sitemapindexを取得できます。 このブログならこのような感じです。

<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=1</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=2</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=3</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=4</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=5</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
</sitemapindex>

あとは、各sitemapごとにアクセスして、記事URLを拾うだけです。

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://tech.guitarrapc.com/entry/2015/11/19/030028</loc>
    <lastmod>2015-11-19</lastmod>
  </url>
  <url>
    <loc>http://tech.guitarrapc.com/entry/2015/11/11/032544</loc>
    <lastmod>2016-09-24</lastmod>
  </url>
   <!-- ..... continue -->
</urlset>

結果がurl一覧で取れればいい感じに使えそうです。

http://tech.guitarrapc.com/ 
http://tech.guitarrapc.com/about 
http://tech.guitarrapc.com/entry/2019/01/05/060326 
http://tech.guitarrapc.com/entry/2019/01/05/044741 
http://tech.guitarrapc.com/entry/2018/12/22/235927 
http://tech.guitarrapc.com/entry/2018/09/29/165215 
http://tech.guitarrapc.com/entry/2018/09/29/154004 
http://tech.guitarrapc.com/entry/2018/09/29/151114 
..... continue

sitemap は所定のフォーマットに沿っているxmlなので適当に処理します。

今回はPowerShell、C#、Golang それぞれで書いてリポジトリにおいておきました。順にみていきます。

github.com

PowerShell

PowerShell 6.0以降 で動作します。(5.0で動かす場合は、Invoke-WebRequest-UseBasicParsing スイッチを追加するといいでしょう)

gist.github.com

特別に難しいことはないのですが、PowerShellの場合はXML型が担保できれば要素名をキーとして辿ることができます。 nullの取り扱いがゆるいこともあり、シンプルに書けます。

    [xml]$index = $res.Content
    $sitemaps = $index.sitemapindex.sitemap.loc

CSharp

C# 7.3 w/.NET Core 2.2 で動作します。*1

gist.github.com

C# でXMLだと、namespaceを毎度指定することになるのですこし面倒な感じがあります。

XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";
var sitemaps = XElement.Parse(res).Descendants(ns + "loc").Select(x => x.Value).ToArray();

Golang

go-sitemapを使うとシンプルになります。

github.com

gist.github.com

練習なので、既存のパッケージを使わずstructを定義して、xmlのUnmarshal で割り当ててみます。

gist.github.com

安直に書いてみたのですが、こんな感じなのでしょうか? slice で要素数が事前にわからないので拡張に任せるのがいやなのと、要素確保をした場合に最初のappendで無駄になるのはどうするといいのかなぁ。

まとめ

とりあえず全ブログ記事URLが取れたので、次はmixed content の警告です。

*1:C# 8.0 / .NET Core 3.0 でも動作します。

PowerShell 本を出版するまでの反省

PowerShell本を書いたのですが、当然多くの反省があります。

tech.guitarrapc.com

どれも自分の苦手とすることへの直視を求められるのでメモしておきます。

プログラミング系の本を書くときの参考になれば幸いです。

目次

そもそもなぜ本を書いたの

本を書く動機はいろいろなケースがあると思います。

私の場合、本を書きたいというより「本を通してPowerShell に感じる悩ましいと思わせる部分への一定の解消を図りたい」という思いで書きました。 結果は読者のみぞ知るので分かりません。

もともと私が持っていたPowerShellの課題に「学習コストが高すぎる」というのがあります。ただコマンドを実行するだけならいいのですが、適当に書いたスクリプトがよくわからないけど動かない、意図した結果にならないというのは、今でもそこかしこで見ます、聞きます。「PowerShellは学の大変だから触るのすら忌避する」という声をよく耳にするたびに、ナルホドタシカニと感じてきました。

自身、今まであったPowerShellの本で自分の知りたいことが知れず、手さぐりで学んできた部分が大きいです。PowerShell InActionは良い本ですが、この本でPowerShellに感じる苦しみは解消されませんでした。配列やスコープは理解できず、モジュールも何それです。英語出版本も10冊近く持っていますが、どれもスコープは小さく、知っている前提、レール上で使う前提でした。そのため、自分がやりたいことをやるためにPowerShell Utils などの小ネタ置き場を作ったり、モジュールを公開し、シェル芸を書き、一つひとつ学んだことを記事にしたり、MSDNやDocs が更新されると読んで新しいことを学び、誤りに気付き、理解のすり合わせを行う繰り返しをしています。

この状況は、ブログを書いていても「調べることができる人」という条件がつくためリーチ層が限定されます。そこで、編集者さんからお話をいただいた時に、リーチできない層に対して、次のニーズを満たすものを伝えるものがないなら書こう、それを書けせてもらえるなら書くというのがこの本の根幹です。

  • 学ぶコストを減らすため一冊で全然書いたことがない人、書いたことあるけどただコピーしただけ、ちょっと書いてる、普段から書いてる、めっちゃ書いているをある程度網羅する *1
  • とりあえずこれ読めば罠とか理解して回避方法もわかる
  • どう書けばいいかのサンプルが分からない状況にサンプルを提供する
  • 後でわからない時にでも読みなおせる
  • 途中まで読んで、分かるようになったときに先も行ける、

執筆期間

本どれぐらいで書いたのか度々聞かれます。だいたい2カ月です。

内訳は、1-2月の深夜、3月半月 + 4月半月 + 5月半月。 3月以降は仕事がないので一日12-18時間ぐらい当ててたようです。*2

仕事をしながら書ける人、心底尊敬します。気持ちの切り替えがポイントなのですが、難しいと感じました。

反省点

いずれも自分への甘えと見込みの甘さが露呈することになります。

書き始めるまでの重さ

最も反省するべきは、エンジンがかかるまでが遅いという悪い癖が露骨に出て足を引っ張りました。

本は書きたい、締め切りもあり書かなきゃなのですが、どうしてもVS Codeを開くまでの気持ちを持っていくのがムズカシイ時期がありました。 仕事をしている時の深夜、締め切りを意識していない時、意識し始めた時それぞれでまんべんなくあるので、私の欠点です。

対策

  • 時間を消費していたことをしようとしたときに閉じる (主に読書とYoutube が大きかったように思います)
  • 音楽を決める

もともとYoutubeでバックグラウンドミュージックを書けて、プログラムや記事を書く習慣があります。が、この時期に動画の音楽を流すことをしてたせいで動画を見る、映像に注目する悪い癖がつきました。そのため、Youtubeで一日の時間が溶けることもあり、開くのをやめました。小説は気分転換がずるずると、パターンだったのでそもそも開かないことにしました。

音楽は意識を集中させるために使っているので、実はなくても書けるのが分かっています。事実、音楽が途中で切れてても一日中書いていることも多いです。しかし音楽を使って意識を書く方に集中させる、書くことの環境であると自己暗示書けることに使っているためある方がスムーズに書き出します。そこで、Playlistを決めて、エンドレスに流すことで、音楽と執筆を結びつけました。主に "Put Your Hearts Up"が多かったです。

一日の書ける量

色々な方が書かれていますが「ブログと全然違って全然書けない」です。「適当を書けない」という当然の事実がびっくりするぐらい書けなくしました。まさか一日10P程度も書けないことがあるとは思いませんでした。*3結果、締め切りを延長したにも関わらず直前まで修正していたので、本当に余裕ありませんでした。

文章構造も書きにくさの原因でした。私が読書をしていて一番嫌いなのが、事前に知らないことを先に出して後から説明されることです。*4知らない概念を先に出した場合、思考のジャンプが生じるためかなり嫌っています。今回、自分で書いていて発生したため、文章構造を5回書き換えています。始めに目次とプロットを書いていても発生したので、レビュー時に考えることにしてある程度割り切りました。*5

私は、RE:View を使っていたのですが、初めてのRE:Viewによる書きにくさは一週間程度ありました。その後もプレーンテキスト書くよりもRE:View書式当てることの手間はありますが書ける量はそこに起因していないように思います。

対策

  • プロットを細かく出す
  • 今日はここまでという目標スケジュールを紙に大きく書いて共有しました
  • 文章構造をブロックごと入れ替え、つなぎはレビューまで割り切る
  • 事実に基づいて淡々と書いてから肉付けをする

プロットと一日に書けるページ数(10P-50P)をある程度同期させることでスケジュール管理しました。同期が崩れると進捗が不明で完了が未定になるので、これはマストな約束事として自分の中で持てました。

目標の達成度を見える化することで、プレッシャーを外部から与えてもらいました。案の定レビュー時に入り乱れて機能しなくなったのですが、文章を書いている分には、自分のダメ具合が露呈するのでオススメです。

「文章構造を考えながら書く」のと「ブロックごとのテーマについて書く」では、難易度がけた違いです。ブロックごとのテーマに集中するのは非常に簡単で、ブログが書きやすい理由はこのテーマが決まっていることにあります。本を書くときに「全体の構成を考えながら書く」のは、章ごとに分離しても難易度が高いため辞めました。ブロックごとに書くようにしたことでかなり書きやすくなりました。

文章に肉付けされていると前後の文脈が作られてしまい、ブロックごとの移動をする修正が困難です。淡々とした文章で書いておいて、文脈を整えるようにすると移動が楽になりました。文章を書く基本なのでしょうが、なかなか気づけず無駄に時間を使いました。

表現の難しさ

網羅するというのが本当に厳しく、文言の表現でも悩みがずっと解消できなかった部分です。そのため、文章に日本人じゃないんじゃないか、というレビューはそうですねと感じます。誤字脱字の多さもレビュー時点から指摘を受けています。編集者さんの修正が結構入っているのですが、それでも誤字脱字あるので元がまずいのは明らかです。

対策

  • もっとすっきりした文体にする

最終版でも、文体がくどいためすっきりしていいと感じます。

ページの超過

当初、編集者さんと合意した目標ページは400Pです。蓋を開けると624Pでした。まさかの224P超過、いいわけもありません。Goを出してくださった編集者さんにはお礼しかありません。

対策

  • プロットをきっちりねる

中盤にプロットを練りなおした時に誤差が小さくなったことからも、当初の見込みの甘さは明らかです。 この影響は多きく、最も面白いであろう5章がページ相対量が少なくなっているのは完全な失敗でした。

サンプルコードの担保

自分が読んでてほしいので、サンプルコードをふんだんに入れましたが、その動作確認をWindows PowerShell / PowerShell Core (Windows / macOS / Linux) の書く環境で行うことが思った以上にコストになりました。

対策は次の通りです。

  • サンプルコードに依存する文章だけコードの変更がないように気を配り、他はコード修正を前提にする

本質的には、マトリックスでクロスプラットフォームテストをかけるのがいいのですが、執筆中にテストを用意するのが過負担でした。誰かにお願いすればよかったです。

兼業はできない

1-3月を通して日中仕事している時に夜書くのは私にはムリでした。1章は深夜1カ月 + 3月の数日を当ててようやく書きましたが、非常に苦しかったことを覚えています。

また、退職と無職期間がまるまる執筆で費やされたのでのびのびできなかったのが悔やまれます。2018年の最優先課題だったのでシカタナイのですが、無駄に溶かした時間を考えると反省しかないです。

編集者との文章共有

GitHub + RE:View で書いていたのですが、編集者さんの印刷用割り付けとGitHub が連動できなかったことが悔やまれます。 編集者さん側の文章をGitHubにわざわざ反映してもらう必要があり、2度手間どころではありませんでした。 RE:View を用いたにも関わらず、RE:View で書籍製本もできなかったので、いろんな意味で悔いが残ります。

対策

  • 編集者さんに合わせる重要性を理解してもらう
  • 編集者さん側の文書フォーマットに合わせるなり、完全に同一フォーマットを用いる
  • Diff を活用する

なに使っていますか? これでいいですか? というすり合わせをはじめにやっていますが、こちらの自由でいいといわれた時に、この問題を予見できなかったのは自分の未熟を感じます。

手間をなくすためには、同一フォーマットを使うのが最善です。これはWeb寄稿ではなかった課題で、結構難しいと感じました。GitHub + RE:View or Markdown が標準になると、私は嬉しいですがどうなんでしょうか。

Diff を活用は実はしていたのですが、フォーマットが同じでないので漏れをお互いにシステム的になくすことができなかったのはムリゲー感じます。

重版予定は?

今はありません。本が分厚く余り出ない一方で、電子書籍が良く売れているということで鈍器なんでしょう。

もし重版が出るなら誤字脱字や表現上の修正、PowerShell Core 6.1/6.2の対応をしたいのですが、予定は未定です。

*1:この時点で欲張り

*2:ぜんぜん遊べておらず仕事よりハードでした。

*3:ブログの文章換算で甘く見てました

*4:言葉を置き換えて先に出すのはいい

*5:案の定レビューで指摘を受け、粛々と直しました