Личный опыт разработки ПО

Сборник рецептов

Чтение настроек приложения

комментариев 8

Мне часто приходится писать гибко конфигурируемые программы. Конфигурационные файлы часто получаются довольно сложными, с развитой иерархией. Для хранения настроек я использовал XML файлы, а разбор конфигурации делал вручную.

Недавно (с версии 1.41.0) в Boost появилась библиотека Property Tree, предназначенная для решения данной задачи. Помимо поддержки XML, также поддерживаются форматы INI, JSON и свой формат INFO.

В данной заметке я рассмотрю указанные форматы и приведу код для разбора файла.


Форматы файлов

Обойду стороной вопрос об удобстве синтаксиса, так как вопрос этот достаточно субъективный, просто приведу примеры файлов описывающих одну и ту же конфигурацию:

INI

; Комментарий
[server]
name	=	"alpha server"
port	=	9010
 
[channels]
; Иерархии в INI-файлах не поддерживаются
 
[channel]
; Не работает
name	=	first
id	=	1
comment	=	"Необязательный комментарий"
enabled	=	true
 
[channel]
; Не работает
name	=	second
id	=	2
enabled	=	false

XML

<?xml version="1.0" encoding="utf-8"?>
<!-- Комментарий -->
<server>
	<name>alpha server</name>
	<port>9010</port>
	<channels>
		<channel>
			<name>first</name>
			<id>1</id>
			<comment>Необязательный комментарий</comment>
			<enabled>true</enabled>
		</channel>
		<channel>
			<name>second</name>
			<id>2</id>
			<enabled>false</enabled>
		</channel>
	</channels>
</server>

JSON

{
	/* Комментарий */
	"server":
	{
		"name":	"alpha server",
		"port":	9010,
		"channels":
		[
			{
				"name":		"first",
				"id":		1,
				"comment":	"Необязательный комментарий",
				"enabled":	true
			},
			{
				"name":		"second",
				"id":		2,
				"enabled":	false
			}
		]
	}
}

INFO

; Комментарий
server
{
	name	"alpha server"
	port	9010
	channels
	{
		channel
		{
			name	"first"
			id	1
			comment	"Необязательный комментарий"
			enabled	true
		}
		channel
		{
			name	"second"
			id	2
			enabled	false
		}
	}
}

Теперь по существу:

  1. Формат INI не поддерживает иерархическое представление данных
  2. C документами сохраненными в UTF-8, проблем при чтении у Boost Property Tree не обнаружено
  3. JSON очень строго регламентирует синтаксис и структуру файла, поэтому при ошибках в файле, например из-за пропущенной запятой, файл разобран не будет
  4. JSON проверяет правильность написания булевых значений – написав "fals" вместо "false", вы получите ошибку на этапе разбора файла. Внимание, в случае XML и INFO ошибки на будет, а код get<bool>("…") вернет true (как и ожидается, если "false" написан верно, возвращаемое значение будет false).
  5. Во всех форматах корректно идет работа как с целыми числами, так и с числами с плавающей точкой. Возможна запись в виде: 1560, 1560.5, 1.5605e3

Разбор файла

Первым делом необходимо подключить один из указанных заголовочных файлов, в зависимости от того, с файлами какого формата вы будете работать:

#include <boost/property_tree/ini_parser.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/info_parser.hpp>

После этого создается объект типа boost::property_tree::ptree, в котором при удачном разборе файла будет сохранена структура файла со значениями. Далее собственно вызывается функция для разбора файла. Об ошибке во время разбора файла можно вывести подробное сообщение:

1
2
3
4
5
6
7
8
9
10
11
12
boost::property_tree::ptree config;
try
{
	Parser("fileName", config);
}
catch (Exception& error)
{
	std::cout 
		<< error.message() << ": " 
		<< error.filename() << ", line " 
		<< error.line() << std::endl;
}

Parser в четвертой строке и Exception в шестой в зависимости от типа файла принимают следующие значения:

Тип файла Parser Exception
INI boost::property_tree::read_ini boost::property_tree::ini_parser_error
XML boost::property_tree::read_xml boost::property_tree::xml_parser_error
JSON boost::property_tree::read_json boost::property_tree::json_parser_error
INFO boost::property_tree::read_info boost::property_tree::info_parser_error

Разбор конфигурации

После успешно выполненного разбора документ будет представлен узлами содержащими элементы (узел из одного значения) или другие узлы.

Доступ к нужному узлу осуществляется методом:

get_child("имя_узла") Обращение к подузлам возможно с использованием разделителя .
  const boost::property_tree::ptree& server = config.get_child("server");

Для доступа к элементу узла используются три метода:

get<тип>("имя_элемента") Позволяет получить значение элемента указанного типа, при отсутствии данного элемента будет выброшено исключение boost::property_tree::ptree_bad_path, при невозможности преобразования boost::property_tree::ptree_bad_data
  server.get<std::string>("name")
get("имя_элемента", значение_по_умолчанию) Если элемент не был найден, будет возвращено значение по умолчанию
  server.get("port", 9000)
get_optional<тип>("имя_элемента") Будет возвращен тип boost::optional, таким образом мы можем узнать был ли данный элемент в файле или нет
  if (const boost::optional<std::string> optionalComment = server.get_optional<std::string>("comment")) { … }

Пример кода:

try
{
	const boost::property_tree::ptree& server = config.get_child("server");
	std::cout 
		<< "\nServer ===================================\n"
		<< "Name:\t" << server.get<std::string>("name") << '\n'
		<< "Port:\t" << server.get("port", 9000) << '\n';
	BOOST_FOREACH (const boost::property_tree::ptree::value_type& channel, 
		config.get_child("server.channels"))
	{
		const boost::property_tree::ptree& values = channel.second;
		std::cout 
			<< "\tChannel --------------------------\n"
			<< "\tName:\t" << values.get<std::string>("name") << '\n'
			<< "\tId:\t" << values.get<unsigned>("id") << '\n'
			<< "\tEnabled:" << values.get<bool>("enabled", true) << '\n';
		if (const boost::optional<std::string> optionalComment = 
				values.get_optional<std::string>("comment"))
		{
			std::cout << "\t" << optionalComment.get() << '\n';
		}
	}
}
catch (const boost::property_tree::ptree_bad_data& error)
{
	std::cout << error.what() << std::endl;
}
catch (const boost::property_tree::ptree_bad_path& error)
{
	std::cout << error.what() << std::endl;
}

Запись конфигурации

Теоретически можно создавать и редактировать конфигурационные файлы, для этого есть соответствующие методы:

config.put("enabled", false);
write_xml("test.xml", config);

К сожалению практически в этом смысла мало:

  1. Файлы XML и JSON становятся трудночитаемыми человеком, так как полностью пропадает форматирование
  2. UTF-8 фрагменты в JSON сохраняются с полной потерей информации
  3. На этом фоне INFO смотрится прилично, но комментарии не сохраняются

Заключение

У меня, как правило не стоит задача сохранения конфигураций из программы. Система конфигурируется человеком в текстовом редакторе, а программа данную конфигурацию читает и настраивает себя. Для этих целей библиотека очень удобна и действительно упрощает разбор конфигураций. В качестве формата файлов я выбрал JSON из-за его легкого синтаксиса и строгих проверок.

Для сохранений конфигураций библиотека, на мой взгляд пока не годится, разве что только файлы будут в формате INFO и вас устроит отсутствие комментариев.

Библиотека тяжеловата и поскольку представлена в виде заголовочных файлов, то компиляция замедлится.

Файлы к заметке

Архив с тестовой программой и конфигурацией в формате INI, XML, JSON, INFO

22nd Февраль 2010
17:23

8 комментариев к 'Чтение настроек приложения'

Подписаться на комментарии по RSS или TrackBack.

  1. Хотелось бы добавить про загрузку атрибутов xml-элементов. В документации по библиотеке об этом сказано весьма вскользь, так что лично мне пришлось потратить некоторое время, чтобы разобраться.

    Очевидно, что дочерний элемент xml-дерева представляется дочерним же элементом в библиотеке. И, опираясь на пример выше, код доступа к значению элемента name выглядит так:

    std::string sName = config.get(«server.name»);

    Что тоже самое, как:

    std::string sName = config.get_child(«server.name»).get_value();

    А вот предположим, что у элемента name есть атрибут, например:

    alpha server

    Оказывается, атрибуты в библиотеке представлены как дочерние элементы специального дочернего элемента «<xmlattr>» самого элемента. То есть, чтобы прочитать значение атрибута, нужно написать:

    std::string sTypeOfName = config.get(«server.name.<xmlattr>.type_of_name»);

    или

    const boost::property_tree::ptree& NameEl = config.get_child(«server.name»)
    std::string sTypeOfName = NameEl.get_value(«<xmlattr>.type_of_name»);

    PS. Может быть, здесь написанное итак очевидно, но, надеюсь, этот комментарий кому-то поможет.

    SG_House

    1 марта 10 19:11

  2. Пример элемента name в предыдущем комментарии не отобразился, вероятно, из-за особенностей данного сайта. Попробую по-другому:

    <name type_of_name=»local»>alpha server</name>

    SG_House

    1 марта 10 19:14

  3. Спасибо! Важное дополнение. Я не затронул тему XML-атрибутов из-за того, что используя их мы не сможем писать общий код для любых форматов представления данных, но конечно используя только XML это очень актуально.

  4. Спасибо за статью. Решил использовать его для рабора с ini подобным фалом , но возникли трудности. В файлах повторяется одна из секций. Эта библиотека позволяет описать исключения? Или придется описывать граматику у spirit`а? Как вариант предварительно удалять повторяющийся ключ, а потом снова его добавлять.

    jershell

    20 августа 10 14:06

  5. Приличная статья, благодарю.

    topright

    1 октября 10 18:19

  6. Форматирование xml можно сохранить если использовать boost::property_tree::xml_writer_settings. Например

    write_xml ( path-to-file, tree, std::locale(), xml_writter_settings(‘ ‘, 1) );

    Андрей

    24 февраля 11 14:10

  7. 75kzg8

  8. z9kmpl

Оставить комментарий