2010年9月24日 星期五

使用 ISAXContentHandler 解讀 XML 的注意事項

在 C++ 底下要解讀 XML 檔案常用的有兩個方法,一是使用 IE6 就有附加的 CLSID_DOMDocument30 元件,二是使用較底層的 Windows API ISAXContentHandler,兩都都需要 include msxml2.h 檔案

使用 XML DOM Document 3.0 的方法比較直觀但效率較差,簡易的範例如下


#include <msxml2.h>

int main()
{
  CComPtr<IXMLDOMDocument> spXMLDOM;
  HRESULT hr;
  hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  if(FAILED(hr))
  {
    CoUninitialize();
  }

  hr = spXMLDOM.CoCreateInstance(CLSID_DOMDocument30, NULL, CLSCTX_INPROC_SERVER);
  if(FAILED(hr))
  {
    CoUninitialize();
  }
  // 讀xml檔案路徑
  hr = spXMLDOM->load(CComVariant(strFileSource), &bSuccess);

  // 或使用以下方法讀xml字串
  // hr = spXMLDOM->loadXML(CComBSTR(p_strXMLString), &vbSuccess);

  // 以下為取得元素值
  CString strValue;
  CComBSTR bstrSS(_T("tagA"));
  CComPtr<IXMLDOMNode> spXMLNode = NULL;
  BSTR bstrXmlText;
  if(SUCCEEDED(spXMLDOM->selectSingleNode(bstrSS, &spXMLNode)) && spXMLNode != NULL)
  {
    spXMLNode->get_text(&bstrXmlText);
    strValue = bstrXmlText;
  }

  // 以下為取得元素屬性
  CString strAttribute;
  CComBSTR bstrSS(_T("tagA"));
  CComQIPtr<IXMLDOMElement> spXMLChildElement;
  VARIANT variantValue;
  spXMLNode = NULL;
  if(SUCCEEDED(m_spXMLDOM->selectSingleNode(bstrSS, &spXMLNode)) && spXMLNode != NULL)
  {
    spXMLChildElement = spXMLNode;
    if(FAILED(spXMLChildElement->getAttribute(CComBSTR(_T("attributeA")), &variantValue)))
    {
      strAttribute = variantValue.bstrVal;
    }
  }

  return TRUE;
}


另外一個方法是繼承 ISAXContentHandler 寫一個專用的 Parser,ISAXContentHandler 是一個純虛擬類別,我們必須 override 所有的 function,例如我們要 Parse 一個以下結構的 XML 檔案


<stocklist>
  <stock>
    <id>2412</id>
    <name>中華電</name>
    <close>60.8</close>
    <price>61.4</price>
    <volume>28697</volume>
  </stock>
  <stock>
    <id>2454</id>
    <name>聯發科</name>
    <close>475</close>
    <price>483</price>
    <volume>18643</volume>
  </stock>
</stocklist>


通常最重要的就是這三個function


HRESULT STDMETHODCALLTYPE CGetStockListParser::startElement(
/* [in] */ const wchar_t __RPC_FAR *pwchNamespaceUri,
/* [in] */ int cchNamespaceUri,
/* [in] */ const wchar_t __RPC_FAR *pwchLocalName,
/* [in] */ int cchLocalName,
/* [in] */ const wchar_t __RPC_FAR *pwchQName,
/* [in] */ int cchQName,
/* [in] */ ISAXAttributes __RPC_FAR *pAttributes)

HRESULT STDMETHODCALLTYPE CGetStockListParser::characters(
/* [in] */ const wchar_t __RPC_FAR *pwchChars,
/* [in] */ int cchChars)

HRESULT STDMETHODCALLTYPE CGetStockListParser::endElement(
/* [in] */ const wchar_t __RPC_FAR *pwchNamespaceUri,
/* [in] */ int cchNamespaceUri,
/* [in] */ const wchar_t __RPC_FAR *pwchLocalName,
/* [in] */ int cchLocalName,
/* [in] */ const wchar_t __RPC_FAR *pwchQName,
/* [in] */ int cchQName)


startElement 是用來指定收到一個起始tag時要做啥米事情,以上面的股市清單為例,通常這麼做


#define TAG_STOCK _T("stock")

m_strCurrentTag = CString( pwchLocalName, cchLocalName );
if ( m_strCurrentTag == TAG_STOCK )
{
  m_pCurrentStockInfo = new CStockListItem();
}


而 characters 是用來指定收到 tag 的內容時要做的事,


#define TAG_STOCK_ID _T("id")
#define TAG_STOCK_NAME _T("name")
#define TAG_STOCK_CLOSE _T("close")
#define TAG_STOCK_PRICE _T("price")
#define TAG_STOCK_VOLUME _T("volume")

wchar_t* wchValue = (wchar_t*)calloc(cchChars + 1, sizeof(wchar_t));
wcsncpy( wchValue, pwchChars, cchChars );
CString strValue( wchValue );
free(wchValue);
if ( m_pCurrentStockInfo )
{
  if ( m_strCurrentTag == TAG_STOCK_ID )
  {
    // 個股編號
    m_strCurrentValue += strValue;
  }
  else if ( m_strCurrentTag == TAG_STOCK_NAME )
  {
    // 個股名稱
    m_strCurrentValue += strValue;
  }
  else if ( m_strCurrentTag == TAG_STOCK_CLOSED )
  {
    // 昨日收盤價
    m_strCurrentValue += strValue;
  }
  else if ( m_strCurrentTag == TAG_STOCK_PRICE )
  {
    // 交易價格
    m_strCurrentValue += strValue;
  }
  else if ( m_strCurrentTag == TAG_STOCK_VOLUME )
  {
    // 累計交易量
    m_strCurrentValue += strValue;
  }
}


endElement 是用來指定收到結束 tag 時要做的事情


m_strCurrentTag = CString(); // 清空目前 tag 的紀錄

CString strTag =CString( pwchLocalName, cchLocalName );

if ( m_strCurrentTag == TAG_STOCK_ID )
{
  // 個股編號
  m_pCurrentStockInfo->SetID(m_strCurrentValue);
  m_strCurrentValue.Empty();
}
else if ( m_strCurrentTag == TAG_STOCK_NAME )
{
  // 個股名稱
  m_pCurrentStockInfo->SetName(m_strCurrentValue);
  m_strCurrentValue.Empty();
}
else if ( m_strCurrentTag == TAG_STOCK_CLOSED )
{
  // 昨日收盤價
  m_pCurrentStockInfo->SetClose(m_strCurrentValue);
  m_strCurrentValue.Empty();
}
else if ( m_strCurrentTag == TAG_STOCK_PRICE )
{
  // 交易價格
  m_pCurrentStockInfo->SetPrice(m_strCurrentValue);
  m_strCurrentValue.Empty();
}
else if ( m_strCurrentTag == TAG_STOCK_VOLUME )
{
  // 累計交易量
  m_pCurrentStockInfo->SetVolume(m_strCurrentValue);
  m_strCurrentValue.Empty();
}

if( m_strCurrentTag == TAG_STOCK)
{
  // 結束一筆完整的 StockItem
  m_pRoot->AppendChild(m_pCurrentStockInfo);
  m_pCurrentStockInfo = NULL;
}


其實整篇的重點在於 characters 這個 function 中,全部都使用 "+=" 來接 tag 中的內容,並且在 endElement 時將 m_strCurrentValue 清空,若使用 "=" 來接內容不就不用清空 m_strCurrentValue 了嗎?反正新的值在 assign 時自然就會把舊的值蓋掉了不是嗎?

不使用 "=" 的原因在於若 tag 的內容比較特殊,例如有 ()[] 等奇怪的字元,會以這種奇怪字元的個數為單位重覆呼叫 characters 數次,例如:"大家好(Hello)我叫小源源",就會呼叫數次 characters 此 function,明確的分法和次數不是重點,大概就是這樣

第一次:大家好(
第二次:Hello)
第三次:我叫小源源

所以才要在同一個 tag 的情況下用 += 來串接收到的值,否則會發生這個 tag 被 parse 出來的值只有最後收到的那次,由於是做用 "+=" 來串接,所以在收到 end tag 時也要記得把暫存字串清空嘍。

沒有留言:

張貼留言