0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

configparser を使わずに INI パーサーを自作した

0
Last updated at Posted at 2026-05-04

きっかけ

Python には標準ライブラリに configparser がある。普通に使える。でもいくつか不満がある。キー名をデフォルトで小文字にする。重複セクションを黙ってマージする。コメントを保持しない。行番号を教えてくれない。そして出力は ConfigParser オブジェクトであり、jq にパイプできるデータではない。

INI ファイルを「設定オブジェクト」ではなく「構造化データ」として扱いたかった。パースした結果を JSON で出力し、セクション・キー・値・コメント・行番号をすべてメタデータとして保持する。重複セクションや重複キーはマージせずにエラーにする。型推論で port = 8080 は文字列ではなく整数として返す。

そういうパーサーを Python でゼロから書いた。依存ゼロ、テスト49件。

🔗 GitHub: https://github.com/sen-ltd/ini-parser

Screenshot

INI フォーマットの曖昧さ

INI には正式な仕様がない。JSON(RFC 8259)や TOML にはスペックがあるが、INI は「慣習」と「使っているパーサーの挙動」で定義されている。Windows の GetPrivateProfileString、Python の configparser、PHP の parse_ini_file、Git の config フォーマット、全部細部が違う。

共通部分はある。[section] でセクション、= でキーと値の分離、;# でコメント、空行は無視。しかしそれ以降は分岐する。セクション外のキーは許すか?キーは大文字小文字を区別するか?複数行の値は?インラインコメントは?重複キーは?

ini-parser では各問題に対して明示的に方針を決め、曖昧なものはエラーにした。

アーキテクチャ

パーサーは3つのモジュールに分かれている。

  • types.py — 型推論(文字列 → int / float / bool / string)
  • parser.py — 1行ずつパースしてデータ構造を構築
  • serializer.py — データ構造を INI テキストに戻す

型推論

INI ファイルはすべてを文字列として保存するが、port = 8080 をパースしたら整数 8080 が欲しい。型推論モジュールがこれを担当する。

_BOOL_TRUE = frozenset({"true", "yes", "on", "1"})
_BOOL_FALSE = frozenset({"false", "no", "off", "0"})
_INT_RE = re.compile(r"^[+-]?\d+$")
_FLOAT_RE = re.compile(r"^[+-]?(\d+\.\d*|\.\d+)([eE][+-]?\d+)?$")

def infer_type(value: str) -> dict[str, Any]:
    stripped = value.strip()
    if stripped.lower() in _BOOL_TRUE:
        return {"raw": value, "value": True, "type": "bool"}
    if stripped.lower() in _BOOL_FALSE:
        return {"raw": value, "value": False, "type": "bool"}
    if _INT_RE.match(stripped):
        return {"raw": value, "value": int(stripped), "type": "int"}
    if _FLOAT_RE.match(stripped):
        return {"raw": value, "value": float(stripped), "type": "float"}
    return {"raw": value, "value": stripped, "type": "string"}

チェック順序が重要で、"1""0" は bool と int の両方に該当するが、INI ファイルでは enabled = 1 は「true」の意味がほとんどなので、bool を先に判定する。すべての結果に元の文字列 (raw) を含めるので、情報を失わない。

1行ずつのパーサー

パーサーは入力を1行ずつ読み、状態を管理する。現在のセクション、直前のキー(複数行の継続用)、既出のセクションとキー(重複検出用)。

各行は固定の優先順で1つのパターンにマッチする。空行 → コメント → セクションヘッダ → 継続行 → キーバリュー → エラー。どれにもマッチしない行は INIParseError を行番号付きで出す。バックトラッキングなし、推測なし。

複数行の値

先頭が空白の行は、直前のキーの継続行として扱う。

[server]
description = This is a long
    description that spans
    multiple lines

空行を挟むと継続がリセットされる。これがないと、ファイル内のどこかにあるインデントされた行がすべて直前のキーに結合されてしまう。

重複検出

重複セクションと重複キーの両方をパースエラーとして扱う。configparser は重複セクションを黙ってマージするが、それだとセクション名のタイプミスで意図しないセクションにキーが混入しても気づけない。

CLI インターフェース

4つのモードに対応している。

# デフォルト: JSON に変換
python main.py config.ini

# バリデーション(exit 0 = OK, exit 2 = NG)
python main.py --validate config.ini

# 特定の値を取得
python main.py --get server.port config.ini

# 値を変更(ファイルを直接書き換え)
python main.py --set server.port=9090 config.ini

--set は特筆に値する。パースして構造を変更してシリアライズし直すのではなく、元のテキストの行配列を直接操作する。これにより、フォーマット・空行・コメントの位置がすべて保持される。変わるのは対象キーの行だけ。

JSON 出力の構造

出力はパーサーが知っているすべてを保持する。

{
  "sections": {
    "server": {
      "keys": {
        "port": {
          "raw": "8080",
          "value": 8080,
          "type": "int",
          "inline_comment": "default HTTP port",
          "line": 6
        }
      },
      "line": 4
    }
  },
  "comments": [
    { "text": "Application configuration", "line": 1 }
  ],
  "globals": { "keys": {}, "line": 0 }
}

各キーは raw(元の文字列)と value(型推論後の値)の両方を持つ。行番号はセクションヘッダとキーの両方に付く。この構造があれば、lint ツールや diff ツールを上に構築するのは簡単。

テスト

テストは49件。基本的なパース、コメント、型推論、複数行、重複検出、エッジケース(BOM 処理、Unicode キー、値の中の =)、シリアライザ、CLI の全モードをカバーしている。

def test_bom_handling(self):
    ini = "\ufeff[s]\nk = v"
    data = parse(ini)
    assert "s" in data["sections"]

def test_unicode_keys(self):
    ini = "[s]\n名前 = テスト"
    data = parse(ini)
    assert data["sections"]["s"]["keys"]["名前"]["value"] == "テスト"

def test_equals_in_value(self):
    data = parse("[s]\nformula = a=b+c")
    assert data["sections"]["s"]["keys"]["formula"]["value"] == "a=b+c"

BOM は Windows のエディタが UTF-8 ファイルの先頭に付けることがある。これを処理しないと最初のセクション名がおかしくなる。

おわりに

パーサーをゼロから書くと、ユーザーとしては気にしないエッジケースを全部考えることになる。BOM、重複キー、継続行の終端条件。それぞれが設計判断であり、フォーマットの理解を深めてくれる。

JSON 出力にしたことで jqgrep と組み合わせる Unix ツールになった。--validate フラグと exit code 2 で CI にも組み込める。

ソースは GitHub にある。MIT ライセンス、依存ゼロ、Python 3.10+。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?