用ConfigParser更改INI文件的格式后是否可以保留?

问题描述 投票:0回答:3

ConfigParser
是否有可能保留
INI
配置文件的格式?我有配置文件,其中包含注释和特定的
section
/
option
名称,如果读取并更改文件的内容,则
ConfigParser
重新格式化它(我可以解决
section
/
option
名称)。

我熟悉

ConfigParser
的工作方式(将键/值对读取到
dict
并在更改后将其转储到文件中)。但我很感兴趣是否有解决方案可以保留
INI
文件中的原始格式和注释。

示例:

test.ini

# Comment line
; Other Comment line
[My-Section]
Test-option = Test-Variable

test.py

import configparser as cp

parser: cp.ConfigParser = cp.ConfigParser()
parser.read("test.ini")

parser.set("My-Section", "New-Test_option", "TEST")

with open("test.ini", "w") as configfile:
    parser.write(configfile)

test.ini
运行脚本后

[My-Section]
test-option = Test-Variable
new-test_option = TEST

如您在上面所看到的,注释行(两种类型的注释)已被删除。此外,

option
名称已重新格式化。

如果我将以下行添加到源代码中,那么我可以保留

options
的格式,但注释仍会被删除:

parser.optionxform = lambda option: option

所以运行上面一行的脚本后的

test.ini
文件:

[My-Section]
Test-option = Test-Variable
New-Test_option = TEST

所以我的问题:

  • 更改后是否可以保留
    INI
    文件中的注释?
  • 是否可以保留文件的格式,例如:空格、制表符、换行符等...?

注:

  • 我已经检查过
    RawConfigParser
    模块,但据我所知,它也不支持格式保留。
python python-3.x configuration ini configparser
3个回答
1
投票

来自文档:

写回配置时,不会保留原始配置文件中的注释。

IF注释位于部分名称之前always,您可以预处理和后处理文件以捕获注释并在更改后恢复它们。这是一个有点 hack,但可能比扩展 configparser.ConfigParser 更容易实现。

不考虑内嵌注释,尽管可以采用相同的方法来查找、关联和恢复内嵌注释。

使用正则表达式查找注释,然后将它们与部分相关联。

import configparser as cp
import re

pattern = r'([#;][^[]*)(\[[^]]*\])'
rex = re.compile(pattern)
# {section_name:comment}
comments = {}
with open('test.ini') as f:
    text = f.read()
for comment,section in rex.findall(text):
    comments[section] = comment

使用 ConfigParser 进行更改。

parser: cp.ConfigParser = cp.ConfigParser()
parser.read("test.ini")

parser.set("My-Section", "New-Test_option", "TEST")

with open("test.ini", "w") as configfile:
    parser.write(configfile)

恢复评论。

with open('test.ini') as f:
    text = f.read()
for section,comment in comments.items():
    text = text.replace(section,comment+section)
with open('test.ini','w') as f:
    f.write(text)

测试.ini

# comment line
; other comment line
[My-Section]
test-option = Test-Variable

; another pesky comment

[foo-section]
this-option = x

comments
字典看起来像:

{'[My-Section]': '# comment line\n; other comment line\n',
 '[foo-section]': '; another pesky comment\n\n'}

更改后的test.ini

# comment line
; other comment line
[My-Section]
test-option = Test-Variable
new-test_option = TEST

; another pesky comment

[foo-section]
this-option = x

最后,这是 ConfigParser 的子类,其中重写了 _read 和 _write_section 方法以查找/关联/恢复注释 IF 它们出现 就在部分之前。

import configparser as cp
from configparser import *
import re
class ConfigParserExt(cp.ConfigParser):
    def _read(self, fp, fpname):
        """Parse a sectioned configuration file.
        Each section in a configuration file contains a header, indicated by
        a name in square brackets (`[]`), plus key/value options, indicated by
        `name` and `value` delimited with a specific substring (`=` or `:` by
        default).
        Values can span multiple lines, as long as they are indented deeper
        than the first line of the value. Depending on the parser's mode, blank
        lines may be treated as parts of multiline values or ignored.
        Configuration files may include comments, prefixed by specific
        characters (`#` and `;` by default). Comments may appear on their own
        in an otherwise empty line or may be entered in lines holding values or
        section names. Please note that comments get stripped off when reading configuration files;
        unless they are positioned just before sections
        """

        # find comments and associate with section
        try:
            text = fp.read()
            fp.seek(0)
        except AttributeError as e:
            text = ''.join(line for line in fp)
        rex = re.compile(r'([#;][^[]*)(\[[^]]*\])')
        self.preserved_comments = {}
        for comment,section in rex.findall(text):
            self.preserved_comments[section] = comment
        
        super()._read(fp,fpname)

    def _write_section(self, fp, section_name, section_items, delimiter):
        """Write a single section to the specified `fp`."""
        # restore comment to section
        if f'[{section_name}]' in self.preserved_comments:
            fp.write(self.preserved_comments[f'[{section_name}]'])
        super()._write_section( fp, section_name, section_items, delimiter)

0
投票

我找到了 configobj Python 模块,它可以实现我想要的功能。

正如他们的文档所述:

文件中的所有注释都会被保留

保留键/部分的顺序

我已将内置

configparser
更改为
configobj
,它就像魅力一样。


0
投票

如果您不想使用外部库,那么您可以在使用 ConfigParser 解析之前预处理您的配置文件,并对 ConfigParser 写入的输出进行后处理,然后您可以更改 INI 文件而不删除注释。

我建议在预处理过程中将每个评论转换为一个选项(键/值对)。那么 ConfigParser 就不会抛出注释了。在后期处理过程中,您可以“解压”评论并恢复它。

为了简化过程,您可能需要子类化 ConfigParser 并重写 _read 和 write 方法。

我已经完成了此操作并在此要点中发布了 CommentConfigParser 类。它有一个限制。它不支持缩进的节标题、注释和键。它们不应该有前导空格。

class CommentConfigParser(configparser.ConfigParser):
"""Comment preserving ConfigParser.

Limitation: No support for indenting section headers,
comments and keys. They should have no leading whitespace.
"""

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    # Backup _comment_prefixes
    self._comment_prefixes_backup = self._comment_prefixes
    # Unset _comment_prefixes so comments won't be skipped
    self._comment_prefixes = ()
    # Template to store comments as key value pair
    self._comment_template = "#{0} = {1}"
    # Regex to match the comment id prefix
    self._comment_regex = re.compile(r"^#\d+\s*=\s*")
    # List to store comments above the first section
    self._top_comments = []

def _read(self, fp, fpname):
    lines = fp.readlines()
    above_first_section = True
    # Preprocess config file to preserve comments
    for i, line in enumerate(lines):
        if line.startswith("["):
            above_first_section = False
        elif line.startswith(self._comment_prefixes_backup):
            if above_first_section:
                # Remove this line for now
                lines[i] = ""
                self._top_comments.append(line)
            else:
                # Store comment as value with unique key based on line number
                lines[i] = self._comment_template.format(i, line)

    # Feed the preprocessed file to the original _read method
    return super()._read(io.StringIO("".join(lines)), fpname)

def write(self, fp, space_around_delimiters=True):
    # Write the config to an in-memory file
    with io.StringIO() as sfile:
        super().write(sfile, space_around_delimiters)
        # Start from the beginning of sfile
        sfile.seek(0)
        lines = sfile.readlines()

    for i, line in enumerate(lines):
        # Remove the comment id prefix
        lines[i] = self._comment_regex.sub("", line, 1)

    fp.write("".join(self._top_comments + lines))
© www.soinside.com 2019 - 2024. All rights reserved.