先日、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データが格納されているかと思います。