L'Isola di Niente
L'Isola di Niente » Python Tips » INI ファイルの読み書き

INI ファイルの読み書き

ini ファイル(config ファイル)を読み書きする一例。
configparser モジュールを使う一般的方法、及び筆者が利用している自前クラスの公開
自前クラスはライセンスフリーとしておきます。

2013.11.30 @ Python3 化

configparser

configparser は Python に標準で含まれているモジュールです。

以下は Window の位置と大きさを保存して再起動時に再現させる方法の例です。
カレントディレクトリに test.conf を作成し読み書きします。
#! /usr/bin/env python3

from gi.repository import Gtk
import os
import configparser # Python2 は import ConfigParser

INI = "test.conf"

class Win(Gtk.Window):
    """
        ini(conf) の読み書き例
        Window の位置と大きさを保存と復元
    """
    def __init__(self):
        """
            起動時に ini を読み込む
        """
        Gtk.Window.__init__(self)      
        self.connect("delete-event", self.on_quit) 
        # 設定ファイルを探す
        if os.path.exists(INI):
            # configparser を作成し読み込む
            conf = configparser.SafeConfigParser()
            conf.read(INI)
            # 後での追記を考えて has_opthon しておこう
            x, y, cx, cy = 0, 0, 200, 200
            if conf.has_option("window", "left"):
                x = conf.getint("window", "left")
            if conf.has_option("window", "top"):
                y = conf.getint("window", "top")
            self.move(x, y)
            if conf.has_option("window", "width"):
                cx = conf.getint("window", "width")
            if conf.has_option("window", "height"):
                cy = conf.getint("window", "height")
            self.resize(cx, cy)
        self.show_all()

    def on_quit(self, widget, data=None):
        """
            終了時に ini に書き込み
        """
        conf = configparser.SafeConfigParser()
        if os.path.exists(INI):
            conf.read(INI)
        # [window] セクションが存在しなければ追加
        if not "window" in conf.sections():
            conf.add_section("window")
        # 位置とサイズを取得(タプルで戻る)して conf にセット
        x, y = self.get_position()
        conf.set("window", "left", str(x))
        conf.set("window", "top", str(y))
        cx, cy = self.get_size()
        conf.set("window", "width", str(cx))
        conf.set("window", "height", str(cy))
        # ファイルに書き込む
        f = open(INI, "w")
        conf.write(f)
        f.close()
        # 書き込み終了
        Gtk.main_quit()
        return True

if __name__ == "__main__":
    Win()
    Gtk.main()

[window]
top = 522
left = 968
width = 552
height = 19
のような test.conf ファイルが作成されたなら成功です。
configparser に関する詳しい使い方は Python のヘルプを見てください。

一つだけ強調すると読み込みは必ず has_option でキーの存在を調べてからやりましょう。
経験上アプリを更新していくと同じセクションにキーを追記する場合が多いです。

自分で読み書きクラスを作成

ini ファイル読み書き程度の単純な処理が自作できない人に何が作れるの?
と問われて困らないために筆者が作ったクラスを例として公開。

通常なら configparser で十分だと思うけどいくつか困ることがある。

一番困るのが値を全自動でソートしてしまうこと。
読み書きの高速化手法だろうけど一部の順番の保持必須アプリには使えない。

それに boolean 値を True 等と文字列で保存しようとする。
通常 0 or 1 なのでアプリケーション側で変換する必要がある。

読み込み時に毎回 has_option でキーの存在確認処理を書くのが面倒。
キーが存在しなかった場合の初期値を引数に指定できればこんな作業は不要になる。

セクションの追加処理が別個になっているのはスペルミス防止のためだろうけどやはり面倒。

他に Opera だけかもしれないけど ini にコメントアウトではないヘッダが付いている場合がある。
その場合 configparser は不正な ini として処理されてしまう。

それなら自前で読み書きするクラスを作ってしまったほうが早い。

他に読み書きに失敗した場合やスペルミス防止に例外処理を追加したい。
たとえば def write_int(self, section, key, value) なんてメソッドを作ったとする。
ココで value が int でない時には例外を投げると整数で保存したいのについ実数なんてコトが防げる。
try:
    ini.write_int("window", "video_size", 1.0)
    # 保存
    ini.save()
except Exception as e:
    print e
なんて感じにすれば簡単に間違いが発見できる。

それと上記のように保存メソッドを作れば本体側でファイルを open する処理が省ける。
しかし上記のような間違いは例外を投げるだけにして本体で処理したほうが使いまわしが楽になる。

最後に一部だけ読み込みたいために全部展開してしまうのも無駄な処理になる。
それなら展開を行わず読み込みだけを行う別のクラスを作成してしまうのが一番早い。
クラスは継承できるのでインターフェイスのようなスケルトンを作ってみよう。

という私的な思想を全部やってみたのが以下。
#!/usr/bin/env python3

ERROR_READ = "Configuration file READ ERROR\n[{0}]\n{1}=\nvalue is not {2}"
ERROR_WRITE = "Configuration file WRITE ERROR\n({0}, {1}) value is not {2}"
ERROR_HEADER = "Configuration file header value is not str"

import os

class _InifileBase():
    """
        This class is Inheritance Base
    """
    def __init__(self, filename):
        """
            Attribute is the file name only
        """
        self._filename = filename

    def _get_value(self, section, key):
        """
            This Methods is Inheritance Base
        """
        return None

    def read_int(self, section, key, default):
        """
            @param default: int.
        """
        result = self._get_value(section, key)
        if result == None:
            return default
        try:
            return int(result)
        except:
            raise ValueError(ERROR_READ.format(section, key, "int"))
    
    def read_float(self, section, key, default):
        """
            @param default: float.
        """
        result = self._get_value(section, key)
        if result == None:
            return default
        try:
            return float(result)
        except:
            raise ValueError(ERROR_READ.format(section, key, "float"))
    
    def read_bool(self, section, key, default):
        """
            @param default: bool.
        """
        result = self._get_value(section, key)
        if result == None:
            return default
        if result == "1":
            return True
        elif result == "0":
            return False
        else:
            raise ValueError(ERROR_READ.format(section, key, "bool"))
    
    def read_str(self, section, key, default):
        """
            @param default: str.
        """
        result = self._get_value(section, key)
        if result == None:
            return default
        return result


class InifileReader(_InifileBase):
    """
        This class is read-only
        ini files to load faster
    """
    def __init__(self, filename):
        """
            @param filename: the full path name.
        """
        _InifileBase.__init__(self, filename)

    def _get_value(self, section, key):
        """
            Read using the file stream
        """
        if os.path.exists(self._filename):
            section_in = False
            f = open(self._filename)
            try:
                for linenn in f:
                    line = linenn.strip()
                    if line == "":
                        continue
                    if section_in:
                        if "=" in line:
                            pos = line.index("=")
                            if key == line[:pos]:
                                return line[pos+1:]
                        if len(line) > 2 and line[0] =="[" and line[-1] == "]":
                            return None
                    if len(line) > 2 and line[0] =="[" and line[-1] == "]":
                        if section == line[1:-1]:
                            section_in = True
            finally:
                f.close()
        return None


class Inifile(_InifileBase):
    """
        ini file to read and write class.
    """
    def __init__(self, filename):
        """
            Loading degradation inifile
            @param filename: the full path name.
        """
        _InifileBase.__init__(self, filename)
        self._header = ""
        self._ini = []
        if os.path.exists(filename):
            f = open(filename)
            x = f.read()
            f.close()
            lines = x.split("\n")
            section = ""
            for line in lines:
                if line == "":
                    continue
                if len(line) > 2 and line[0] =="[" and line[-1] == "]":
                    section = line[1:-1]
                elif section == "":
                    pass # Nothing
                elif "=" in line:
                    pos = line.index("=")
                    self._add(section, line[:pos], line[pos+1:])
    
    def _add(self, section, key, value):
        """
            Add to contents.
        """
        for dic1 in self._ini:
            if section in dic1.keys():
                for dic2 in dic1[section]:
                    if key in dic2.keys():
                        dic2[key] = value
                        return
                dic1[section].append({key: value})
                return
        self._ini.append({section: [{key: value}]})
    
    def _get_value(self, section, key):
        """
            Get to contents.
        """
        for dic1 in self._ini:
            if section in dic1.keys():
                for dic2 in dic1[section]:
                    if key in dic2.keys():
                        return dic2[key]
        return None
    
    def save(self):
        """
            Save the contents.
        """
        s = ""
        if self._header:
            s += self._header
        for dic1 in self._ini:
            for section in dic1.keys():
                s += "[{0}]\n".format(section)
                for dic2 in dic1[section]:
                    for key in dic2.keys():
                        s += "{0}={1}\n".format(key, dic2[key])
                s += "\n"
        if s != "":
            f = open(self._filename, "w")
            f.write(s)
            f.close()

    def add_header(self, header):
        """
            @param headre: str.
        """
        if type(header) is str:
            self._header = header
        else:
            raise ValueError(ERROR_HEADER)

    def section_exists(self, section):
        """
            Check existence of section.
        """
        for dic1 in self._ini:
            if section in dic1.keys():
                return True
        return False
    
    def erase_section(self, section):
        """
            Remove the specified section.
        """
        for dic1 in self._ini:
            if section in dic1.keys():
                dic1.pop(section)
                return True
        return False

    def erase_key(self, section, key):
        """
            Erase Key
        """
        for dic1 in self._ini:
            if section in dic1.keys():
                for dic2 in dic1[section]:
                    if key in dic2.keys():
                        dic2.pop(key)
                        return True
        return False

    def write_int(self, section, key, value):
        """
            @param value: int.
        """
        if type(value) is int:
            self._add(section, key, str(value))
        else:
            raise ValueError(ERROR_WRITE.format(section, key, "int"))
    
    def write_float(self, section, key, value):
        """
            @param value: float.
        """
        if type(value) is float:
            self._add(section, key, str(value))
        else:
            raise ValueError(ERROR_WRITE.format(section, key, "float"))
    
    def write_bool(self, section, key, value):
        """
            @param value: bool.
        """
        if type(value) is bool:
            if value:
                self._add(section, key, "1")
            else:
                self._add(section, key, "0")
        else:
            raise ValueError(ERROR_WRITE.format(section, key, "bool"))
    
    def write_str(self, section, key, value):
        """
            @param value: str.
        """
        if type(value) is str:
            self._add(section, key, value)
        else:
            raise ValueError(ERROR_WRITE.format(section, key, "str"))

これに inifile8.py という名前を付けてテストコードと同じディレクトリに置き書き換えてみる。

#! /usr/bin/env python3

from gi.repository import Gtk
import os
import inifile8

INI = "test2.conf"
HEAD = ";Header\n;Test\n\n"

class Win(Gtk.Window):
    """
        ini(conf) の読み書き例
        Window の位置と大きさを保存と復元
    """
    def __init__(self):
        """
            起動時に ini を読み込む
        """
        Gtk.Window.__init__(self)      
        self.connect("delete-event", self.on_quit) 
        # 設定ファイルを探す
        if os.path.exists(INI):
            # Inifile を作成し読み込む
            conf = inifile8.Inifile(INI)
            try:
                x = conf.read_int("window", "left", 0)
                y = conf.read_int("window", "top", 0)
                self.move(x, y)
                cx = conf.read_int("window", "width", 200)
                cy = conf.read_int("window", "height", 200)
                self.resize(cx, cy)
            except Exception as e:
                self.messagebox(e)
        self.show_all()

    def on_quit(self, widget, event=None):
        """
            終了時に ini に書き込み
        """
        conf = inifile8.Inifile(INI)
        # 位置とサイズを取得(タプルで戻る)して conf にセット
        try:
            x, y = self.get_position()
            conf.write_int("window", "left", x)
            conf.write_int("window", "top", y)
            cx, cy = self.get_size()
            conf.write_int("window", "width", cx)
            conf.write_int("window", "height", cy)
            # ファイルに書き込む
            conf.add_header(HEAD)
            conf.save()
        except Exception as e:
            self.messagebox(e)
        # 書き込み終了
        Gtk.main_quit()
        return True

    def messagebox(self, message):
        dlg = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL,
            Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, message)
        dlg.run()
        dlg.destroy()

if __name__ == "__main__":
    Win()
    Gtk.main()

self._ini は [{section: [{key: value}]}] という「辞書のリストの辞書のリスト」である。
辞書の辞書にするだけで階層は作れるのですが辞書の仕様で順番が保持されないので一旦リストで挟む方法にしています。

その「辞書のリストの辞書のリスト」に _add メソッドで追記、_get_value メソッドで取り出し。
Python ではこの階層を全部 in 演算子で辿れるので理解しやすいと感じます。

その他はメソッド名を見ればわかるとおりです。
書き込みは type() で値を調べて違っていれば容赦なく例外を投げます。
読み込みは変換に失敗すると例外が渡されるので文字列を生成して呼び出し元に raize します。

イコールの前後に空白を付けないのは仕様です。

ということで、例の一つでした。
Copyright(C) sasakima-nao All rights reserved 2002 --- 2017.