Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

C++でxml形式のファイルを扱う

先日、json形式のファイルを読み出し・パースする方法を備忘録として掲載しました。
elsammit-beginnerblg.hatenablog.com

今回はC++xml形式のファイルを読み出し・パースを行ったりデータをxmlファイルで出力したりしたいと思います。



■環境・準備

C++xmlデータをパースするにあたり、
libxml
を用いることにしました。

libxmlは、
Gnomeプロジェクト用に開発されたXML Cパーサーおよびツールキットで、
MITライセンスの下で利用できる無料のソフトウェアです。
The XML C parser and toolkit of Gnome

事前に下記でlibxmlをインストールしてください。
私はubuntuを用いているので、下記でインストールを行いました。

sudo apt-get install libxml2-dev

■libxmlを用いる上での注意点

libxmlですが、2018年ごろに脆弱性が見つかっております。
最新バージョンでは対策されておりますので、必ず最新バージョンを用いることにしましょう!!
具体的なバージョンですが、
libxml 2.9.10以上
であればOKのようです。

脆弱性の内容ですが、
2.9.8以前のlibxml2では、xpath.cのxmlXPathCompOpEval()関数に問題が有り、不正なXPath式(XPATH_OP_AND や XPATH_OP_OR)をパースした際にNULLポインタ被参照の脆弱性が存在します。信頼できないXSLフォーマット入力をlibxml2ライブラリを用いて処理するアプリケーションは、アプリケーションのクラッシュのため、DoS攻撃脆弱性を持つことになります。
とのことです。
libxml2の脆弱性情報(CVE-2018-14404) - security.sios.com

■利用するxmlデータ

今回サンプルとして用いるデータですが、下記とします。

<?xml version="1.0" encoding="UTF-8"?>
<TestScore>
    <user>
        <name>Tom</name>
        <English>30</English>
        <Math>90</Math>
        <Science>80</Science>
    </user>
    <user>
        <name>Alice</name>
        <English>95</English>
        <Math>85</Math>
        <Science>75</Science>
    </user>
    <user>
        <name>Bob</name>
        <English>20</English>
        <Math>35</Math>
        <Science>30</Science>
    </user>
</TestScore>

xmlデータを読み出し・パース

では読み出しを行っていきます。
parser.hを用いる方法とxmlreader.hを用いる方法の2つを載せます。
個人的にはparser.hを利用する方法の方が分かりやすかったです。

まずは、parser.hを用いる方法から。
全体コードはこちら。

#include <stdio.h>
#include <libxml/xmlmemory.h>
#include <libxml/parser.h>

void parseSub(xmlDocPtr doc, xmlNodePtr cur){
    xmlChar *key;
    
    cur = cur->xmlChildrenNode;
    while(cur != NULL){
        if((!xmlStrcmp(cur->name, (const xmlChar*)"name"))){
            key = xmlNodeListGetString(doc, cur->xmlChildrenNode,1);
            printf("name: %s \n", key);
            xmlFree(key);
        }else if((!xmlStrcmp(cur->name, (const xmlChar*)"English"))){
            key = xmlNodeListGetString(doc, cur->xmlChildrenNode,1);
            printf("English: %s \n", key);
            xmlFree(key);           
        }else if((!xmlStrcmp(cur->name, (const xmlChar*)"Math"))){
            key = xmlNodeListGetString(doc, cur->xmlChildrenNode,1);
            printf("Math: %s \n", key);
            xmlFree(key);           
        }else if((!xmlStrcmp(cur->name, (const xmlChar*)"Science"))){
            key = xmlNodeListGetString(doc, cur->xmlChildrenNode,1);
            printf("Science: %s \n", key);
            xmlFree(key);           
        }
        cur = cur->next;
    }
}

void parseDoc(char* FilePath){
    xmlDocPtr doc;
    xmlNodePtr cur;

    doc = xmlParseFile(FilePath);
    if(doc == NULL){
        printf("Document not parsed \n");
        return;
    }

    cur = xmlDocGetRootElement(doc);
    if(cur == NULL){
        printf("empty document \n");
        xmlFreeDoc(doc);
        return;
    }

    if(xmlStrcmp(cur->name, (const xmlChar*)"TestScore")){
        printf("document of wrong type \n");
        xmlFreeDoc(doc);
        return;
    }

    cur = cur->xmlChildrenNode;
    while(cur != NULL){
        printf("name1: %s \n", cur->name);
        if((!xmlStrcmp(cur->name, (const xmlChar*)"user"))){
            parseSub(doc, cur);
        }
        cur = cur->next;
    }
    xmlFreeDoc(doc);
    return;
}

int main(){
    char FilePath[30] = "./sample.xml";
    parseDoc(FilePath);
}

xmlStrcmp関数にて読み出した文字列の比較を行っております。
strcmpと同様に文字列が合致すれば0が返ってきますので、

if((!xmlStrcmp(cur->name, (const xmlChar*)"name"))){
・・・
}

としております。
xmlStrcmp関数を用いて該当するノードを取得して、

xmlNodeListGetString(doc, cur->xmlChildrenNode,1);

にてノード内のテキストを読み出しております。
こちらの動作をノード毎に繰り返し実施しております。

次にxmlreader.hを用いる方法です。
コードはこちら。

#include <stdio.h>
#include <stdlib.h>
#include <libxml/xmlreader.h>

typedef enum {
    STATE_NONE,
    STATE_NAME,
    STATE_ENGLISH,
    STATE_MATH,
    STATE_SCIENCE
} parsingStatus;

parsingStatus state = STATE_NONE;

void processNode(xmlTextReaderPtr reader){
    int nodeType;
    xmlChar *name, *value;

    nodeType = xmlTextReaderNodeType(reader);
    name = xmlTextReaderName(reader); 

    if (!name) {
        name = xmlStrdup(BAD_CAST "---");
    }

    if (nodeType == XML_READER_TYPE_ELEMENT) {
        if ( xmlStrcmp(name, BAD_CAST "name") == 0 ) {  
            state = STATE_NAME;
        }else if ( xmlStrcmp(name, BAD_CAST "English") == 0 ) {
            state = STATE_ENGLISH;
        }else if ( xmlStrcmp(name, BAD_CAST "Math") == 0 ) {
            state = STATE_MATH;
        }else if ( xmlStrcmp(name, BAD_CAST "Science") == 0 ) {
            state = STATE_SCIENCE;
        }
    }else if (nodeType == XML_READER_TYPE_END_ELEMENT) {
        if ( xmlStrcmp(name, BAD_CAST "user") == 0 ) {
            printf("-----------------------\n"); 
        }
        state = STATE_NONE;
    }else if(nodeType == XML_READER_TYPE_TEXT) { 
        value = xmlTextReaderValue(reader);

        if (!value){
            value = xmlStrdup(BAD_CAST "---");
        }
        
        if( state == STATE_NAME ) {
            printf("名前: %s\n", value);
        }else if ( state == STATE_ENGLISH ) {
            printf("英語: %s点\n", value);
        }else if ( state == STATE_MATH ) {
            printf("数学: %s点\n", value);
        }else if ( state == STATE_SCIENCE ) {
            printf("理科: %s点\n", value);
        }
        xmlFree(value);
    }
     xmlFree(name);
}

int main(){
    xmlTextReaderPtr reader;
    int ret;   

    reader = xmlNewTextReaderFilename("./sample.xml");
    if ( !reader ) {
        printf("Failed to open XML file.\n");
        return 1;
    }

    printf("-----------------------\n"); 

    ret = xmlTextReaderRead(reader);
    while (ret == 1) {
        processNode(reader);
        ret = xmlTextReaderRead(reader);
    }
    xmlFreeTextReader(reader);
    if (ret == -1) {
        printf("Parse error.\n");
        return 1;
    }

    return 0;
}

xmlreader.hですが、
読み出したノードタイプを取得し、
そのタイプに応じてノード名の比較を行ったりノード内のテキストを読み出したりします。
こちらの処理を逐次行っているのが、
processNode(xmlTextReaderPtr reader)関数
になります。

ノードの種類としては、
 ・XML_READER_TYPE_ELEMENT
 ・XML_READER_TYPE_END_ELEMENT
 ・XML_READER_TYPE_TEXT
に分かれております。
XML_READER_TYPE_TEXTの場合にノード内のテキストを読み出すことが出来るのですが、
その時にノード名が分からなくならないように、
state変数に前もってノードをセットしておいています。

xmlファイル出力

次はデータをxml形式のファイルとして出力するパターンです。
こちらもまずはコードを載せておきます。

#include <stdio.h>
#include <libxml/tree.h>
#include <libxml/parser.h>
#include <string.h>

xmlNodePtr add_node(xmlNodePtr node, 
    char* node_name, char* text, char* attr, char* attr_val){
    
    xmlNodePtr new_node = NULL;
    xmlNodePtr new_text = NULL;

    new_node = xmlNewNode(NULL, (const xmlChar*)node_name);

    if(text != NULL){
        new_text = xmlNewText((const xmlChar*)text);
        xmlAddChild(new_node, new_text);
    }

    if(attr != NULL && attr != ""){
        xmlNewProp(new_node, (const xmlChar*)attr, (const xmlChar*)attr_val);
    }

    xmlAddChild(node, new_node);
    
    return new_node;
}

int main(){
    xmlDocPtr doc;
    xmlNodePtr root_node = NULL;
    xmlNodePtr new_node = NULL;
    std::string outputFlile = "./output.xml";
    int ret = 0;

    char nameList[3][10] = {"Tom", "Alice", "Bob"};
    char englishList[3][10] = {"30", "95", "20"};
    char mathList[3][10] = {"90", "85", "35"};
    char scienceList[3][10] = {"80", "75", "30"};

    doc = xmlNewDoc((const xmlChar*)"1.0");

    root_node = xmlNewNode(NULL, (const xmlChar*)"TestScore");
    xmlDocSetRootElement(doc, root_node);

    for(int i=0;i<3;i++){
        new_node = add_node(root_node, "user", NULL, "", "");

        add_node(new_node, "name", nameList[i], "", "");
        add_node(new_node, "English", englishList[i], "", "");
        add_node(new_node, "Math", mathList[i], "", "");
        add_node(new_node, "Science", scienceList[i], "", "");
    }

    ret = xmlSaveFormatFileEnc(outputFlile.c_str(), doc, "UTF-8",1);

    if(ret < 0){
        printf("error \n");
        return -1;
    }

    xmlFreeDoc(doc);
    xmlCleanupParser();

    return 0;
}

xml形式のデータを作成している関数は、
xmlNodePtr add_node(xmlNodePtr node,
char* node_name, char* text, char* attr, char* attr_val)
になります。

new_node = xmlNewNode(NULL, (const xmlChar*)node_name);

にて新規ノードを生成し、下記にて生成した新規ノードへノード名やテキストデータを入力していきます。

    if(text != NULL){
        new_text = xmlNewText((const xmlChar*)text);
        xmlAddChild(new_node, new_text);
    }

最後に、新規ノードを元データに追加していきます。

xmlAddChild(node, new_node);

xml形式のデータが生成完了したら、

ret = xmlSaveFormatFileEnc(outputFlile.c_str(), doc, "UTF-8",1);

によりoutputFileへデータを出力していきます。
ファイル形式はUTF-8としました。

書き込みが完了しましたら、

    xmlFreeDoc(doc);
    xmlCleanupParser();

を実行するようにしましょう。

こちらを実行するとoutput.xmlが生成されます。
output.xmlには先ほどのサンプルデータとして定義したxmlデータが格納されているかと思います。

■最後に

今回はxmlファイル形式のデータをC++で扱ってみました。
この記事書いていてなんだけど、
最近xmlファイルって使われているのだろうか??
最近はjson形式の方が主流になりつつなる気がするんだけど、、、
もし知っている方いらっしゃいましたらご意見聞いてみたいです!!