はじめに
何かソフトウェアを作ったとき、メモ帳などでソフトの振る舞いを簡単に変えられるようにすることは多いです。
昔だとxxxxx.ini
みたいなファイルに設定が書いてあったり、今だとJSON使って設定を記述して、ソフトの振る舞いを変えられるようにしています。
しかし、そこでいつも問題になるのは、設定ファイルの変更手順が設定ファイルに直接書けないというところです。
以下のような努力である程度は解決しますが、不足する場面があります。
Pythonでコメント付きのJSONファイル(JSONC)を読み込む
何が不満なのか?
簡単なソフトを作って配布する場面が増えてきました。
そして、人に渡すならドキュメントが必要です。
設定ファイルを弄りたいユーザーの立場
- 設定ファイルの書き方、どこにあったっけ?探すの面倒。
- なんかコメント書いてあるけど、これだけじゃ分からん。しかも長すぎて読む気がしない!
- え?なに?詳細は別のエクセルファイルを見ないといけないの?勘弁して!
- ドキュメントが古くて設定のやり方書いてないんだけど・・・
ソフトを作る開発者の立場
- 設定の仕様を変えたけど、ドキュメントのメンテナンス忘れてた😛
- 画像で一発で伝わるものをコメントの文章で書くのは辛い・・・
- それなりにリッチなドキュメントにしたいけど、Git管理はやりたいよね(バイナリデータNG)
みたいな贅沢な話が出てきます。
際限ありませんね。
Markdownを設定ファイルに使うという発想
正直、この発想はどうかと思うところはあるのですが、
個人で作る簡単なソフトや、即席ツールとか、素人が使う前提のソフトとか、
細かいことを気にしない用途で結構ありなんじゃないか?とは思っています。
しかし、Markdownは軽量マークアップ言語であって、設定データを記述することには向かないです。
でも、データ構造を記述できる、YAML Front Matterという仕様が存在していて、
多くのMarkdownパーサはエラーなくその部分を処理できます。
YAML Front Matter
さて、このFront Matterですが、色々使われています。
例えばPandocだと書き方としては以下のような感じです。
---
author: 筆者の名前
title: 文章のタイトル
---
ここ以降に本文を記載
ファイルの先頭から---
で挟んだブロックを用意して、文章のメタデータをYAMLで記載します。
ドキュメントのメタデータを埋め込んだり、それを処理するソフトへ指示を出しています。
じゃあ設定ファイルに使えますね
設定ファイル用Markdownファイルのイメージ
つまり、やりたいことは以下のファイルから設定値を取得できることです。
---
log_path: './log'
log_enable: true
date: 2022-08-01 22:05
offset: 20.5
---
# 適当な設定ファイル
ここに設定ファイルの概要説明
## 設定項目一覧
- [log_path](#log_enable)
- [log_enable](#log_enable)
- [date](#date)
- [offset](#offset)
### log_path
出力先のファイルパス。相対指定。
### log_enable
ログ出力の有効/無効を設定。
- true: 有効
- false: 無効
### date
基準になる日付を設定
### offset
センサのオフセット値を設定
そうそうこんな感じ。
TeXやmermaid記法で数式や図形も書けそうな雰囲気です。
処理の検討
正直、上記の例は大した中身ではないので、自前でパーサを組んでも良いのですが、
やはりここはプログラマらしく怠けた実装で行きたいと思います。
- ファイルを読み込む
- Front Matterブロックを抽出する
- その辺の(ディファクトスタンダードの)YAMLパーサで解釈する
ファイルを読み込むのは適当にその辺のサンプルをコピペすることにして、
どうやって抽出するか悩むところです。
まぁ、安直に正規表現ですかね。
ちなみに、Front Matterを読み込むモジュール自体はpypiにあります
python-frontmatter 1.0.0
まぁ今回は大した実装にならないのと、素性の良さを優先して自前実装で行きます。
Pythonによる実装
抽出処理
1行ずつ読み込んでハイフンがあるかどうかとか見つけるのは辛いので、
まとめて読み込んで正規表現でマッチさせます。
ここで、最初と最後の---
の部分は不要なので、
サブパターンを使って欲しいところを抽出します。
import os
import re
ptt = r"^---*[\r\n]*([\s\S]*?)---*[\r\n]"; # パターン
filepath = "setting.md"
re_text = None
with open(filepath, 'r', encoding='utf-8') as f: # 開く
text = f.read() # 文字列を取得
re_text = re.match(ptt, text).groups()[0] # サブパターン部分の文字列を取得
print(re_text) # 確認
全体のパターンは^---*[\r\n]*([\s\S]*?)---*[\r\n]
で、
文字列として欲しいのは括弧の部分([\s\S]*?)
なので、re.match(ptt, text).groups()[0]
で
括弧の中身を取り出します。
以下の文字列が得られます
log_path: './log'
log_enable: true
date: 2022-08-01 22:05
offset: 20.5
構文解析
抽出したものは純粋にYAMLなので、YAMLパーサに入れます。
使用するのはたぶんディファクトスタンダードのPyYaml
です。
無い場合、pip install pyyaml
でインストールできます。
Pythonでは以下の構文で読み込むことができるようです。
import yaml
with open("ファイル名") as f:
yamldat = yaml.safe_load(f)
昔はload()
を使っていたらしいですが、セキュリティ上の理由からsafe_load()
を使うことが推奨みたいです。
見て分かる通り、ストリームを渡さないといけないようで、このままでは文字列を解釈できません。
ということで、一旦文字列をファイル同じような状態にしてから読み込む必要があります。
とはいえ、読み込むためだけにHDD上にファイルを作ると無駄が大きいので、
メモリ上にストリームを用意して解決することにします。
(C#でいうところのMemoryStream
ですね。)
使うのは、io.StringIO()
です。
使い方は以下の通り。
from io import StringIO()
with StringIO() as st:
st.write(re_text) # ストリームへ文字列を書き込み
st.seek(0) # ストリームのカーソル位置を先頭に移動
yaml_meta = yaml.safe_load(st) # YAMLを解釈
最終的なコード
ここまでのコードを連結して関数化すると、以下のような感じになります。
import re
import yaml
from io import StringIO
def get_md_frontmatter(filepath, enc='utf-8'):
ptt = r"^---*[\r\n]*([\s\S]*?)---*[\r\n]"; # パターン
# Front Matterの抽出
re_text = None
with open(filepath, 'r', encoding=enc) as f: # 開く
text = f.read() # 文字列を取得
re_text = re.match(ptt, text).groups()[0] # Front Matterを抽出
# YAMLパーサ
yaml_meta = None
with StringIO() as st: # メモリストリームを初期化
st.write(re_text) # ストリームへ文字列を書き込み
st.seek(0) # ストリームのカーソル位置を先頭に移動
yaml_meta = yaml.safe_load(st) # YAMLを解釈
return yaml_meta # 解釈結果を返す
別のファイルからfrom .get_md_frontmatter import get_md_frontmatter
みたいに宣言して使うことができますね。
C#による実装
元気があるときに執筆します
まとめ
Markdownファイルを設定ファイルとして使うための準備が整いました。
今回は正規表現で抽出しているので、もう少しコードを書けば、
設定を保存することも可能なので、JSONにコメントを入れる方式ではできなかったことも可能になります。
YAML Front MatterのところはYAMLのサブセットとも言えるJSONで書いても成立しますね。
ちょっと見慣れない感じにはなりますが、メンテナンス性は良さそうです。
また、別に設定ファイルとして使わなくても、メタデータを使って自動処理を組む場合などに今回のコードは参考になる?かもしれません。
以上です。
ここまで長々と記事を読んでいただきありがとうございました。