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