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
模块,但据我所知,它也不支持格式保留。来自文档:
写回配置时,不会保留原始配置文件中的注释。
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)
我找到了 configobj Python 模块,它可以实现我想要的功能。
正如他们的文档所述:
文件中的所有注释都会被保留
保留键/部分的顺序
我已将内置
configparser
更改为 configobj
,它就像魅力一样。
如果您不想使用外部库,那么您可以在使用 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))