Мне часто приходится писать гибко конфигурируемые программы. Конфигурационные файлы часто получаются довольно сложными, с развитой иерархией. Для хранения настроек я использовал 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 } } } |
Теперь по существу:
- Формат INI не поддерживает иерархическое представление данных
- C документами сохраненными в UTF-8, проблем при чтении у Boost Property Tree не обнаружено
- JSON очень строго регламентирует синтаксис и структуру файла, поэтому при ошибках в файле, например из-за пропущенной запятой, файл разобран не будет
- JSON проверяет правильность написания булевых значений – написав "fals" вместо "false", вы получите ошибку на этапе разбора файла. Внимание, в случае XML и INFO ошибки на будет, а код get<bool>("…") вернет true (как и ожидается, если "false" написан верно, возвращаемое значение будет false).
- Во всех форматах корректно идет работа как с целыми числами, так и с числами с плавающей точкой. Возможна запись в виде: 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); |
К сожалению практически в этом смысла мало:
- Файлы XML и JSON становятся трудночитаемыми человеком, так как полностью пропадает форматирование
- UTF-8 фрагменты в JSON сохраняются с полной потерей информации
- На этом фоне INFO смотрится прилично, но комментарии не сохраняются
Заключение
У меня, как правило не стоит задача сохранения конфигураций из программы. Система конфигурируется человеком в текстовом редакторе, а программа данную конфигурацию читает и настраивает себя. Для этих целей библиотека очень удобна и действительно упрощает разбор конфигураций. В качестве формата файлов я выбрал JSON из-за его легкого синтаксиса и строгих проверок.
Для сохранений конфигураций библиотека, на мой взгляд пока не годится, разве что только файлы будут в формате INFO и вас устроит отсутствие комментариев.
Библиотека тяжеловата и поскольку представлена в виде заголовочных файлов, то компиляция замедлится.
Файлы к заметке
Архив с тестовой программой и конфигурацией в формате INI, XML, JSON, INFO
Хотелось бы добавить про загрузку атрибутов 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
Пример элемента name в предыдущем комментарии не отобразился, вероятно, из-за особенностей данного сайта. Попробую по-другому:
<name type_of_name=»local»>alpha server</name>
SG_House
1 марта 10 19:14
Спасибо! Важное дополнение. Я не затронул тему XML-атрибутов из-за того, что используя их мы не сможем писать общий код для любых форматов представления данных, но конечно используя только XML это очень актуально.
Максим Тремпольцев
1 марта 10 20:49
Спасибо за статью. Решил использовать его для рабора с ini подобным фалом , но возникли трудности. В файлах повторяется одна из секций. Эта библиотека позволяет описать исключения? Или придется описывать граматику у spirit`а? Как вариант предварительно удалять повторяющийся ключ, а потом снова его добавлять.
jershell
20 августа 10 14:06
Приличная статья, благодарю.
topright
1 октября 10 18:19
Форматирование xml можно сохранить если использовать boost::property_tree::xml_writer_settings. Например
write_xml ( path-to-file, tree, std::locale(), xml_writter_settings(‘ ‘, 1) );
Андрей
24 февраля 11 14:10
75kzg8
Get free iPhone 15: https://furiousbyte.com/uploads/go.php hs=54a67f9ff1b4b8935ecc20afd2cd9fdb*
11 ноября 23 15:02
z9kmpl
* * * GET FREE iPhone 15: https://avisdevelopers.com/attachment/go.php * * * hs=54a67f9ff1b4b8935ecc20afd2cd9fdb*
20 февраля 24 21:32