使用 ConfigParser 读取没有节名的文件

我使用 ConfigParser来读取脚本的运行时配置。

我希望具有不提供节名称的灵活性(有一些脚本非常简单,它们不需要“节”)。ConfigParser将抛出 NoSectionError异常,并且不接受该文件。

如何让 ConfigParser 只检索配置文件的 (key, value)元组而不使用节名?

例如:

key1=val1
key2:val2

我宁愿不写入配置文件。

110570 次浏览

Alex Martelli provided a solution for using ConfigParser to parse .properties files (which are apparently section-less config files).

His solution is a file-like wrapper that will automagically insert a dummy section heading to satisfy ConfigParser's requirements.

You can use the ConfigObj library to do that simply : http://www.voidspace.org.uk/python/configobj.html

Updated: Find latest code here.

If you are under Debian/Ubuntu, you can install this module using your package manager :

apt-get install python-configobj

An example of use:

from configobj import ConfigObj


config = ConfigObj('myConfigFile.ini')
config.get('key1') # You will get val1
config.get('key2') # You will get val2

Having ran into this problem myself, I wrote a complete wrapper to ConfigParser (the version in Python 2) that can read and write files without sections transparently, based on Alex Martelli's approach linked on the accepted answer. It should be a drop-in replacement to any usage of ConfigParser. Posting it in case anyone in need of that finds this page.

import ConfigParser
import StringIO


class SectionlessConfigParser(ConfigParser.RawConfigParser):
"""
Extends ConfigParser to allow files without sections.


This is done by wrapping read files and prepending them with a placeholder
section, which defaults to '__config__'
"""


def __init__(self, *args, **kwargs):
default_section = kwargs.pop('default_section', None)
ConfigParser.RawConfigParser.__init__(self, *args, **kwargs)


self._default_section = None
self.set_default_section(default_section or '__config__')


def get_default_section(self):
return self._default_section


def set_default_section(self, section):
self.add_section(section)


# move all values from the previous default section to the new one
try:
default_section_items = self.items(self._default_section)
self.remove_section(self._default_section)
except ConfigParser.NoSectionError:
pass
else:
for (key, value) in default_section_items:
self.set(section, key, value)


self._default_section = section


def read(self, filenames):
if isinstance(filenames, basestring):
filenames = [filenames]


read_ok = []
for filename in filenames:
try:
with open(filename) as fp:
self.readfp(fp)
except IOError:
continue
else:
read_ok.append(filename)


return read_ok


def readfp(self, fp, *args, **kwargs):
stream = StringIO()


try:
stream.name = fp.name
except AttributeError:
pass


stream.write('[' + self._default_section + ']\n')
stream.write(fp.read())
stream.seek(0, 0)


return ConfigParser.RawConfigParser.readfp(self, stream, *args,
**kwargs)


def write(self, fp):
# Write the items from the default section manually and then remove them
# from the data. They'll be re-added later.
try:
default_section_items = self.items(self._default_section)
self.remove_section(self._default_section)


for (key, value) in default_section_items:
fp.write("{0} = {1}\n".format(key, value))


fp.write("\n")
except ConfigParser.NoSectionError:
pass


ConfigParser.RawConfigParser.write(self, fp)


self.add_section(self._default_section)
for (key, value) in default_section_items:
self.set(self._default_section, key, value)

Enlightened by this answer by jterrace, I come up with this solution:

  1. Read entire file into a string
  2. Prefix with a default section name
  3. Use StringIO to mimic a file-like object
ini_str = '[root]\n' + open(ini_path, 'r').read()
ini_fp = StringIO.StringIO(ini_str)
config = ConfigParser.RawConfigParser()
config.readfp(ini_fp)


EDIT for future googlers: As of Python 3.4+ readfp is deprecated, and StringIO is not needed anymore. Instead we can use read_string directly:

with open('config_file') as f:
file_content = '[dummy_section]\n' + f.read()


config_parser = ConfigParser.RawConfigParser()
config_parser.read_string(file_content)

The easiest way to do this is to use python's CSV parser, in my opinion. Here's a read/write function demonstrating this approach as well as a test driver. This should work provided the values are not allowed to be multi-line. :)

import csv
import operator


def read_properties(filename):
""" Reads a given properties file with each line of the format key=value.  Returns a dictionary containing the pairs.


Keyword arguments:
filename -- the name of the file to be read
"""
result={ }
with open(filename, "rb") as csvfile:
reader = csv.reader(csvfile, delimiter='=', escapechar='\\', quoting=csv.QUOTE_NONE)
for row in reader:
if len(row) != 2:
raise csv.Error("Too many fields on row with contents: "+str(row))
result[row[0]] = row[1]
return result


def write_properties(filename,dictionary):
""" Writes the provided dictionary in key-sorted order to a properties file with each line of the format key=value


Keyword arguments:
filename -- the name of the file to be written
dictionary -- a dictionary containing the key/value pairs.
"""
with open(filename, "wb") as csvfile:
writer = csv.writer(csvfile, delimiter='=', escapechar='\\', quoting=csv.QUOTE_NONE)
for key, value in sorted(dictionary.items(), key=operator.itemgetter(0)):
writer.writerow([ key, value])


def main():
data={
"Hello": "5+5=10",
"World": "Snausage",
"Awesome": "Possum"
}


filename="test.properties"
write_properties(filename,data)
newdata=read_properties(filename)


print "Read in: "
print newdata
print


contents=""
with open(filename, 'rb') as propfile:
contents=propfile.read()
print "File contents:"
print contents


print ["Failure!", "Success!"][data == newdata]
return


if __name__ == '__main__':
main()

You can do this in a single line of code.

In python 3, prepend a fake section header to your config file data, and pass it to read_string().

from configparser import ConfigParser


parser = ConfigParser()
with open("foo.conf") as stream:
parser.read_string("[top]\n" + stream.read())  # This line does the trick.

You could also use itertools.chain() to simulate a section header for read_file(). This might be more memory-efficient than the above approach, which might be helpful if you have large config files in a constrained runtime environment.

from configparser import ConfigParser
from itertools import chain


parser = ConfigParser()
with open("foo.conf") as lines:
lines = chain(("[top]",), lines)  # This line does the trick.
parser.read_file(lines)

In python 2, prepend a fake section header to your config file data, wrap the result in a StringIO object, and pass it to readfp().

from ConfigParser import ConfigParser
from StringIO import StringIO


parser = ConfigParser()
with open("foo.conf") as stream:
stream = StringIO("[top]\n" + stream.read())  # This line does the trick.
parser.readfp(stream)

With any of these approaches, your config settings will be available in parser.items('top').

You could use StringIO in python 3 as well, perhaps for compatibility with both old and new python interpreters, but note that it now lives in the io package and readfp() is now deprecated.

Alternatively, you might consider using a TOML parser instead of ConfigParser.

Blueicefield's answer mentioned configobj, but the original lib only supports Python 2. It now has a Python 3+ compatible port:

https://github.com/DiffSK/configobj

APIs haven't changed, see it's doc.