Tanabebe Env

シングルファザーが技術への葛藤を綴る

Go×agoutiで病院の予約戦争に勝利する

f:id:tanabebe:20191224155150p:plain

平日,休日問わず子供の通院時に困った事がある.かかりつけの病院のWeb予約は競争率が高く,予約開始時間に1分でも遅れてしまうと受付が終了してしまう.平日も午前中に休みを取って行かざるを得ない場合もあるのだが,予約を確実に取れる確証がない.
この不安が少しストレスとなっている.そこで今回は病院予約を確実に勝ち取るためにGo×agoutiでWeb予約受付を自動化していく.

実現したいこと

以下のオペレーションを病院予約当日の08:00に処理する.

  1. 病院のWeb予約受付サイトを開く
  2. 自身のアカウントでログインを行う
  3. 予約受付確認を行う
  4. 予約登録を行う

実際に動作させたプログラムはこちらにあげている.

github.com

環境

下環境で動作を確認した.

Name version
macOS Mojave 10.14.6
GoogleChrome 79.0.3945.79
ChromeDriver 79.0.3945.36
Go 1.12.9

導入準備

ChromeDriverのインストール

macなのでbrew installchromedriverをインストールする.

❯❯❯ brew install chromedriver

agoutiをインストール

go getagoutiをインストールする.

❯❯❯ go get github.com/sclevine/agouti

https://agouti.org/

処理の流れ

エラー時については割愛するが,プログラム全体の流れは以下とした.

  1. init関数を実行
  2. waitingファイルを確認し,処理継続可否の判断
  3. configファイルを読み込み
  4. ScrapingListconfigファイルの値を設定
  5. imgファイルの削除
  6. Google Chromeをヘッドレスモードで起動
  7. 予約受付サイトへ遷移
  8. スクリーンショットを取得
  9. 予約受付リンクをクリックし、ログイン画面へ遷移
  10. ログイン情報の入力
  11. スクリーンショットを取得
  12. ログインの実行
  13. スクリーンショットを取得
  14. 予約受付確認を実行
  15. スクリーンショットを取得
  16. 予約受付登録を実行
  17. スクリーンショットを取得する
  18. アクティブなウインドウを閉じる
  19. waitingファイルの削除
  20. Google Chromeを終了する

実装

今回,agoutiで使用した機能を以下に抜粋する.

WebDriver

Webブラウザを操作するためプロセスを制御する.

ChromeDriver

Google ChromeWebブラウザ操作を制御する.ここではヘッドレスとしてブラウザを非表示で起動を行う.オプションを付けずにdriver := agouti.ChromeDriver()とすることでブラウザの表示が行われる.

driver := agouti.ChromeDriver(
        agouti.ChromeOptions(
            "args", []string{
                "--headless", 
            }),
    )

Start

WebDriverプロセスを開始するために必要となる.

Stop

WebDriverプロセスを停止するために必要となる.main関数の最後でプロセスを停止したいのでdeferで処理を行う.

NewPage

WebDriverに対応したPageのレシーバが返ってくる.ここではGoogle Chromeが対象,以降のPage機能を使用するために必要となる.

target, err := driver.NewPage()

Page

Pageは開いているブラウザのセッションを指す.前提条件として以下のように指定したWebDriverとそれに対応するセッションを作成する事が必要となる.

driver := agouti.ChromeDriver()
target, err := driver.NewPage()

URLを渡して画面遷移を行う.

target.Navigate("targetUrl")

CloseWindow

アクティブなウィンドウを閉じる.

target.CloseWindow()

Screenshot

現在開いているページのスクリーンショットを指定したファイル名で保存する.相対パス,もしくは絶対パスで指定する.

target.Screenshot("filePath")

Selection

要素の取得,選択や実行を行う.

引数に指定したアンカー要素のテキストを検索する.

target.FindByLink("今すぐ受付")

Fill

要素を検索し,引数に指定したテキストで埋める.

target.FindByID("user_email").Fill("your mailadress")

Click

要素を検索し,クリックを行う.

target.FindById("sample").Click()

Submit

指定した要素を検索し,フォーム値を送信する.

target.FindByName("commit").Submit()

cronを設定する

maccronの設定を行っていく.実行結果を自分宛にメールで送信,毎日08:00の起動とするようcrontab -eで編集を行う.
※ここではcrontab -eとしているが,このコマンドはeの隣にあるキーがrなのでオプションに-rで大惨事となる可能性があるので,普段からこのオプションに慣れないよう注意して欲しい.

MAILTO = "example@example.com"
0 8 * * * cd /your project path}; bash -l -c 'go run /your project path/main.go'

実行結果

12月21日(土)に実行した結果は以下となった.これはアクセス先のシステムから送信されてきたメール内容を抜粋している.

日付:2019/12/21 8:00

◇ 受付完了のお知らせ

・診察日:2019年12月21日
・時間 :午前診察
・受付No:31

受付Noがニ桁だった事に驚きではあるが,予約受付が無事完了していることが確認出来た.これでストレスは抱えずともかかりつけの病院予約がスムーズに行える.
また,どのような動作となるかヘッドレスオプションを外し,掲載可能範囲の画面を以下に記す.ここではログインを失敗させているが,実際はログイン以降の処理も問題なく行われている事は確認済みだ.

f:id:tanabebe:20191224151445g:plain

課題

Web予約受付を自動化する.と題しプログラムを作成したが,蓋を開けると完全な自動化とは言い難い点があったため,下表に内容をまとめた.

# 詳細
1 macがスリープ状態だとcronが起動しないため,サーバー配置が必要.
3 waitingファイルを手動で作成する運用になっているため,スマートフォンからwaitingファイルを作成するように改善する.
4 スクリーンショットを取得しているものの,現状だと息をしていない.
5 予約受付順にムラがある.

まとめ

課題はあるものの,Web予約受付を自動操作することで確実な予約が可能となった.前日にwatingファイルを仕込んでいく事で当日の不安も解消され,いつもより惰眠も貪る事が出来る.これは良いことづくしと言える.他の展開としては,ブログに書いた内容を連携対象外のWebアプリに書き込んだり,手動のテストも自動化が出来るだろう.実装するにあたって特段難しい事はなかったので,今後も自動化出来る事は積極的に取り組む.

参考

agouti - GoDoc

Agouti

golang.hateblo.jp

program.okitama.org

UdemyでGoの配列とスライスの扱いを学んだので初心者ながらに少しだけ深堀りしてみた

f:id:tanabebe:20191118123120p:plain

現在Udemyで現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインシストレFintechアプリの開発を受講しています。講義の中でスライスのmakeとcapacityの内容があり、スライスと配列についての挙動が気になったのでまとめました。

はじめに

受講している内容

現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発

講師の酒井潤さんについて

twitter.com

シリエン戦隊JUN TVでYouTuberとして活動もされています。シリコンバレーのエンジニア事情などとても面白いです。 www.youtube.com

Splunkで働いている現役シリコンバレーエンジニアの方の講義を手軽に受けられるのは魅力的です。講義の内容や話のテンポも無駄な事を削ぎ落としていると感じたので、私にはとても心地良く感じました。シリコンバレーの風景なども動画におさめられているのはとても面白いと思いました。私はもはやファンになってしまっているので自身の学習内容にマッチしているのであれば、私は酒井潤さん推しです。気になる方は是非チェックしてみて下さい。

実践していく

講義から学んだGoの配列とスライスについては14〜16のレクチャーが該当となり、動画で言うと10分ほどです。プログラムは以下に公開しています。講義内容には影響ないように考慮しています。
リポジトリ内のreflectのケースは当記事では未記載

github.com

配列について

宣言方法

以下のような形で宣言する事が可能です。

var array1 [2]int
array2 := [3]int{1, 2, 3}
array3 := [...]int{1, 2, 3, 4}

配列の挙動を試す

以下のようにして、配列内の長さ、容量、値、アドレスを見てみます。

var array1 [2]int
fmt.Printf("array1 => length=%d capacity=%d value=%v address=%p \n", len(array1), cap(array1), array1, &array1)

array2 := [4]int{}
fmt.Printf("array2 => length=%d capacity=%d value=%v address=%p \n", len(array2), cap(array2), array2, &array2)

array3 := [4]int{1, 2, 3}
fmt.Printf("array3 => length=%d capacity=%d value=%v address=%p \n", len(array3), cap(array3), array3, &array3)
array3[3] = 4
fmt.Printf("array3 => length=%d capacity=%d value=%v address=%p \n", len(array3), cap(array3), array3, &array3)
実行結果

実行すると以下となります。

array1 => length=2 capacity=2 value=[0 0] address=0xc000016050 
array2 => length=4 capacity=4 value=[0 0 0 0] address=0xc00008e000 
array3 => length=4 capacity=4 value=[1 2 3 0] address=0xc0000180e0 
array3 => length=4 capacity=4 value=[1 2 3 4] address=0xc0000180e0

配列は要素数を宣言時に必ず決めるため、各変数での挙動が上記結果となります。アドレスを出力しているのはメモリ上に確保されるアドレスに変化が有るかどうかを見たかったためです。

配列の注意点

配列には要素を追加することは出来ないため、以下のようにするとエラーとなります。

// 配列にappendすることは出来ない
array3 = append(array3, 4)

配列の値の取り出し方

パターン1
// indexは不要なので値のみを出力する
for _, v := range array3 {
    fmt.Println(v)
}
パターン2
for i := 0; i < len(array3); i++ {
    fmt.Println(array3[i])
}

配列まとめ

  • 配列の要素数は固定長
  • 宣言時に要素数を決めるため、宣言の要素追加は不可能
  • メモリ上に容量を無駄に確保される事がない
  • appendは出来ないため、書き換える場合は要素位置を指定する

スライスについて

ここが今回のメインです。講義内でもスライスの宣言方法によってのメモリ上へどのように確保されるのかという点は触れているのですが、この時点では「まだ、気にすることはない」という事から最初はスルーしていました。深く考えると進まないのも事実です。ですが良く使いそうだなと感じたので、気になって仕方ない…という事で1度立ち止まって色々と試してみました。

宣言方法

スライスは以下のような形で宣言する事が可能です。

var slice1 []int
slice2 := []int{1, 2, 3, 4}
slice3 := make([]int, 10)

まずはスライスの挙動を試す

以下のようにして、スライスの長さ, 容量, 値, アドレスを見てみます。

var slice1 []int
fmt.Printf("slice1 => length=%d capacity=%d value=%v address=%p \n", len(slice1), cap(slice1), slice1, slice1)
slice2 := []int{1, 2, 3, 4}
fmt.Printf("slice2 => length=%d capacity=%d value=%v address=%p \n", len(slice2), cap(slice2), slice2, slice2)
slice3 := make([]int, 10)
fmt.Printf("slice3 => length=%d capacity=%d value=%v address=%p \n", len(slice3), cap(slice3), slice3, slice3)
実行結果

配列の時とは違い、宣言と同時に値を入れていない場合はnil扱いとなります。makeはここでは容量を10としていますが0でもnil扱いにはなりません。
また、var slice1 []intでは宣言したスライスの実体がメモリ上に確保されていますが(もしかしたら間違っているかもしれないです)どのアドレスも見ていないため0x0になります。

slice1 => length=0 capacity=0 value=[] address=0x0 
slice2 => length=4 capacity=4 value=[1 2 3 4] address=0xc000096080 
slice3 => length=10 capacity=10 value=[0 0 0 0 0 0 0 0 0 0] address=0xc00009e000

makeなしのスライスを試す

makeをせず、容量を確保しない場合のスライスを宣言します。

var slice4 []int
fmt.Printf("slice4 => length=%d capacity=%d value=%v address=%p \n", len(slice4), cap(slice4), slice4, slice4)
// appendするとどうなるか
slice4 = append(slice4, 2)
fmt.Printf("slice4 => length=%d capacity=%d value=%v address=%p \n", len(slice4), cap(slice4), slice4, slice4)
実行結果

宣言直後のslice4では容量も確保されていないため、address0x0となり、appendで値を追加すると容量が確保されています。次はmakeで宣言した場合のスライスを試していきます。

slice4 => length=0 capacity=0 value=[] address=0x0 
slice4 => length=1 capacity=1 value=[2] address=0xc0000160f8 

makeありのスライスを試す

ここではmakeした場合のスライスの挙動を見ていきます。確認したいポイントをPrintfで出力していきます。やっている事としては単純です。

// makeを使う。スライスの長さは容量との関係性が見たいので0とする
slice5 := make([]int, 0, 1)
fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
slice5 = append(slice5, 1)
// この時点ではcapacityを超えてこないのでアドレスは変わらない
fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
slice5 = append(slice5, 2)
// capacityを超えるのでアドレスが変わる
fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
slice5 = append(slice5, 3)
// capacityを超えるのでまたアドレスが変わる、capacityは以前確保していたcapacity^2で増加していく
fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
実行結果

面白い結果となりました。2度目のslice5出力までは宣言時のスライス容量を超えないためaddressは同じです。 しかし、確保した領域を超えるとaddressに変化があり、スライスの容量も自動で増えています。また、最後の出力時には拡張される前の容量^2で増えています。ここではaddressが変わっているのでslice5の容量が拡張される度に、メモリ上に領域が確保されていきます。これは扱いを知らないと意識せずに書いてしまいそうです。

slice5 => length=0 capacity=1 value=[] address=0xc00008c160 
slice5 => length=1 capacity=1 value=[1] address=0xc00008c160 
slice5 => length=2 capacity=2 value=[1 2] address=0xc00008c190 
slice5 => length=3 capacity=4 value=[1 2 3] address=0xc0000940e0 

スライスの宣言パターンによるベンチマークを取る

スライスの宣言時に明示的に容量を確保しない場合、容量が拡張される度にメモリ上に新たな領域が確保され、無駄にメモリを喰い潰して行くという挙動だと思います。大量にループを回した場合、処理速度に大きく影響がありそうだと感じました。
Goの標準パッケージtestingを使用してベンチマークを取り、宣言パターンによって処理速度にどれほどの差があるか見ていきます。
_test.goと終わるファイルを作成することでtest対象と出来ます。また、ベンチマークを取りたいのでBenchmarkから始まるテスト関数を作成してgo test -bench . -benchmemで実行します。

// slice_test.go
package main

import "testing"

// sliceの容量を指定しない場合
func BenchmarkInitSliceVariable(b *testing.B) {
    var target []int
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        target = append(target, i)
    }
}

// sliceでmakeで容量を確保しているがlength指定している場合(値が0で初期化されている場合)
func BenchmarkSliceCapacityNo(b *testing.B) {
    var target = make([]int, b.N)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        target = append(target, i)
    }
}

// sliceのmake時にc容量を設定する場合
func BenchmarkSliceCapacityYes(b *testing.B) {
    var target = make([]int, 0, b.N)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        target = append(target, i)
    }
}
実行結果

以下結果の通り、スライスの宣言方式で処理速度が大きく変わりました。

BenchmarkInitSliceVariable-4    100000000               22.3 ns/op            49 B/op          0 allocs/op
BenchmarkSliceCapacityNo-4      200000000               93.6 ns/op            57 B/op          0 allocs/op
BenchmarkSliceCapacityYes-4     2000000000               6.80 ns/op            0 B/op          0 allocs/op

実行結果についての見方は以下参考にさせていただきました。

qiita.com

// 関数の実行回数、有用な結果が得られるまで実行される
// 多ければ多いほど良い
2000000

// 1回の実行にかかった時間
// 少ないほど良い
815 ns/op

// 実行ごとに割り当てられたメモリのサイズ
// 少ないほど良い
336 B/op

// 1回の実行でメモリアロケーションが行われた回数
// 少ないほど良い
9 allocs/op

今回の結果に当てはめると以下ですね。

// 関数の実行回数、有用な結果が得られるまで実行される(多いほど良い)
100000000
200000000
2000000000

// 1回の実行にかかった時間(少ないほど良い)
22.3 ns/op
93.6 ns/op
6.80 ns/op

// 実行ごとに割り当てられたメモリのサイズ(少ないほど良い)
49 B/op
57 B/op
0 B/op

//1回の実行でメモリアロケーションが行われた回数(少ないほど良い)

0 allocs/op
0 allocs/op
0 allocs/op
スライスまとめ
  • 素数は可変長
  • スライスのmake時は要素数がわかっているなら宣言時にlenで確保
  • make([]int, 要素数)としてしまいそう注意が必要
  • 宣言時は長さが膨大だとスライスの中身は型による初期化が行われる のでlength0が良い

まとめ

講義の内容では約10分ほどの配列とスライスですが、とても考えさせられました。アロケーション0だったのが私の考えていた想定と違ったので、アロケーション回数が大きく増えてしまうようなバッドプラクティスなども今後学習を続ける中で深堀りしていきたいと思っています。

参考

testing - The Go Programming Language

Golangでベンチマークを取ってみた - Qiita

go - The Go Programming Language

@kakakakakkuさんのブログメンタリングを卒業しました

こちらにブログスタート時の初回記事がありますが、3か月間に及ぶブログメンタリングはあっという間でした。しかし終わってからが本当の始まりなので、どんな事を行ったのか、何を学んだのか振り返ってみました。

なぜブログメンタリングを受けたのか

  • 働き方が変わるので今後のセルフブランディングが必要と考えていた
  • アウトプットする場所としてブログを考えていた
  • 技術に好奇心があるものの、個人的には浅い所が多いと感じていた
  • 自分を叩き直す

このように思い悩んでいる時に「技術ブロガーを育てる!ブログメンタリングで何を教えているのか」という記事と出会いました。調べてみると、丁度メンティー募集のタイミングだったので即応募しました。同じように悩みを抱えている方、心が突き動かされるので是非チェックしてみて下さい。

kakakakakku.hatenablog.com

また、Twitterハッシュタグ#ブログメンタリングや、ブログメンタリングGoogle検索するとカックさんの記事やメンティーとして卒業された記事などがたくさん出てきますのでこちらもチェックしてみて下さい。

何を学んだか

技術文書を書く上での表記揺れですます調など、恥ずかしながら日本語の使い方については特に指摘をいただきました。もちろん他にもたくさんあるのですが、個人的に感じた事をまとめていきます。

自己管理は最重要

ブログメンタリング時はブログの更新本数のノルマがありますが、記事の平日公開をしたほうが読まれやすいのもあり、平日公開を狙った活動を行っていく必要があります。また、「コツコツと積み上げて経験値を稼ぐという感覚を如何に楽しめるか」というのもポイントです。私的な理由ですと「仕事、子供、ブログ」と並べた時にスイッチの切り替えが出来ていなかったです。子育てを優先する中でどうやって時間を捻出するかというのが2か月目に突入した時の課題でした。「22時以降はブログ」と掲げてはみたものの、ふとしたトラブルがあったりするとブレてきます。こういった想定外に耐え得るためにも計画性を持った自己管理が重要です。

ブログ記事を書くだけではダメ

「自分のためのブログ」でないのであれば「他人の目線」を常に意識すること。記事内の温度感他人が見た時何を思うか。こういった点は日常生活では出来ていたつもりでしたが、ブログ内でのアプローチ方法は全く違うモノでした。ブログ外の活動も含めて、今後も別目線での動きも積極的に取り組んでいきます。

モチベーションコントロール

毎記事で新しい取り組みを書くのは大変です。世に出ている同じ記事でも書くべきという点なども継続していく上で重要なポイントだと思いました。ブログを育てていくという意識をすることで「ブログ」を中心に考えた時、自己研鑽のモチベーションが断然変わりました。楽しんで取り組む姿勢、これ大事です。

3か月の実績

年月 書いた記事数
2019/07 5記事
2019/08 4記事
2019/09 6記事

これとは別で振り返ってみると平日に公開出来た記事は4記事、週2での記事公開はわずか1回でした。思っていたより書けていない!

まとめ

メンタリング中は学びが多くメンターの方がついてくれて何かを行うという経験が私はなかったので、ブログメンタリングに応募して本当に良かったです。書く楽しさと、その裏側にある厳しさを教えていただきました。不安はあれどやり遂げると決めていたので、ここをクリア出来たのは大きかったです。
また、カックさんの圧倒的なレスの速さなどから「いつ寝てるの?」という場面で良く驚いていましたが…数年前から私自身が抱えていた悩みにも気付いた上でアドバイスをくれたり、とにかくご自身で体現していることが本当にカッコ良く、凄かったです。

そしてブログそのものでした。

同時にもっとカックさんを使い倒す事が出来れば良かったという後悔もあります。今後の課題は山ほどありますが、良い意味での課題です。これからも1つずつステップを踏んでいきます。ありがとうございました!

同期メンティーの方々

最後になりますが、同時期にメンタリングを受けられていたメンティーの方々のご紹介です。メンタリング期間中も記事を良く拝見させていただきました。自分も頑張らないと!という良い刺激があったりモチベーションが上がります。今後の皆さんの活躍にも乞うご期待です!

lopburnyさん

riotz.works

y_zumi3さん

y-zumi.hatenablog.com

imaharuさん

www.imaharutech.work

期限切れとなってしまったLet’s EncryptのSSL証明書の新規発行と自動更新を行う

f:id:tanabebe:20191007143528p:plain

社内で使用しているknowledgeのWebアプリについてSSL証明書が切れてしまっており、自動更新の設定も当時はかけていなかったため、SSL証明書の新規発行と自動更新設定を行いました。

実行環境

SSL証明書の期限が切れているか確認する

rootユーザーへ切り替えます。

❯❯❯ sudo -i

期限が切れているのは明白なのですが、状況を確認します。 ドメイン名は伏せてあります

❯❯❯ certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Revocation status for /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem is unknown

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
  Certificate Name: xxxxx.xxxxxxx.com
    Domains: xxxxx.xxxxxxx.com
    Expiry Date: 2019-09-17 08:10:00+00:00 (INVALID: EXPIRED)
    Certificate Path: /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/xxxxx.xxxxxxx.com/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

INVALID: EXPIREDと出ているので期限が切れています。期限が切れている場合、SSL証明書の新規発行を行う必要があるため、再度発行していきます。

SSL証明書の新規発行を行う

ファイルは退避しておきます。

❯❯❯ mkdir /etc/letsencrypt/live/_bk
❯❯❯ mv /etc/letsencrypt/live/xxxxx.xxxxxxx.com /etc/letsencrypt/live/_bk

80, 443portが使われているとエラーとなるのでhttpdを停止しておきます。

❯❯❯ systemctl stop httpd

SSL証明書の新規発行を行います。

❯❯❯ certbot-auto certonly --standalone -d xxxxx.xxxxxxx.com

httpdを起動します。

❯❯❯ systemctl start httpd
Job for httpd.service failed because the control process exited with error code. See "systemctl status httpd.service" and "journalctl -xe" for details.

エラーとなってしまいました…ここは内容を見て探っていきます。

❯❯❯ systemctl status httpd 
Oct 06 13:03:31 xxxxx-instance httpd[10894]: AH00526: Syntax error on line 101 of /etc/httpd/conf.d/ssl.conf:
Oct 06 13:03:31 xxxxx-instance httpd[10894]: SSLCertificateFile: file '/etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem' does not exist or is empty
Oct 06 13:03:31 xxxxx-instance systemd[1]: httpd.service: main process exited, code=exited, status=1/FAILURE
Oct 06 13:03:31 xxxxx-instance kill[10895]: kill: cannot find process ""
Oct 06 13:03:31 xxxxx-instance systemd[1]: httpd.service: control process exited, code=exited status=1
Oct 06 13:03:31 xxxxx-instance systemd[1]: Failed to start The Apache HTTP Server.

エラーを見るとSyntax errorと言っており、更にfullchain.pemが存在しないと言っています。中身を見ていきます。

❯❯❯ vim /etc/httpd/conf.d/ssl.conf

該当箇所を見ると以下とありました。

SSLCertificateFile /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem

フォルダも移動したので問題ない、と思っていましたがどうやら違いました。確認してみます。

❯❯❯  ll /etc/letsencrypt/live/
total 4
drwxr-xr-x. 3 root root  38 Oct  6 12:47 _bk
drwxr-xr-x. 2 root root  93 Oct  6 19:59 xxxxx.xxxxxxx.com-0001
-rw-r--r--. 1 root root 740 Oct  6 12:56 README

フォルダ名がxxxxx.xxxxxxx.com-0001となっており、名前が拡張されていました。少し気持ち悪さは残りますが、ssl.conf内の以下で始まる定義のファイルパスに0001を付与し変更します。

SSLCertificateFile
SSLCertificateKeyFile
SSLCertificateChainFile

再度httpdの起動を行います。

❯❯❯ systemctl start httpd && systemctl status httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2019-10-06 20:28:55 UTC; 13ms ago
     Docs: man:httpd(8)
           man:apachectl(8)
  Process: 11556 ExecStop=/bin/kill -WINCH ${MAINPID} (code=exited, status=0/SUCCESS)
  Process: 11515 ExecReload=/usr/sbin/httpd $OPTIONS -k graceful (code=exited, status=0/SUCCESS)
 Main PID: 11579 (httpd)
   Status: "Processing requests..."
   CGroup: /system.slice/httpd.service
           ├─11579 /usr/sbin/httpd -DFOREGROUND
           ├─11580 /usr/sbin/httpd -DFOREGROUND
           ├─11581 /usr/sbin/httpd -DFOREGROUND
           ├─11582 /usr/sbin/httpd -DFOREGROUND
           ├─11583 /usr/sbin/httpd -DFOREGROUND
           └─11584 /usr/sbin/httpd -DFOREGROUND

Oct 06 20:28:55 xxxxx-instance systemd[1]: Starting The Apache HTTP Server...
Oct 06 20:28:55 xxxxx-instance systemd[1]: Started The Apache HTTP Server.

修正箇所は正解ですね。これでhttpdが動作しました。

SSL証明書の自動更新を設定する

元を正せば更新を自動化していないのが問題ですね。期限が迫る度に手動で更新とか手間ですし、少し面倒な事を後回しにすると人間は大体忘れます。なので、このままSSL証明書の自動更新を行うように設定していきます。実際にSSL証明書の更新はかけずにチェックを行いたいので—dry-runをオプションにつけてrenewを実行します。

❯❯❯ cd /usr/local/certbot/
❯❯❯ ./certbot-auto renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Traceback (most recent call last):
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/renewal.py", line 64, in _reconstitute
    renewal_candidate = storage.RenewableCert(full_path, config)
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/storage.py", line 465, in __init__
    self._check_symlinks()
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/storage.py", line 523, in _check_symlinks
    "expected {0} to be a symlink".format(link))
CertStorageError: expected /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem to be a symlink
Renewal configuration file /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf is broken. Skipping.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)

Additionally, the following renewal configurations were invalid: 
  /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf (parsefail)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0 renew failure(s), 1 parse failure(s)

またまたエラーです。ここでは以下のエラーが問題となっています。

CertStorageError: expected /etc/letsencrypt/live/knowledge.miracleave.biz/cert.pem to be a symlink
Renewal configuration file /etc/letsencrypt/renewal/knowledge.miracleave.biz.conf is broken. Skipping.

シンボリックリンクについてファイルが壊れているという内容ですね。そもそもですが、前述したhttpdが起動しない問題と同じようにパスが違うだけと想定しますが、確認してみましょう。

どのファイル末尾に2prefixが振られてしまっているのは気になりますが、live内ではarchiveシンボリックリンクがはられているのでこちらについては問題なさそうです。

❯❯❯ ll /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/
total 4
lrwxrwxrwx. 1 root root  53 Oct  6 19:59 cert.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/cert2.pem
lrwxrwxrwx. 1 root root  54 Oct  6 19:59 chain.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/chain2.pem
lrwxrwxrwx. 1 root root  58 Oct  6 19:59 fullchain.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/fullchain2.pem
lrwxrwxrwx. 1 root root  56 Oct  6 19:59 privkey.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/privkey2.pem
-rw-r--r--. 1 root root 692 Oct  6 12:56 README

renewal内のファイルを確認します。

❯❯❯  vim /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf

中身を見ると存在しないパスを指定しているのが問題でした。0001を付与していきます。

# 変更前
archive_dir = /etc/letsencrypt/archive/xxxxx.xxxxxxx.com
cert = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem
privkey = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/privkey.pem
chain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/chain.pem
fullchain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem

# 変更後
archive_dir = /etc/letsencrypt/archive/xxxxx.xxxxxxx.com-0001
cert = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/cert.pem
privkey = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/privkey.pem
chain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/chain.pem
fullchain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem

再度、更新はかけずにチェックを行うコマンドを実行します。

❯❯❯  ./certbot-auto renew --dry-run

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

今度は成功しました。これでSSL証明書の更新についてテストが出来ました。

自動更新の設定を行う

renewの更新作業後はhttpdの起動を実行したいので、cronを使用します。念の為cronが動作しているかを確認します。

❯❯❯  systemctl status crond
● crond.service - Command Scheduler
   Loaded: loaded (/usr/lib/systemd/system/crond.service; enabled; vendor preset: enabled)
   Active: active (running) since Sun 2019-10-06 18:41:14 UTC; 2h 42min ago
 Main PID: 462 (crond)
   CGroup: /system.slice/crond.service
           └─462 /usr/sbin/crond -n

Oct 06 18:41:14 knowledge-instance systemd[1]: Started Command Scheduler.
Oct 06 18:41:14 knowledge-instance crond[462]: (CRON) INFO (RANDOM_DELAY will be scaled with factor 36% if used.)
Oct 06 18:41:14 knowledge-instance crond[462]: (CRON) INFO (running with inotify support)

cronは動作しているので以下のように記述していきます。crontab -eで作成しようと思いましたがここは別ファイルでの管理にします。理由として間違ってキーボード隣のrをタイプしcrontab -rを実行すると無慈悲に全て消え失せますので要注意です。

❯❯❯  vim /etc/cron.d/letsencrypt

# 毎週日曜日の23:50に実行。/var/log/letsencrypt/result-renewal.logへ書き込むようにしておく。
50 23 * * 0 root /usr/local/certbot/certbot-auto renew --post-hook "systemctl httpd reload" > /var/log/letsencrypt/result-renewal.log

cronへ作成したファイルを反映します。

❯❯❯ crontab /etc/cron.d/letsencrypt

一度、毎分のcronが実行されるように変更を行い、実際に動いているかを確認します。

❯❯❯ vim /etc/cron.d/letsencrypt

# 毎週日曜日の23:50に実行。/var/log/letsencrypt/result-renewal.logへ書き込むようにしておく。
* * * * * root /usr/local/certbot/certbot-auto renew --post-hook "systemctl httpd reload" > /var/log/letsencrypt/result-renewal.log

❯❯❯ less /var/log/letsencrypt/result-renewal.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

The following certs are not due for renewal yet:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem expires on 2020-01-04 (skipped)
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem expires on 2020-01-04 (skipped)
No renewals were attempted.
No hooks were run.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -                                                                         

確認が出来たら実行したいスケジュールに戻しておきます。これでSSL証明書の期限が迫っていたら自動更新が行われるようになります。

まとめ

今回については既にSSL証明書を導入済みという前提で進めてはいますが、Let’s Encryptの期限は3か月で切れるため、自動更新をしておくと後々かなり手間が省けます。「忘れんなよ」という話しではあるのですが…そこまで使われていないシステムだとSSL証明書の更新は忘れがちです。自動化のためにある程度の手間をかけることで後で楽が出来ます。SSL証明書の自動更新に関わらず、こういった考えは他の業務を遂行する上でも有用ではないでしょうか。

参考

qiita.com

イケてるcrontabのいじり方 - Qiita

ozuma.hatenablog.jp

textlintのruleプリセットtextlint-rule-preset-ja-technical-writingを掘り下げる

f:id:tanabebe:20190929180448p:plain

前回記事でtextlintで文章校正を見直しました。ただ、校正を見直すのは良いのですがインストールしたruleについて書き手がしっかりと理解することも大事だと思います。今回はtextlint-rule-preset-ja-technical-writingruleプリセットについて少しだけ掘り下げていきます。

textlint-rule-preset-ja-technical-writingに含まれているルール一覧

表でまとめてみました。

Rule Name Description
textlint-rule-sentence-length 1文の長さは100文字以下とする
textlint-rule-max-comma カンマは1文中に3つまで
textlint-rule-max-ten 読点は1文中に3つまで
textlint-rule-max-kanji-continuous-len 連続できる最大の漢字長は6文字まで
textlint-rule-preset-JTF-style 漢数字と算用数字を使い分けます
textlint-rule-no-mix-dearu-desumasu 「ですます調」、「である調」を統一します
textlint-ja/textlint-rule-ja-no-mixed-period 文末の句点記号として「。」を使います
textlint-rule-no-double-negative-ja 二重否定は使用しない
textlint-rule-no-dropping-the-ra ら抜き言葉を使用しない
textlint-rule-no-doubled-conjunctive-particle-ga 逆接の接続助詞「が」を連続して使用しない
textlint-rule-no-doubled-conjunction 同じ接続詞を連続して使用しない
textlint-rule-no-doubled-joshi 同助詞を連続して使用しない
textlint-rule-no-nfd UTF8-MAC 濁点を使用しない
https://github.com/textlint-rule/textlint-rule-no-invalid-control-character 不必要な制御文字を使用しない
textlint-rule-no-exclamation-question-mark 感嘆符!!、感嘆符??を使用しない
textlint-rule-no-hankaku-kana 半角カナを使用しない
textlint-rule-ja-no-weak-phrase 弱い日本語表現の利用を使用しない
textlint-rule-ja-no-successive-word 同一の単語を間違えて連続しているのをチェックする
textlint-rule-ja-no-abusage よくある日本語の誤用をチェックする
textlint-rule-ja-no-redundant-expression 冗長な表現をチェックする
textlint-rule-ja-unnatural-alphabet 入力ミスで発生する不自然なアルファベットをチェックする
textlint-rule-no-unmatched-pair 対になっていない括弧をチェックする

ruleを掘り下げていく

今回は以下のルールについて掘り下げてみました。

また、以降で実際に試した内容については以下リポジトリにあげてあります。 github.com

textlint-rule-preset-ja-technical-writing

このrule長過ぎる文についてチェックを行います。デフォルト値は100です。以下のとおり試してみます。

f:id:tanabebe:20190928024241p:plain

textlintのコマンドを実行すると以下の通り、エラーとなります。

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-sentence-length/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-sentence-length/file.md
  11:1  error  Line 11 sentence length(101) exceeds the maximum sentence length of 100.
Over 1 characters  ja-technical-writing/sentence-length

✖ 1 problem (1 error, 0 warnings)

~/rule-sandbox ❯❯❯ 

デフォルト値に設定されている100文字を超えている文があるとチェックしてくれていますね。

設定値の変更方法

次はチェックの最大値を100文字から90文字へ変更してみます。.textlintrcファイルを以下のように修正します。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
    }
  }
}

再度textlintのコマンドを実行します。想定だと全ての文章に対してエラーが出るはずです。

f:id:tanabebe:20190928031650p:plain

以下の通り、エラーとなり文字数に対してのチェックが適切に行われていることがわかります。

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-sentence-length/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-sentence-length/file.md
   3:1  error  Line 3 sentence length(99) exceeds the maximum sentence length of 90.
Over 9 characters    ja-technical-writing/sentence-length
   7:1  error  Line 7 sentence length(100) exceeds the maximum sentence length of 90.
Over 10 characters   ja-technical-writing/sentence-length
  11:1  error  Line 11 sentence length(101) exceeds the maximum sentence length of 90.
Over 11 characters  ja-technical-writing/sentence-length

✖ 3 problems (3 errors, 0 warnings)

チェックの例外

こちらのREADMEでは以下とあり、引用もしくはリンクで書かれた文はチェック対象外となります。

Exception

  • Except BlockQuote
  • Except a single link node

こちらも試してみます。 f:id:tanabebe:20190928034231p:plain

見ての通り、引用で書かれた文とリンクについてはチェック対象外となりました。これでtextlint-rule-sentence-lengthのカスタマイズ方法チェックの例外パターンについて確認が出来ました。

textlint-rule-max-comma

このrule1文中のカンマが出現する個数についてチェックを行います。こちらをみるとデフォルト値は4とあるのですが、textlint-rule-preset-jtf-styleに含まれるtextlint-rule-max-commaデフォルト値は3です。動作を試してみます。

f:id:tanabebe:20190929100605p:plain

textlintのコマンドを実行すると以下の通り、エラーとなります。

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-max-comma/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-max-comma/file.md
  5:1  error  This sentence exceeds the maximum count of comma. Maximum is 3  ja-technical-writing/max-comma

✖ 1 problem (1 error, 0 warnings)

文にカンマが何個あるかチェックが行われています。ここでは4つ以上のカンマが含まれているのでエラーとなります。

設定値の変更方法

次はカンマの最大個数を3つから4つへ変更してみます。.textlintrcファイルを以下のように修正します。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
    }
  }
}

再度実行します。 f:id:tanabebe:20190929083408p:plain

想定通り、カンマが5つ以上存在している文のチェックが行われています。これでtextlint-rule-max-commaのカスタム方法がわかりました。このruleについてもOKですね。

チェックの例外

textlint-rule-max-commaについてのチェック例外は言及されていないため、なしと考えて良いでしょう。

textlint-rule-max-ten

このrule1文に利用できるの数をチェックします。デフォルト値は3です。動作を試してみます。

f:id:tanabebe:20190929101436p:plain

textlintのコマンドを実行すると以下の通り、エラーとなります。

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-max-ten/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-max-ten/file.md
  9:22  error  一つの文で""3つ以上使用しています  ja-technical-writing/max-ten

✖ 1 problem (1 error, 0 warnings)

文に読点が何個あるかチェックが行われています。ここでは読点が3つ以上存在する文がエラーとなります。

設定値の変更方法

次はチェックの最大値を3から4へ変更してみます。.textlintrcファイルを以下のように修正します。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
      "max-ten": {
        "max": 4
      },
    }
  }
}

チェックの例外

このruleについてはこちらにある通り、以下となります。

<名詞>、<名詞> のように名詞に挟まれた読点はカウントしません。 箇条書きとしての区切り文字として使われているため無視します。

試してみます。 f:id:tanabebe:20190929102434p:plain

textlintのコマンドを再度実行すると以下の通り、エラーとなります。

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-max-ten/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-max-ten/file.md
  13:32  error  一つの文で""4つ以上使用しています  ja-technical-writing/max-ten

✖ 1 problem (1 error, 0 warnings)

チェックの例外時パターンについて確認が出来ました。これでtextlint-rule-max-tenについてはOKですね。

textlint-rule-max-kanji-continuous-len

このruleは1文に利用できる漢字が連続する最大文字数をチェックします。デフォルト値は6です。動作を試してみます。

f:id:tanabebe:20190929110152p:plain

textlintのコマンドを実行すると以下の通り、エラーとなります。

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-max-kanji-continuous-len/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-max-kanji-continuous-len/file.md
  9:1  error  漢字が7つ以上連続しています: 倍精度浮動小数  ja-technical-writing/max-kanji-continuous-len

✖ 1 problem (1 error, 0 warnings)

エラーの通り、連続する漢字の個数のチェックが行われています。ここでは連続する漢字が7つ以上存在するとエラーとなります。

設定値の変更方法

次はチェックの最大値を6から5へ変更します。更に許可する固有名詞(チェックの例外)を登録します。 .textlintrcファイルを以下のように修正します。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
      "max-ten": {
        "max": 4
      },
      "max-kanji-continuous-len": {
        "max"  : 5,
        "allow": ["漢字百科大事典"]
      }
    }
  }
}

チェックの例外

上記にある通りallowオプションに固有名詞を記述することでチェック対象外とする事が出来ます。動作を試してみます。

f:id:tanabebe:20190929110443p:plain

textlintのコマンドを実行すると以下の通り、エラーとなります。

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-max-kanji-continuous-len/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-max-kanji-continuous-len/file.md
  5:1  error  漢字が6つ以上連続しています: 一二三四五六    ja-technical-writing/max-kanji-continuous-len
  9:1  error  漢字が6つ以上連続しています: 倍精度浮動小数  ja-technical-writing/max-kanji-continuous-len

✖ 2 problems (2 errors, 0 warnings)

連続する漢字の個数が変更されているのと、allowオプションで追加した漢字百科大辞典についてはチェック対象外となっていますね。想定通りの動きです。
設定値の変更による連続する漢字の個数チェックチェックの例外時パターンについて確認が出来ました。

textlint-rule-preset-JTF-style

このrule算用数字と漢数字の使い分けをチェックします。ベースとなっているのはJTF日本語標準スタイルガイド 第3.0版です。 またtextlint-rule-preset-ja-technical-writing内で有効なrule"arabic-kanji-numbers": trueと書いてあり、2.2.2.算用数字と漢数字の使い分けがデフォルトで有効となっています。こちらも試していきましょう。

textlintのコマンドを実行すると以下の通り、エラーとなります。

f:id:tanabebe:20190929141804p:plain

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-preset-JTF-style/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-preset-JTF-style/file.md
  5:1  ✓ error  第3=> 第三者
慣用的表現、熟語、概数、固有名詞、副詞など、漢数字を使用することが一般的な語句では漢数字を使います。  ja-technical-writing/arabic-kanji-numbers
  6:1  ✓ error  世界1 => 世界一
慣用的表現、熟語、概数、固有名詞、副詞など、漢数字を使用することが一般的な語句では漢数字を使います。  ja-technical-writing/arabic-kanji-numbers
  7:1  ✓ error  1部の => 一部の
慣用的表現、熟語、概数、固有名詞、副詞など、漢数字を使用することが一般的な語句では漢数字を使います。  ja-technical-writing/arabic-kanji-numbers

✖ 3 problems (3 errors, 0 warnings)3 fixable problems.

文に漢数字を使用することが一般的ではない語句のチェックしてくれています。ここでは第3者世界11部の語句が対象です。

設定値の変更方法

次はruleカタカナカタカナの長音について有効化します。.textlintrcファイルを以下のように修正します。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
      "max-ten": {
        "max": 4
      },
      "max-kanji-continuous-len": {
        "max"  : 5,
        "allow": ["漢字百科大事典"]
      }
    },
    "preset-jtf-style": {
      "2.1.5.カタカナ": true,
      "2.1.6.カタカナの長音": true
    }
  }
}

f:id:tanabebe:20190929142113p:plain

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-preset-JTF-style/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-preset-JTF-style/file.md
  17:3   ✓ error  アベレイジ => アベレージ                jtf-style/2.1.5.カタカナ
  21:1   ✓ error  サーバ間通 => サーバー間通              jtf-style/2.1.6.カタカナの長音
  21:13  ✓ error  コンピュータ同士 => コンピューター同士  jtf-style/2.1.6.カタカナの長音

✖ 3 problems (3 errors, 0 warnings)
✓ 3 fixable problems.

エラーにある通り、カタカナカタカナの長音についてチェックが行われていることがわかります。

チェックの例外

textlint-rule-preset-JTF-styleに含まれているruleを除外したい場合は有効化されているrulefalseとし、無効化を行います。例えばtextlint-rule-preset-ja-technical-writing内で使用されている"2.2.2.算用数字と漢数字の使い分け"を無効化したい場合は以下のようにします。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
      "max-ten": {
        "max": 4
      },
      "max-kanji-continuous-len": {
        "max"  : 5,
        "allow": ["漢字百科大事典"]
      },
      // preset-ja-technical-writing内の中でfalseとする
      "arabic-kanji-numbers": false
    },
    "preset-jtf-style": {
      "2.1.5.カタカナ": true,
      "2.1.6.カタカナの長音": true
    }
  }
}

rule名がarabic-kanji-numbersとして宣言されているのとpreset-ja-technical-writingの中に含まれているという点において注意が必要です。これでtextlint-rule-preset-JTF-styleで対応するルールのカスタマイズも変更出来ることが確認出来ました。

textlint-rule-no-mix-dearu-desumasu

このruleですます調である調の混在をチェックします。チェックするのは見出し、本文、箇条書きを対象としてくれます。デフォルト値は以下となります。

{
    "rules": {
        "no-mix-dearu-desumasu": {
            "preferInHeader": "",
            "preferInBody": "ですます",
            "preferInList": "である",
            "strict": false
        }
    }
}

""は多く使われている表記を自動的に優先します。各値の対象は以下となります。

Param Target
preferInHeader 見出し
preferInBody 本文
preferInList 箇条書き
strict 文末以外でも厳しくチェックするかどうか

動きを試す前にtextlint-rule-preset-JTF-style本文、箇条書きに対してですます調あるいはである調を統一するためのruleがあるので.textlintrcを以下の通り修正し、無効化しています。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
      "max-ten": {
        "max": 4
      },
      "max-kanji-continuous-len": {
        "max"  : 5,
        "allow": ["漢字百科大事典"]
      },
      "arabic-kanji-numbers": false,
    },
    "preset-jtf-style": {
      "1.1.1.本文": false,
      "1.1.3.箇条書き": false,
      "2.1.5.カタカナ": true,
      "2.1.6.カタカナの長音": true
    },
  }
}

この状態で試していきましょう。

textlintのコマンドを実行すると以下の通り、エラーとなります。

f:id:tanabebe:20190929155806p:plain

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-no-mix-dearu-desumasu/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-no-mix-dearu-desumasu/file.md
  3:19  error  見出し: "である"調 と "ですます"調 が混在
=> "である" がである調
Total:
である  : 1
ですます: 1
    ja-technical-writing/no-mix-dearu-desumasu
  6:15  error  本文: "である"調 と "ですます"調 が混在
=> "である。" がである調
Total:
である  : 1
ですます: 1
      ja-technical-writing/no-mix-dearu-desumasu
  8:20  error  箇条書き: "である"調 と "ですます"調 が混在
=> "です。" がですます調
Total:
である  : 1
ですます: 1
  ja-technical-writing/no-mix-dearu-desumasu

✖ 3 problems (3 errors, 0 warnings)

見出し、本文、箇条書きである調とですます調が混在していることのチェックが行われていることが確認出来ました。

設定値の変更方法

次はrule文末以外でも厳しくチェックするよう.textlintrcファイルを以下のように修正します。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
      "max-ten": {
        "max": 4
      },
      "max-kanji-continuous-len": {
        "max"  : 5,
        "allow": ["漢字百科大事典"]
      },
      "arabic-kanji-numbers": false,
      "no-mix-dearu-desumasu": {
        "preferInHeader": "",
        "preferInBody": "ですます",
        "preferInList": "である",
        "strict": true
      }
    },
    "preset-jtf-style": {
      "1.1.1.本文": false,
      "1.1.3.箇条書き": false,
      "2.1.5.カタカナ": true,
      "2.1.6.カタカナの長音": true
    },
  }
}

これで文末以外のチェックも行うので、先程よりエラーの内容が増えるはずです。では試していきます。

textlintのコマンドを実行すると以下の通り、エラーとなります。

f:id:tanabebe:20190929161156p:plain

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-no-mix-dearu-desumasu/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-no-mix-dearu-desumasu/file.md
  3:19  error  見出し: "である"調 と "ですます"調 が混在
=> "である" がである調
Total:
である  : 1
ですます: 1
    ja-technical-writing/no-mix-dearu-desumasu
  6:15  error  本文: "である"調 と "ですます"調 が混在
=> "である。" がである調
Total:
である  : 1
ですます: 2
      ja-technical-writing/no-mix-dearu-desumasu
  8:20  error  箇条書き: "である"調 と "ですます"調 が混在
=> "です。" がですます調
Total:
である  : 2
ですます: 2
  ja-technical-writing/no-mix-dearu-desumasu
  9:8   error  箇条書き: "である"調 と "ですます"調 が混在
=> "ですが、" がですます調
Total:
である  : 2
ですます: 2
  ja-technical-writing/no-mix-dearu-desumasu

✖ 4 problems (4 errors, 0 warnings)

チェックが厳しくなっているので、エラーが1つ増えているのとである調ですます調の接続的な文についてのカウント数が増えているのがわかります。文章を厳密にチェックするというのであればとても有用ですね。

チェックの例外

このruleについてのチェック例外は言及されていないため、なしと考えて良いでしょう。

textlint-rule-ja-no-mixed-period

このrule文末の句点の抜けについてパラグラフを対象にチェックします。

動きを試す前にtextlint-rule-preset-JTF-styleには本文と箇条書きに対し、句読点に関する4.1.3.ピリオド(.)カンマ(,)ruleがあるので.textlintrcを以下の通り修正し、無効化しています。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
      "max-ten": {
        "max": 4
      },
      "max-kanji-continuous-len": {
        "max"  : 5,
        "allow": ["漢字百科大事典"]
      },
      "arabic-kanji-numbers": false,
      "no-mix-dearu-desumasu": {
        "preferInHeader": "",
        "preferInBody": "ですます",
        "preferInList": "である",
        "strict": true
      }
    },
    "preset-jtf-style": {
      "1.1.1.本文": false,
      "1.1.3.箇条書き": false,
      "1.2.1.句点(。)と読点(、)" :false,
      "2.1.5.カタカナ": true,
      "2.1.6.カタカナの長音": true,
      "4.1.3.ピリオド(.)、カンマ(,)": false
    },
  }
}

この状態で試していきましょう。

f:id:tanabebe:20190929164947p:plain

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-ja-no-mixed-period/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-ja-no-mixed-period/file.md
  5:15  error    文末が""で終わっていません。  ja-technical-writing/ja-no-mixed-period
  7:16  ✓ error  文末が""で終わっていません。  ja-technical-writing/ja-no-mixed-period

✖ 2 problems (2 errors, 0 warnings)1 fixable problem.

文末の句点の抜けについてチェックが行われていることが確認出来ました。

設定値の変更方法

次はチェックのrule文末の句点(半角ピリオド)になるよう.textlintrcファイルを以下のように修正します。

{
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": {
        "max": 90
      },
      "max-comma": {
        "max": 4
      },
      "max-ten": {
        "max": 4
      },
      "max-kanji-continuous-len": {
        "max"  : 5,
        "allow": ["漢字百科大事典"]
      },
      "arabic-kanji-numbers": false,
      "no-mix-dearu-desumasu": {
        "preferInHeader": "",
        "preferInBody": "ですます",
        "preferInList": "である",
        "strict": true
      },
      "ja-no-mixed-period": {
        "periodMark": "."
      },
    },
    "preset-jtf-style": {
      "1.1.1.本文": false,
      "1.1.3.箇条書き": false,
      "1.2.1.句点(。)と読点(、)" :false,
      "2.1.5.カタカナ": true,
      "2.1.6.カタカナの長音": true,
      "4.1.3.ピリオド(.)、カンマ(,)": false
    },
  }
}

これで文末に.(半角ピリオド)が存在しないとエラーとなるはずです。では試していきます。

textlintのコマンドを実行すると以下の通り、エラーとなります。

f:id:tanabebe:20190929165009p:plain

~/rule-sandbox ❯❯❯ npx textlint textlint-rule-ja-no-mixed-period/file.md

/Users/n_tanabe/rule-sandbox/textlint-rule-ja-no-mixed-period/file.md
   5:15  error    文末が"."で終わっていません。  ja-technical-writing/ja-no-mixed-period
  12:15  ✓ error  文末が"."で終わっていません。  ja-technical-writing/ja-no-mixed-period

✖ 2 problems (2 errors, 0 warnings)1 fixable problem.

先程エラーになった7行目がエラーにならない事が確認出来ました。これで文末の句点の変更が可能となります。

チェックの例外

このruleについてのチェック例外は言及されていないため、なしと考えて良いでしょう。

まとめ

textlint-rule-preset-ja-technical-writingruleを掘り下げる事で各ruleのカスタマイズ方法が理解出来ました。私の場合textlintをインストールしたものの扱いがわからないruleなどがあったので、根気良くドキュメントを眺めたりnode_module内のプログラムを眺めたりし、理解を深めました。当記事で紹介したrule以外をインストールした場合にぶつかり合うruleが今後出てくる可能性はあるので、各ruleに慣れておくことは自分なりのruleを定義する上で非常に重要だと言えます。

参考

github.com

github.com

github.com

github.com

github.com

github.com

github.com

github.com