「C++の応用 - YAML」の版間の差分

提供: MochiuWiki : SUSE, EC, PCB

276行目: 276行目:
       std::cout << "\nNew YAML:\n" << newNode << std::endl;
       std::cout << "\nNew YAML:\n" << newNode << std::endl;
   
   
       // TAMLファイルへの書き込み
       // YAMLファイルへの書き込み
       std::ofstream fout("output.yaml");
       std::ofstream fout("output.yaml");
       fout << newNode;
       fout << newNode;

2024年8月20日 (火) 17:11時点における版

概要

YAML (YAML Ain't Markup Language) は、人間にとって読み書きしやすいデータシリアライゼーション形式である。
設定ファイル、データ交換、データ保存等、様々な用途で使用されている。

YAMLの特徴を以下に示す。

  • 可読性が高い
    インデントを使用して構造を表現するため、人間が読みやすい形式である。
  • 豊富なデータ型
    文字列、数値、ブール値、リスト、マップ (辞書) 等の基本的なデータ型をサポートしている。
  • 柔軟性
    複雑なデータ構造も表現できる。
  • コメントのサポート
    #を使用して、コメントを記述することができる。
  • 参照と別名
    データの再利用や循環参照が可能である。


YAMLの使用するメリットを以下に示す。

  • 設定ファイルに適している
    人間が読み書きしやすいため、アプリケーションの設定ファイルとしてよく使用されている。
  • データ交換に適している
    JSONの代替として使用でき、より読みやすい形式でデータを表現できる。
  • 柔軟性が高い
    複雑なデータ構造も簡単に表現できる。
  • 多くの言語でサポートされている
    多くのプログラミング言語にYAMLパーサーが存在する。


YAMLは、可読性の高さと柔軟性から、多くの開発者に好まれている。
特に、設定ファイルやデータ交換の用途で広く使用されており、多くのモダンなアプリケーションやフレームワークでYAMLが採用されている。

C++では、YAMLの処理によく使用されるライブラリの1つにyaml-cppライブラリが存在する。


YAMLの基本的な構造

※注意

  • インデントが重要
    スペースを使用してインデントを行い、一貫性を保つ必要がある。
  • コロンの後にはスペースが必要
    "<キー>: <値>"のように、コロンの後にスペースを入れる必要がある。
  • 文字列のクォート
    特殊文字を含む場合や曖昧さを避けたい場合は、文字列をクォートで囲むことができる。


<syntaxhighlight lang="yaml">
# This is a comment
---  # Document start

# スカラー値 (文字列, 数値, ブール値)
name: John Doe
age: 30
is_student: false

# リスト (ハイフンを使用)
hobbies:
  - reading
  - traveling
  - photography

# マッピング (キー: 値の形式)
address:
  street: 123 Main St
  city: Anytown
  country: USA

# 複雑なデータ構造 (リストとマッピングの組み合わせ)
employees:
  - name: Alice
    position: Developer
    skills:
      - Python
      - JavaScript
  - name: Bob
    position: Designer
    skills:
      - Photoshop
      - Illustrator

# 複数行の文字列
description: >
  This is a long description
  that spans multiple lines.
  The > symbol preserves newlines
  but removes extra whitespace.

# 複数行の文字列 (> と | を使用)
code_sample: |
  def hello_world():
     print("Hello, World!")

# アンカー ("&") と エイリアス ("*") を使用したデータの再利用
defaults: &defaults
  timeout: 30
  retries: 3

production:
  <<: *defaults
  host: production.example.com
development:
  <<: *defaults
  host: dev.example.com
  timeout: 10  # デフォルト値のオーバーライド

# Document end
</syntaxhighlight>



yaml-cppライブラリ

yaml-cppライブラリとは

yaml-cppライブラリは、C++向けのYAMLパーサーおよびエミッタライブラリである。

yaml-cppライブラリを使用することにより、C++でYAMLファイルの読み込み、操作、書き込みが可能になる。
また、設定ファイルの処理、データのシリアライズ / デシリアライズ、構造化データの操作等に広く使用されている。

yaml-cppライブラリの特徴を以下に示す。

  • C++ 11以降をサポート
    モダンなC++の機能を活用している。
  • ヘッダオンリーライブラリ
    使用する場合は、ヘッダファイルをインクルードするだけで済む。
  • YAML 1.2仕様のサポート
    最新のYAML仕様に準拠している。
  • 例外ベースのエラー処理
    エラーが発生した場合は、適切な例外をスローする。
  • STLとの統合
    STLコンテナとの相互運用性が高い。
  • ストリーミングAPI
    大きなファイルや継続的なデータストリームの処理に適している。


yaml-cppライブラリのライセンス

yaml-cppライブラリのライセンスは、MITライセンスに準拠している。

yaml-cppの主要なクラス

  • YAML::Nodeクラス
    YAMLデータを表現する基本的なクラスである。
    スカラー、シーケンス、マップ等、あらゆる種類のYAMLノードを表現できる。
  • YAML::Exceptionクラス
    yaml-cppライブラリが投げる例外の基本クラスである。
    パース時のエラーや型変換エラー等をキャッチできる。
  • YAML::Emitterライブラリ
    YAMLデータを生成するためのクラスである。
    プログラム上において、YAMLを構築する時に使用する。
  • YAML::Parserクラス
    低レベルのパーシング操作を行うためのクラスである。
    一般的には、YAML::LoadクラスやYAML::LoadFileクラスを使用するため、直接使用することは少ない。


※注意

  • 型安全性
    YAML::Nodeクラスは、テンプレートベースの.as<T>メソッドを提供するが、型が一致しない場合は例外がスローされる。
    そのため、適切な型チェックを行うことが重要である。
  • メモリ管理
    YAML::Nodeクラスのオブジェクトは参照カウント方式で管理されているため、通常はメモリリークを心配する必要は無い。
  • パフォーマンス
    大規模なファイルを扱う場合、YAML::LoadFileクラスはファイル全体をメモリに読み込むため、メモリ使用量が増大する可能性がある。
    この場合、ストリーミングAPIの使用を検討すること。
  • エラー処理
    yaml-cppライブラリは例外を使用してエラーを報告する。
    そのため、適切なtry-catchブロックを使用して、エラーをハンドリングすることが重要である。


yaml-cppライブラリのインストール

パッケージ管理システムからインストール
# RHEL
sudo dnf install yaml-cpp yaml-cpp-devel

# SUSE
sudo zypper install yaml-cpp yaml-cpp-devel


ソースコードからインストール

yaml-cppライブラリのGithubにアクセスして、ソースコードをダウンロードする。
ダウンロードしたファイルを解凍する。

tar xf yaml-cpp-<バージョン>.tar.gz
cd yaml-cpp-<バージョン>


yaml-cppライブラリをビルドおよびインストールする。

mkdir build && cd build

cmake -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_INSTALL_PREFIX=<yaml-cppライブラリのインストールディレクトリ> \
      -DYAML_BUILD_SHARED_LIBS=ON \
      ..
make -j $(nproc)
make install



ライブラリの指定

Qtプロジェクトファイルを使用する場合

<syntaxhighlight lang="make">
# Qtプロジェクトファイル

# pkg-configを使用してyaml-cppライブラリを設定
CONFIG += link_pkgconfig
PKGCONFIG += yaml-cpp

# pkg-configを使用しない場合
LIBS += -lyaml-cpp
</syntaxhighlight>


CMakeLists.txtファイルを使用する場合

<syntaxhighlight lang="cmake">
# CMakeLists.txtファイル

# ...略

find_package(PkgConfig REQUIRED)

# yaml-cppライブラリの指定
pkg_check_modules(YAML_CPP REQUIRED yaml-cpp)

# インクルードディレクトリの指定
target_include_directories(${PROJECT_NAME} PRIVATE
  # ...略
  ${YAML_CPP_INCLUDE_DIRS}
)

# ...略

# ライブラリのリンク
target_link_libraries(${PROJECT_NAME} PRIVATE
   # ...略
   ${YAML_CPP_LIBRARIES}
)

# コンパイルオプションの設定
target_compile_options(${PROJECT_NAME} PRIVATE
   # ...略
  ${YAML_CPP_CFLAGS_OTHER}
)
</syntaxhighlight>



yaml-cppライブラリの使用例

以下の例では、yaml-cppライブラリの主な機能を使用している。

  • YAMLファイルの読み込み (YAML::LoadFileクラスの使用)
  • スカラー値の取得 (.as<T>メソッドの使用)
  • リストの処理 (範囲ベースのforループ)
  • ネストされたマップの処理
  • 新しいYAMLノードの作成と操作
  • YAMLデータの出力 (ストリーム演算子<<の使用)
  • YAMLデータのファイルへの書き込み


<syntaxhighlight lang="c++">
// main.cppファイル

#include <iostream>
#include <yaml-cpp/yaml.h>

int main()
{
   try {
      // YAMLファイルの読み込み
      YAML::Node config = YAML::LoadFile("config.yaml");

      // スカラー値の取得
      std::string name = config["name"].as<std::string>();
      int age = config["age"].as<int>();
      std::cout << "Name: " << name << ", Age: " << age << std::endl;

      // リストの処理
      std::cout << "Hobbies:" << std::endl;
      for (const auto &hobby : config["hobbies"]) {
         std::cout << "- " << hobby.as<std::string>() << std::endl;
      }

      // ネストされたマップの処理
      if (config["address"]) {
         std::cout << "Address:" << std::endl;
         std::cout << "  Street: " << config["address"]["street"].as<std::string>() << std::endl;
         std::cout << "  City: " << config["address"]["city"].as<std::string>() << std::endl;
      }

      // 新しいYAMLノードの作成
      YAML::Node newNode;
      newNode["key"] = "value";
      newNode["list"].push_back(1);
      newNode["list"].push_back(2);
      newNode["list"].push_back(3);

      // YAMLの出力
      std::cout << "\nNew YAML:\n" << newNode << std::endl;

      // YAMLファイルへの書き込み
      std::ofstream fout("output.yaml");
      fout << newNode;
      fout.close();
   }
   catch (const YAML::Exception& e) {
      std::cerr << "YAML error: " << e.what() << std::endl;
      return -1;
   }

   return 0;
}
</syntaxhighlight>


yaml-cppライブラリの詳細な使用方法は、公式ドキュメントを参照すること。

YAMLを出力するモデルは、std::ostreamマニピュレータである。
YAML::Emitterクラスのオブジェクトは出力ストリームとして動作して、その出力はc_strメソッドで取得できる。
YAML::Emitterクラスの詳細な使用方法は、公式ドキュメントを参照すること。


yaml-cppライブラリの応用例

設定ファイルの管理

以下の例では、アプリケーションの設定をYAMLファイルで管理して、読み込み、更新、保存している。

<syntaxhighlight lang="c++">
#include <iostream>
#include <fstream>
#include <yaml-cpp/yaml.h>

class ConfigManager {
private:
   std::string m_filename;
   YAML::Node  m_config;

public:
   ConfigManager(const std::string &filename) : m_filename(filename)
   {
      load();
   }

   void load()
   {
      try {
         m_config = YAML::LoadFile(m_filename);
      }
      catch (const YAML::Exception &e) {
         std::cerr << "Error loading config: " << e.what() << std::endl;
      }
   }

   void save()
   {
      try {
         std::ofstream fout(m_filename);
         fout << m_config;
      }
      catch (const YAML::Exception &e) {
         std::cerr << "Error saving config: " << e.what() << std::endl;
      }
   }

   template<typename T>
   T get(const std::string& key, const T &defaultValue)
   {
      try {
         return m_config[key].as<T>();
      }
      catch (const YAML::Exception&) {
         return defaultValue;
      }
   }

   template<typename T>
   void set(const std::string &key, const T &value)
   {
      m_config[key] = value;
   }
};

int main()
{
   ConfigManager config("app_config.yaml");

   // 設定の読み取り
   std::string username = config.get<std::string>("username", "default_user");
   int port = config.get<int>("port", 8080);

   std::cout << "Username: " << username << std::endl;
   std::cout << "Port: " << port << std::endl;

   // 設定の更新
   config.set("username", "new_user");
   config.set("port", 9000);

   // 設定の保存
   config.save();

   return 0;
}
</syntaxhighlight>


データ変換

以下の例では、YAML形式のデータを読み込み、新しい構造のYAMLデータとして出力している。
これは、例えば、ユーザデータを処理して統計情報を生成する場合に有用である。

<syntaxhighlight lang="c++">
#include <iostream>
#include <vector>
#include <algorithm>
#include <yaml-cpp/yaml.h>

struct UserData {
   std::string name;
   int age;
   std::vector<std::string> skills;
};

YAML::Node processUserData(const YAML::Node& input)
{
   std::vector<UserData> users;

   for (const auto& user : input["users"]) {
      UserData userData;
      userData.name   = user["name"].as<std::string>();
      userData.age    = user["age"].as<int>();
      userData.skills = user["skills"].as<std::vector<std::string>>();
      users.push_back(userData);
   }

   // データ処理: 平均年齢とスキルの集計
   int totalAge = 0;
   std::map<std::string, int> skillCount;

   for (const auto &user : users) {
      totalAge += user.age;
      for (const auto &skill : user.skills) {
         skillCount[skill]++;
      }
   }

   double averageAge = users.empty() ? 0 : static_cast<double>(totalAge) / users.size();

   // 結果のYAMLノードを作成
   YAML::Node output;
   output["total_users"] = users.size();
   output["average_age"] = averageAge;

   YAML::Node skillStats;
   for (const auto &[skill, count] : skillCount) {
      skillStats[skill] = count;
   }
   output["skill_statistics"] = skillStats;

   return output;
}

int main()
{
   try {
      YAML::Node input = YAML::LoadFile("user_data.yaml");
      YAML::Node output = processUserData(input);

      std::cout << "Processed Data:\n" << output << std::endl;

      std::ofstream fout("user_statistics.yaml");
      fout << output;
   }
   catch (const YAML::Exception &e) {
      std::cerr << "Error processing YAML: " << e.what() << std::endl;
      return -1;
   }

   return 0;
}
</syntaxhighlight>


階層的データ構造の操作

以下の例では、yaml-cppライブラリを使用して、複雑な階層的データ構造を操作している。
組織構造を表すYAMLデータを読み込み、特定の条件に基づいて検索や更新を行う。

<syntaxhighlight lang="c++">
#include <iostream>
#include <functional>
#include <yaml-cpp/yaml.h>

// 再帰的にYAMLノードを探索する関数
void traverseNode(YAML::Node& node, const std::function<void(YAML::Node&)>& callback)
{
   if (node.IsMap()) {
      for (auto it = node.begin(); it != node.end(); ++it) {
         callback(it->second);
         traverseNode(it->second, callback);
      }
   }
   else if (node.IsSequence()) {
      for (auto& item : node) {
         callback(item);
         traverseNode(item, callback);
      }
   }
}

// 特定の条件に一致するノードを検索する関数
std::vector<YAML::Node> findNodes(YAML::Node& root, const std::function<bool(const YAML::Node&)>& predicate)
{
   std::vector<YAML::Node> results;
   traverseNode(root, [&](YAML::Node &node) {
      if (predicate(node)) {
         results.push_back(node);
      }
   });

   return results;
}

int main()
{
   try {
      YAML::Node org = YAML::LoadFile("organization.yaml");

      // 例: 全ての部門の予算を10%増加
      traverseNode(org, [](YAML::Node& node) {
         if (node["budget"]) {
            double budget = node["budget"].as<double>();
            node["budget"] = budget * 1.1;
         }
      });

      // 例: 特定の役職を持つ従業員を検索
      auto managers = findNodes(org, [](const YAML::Node& node) {
         return node["position"] && node["position"].as<std::string>() == "Manager";
      });

      std::cout << "Managers found: " << managers.size() << std::endl;
      for (const auto &manager : managers) {
         std::cout << "Name: " << manager["name"].as<std::string>() << std::endl;
      }

      // 更新されたデータを保存
      std::ofstream fout("updated_organization.yaml");
      fout << org;
   }
   catch (const YAML::Exception &e) {
      std::cerr << "Error processing YAML: " << e.what() << std::endl;
      return -1;
   }

   return 0;
}
</syntaxhighlight>