본문 바로가기

도구의발견

msxml 파서 사용하기

MSXML에 관한 VB나 C# 같은 언어의 예제는 많은데 C++의 예제는 별로 없는 것 같아 간단하게 나마 예제 코드를 만들어 봅니다. 코드를 보시기 전에 xml에 대한 기본적인 개념을 익히시고 싶으신 분은 XML 기초를 참조해 주세요.

MSXML API를 사용하기 위해서는 COM을 어느정도 알면 상당히 편하겠지만 몰라도 상관은 없습니다. 저도 COM에 관해서는 잘 모르기 때문에 이번 포스트에서 그와 관련된 설명은 건너 뛰기로 하겠습니다. 또한 MSXML 파서 설치 같은 것은 기본적으로 다 되어 있다고 가정하고 시작하도록 하겠습니다. 혹시나 설치나 설정 등에 어려움을 겪으신다면 댓글로 남겨 주세요. 그에 대한 포스팅을 따로 마련 해보도록 하겠습니다.

이 포스트는 MSXML4.0 버젼을 기준으로 작성 되었으며 포스트에서 예제로 사용하고 있는 xml파일은 첨부 파일로 저장되어 있으니 포스트를 읽어 보시기 전에 첨부 파일을 다운로드 받으시는 것이 전체적으로 이해 하시기 편할 것이라 생각 됩니다.

Test_msXml.xml
0.00MB
Test_msXml.xsd
0.00MB
Test_msXml.xsl
0.00MB

1. Dll import

MSXML 파서를 사용하기 위해서는 DLL을 import해야 합니다. 헤더 파일과 라이브러리를 로딩 하는 방법도 있지만 여기서는 dll을 import하는 방법을 사용하도록 하겠습니다. 우리가 사용하는 버젼은 msxml4.0 버젼이므로 아래와 같은 코드를 추가 합니다 :

#import <msxml4.dll>

2. 초기화

초기화 과정에는 COM객체를 사용하기 위한 초기화와 XML Document를 사용하기 위한 초기화 과정. 이렇게 두 단계가 있습니다. 물론 XML 파서가 COM을 사용하고 있으므로 COM에 대한 초기화가 먼저 이루어 져야 합니다 :

// COM 객체를 사용하기 위해 초기화를 한다.
// 윈도우 버젼 버젼이 올라가면서 확장된 초기화 함수를 사용 할 수 있다.
bool initializeCOM() {
#if _WIN32_WINNT >= 0x0400 & defined(_ATL_FREE_THREADED)
    CoInitializeEx(NULL, COINIT_MULTITHREADED);
#else
    CoInitialize(NULL);
#endif
    return true;
}

COM객체를 사용할 준비를 마치고 나면 XMLDocument 객체를 초기화 해야 합니다. 여기서 사용되는 것XMLDOMDocument 인터페이스를 초기화 해야 합니다. 여기서는 편의상 IXMLDOMDocuement를 전역 변수로 놓도록 하겠습니다. 그리고 코드 작성의 편의를 위해 스마트 포인터로 정의 되어 있는 IXMLDOMDocument2Ptr을 사용하도록 하겠습니다 :

// DOM 의 핵심이 되는 객체
// 파싱된 XML을 저장하고 기타 각종 설정들을 담고 있다.
MSXML2::IXMLDOMDocument2Ptr g_pXMLDoc;

bool initializeMSXML() {
    g_pXMLDoc.CreateInstance(__uuidof(MSXML2::DOMDocument40));
    g_pXMLDoc->async = VARIANT_FALSE;
    g_pXMLDoc->validateOnParse = VARIANT_TRUE;
    g_pXMLDoc->resolveExternals = VARIANT_TRUE;
    return true;
}

3. Validation check

MSXML파서의 기능중 하나는 xml파일의 정합성(validation)을 검증하는 것입니다. 여기서 정합성이란 단순한 well-formed 문서 뿐만 아니라, 스키마나 DTD에서 정의한 규칙을 제대로 따르고 있는가하는 것 까지 모두 검사하는 것입니다. 사용된 xml문서는 첨부 파일에서 다운 받으실 수 있습니다 :

bool validateDocument(const std::string& fileName) {
    g_pXMLDoc->load(fileName.c_str());

    // 최근 실행 된 XML관련 오퍼레이션에 대한 결과를 리턴
    MSXML2::IXMLDOMParseErrorPtr pError = g_pXMLDoc->parseError;

    _bstr_t strResult = "";
    bool isValidate = true;
    if (pError->errorCode != S_OK) {
        strResult = _bstr_t("Validation failed on ") + fileName.c_str() +
        _bstr_t("\n=====================") +
        _bstr_t("\nReason: ") + _bstr_t(pError->Getreason()) +
        _bstr_t("\nSource: ") + _bstr_t(pError->GetsrcText()) +
        _bstr_t("\nLine: ") + _bstr_t(pError->Getline()) +
        _bstr_t("\n");
        isValidate = false;
    }
    else {
        strResult = _bstr_t("Validation succeeded for ") + fileName.c_str() +
        _bstr_t("\n======================\n") +
        _bstr_t(g_pXMLDoc->xml) + _bstr_t("\n");
        isValidate = true;
    }

    pError.Release(); // RefCount를 사용하는 포인터이므로 Release과정이 꼭 필요

    std::cout << ConverseBstrToString(strResult) << std::endl;
    return isValidate;
}

4. _bstr_t를 char* 로 변환 하기

XML파서에서 리턴하는_bstr_t 라는 문자열을 일반 cout이나 printf에서 출력 하면 정상적인 문자열이 출력되는 것이 아니라 알 수 없는 숫자들의 나열로 출력이 됩니다. 이유인즉슨 wide char를 사용하는 xml과 아스키코드를 사용하는 C/C++간의 문자열 바이트 차이 때문인데요, 이 문자열들을 정상적으로 보기 위해서는 간단한 변환 과정을 거쳐야 합니다 :

#include <atlconv.h> // USES_CONVERSION과 W2A를 사용하기 위해 include

std::string ConverseBstrToString(const _bstr_t& str) {
    USES_CONVERSION;
    return std::string(W2A(str));
}

altconv.h 파일을 인클루드 하기 싫으시다면 아래의 방법도 있습니다 :

std::string ConverseBstrToString(const _bstr_t& str) {
    char buf[1024] = ;
    sprintf(buf, "%S", (LPCTSTR)str);
    return std::string(buf);
}

5. 엘리먼트의 텍스트 읽어 오기

첨부 되어 있는 예제 xml 문서를 보면 아래와 같은 형식의 comment 라는 엘리먼트가 있습니다 :

<purchaseOrder ... >
    ....
    <comment>Hurry, my lawn is going wild!</comment>
    ....
</purchaseOrder>

위의 comment가 가지고 있는 텍스트를 읽어오기 위해서는 먼저 comment의 엘리먼트를 얻어오고, 그 엘리먼트에서 텍스트를 가지고 오면 됩니다 :

void selectSingleElement_and_getText() {
    MSXML2::IXMLDOMNodePtr singleNodePtr = g_pXMLDoc->selectSingleNode(_bstr_t("/purchaseOrder/comment"));
    if(NULL == singleNodePtr) {
        return;
    }
    const BSTR str = singleNodePtr->Gettext();
    std::cout << ConverseBstrToString(str) << std::endl;

    singleNodePtr.Release();
}

코드 자체가 워낙 간단하여 긴 설명 보다는 개조식의 간단한 설명을 드리겠습니다 :

  1. 엘리먼트를 저장하기 위해서 IXMLDOMNodePtr을 선언 합니다.
  2. XMLDocument객체에서 selectSingleNode를 이용해 엘리먼트 객체를 얻어 옵니다. 이 때 XPath를 이용합니다.
    (이 함수는 특정 하나의 엘리먼트를 얻어오기 위해 사용되는 것입니다. 엘리먼트 리스트를 얻어오기 위한 함수는 뒤에서 다시 한번 살펴 보기로 하겠습니다.)
  3. 얻어진 엘리먼트에서 Gettext()를 호출 하여 텍스트 내용을 얻어 옵니다.
  4. 얻어진 텍스트를 아스키 형태로 변환하기 위해 위에서 작성한 ConverseBstrToString() 함수를 이용 합니다.

6. 엘리먼트의 속성(attribute) 읽어 오기

void selectSingleElement_and_getAttribute() {
    MSXML2::IXMLDOMNodePtr singleNodePtr;
    MSXML2::IXMLDOMAttributePtr attrPtr;
    std::string retString("");

    do {
        singleNodePtr = g_pXMLDoc->selectSingleNode(_bstr_t("/purchaseOrder/shipTo"));
        if(NULL == singleNodePtr) {
            break;
        }
        attrPtr = singleNodePtr->Getattributes()->getNamedItem("country");
        if(NULL == attrPtr) {
            break;
        }
        const BSTR str = attrPtr->Getvalue().bstrVal;
        // or 'const BSTR str = attrPtr->Gettext() is ok'
        retString = ConverseBstrToString(str);
    } while(false);

    if(NULL != singleNodePtr) {
        singleNodePtr.Release();
    }
    if(NULL != attrPtr) {
        attrPtr.Release();
    }

    std::cout << "Attribute in Element : " << retString << std::endl;
}
  1. selectSingleNode() 함수를 이용하여 엘리먼트를 얻어 오는 것 까지는 위에서 살펴 본 것과 동일합니다.
  2. 엘리먼트를 얻고 난 후에는 Getattributes() 함수를 이용해 어트리뷰트들을 얻고, getNamedItem() 함수를 이용해 특정 어트리뷰트를 이름을 통해 가지고 옵니다.
  3. 어트리뷰트가 가지고 있는 값을 가지고 올 때, 어트리뷰트의 타입을 지정할 수 있습니다. 만일 어트리뷰트가 단순한 문자열이라면 Getvalue().bstrVal 이나 Gettext() 함수를 이용해 가지고 올 수 있습니다. 만일 정수라던지 하는 다른 타입일 경우 Getvalue().xxxVal에서 적절한 타입을 지정해 주시면 됩니다. 지정될 타입에 대해서는 MSDN이나 해당 union 구조체를 확인 하시면 됩니다.

7. 엘리먼트 리스트 읽어 오기

엘리먼트 리스트에서 값을 얻어 오는 것 또한 위의 과정들과 크게 다르지 않습니다 :

void selectElementArray() {
    MSXML2::IXMLDOMNodeListPtr nodeListPtr = g_pXMLDoc->selectNodes(_bstr_t("/purchaseOrder/items/item"));
    for(int i=0; i<nodeListPtr->Getlength(); i++) {
        MSXML2::IXMLDOMNodePtr parentNodePtr = nodeListPtr->Getitem(i);
        MSXML2::IXMLDOMNodePtr childNodePtr = parentNodePtr->selectSingleNode(_bstr_t("productName"));
        if(NULL != childNodePtr) {
            const BSTR str = childNodePtr->Gettext();
            std::cout << util::ConverseBstrToString(str) << std::endl;
        }
        childNodePtr.Release();
        parentNodePtr.Release();

    }
    nodeListPtr.Release();
}
  1. 가장 먼저 하는 일은 selectNodes() 함수를 이용해 엘리먼트 리스트를 얻어 오는 것입니다.
  2. 엘리먼트 리스트를 얻어 오는 것이 성공하면 IXMLDOMNodeListPtr에 그 값이 저장 됩니다. 만일 엘리먼트가 하나 뿐이거나 기타등등 조건에 맞지 않는 경우 NULL을 리턴하므로 다른 함수도 물론 그렇지만 이 함수를 호출 하고난 후에 NULL체크를 하여 성공 여부를 판단하는 것이 중요 합니다.
  3. 몇 개의 엘리먼트를 얻어 왔는지는 Getlength() 함수를 통해 알아 낼 수 있습니다.
  4. 엘리먼트 리스트는 인덱스를 지정하므로써 각각의 엘리먼트들에게 접근 할 수 있습니다. 이 때 사용되는 함수는 Getitem()이며 파라메터로 인덱스를 받습니다.
  5. 그 외의 나머지 과정의 위에서 설명한 과정과 동일 합니다.

결론

간단하게 나마 msxml 파서의 사용법에 대해서 알아 보았습니다. msxml에 대해서 정확하게 알아 보기 위해서는 DOM과 COM 스펙에 대해 좀 더 조사하고, 위에 언급한 API말고도 수많은 편리함을 제공 해 주는 API들이 많지만 필자가 공부를 하면서 가장 많이 사용했던 API 몇 가지에 대해서만 언급을 하였습니다. 보다 더 많은 정보가 필요하신 분들은 MSDN이나 W3C에 방문하셔서 검색해 보시면 심도 있는 정보를 얻으실 수 있으실 겁니다.

부록 1. 전체 코드

#include <string>
#include <atlconv.h> // USES_CONVERSION과 W2A를 사용하기 위해 include
#import <msxml4.dll>

std::string ConverseBstrToString(const _bstr_t& str) {
    USES_CONVERSION;
    return std::string(W2A(str));
}

MSXML2::IXMLDOMDocument2Ptr g_pXMLDoc;

bool initializeCOM();
bool initializeMSXML();
bool validateDocument(const std::string& fileName);

void selectSingleElement_and_getText();
void selectSingleElement_and_getAttribute();
void selectElementArray();

int main(int argc, char* argv[])
{
    initializeCOM();
    initializeMSXML();
    // xml 파일을 연다.
    do {
        if(false == validateDocument("Test_MSXML.xml")) {
            break;
        }

        selectSingleElement_and_getText();
        selectSingleElement_and_getAttribute();
        selectElementArray();
        
    }while(false);
   
    g_pXMLDoc.Release();
    CoUninitialize();

    return 0;
}

bool validateDocument(const std::string& fileName) {
    g_pXMLDoc->load(fileName.c_str());

    // 최종에 실행 된 XML관련 오퍼레이션에 대한 결과를 리턴한다.
    MSXML2::IXMLDOMParseErrorPtr pError = g_pXMLDoc->parseError;

    // Return validation results in message to the user.
    _bstr_t strResult = "";
    bool isValidate = true;
    if (pError->errorCode != S_OK)
    {
        strResult = _bstr_t("Validation failed on ") + fileName.c_str() +
            _bstr_t("\n=====================") +
            _bstr_t("\nReason: ") + _bstr_t(pError->Getreason()) +
            _bstr_t("\nSource: ") + _bstr_t(pError->GetsrcText()) +
            _bstr_t("\nLine: ") + _bstr_t(pError->Getline()) +
            _bstr_t("\n");
        isValidate = false;
    }
    else {
        strResult = _bstr_t("Validation succeeded for ") + fileName.c_str() +
        _bstr_t("\n======================\n") +
        _bstr_t(g_pXMLDoc->xml) + _bstr_t("\n");
        isValidate = true;
    }

    pError.Release();

    std::cout << util::ConverseBstrToString(strResult) << std::endl;
    return isValidate;
}

bool initializeCOM() {
    // COM 객체를 사용하기 위해 초기화를 한다.
    // 윈도우 버젼 버젼이 올라가면서 확장된 초기화 함수를 사용 할 수 있다.
#if _WIN32_WINNT >= 0x0400 & defined(_ATL_FREE_THREADED)
    CoInitializeEx(NULL, COINIT_MULTITHREADED);
#else
    CoInitialize(NULL);
#endif

    return true;
}

bool initializeMSXML() {
    // DOM 의 핵심이 되는 객체
    // 파싱된 XML을 저장하고 기타 각종 설정들을 담고 있다.
    g_pXMLDoc.CreateInstance(__uuidof(MSXML2::DOMDocument40));
    g_pXMLDoc->async = VARIANT_FALSE;
    g_pXMLDoc->validateOnParse = VARIANT_TRUE;
    g_pXMLDoc->resolveExternals = VARIANT_TRUE;

    return true;
}

void selectSingleElement_and_getText() {
    MSXML2::IXMLDOMNodePtr singleNodePtr = g_pXMLDoc->selectSingleNode(_bstr_t("/purchaseOrder/comment"));
    if(NULL == singleNodePtr) {
        return;
    }
    const BSTR str = singleNodePtr->Gettext();
    singleNodePtr.Release();
    std::cout << ConverseBstrToString(str) << std::endl;
}

void selectSingleElement_and_getAttribute() {
    MSXML2::IXMLDOMNodePtr singleNodePtr;
    MSXML2::IXMLDOMAttributePtr attrPtr;
    std::string retString("");

    do {
        singleNodePtr = g_pXMLDoc->selectSingleNode(_bstr_t("/purchaseOrder/shipTo"));
        if(NULL == singleNodePtr) {
            break;
        }
        attrPtr = singleNodePtr->Getattributes()->getNamedItem("country");
        if(NULL == attrPtr) {
            break;
        }
        const BSTR str = attrPtr->Getvalue().bstrVal;
        // or const BSTR str = attrPtr->Gettext() is ok
        retString = util::ConverseBstrToString(str);
    } while(false);


    if(NULL != singleNodePtr) {
        singleNodePtr.Release();
    }
    if(NULL != attrPtr) {
        attrPtr.Release();
    }

    std::cout << "Attribute in Element : " << retString << std::endl;
}

void selectElementArray() {
    MSXML2::IXMLDOMNodeListPtr nodeListPtr = g_pXMLDoc->selectNodes(_bstr_t("/purchaseOrder/items/item"));
    for(int i=0; i<nodeListPtr->Getlength(); i++) {
        MSXML2::IXMLDOMNodePtr parentNodePtr = nodeListPtr->Getitem(i);
        MSXML2::IXMLDOMNodePtr childNodePtr = parentNodePtr->selectSingleNode(_bstr_t("productName"));
        if(NULL != childNodePtr) {
            const BSTR str = childNodePtr->Gettext();
            std::cout << util::ConverseBstrToString(str) << std::endl;
        }
        childNodePtr.Release();
        parentNodePtr.Release();

    }
    nodeListPtr.Release();
}

부록 2. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!