Paepoi

Paepoi » Python Tips » INI ファイルの読み書き

INI ファイルの読み書き

最終更新日 2023.02.02

- SafeConfigParser 廃止による書き換え
- GTK3 コードの GTK4 化
- 文字列のシングルクォート統一や正規表現採用等

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

configparser
configparser は Python に標準で含まれているモジュールです。
以下は GtkWindow のサイズを保存して再起動時に再現させるサンプルです。
カレントディレクトリに test.conf を作成し読み書きします。
#! /usr/bin/env python3

import os, configparser, gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk

INI = 'test.conf'

class Win(Gtk.ApplicationWindow):
    '''
        ini(conf) の読み書き例
        Window のサイズを保存と復元
    '''
    def __init__(self, a):
        '''
            起動時に ini を読み込む
        '''
        Gtk.ApplicationWindow.__init__(self, application=a, title='ini')
        # 設定ファイルを探す
        if os.path.exists(INI):
            # configparser を作成し読み込む
            conf = configparser.ConfigParser()
            conf.read(INI)
            # 後での追記を考えて has_opthon しておこう
            # アプリを更新していくと同じセクションにキーを追記する場合が多い
            cx = 200
            cy = 200
            if conf.has_option('window', 'width'):
                cx = conf.getint('window', 'width')
            if conf.has_option('window', 'height'):
                cy = conf.getint('window', 'height')
            self.set_default_size(cx, cy)

    def do_close_request(self):
        '''
            GTK3 では do_delete_event
            終了時に ini に書き込み
        '''
        conf = configparser.ConfigParser()
        if os.path.exists(INI):
            conf.read(INI)
        # [window] セクションが存在しなければ追加
        if not 'window' in conf.sections():
            conf.add_section('window')
        # サイズを取得(タプルで戻る)して conf にセット
        cx, cy = self.get_default_size()
        conf.set('window', 'width', str(cx))
        conf.set('window', 'height', str(cy))
        # ファイルに書き込む
        with open(INI, 'w') as f:
            conf.write(f)
        return False

app = Gtk.Application()
app.connect('activate', lambda a: Win(a).present())
app.run()

内容が以下のような test.conf ファイルが作成されたなら成功です。
[window]
top = 522
left = 968
width = 552
height = 19

自分で読み書きクラスを作成
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'
INI_SECTION = r'^\[[^\]]+\]$'

import os, re

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
            with open(self._filename) as f:
                for linenn in f:
                    line = linenn.strip() # remove LF
                    if line == '':
                        continue
                    if section_in:
                        if '=' in line:
                            pos = line.index('=')
                            if key == line[:pos]:
                                return line[pos+1:]
                        if re.search(INI_SECTION, line):
                            return None
                    if re.search(INI_SECTION, line):
                        if section == line[1:-1]:
                            section_in = True
        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):
            with open(filename) as f:
                lines = f.read().split('\n')
                section = ''
                for line in lines:
                    if line == '':
                        continue
                    if re.search(INI_SECTION, line):
                        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 = f';{self._header}\n\n'
        for dic1 in self._ini:
            for section in dic1.keys():
                s += f'[{section}]\n'
                for dic2 in dic1[section]:
                    for key in dic2.keys():
                        s += f'{key}={dic2[key]}\n'
                s += '\n'
        if s != '':
            with open(self._filename, 'w') as f:
                f.write(s)

    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.tar.xz (1.6KB)
これに inifile8.py という名前を付けてテストコードと同じディレクトリに置き書き換えてみる。

#! /usr/bin/env python3

import os, inifile8, gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk

INI = 'test.ini'

class Win(Gtk.ApplicationWindow):
    '''
        自作 class での ini(conf) の読み書き例
        Window の位置と大きさを保存と復元
    '''
    def __init__(self, a):
        '''
            起動時に ini を読み込む
        '''
        Gtk.ApplicationWindow.__init__(self, application=a, title='ini2')
        # 設定ファイルを探す
        if os.path.exists(INI):
            conf = inifile8.Inifile(INI)
            try:
                cx = conf.read_int('window', 'width', 200)
                cy = conf.read_int('window', 'height', 200)
                self.set_default_size(cx, cy)
            except Exception as e:
                print(e)

    def do_close_request(self):
        '''
            GTK3 では do_delete_event
            終了時に ini に書き込み
        '''
        conf = inifile8.Inifile(INI)
        try:
            cx, cy = self.get_default_size()
            conf.write_int('window', 'width', cx)
            conf.write_int('window', 'height', cy)
            conf.save()
        except Exception as e:
            print(e)
        return False

app = Gtk.Application()
app.connect('activate', lambda a: Win(a).present())
app.run()

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

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

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

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

ということで、例の一つでした。

Copyright(C) sasakima-nao All rights reserved 2002 --- 2024.