COM, ATL

모니커와 MSHTML을 이용한 HTML 파싱

디버그정 2008. 9. 10. 20:29

이번 호에서 다룰 내용은 지정한 URL의 내용을 HTTP 프로토콜을 이용해 읽어 HTML을 파싱(Parsing)해보는 것이다. 사실 이전에 WinInet을 이용해 웹 서버에 존재하는 HTML 파일을 읽어들이는 예제를 여러 번 다루었으므로 그것과 어떤 면에서 다른가 의아해 하는 독자 여러분이 계실지도 모르겠다. 이번에 다룰 주제는 HTTP 프로토콜을 이용해 HTML 파일이나 그림 등의 바이너리 파일을 읽어오는 것이 아니라 읽어온 HTML을 파싱하는 것이 중심이 된다.

글 한기용 WISEnut, Inc.
keeyong@wisenut.com

만일 직접 HTML 파서를 만들 실력과 시간이 있다면 굳이 이번 회의 내용을 읽을 필요는 없다. 기존의 WinInet 모듈에 자신이 만든 HTML 파서를 붙여버리면 된다. 하지만 이미 만들어져서 제대로 동작하는 HTML 파서가 있다면 번거롭게 새로 만들 필요는 없다. 인터넷 익스플로어를 설치하면 기본으로 HTML 파싱 관련 모듈인 MSHTM이 설치 된다. 여기에는 IHTMLDocument2라는 인터페이스가 제공되는데 이것이 바로 HTML 파싱 기능을 제공해 준다(사용법이 좀 까다롭긴 하지만 조금만 손에 익으면 아주 편리하게 사용할 수 있다).
또 이번 회에서 WinInet으로 HTML 파일을 읽어오지 않고 URL 모니커(Moniker)라는 것을 이용해서 읽어보도록 하겠다. 이렇게 하는 이유는 URL 모니커와 IHTMLDocument2 인터페이스 간의 궁합이 훨씬 더 좋기 때문이다. 예제 프로그램은 비주얼 C++과 MFC를 이용해서 만들어 보도록 하겠다. COM에 대한 지식이 필요하다.

1 모니커란
모니커(Moniker)는 별명을 뜻하는 단어지만 윈도우 프로그래밍에서 모니커는 원래 의미와 비슷하게 다른 객체를 지칭하는데 사용된다. 그런데 그렇게 사용되는 모니커 자체도 객체(IMoniker라는 인터페이스를 구현해야 하며 대개 DLL로 존재한다)이며 모니커는 자신이 가리키는 객체에 대한 포인터를 제공해주는 역할을 담당한다. 이렇게 모니커를 만들어 다른 객체를 가리키도록 한 다음 그것에 접근하기 위해 그 객체에 대한 포인터를 얻어내는 절차를 바인딩(Binding)이라 한다. 모니커를 사용하여 다른 객체의 내용에 접근하는 쪽을 모니커 클라이언트(IMoniker 인터페이스를 사용해서 모니커가 지칭하는 내용에 대한 포인터를 얻어낸다)라 하고 반대로 자신의 내용을 모니커 클라이언트에게 제공하는 쪽을 모니커 프로바이더(Provider)라고 한다.
사실 모니커는 OLE 복합문서(Compound Document)에서 객체에 연결하고 객체를 활성화는데 쓰인다. 모니커의 가장 큰 장점은 같은 컴퓨터 안에 존재하는 객체이건 다른 컴퓨터에 존재하는 객체이건 간에 사용하는 방법이 같다는 점이다. OLE에서 제공되는 모니커의 종류로는 다음과 같은 것들이 있다.

파일 모니커, 컴포지트 모니커, 아이템 모니커, 안티 모니커, 포인터 모니커, 클래스 모니커

포인터 모니커와 안티 모니커는 주로 OLE 내부적인 용도로 사용되는 것이라 일반 프로그래머가 쓸 일은 거의 없을 것이다. 클래스 모니커는 CLSID를 인자로 COM 객체를 생성하는데 사용되던 CoCreateInstance, CoGetClassObject 등의 함수를 모니커라는 틀에 맞게 재구성한 것이다. 이렇게 한 이유는 이를 다른 모니커와 함께 컴포지트로 구성하면 유용하게 쓸 수 있기 때문이다. 여기서는 이 모니커들보다는 인터넷 다운로드에 사용할 수 있는 비동기 모니커와 URL 모니커에 대해 살펴보겠다.

1-1 비동기 모니커(Asynchronous Moniker)
앞서 살펴본 모니커들은 표준 OLE 모니커들로 작업이 동기적이라는 제한점이 존재했다. 즉 어떤 객체에 대한 포인터를 요청했을 때 그 작업이 종료되어야만 리턴했다는 것이다. 이런 점은 네트워크 환경, 특히 인터넷 환경에서는 적절하지 않은 특성이다. 인터넷 환경이 일반화되면서 모니커 디자인에도 영향을 끼치게 되었다. 필요한 기능은 두 가지 정도로 요약될 수 있는데 첫 번째는 현재 네트워크를 통해 데이터가 다운로드되는 상황을 모니터링할 수 있는 기능으로 이는 사용자에게 현재 상황을 피드백하기 반드시 필요한 기능이다. 두 번째는 비동기적으로 데이터를 받아들일 수 있는 기능이다.
비동기 모니커는 바로 이러한 기능을 제공한다. 비동기 모니커 객체를 이용하려면 모니커 클라이언트는IBindStatusCallback이라는 인터페이스를 지원해야 한다. 비동기 모니커 중의 하나가 바로 뒤에서 살펴볼 URL 모니커이다.

1-2 URL 모니커
URL 모니커는 URL로 접근 대상이 되는 객체를 가리키는 모니커다. URL 모니커는 비동기 작업 뿐만 아니라 동기 작업도 지원한다. URL 모니커의 생성은 CreateURLMoniker API를 이용해 수행하게 된다. 앞의 비동기 모니커에서 이야기한 것처럼 URL 모니커에서 비동기 작업을 하려면 URL 모니커 클라이언트가IBindStatusCallback 인터페이스를 지원해야 한다.
그런데 본 예제 프로그램에서는 이를 지원하지 않았다. 그 이유는 HTML 파싱을 위해 MSHTML서 지원하는 모니커(IPersistMoniker)를 같이 사용했기 때문이다. 그리고 비동기 작업을 위해URL 모니커에서 필요로 하는 IBindStatusCallback 인터페이스를 구현하지 않고 MSHTML에서 필요로 하는  IPropertyNotifySink 인터페이스를 구현하였다.
URL 모니커와 MSHTML 모니커를 어떤 식으로 사용할 수 있는지 실제 코드를 보면서 설명하도록 하겠다. 여기 나온 코드는 실제로 뒤에서 살펴볼 예제 프로그램에 있는 코드들이며 이 예제 코드는 MSDN에서 제공해 주는 Walkall이라는 프로그램을 HTTP 프로토콜만 사용하도록 간단하게 수정하고 MFC 기반으로 변경한 것이다. 필자는 모니커와 HTML 파싱 관련 코드를 모두 CHtmlPage라는 하나의 클래스로 구성하였다. 이 클래스를 사용하려면 제일 먼저 Initialize 함수를 호출하여 클래스를 초기화해야 한다. 이 함수의 소스 코드는 다음과 같다.


HRESULT CHtmlPage::Initialize(){
 HRESULT hr;

 // COM 라이브러리를 초기화한다.
 if (FAILED(hr = CoInitialize(NULL)))
 {
  return hr;
 }
 // MSHTML과 모니커 관련 초기화를 수행한다.
              return InitMSHTML();
}

위의 함수에서 InitMSHTML이라는 함수를 호출하는데 그 소스 코드는 리스트 1과 같다.

리스트 1 : Init1MSHTML 함수의 소스 코드 ----------------------------
HRESULT CHtmlPage::InitMSHTML()
{
 HRESULT hr;
 LPCONNECTIONPOINTCONTAINER pCPC = NULL;
 LPOLEOBJECT pOleObject = NULL;
 LPOLECONTROL pOleControl = NULL;

 // HTML 파싱에 사용할 MSHTML 객체를 생성한다
 if (FAILED(hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER,                                           IID_IHTMLDocument2, (LPVOID*)&m_pMSHTML)))
  return hr;

               // 모니커를 통해 비동기 읽기를 수행했을 때 그 결과를 리턴받을 CNotSink 클래스의 객체를 만든다
 if (m_pSink)
  m_pSink->Release(); m_pSink = new CNotSink(m_pMSHTML, &m_lReadyState); // 인자로 상태 변화를 받아들일 변수를 지정

 // 앞에서 만든 CnotSink 클래스의 인스턴스를 이벤트 객체로 연결한다
 if (FAILED(hr = m_pMSHTML->QueryInterface(IID_IConnectionPointContainer, (LPVOID*)&pCPC)))
  goto Error;

 if (FAILED(hr = pCPC->FindConnectionPoint(IID_IPropertyNotifySink, &m_pCP)))  goto Error;

 m_hrConnected = m_pCP->Advise((LPUNKNOWN)(IPropertyNotifySink*)m_pSink, &m_dwCookie);

Error:
 if (pCPC) pCPC->Release();

 return hr;
}
-------------------------------

리스트 1을 살펴보면 HTML 파싱에 쓸 IHTMLDocument2 인터페이스를 먼저 생성하고 있다. CHtmlPage 클래스에는 IHTMLDocument2 * 타입의 m_pMSHTML라는 변수가 존재하는데 이것에 그 포인터를 저장해 둔다. 이 인터페이스가 바로 뒤에서 살펴볼 MSHTML에 포함되는 인터페이스 중 하나로 HTML 파싱을 하는데 쓸 수 있다(뒤에서 HTML 파싱을 설명하는 부분에서 많이 보게 될 것이다).
다음으로 비동기 작업 상황을 모니터링하는데 쓸 CNotSink 객체를 하나 생성한다. 이 객체가 바로 앞에서 설명한IPropertyNotifySink 인터페이스를 구현한 클래스에 해당한다. CNotSink 클래스의 두 번째 인자를 보면m_lReadyState라는 변수가 지정된다. 이 변수로 작업 진행 상황이 넘어오게 구현했으며 이 변수로 들어오는 값은 다음과 같다.

READYSTATE_UNINITIALIZED  // 작업이 아직 시작되지 않음
READYSTATE_LOADING        // 로딩 중임
READYSTATE_LOADED         // 로딩되었음
READYSTATE_INTERACTIVE   // 데이터를 읽고 있는 중임
READYSTATE_COMPLETE      // 다 읽었음

나중에 비동기 HTML 읽기 요청이 종료되었는지 확인하고 싶다면 이 변수의 값이READYSTATE_COMPLETE가 될 때까지 계속 모니터링하면 된다. 사실 이런 방법은 굉장히 안 좋은 방법이고 이벤트 등을 사용하거나 윈도우 기반의 프로그램이라면 메시지를 사용하여 작업이 끝났음을 알리는 것이 좋다. 아무튼 CNotSink 클래스의 객체를 COM의 커넥션 포인트를 이용해 MSHTML 쪽의 이벤트 객체로 연결한다. 비동기 작업의 진척 상황이 발생할 때마다 CNotSink 클래스의 멤버 함수가 호출된다. 이것으로 CHtmlPage 클래스를 사용할 준비는 되었다.
이제 실제로 URL을 지정해서 그 내용을 읽어들이는 부분을 살펴보자. CHtmlPage 클래스의 LoadURLFromMoniker라는 멤버 함수에서 인자로 주어진 URL의 내용을 읽어들이는 역할을 한다.

리스트 2 : URL을 지정해서 그 내용을 읽어들임-------------------------------
HRESULT CHtmlPage::LoadURLFromMoniker(LPCSTR lpURL)
{
 HRESULT hr;
 OLECHAR  wszURL[MAX_PATH*sizeof(OLECHAR)];

               // URL 모니커의 바인딩 작업의 상태를 가리키는 변수를 초기화한다
 m_lReadyState = READYSTATE_UNINITIALIZED;
               // 접근하고자 하는 URL을 기록한다
 m_strMainURL = lpURL;
               // lpURL이 가리키는 값을 유니코드로 변환한다
 if (0 == MultiByteToWideChar(CP_ACP, 0, lpURL, -1, wszURL, MAX_PATH*sizeof(OLECHAR))) {
  return E_FAIL;
 }

 LPMONIKER pMk = NULL;
 LPBINDCTX pBCtx = NULL;
 LPPERSISTMONIKER pPMk = NULL;
 // 시스템에 URL 모니커를 요청한다
 if (FAILED(hr = CreateURLMoniker(NULL, wszURL, &pMk)))
 {
  return hr;
 }
 // 바인딩 컨텍스트를 생성한다
 if (FAILED(hr = CreateBindCtx(0, &pBCtx))) {
  goto Error;
 }

 // MSHTML 모니커를 얻어내 그것과 URL 모니커를 연결한다
 if (SUCCEEDED(hr = m_pMSHTML->QueryInterface(IID_IPersistMoniker,(LPVOID*)&pPMk)))
 {
  // MSHTML 모니커(IPersistMoniker)의 Load 함수를 부른다. 이 함수는
                                // 바로 리턴하지만IPropertyNotifySink::OnChanged에서READYSTATE_COMPLETE를
                                // 리턴하기 전까지는 요청한 내용은 아직 읽혀지지 않은 상태이다
  hr = pPMk->Load(FALSE, pMk, pBCtx, STGM_READ);
 }
 
Error:
 if (pMk)
                    pMk->Release();
 if (pBCtx)
                    pBCtx->Release();
 return hr;
}
-----------------------------------------------

리스트 2에서 최종적으로 HTML 파일을 읽어오도록 하는 부분은 hr = pPMk->Load(FALSE, pMk, pBCtx, STGM_READ);이다. 현재 비동기 호출이 되기 때문에 언제 다 읽었는지는 다른 부분에서 판단해야 한다. 그 부분의 코드가 바로 CNotSink 클래스에 존재한다. 클라이언트에서 요청한 데이터를 MSHTML 모니커가 다 읽었으면 IPropertyNotifySink 클래스의 OnChanged 멤버 함수를 호출하여 알려주도록 되어 있다. NotSink.cpp의 CNotSink::OnChanged 함수의 소스 코드를 확인해 보기 바란다.
그러면 LoadURLFromMoniker 함수를 호출하는 부분의 코드를 살펴보도록 하자. 실제로 CHtmlPage 클래스를 사용하는 프로그래머가 특정 URL을 주고 그 내용을 읽어들이고 싶을 때 호출하는 함수는 리스트 3의 LoadURL이다. LoadURL 안에서 LoadURLFromMoniker 함수를 호출한다.

리스트 3 : LoadURL-----------------------------
HRESULT CHtmlPage::LoadURL(LPCSTR lpcURL)
{
 if (!m_pMSHTML||!m_pSink)
 {
  return E_UNEXPECTED;
 }

 try
 {
  // 지정된 URL을 읽어온다
  LoadURLFromMoniker(lpcURL); }
 catch(CException e)
 {
  char szCause[255];
 
  e.GetErrorMessage(szCause, 255);
  AfxMessageBox(szCause);
  return S_FALSE;
 }

 // 다 읽어올 때까지 대기한다
 while(READYSTATE_COMPLETE != m_lReadyState)
 {
  MSG       msg;

  while (PeekMessage(&msg, NULL, NULL, NULL, TRUE))   {
      TranslateMessage(&msg);
                    DispatchMessage(&msg);
  }
 }
 return S_OK;
}
-------------------------------------------

LoadURLFromMoniker 함수를 호출한 다음 그 내용을 다 읽을 때까지 대기하는 while 루프가 있음을 알 수 있다. 단순히 대기하면 다른 작업이 모두 블록되므로 여기서는 메시지 루프를 읽어서 처리할 메시지가 존재하면 그것을 처리하면서 대기한다. 하지만 이것은 앞서 이야기한 것처럼 그리 좋은 방법은 아니다. 원래 WalkAll 프로그램에서는 PostThreadMessage 함수를 이용하도록 되어있었는데 이를 수정한 것이다.

1-3 MFC에서의 비동기 모니커 지원
지금까지 살펴본 내용과는 조금 관계없는 내용이지만 모니커를 MFC에서 사용하는 예를 잠시 살펴보자. MFC에는 모니커 관련 작업을 쉽게 해주는 많은 클래스들이 제공된다. 예를 들어 비동기 모니커를 이용해 파일을 다운 로드 받고 싶다면 CasyncMonikerFile을 쓰면 된다. 사실 이 클래스는 CFileMoniker로부터 계승되었으며 내부에 IBindStatusCallback 인터페이스를 구현해 놓아 쓰기 편하다. CAsyncMonikerFile 클래스를 이용해 HTML 파일을 다운로드 받는 예를 살펴보자.

1. CAsyncMonikerFile 클래스로부터 새로운 클래스를 하나 계승받는다.
2. 계승받은 클래스의 OnDataAvailable 멤버 함수를 오버라이드한다. 사실 이 함수만 오버라이드해도 대부분의 작업을 수행할 수 있다. 필요하면 OnProgress, OnStartBinding, OnStopBinding과 같은 메소드를 오버라이드한다. OnDataAvailable 함수의 원형은 다음과 같다.

virtual void OnDataAvailable( DWORD dwsize, DWORD bscfFlag );
첫 번째 인자인 dwsize에는 바인딩 작업이 시작된 이래로 읽어들인 데이터의 크기가 들어온다. 두 번째 인자인 bscfFlag로는 다음과 같은 값이 들어와서 현재 작업의 상태를 알려준다.

* BSCF_FIRSTDATANOTIFICATION : 데이터의 로딩 작업이 처음 시작되었을 때 이 값이 들어온다.
* BSCF_INTERMEDIATEDATANOTIFICATION : 데이터 로딩 작업의 중간에 계속 이 값이 인자로 들어온다.
* BSCF_LASTDATANOTIFICATION : 데이터의 로딩 작업이 종료되었을 때 이 인자가 값으로 들어온다.
그럼 실제로 사용법을 알아보자. 클래스 위저드를 띄운 후 오른편의 Add Class  버튼을 눌러 New 명령을 선택한다. New Class 다이얼로그 박스에서 Base Class로는 CAsyncMonikerFile을 선택하고 Name 박스에는 새로운 클래스의 이름을 넣는다. 여기서는 CInternetMoniker를 입력하도록 하겠다. OK 버튼을 눌러 클래스를 생성한다. 그러면 클래스 위저드 다이얼로그 화면으로 다시 돌아갈 텐데 메시지 박스에서 OnDataAvailable 항목을 두번 클릭한다. 그리고 나서 Edit 버튼을 눌러서 편집 모드로 들어간다. 그 함수의 내부를 리스트 4와 같이 코딩한다. 하는 일은 간단하다. 내용을 다 받으면 메시지 박스로 띄우기만 한다.

리스트 4 : OnDataAvailable 항목에 들어갈 내용-----------
void CInternetMoniker::OnDataAvailable(DWORD dwSize, DWORD bscfFlag) {
    char *buf;
    DWORD dwArriving = dwSize - m_dwReadAlready;

    if (bscfFlag & BSCF_FIRSTDATANOTIFICATION) // 처음 읽힌 것이면
    {
        m_dwReadAlready = 0;
        m_strHTML.Empty();
    }

    if (dwArriving > 0) // 새로 도착한 데이터가 있으면     {
        buf = new char[dwArriving+1];
        Read(buf, dwArriving); // CAsyncMonikerFile의 Read 함수를 통해 데이터를 읽어들인다
        buf[dwArriving] = '\0';
        m_dwReadAlready = dwSize;
 
        // 읽어들인 데이터를 계속해서 m_strHTML의 끝에 추가한다
        m_strHTML += CString(buf);
        delete[] buf;
    }

    if (bscfFlag & BSCF_LASTDATANOTIFICATION) // 마지막으로 읽힌 것이면
    {
         AfxMessageBox(m_strHTML);
    }

    CAsyncMonikerFile::OnDataAvailable(dwSize, bscfFlag);
}
-------------------------------------------------------
그리고 다음을 CInternetMoniker 클래스의 멤버 변수로 추가한다.

    DWORD m_dwReadAlready;
    CString m_strHTML;

다음은 실제로 위의 클래스를 이용해 마이크로소프트 홈페이지의 내용을 읽어오는 예제다. Open 메소드를 사용하면서 인자로 URL을 지정해주면 된다.

    m_myMoniker.Open("http://www.microsoft.com");

m_myMoniker는 CInternetMoniker 클래스 타입의 객체인데 이 변수는 로컬 변수로 선언하면 안 된다는 점에 유의하자. 예제 프로그램에서는 이 객체가 다이얼로그 클래스의 멤버 변수로 선언되어 있다.  그 이유는 여기서의 Open 호출은 비동기이기 때문에 바로 리턴하게 된다. 그런데 만일 m_myMoniker가 로컬 변수라면 데이터가 도착하기 전에 프레임이 끝나면서 자동으로 삭제되기 때문에 에러가 발생하게 된다.

2 MSHTML이란 ?
다음으로 MSHTML에 대해 알아보자. MSHTML은 인터넷 익스플로러 4.0에서 처음 사용되기 시작한 모듈로 HTML을 파싱하고 파싱한 내용을 사용자에게 그래픽하게 보여주는 역할을 담당한다. 즉 웹 브라우저에서 가장 핵심적인 모듈에 해당(그림 1 참조)하는데 파싱 인터페이스를 이용하면IE의 DHTML 객체 모델을 액세스할 수 있다.

{{
}}
그림 1 : 인터넷 익스플로러의 구성
이미지 fig1.bmp

기본적으로 MSHTML을 이용해 HTML을 파싱하려면 웹 브라우저 컨트롤을 사용해 HTML 파일을 다운로드한 다음에 이를 파싱하는 것이 일반적이긴 하지만 경우에 따라서는 사용자 인터페이스 없이 HTML을 다운로드 받아서 파싱하고 싶은 경우도 있을 것이다. MSHTML과 앞서 배운 URL 모니커를 잘 사용하면 그런 일을 수행할 수 있으며 뒤에서 살펴볼 예제 프로그램에서 구현할 기능도 바로 그것이다.
앞에서 HTML을 비동기적으로 로드하는 절차를 다시 한번 보면 로드할 URL을 인자로 CreateURLMoniker 함수를 호출하여 URL 모니커를 만든 다음에 MSHTML의 IPersistMoniker 인터페이스를 얻어내 그것의 Load 함수를 호출한다. IPersistMoniker::Load는 속도가 느린 네트워크에서 데이터를 비동기적으로 읽어들이기 위해 만들어진 메소드라는 것을 알아두기 바란다. 그리고 여러 번 언급한 바와 같이 이렇게 비동기적으로 읽어들인 데이터는 데이터가 완전하게 읽혀진 시점을 알기 위해서 IPropertyNotifySink 인터페이스를 구현해 MSHTML 쪽에 알려 주어야 한다. 여기에는 OnChanged 메소드가 있어서 MSHTML이 이 메소드를 통해 비동기 로드의 상황을 알려준다.
MSHTML은 수많은 인터페이스로 구성되어 있는데 이 중에서 HTML 파싱을 담당하는 것은 IHTMLDocument2 인터페이스이다. 이 인터페이스의 사용법에 대해 배우는 것이 이 섹션에서의 할 일이다. 사실 IHTMLDocument2 인터페이스 자체도 상당히 복잡하므로 여기서 모두 설명할 수는 없고 어떤 식으로 사용한다는 예만 든다. 자세한 것은 IHTMLDocument2 인터페이스의 도움말을 참고하기 바란다.
다음의 소스 코드를 살펴보자. 앞서CHtmlPage 클래스의 InitMSHTML 함수에서 이 인터페이스에 대한 포인터를m_pMSHTML 멤버 변수에 저장해 두었다는 것을 일단 기억해두기 바란다. 예를 들어 읽어들인 HTML에서 앵커 텍스트 태그(<A>)만 빼내는 예제 코드를 보자. CHtmlPage 클래스에GetAnchorTags 메소드가 존재하는데 이 함수는 현재 읽어들인 HTML에 몇 개의 앵커 텍스트가 있는지 리턴하고 모든 앵커 텍스트 정보를 CHtmlPage의 m_pAnchor 멤버 변수에 저장한다. 이 변수의 타입은IHTMLElementCollection이다. 이 타입은 HTML요소를 여러 개 저장할 수 있는 리스트 같은 역할을 한다.

CHtmlPage::GetAnchorTags()
{
 HRESULT hr;
 long cElems = 0;

 if (m_pAnchor)
 {
  m_pAnchor->Release();
  m_pAnchor = NULL;
 }
 
 IHTMLElementCollection *pAll = NULL;
                // HTML의 모든 내용을 pAll로 읽어들인다.
 if (SUCCEEDED(hr = m_pMSHTML->get_all(&pAll))) {
                                // A 태그를 모두 읽어들인다.
  if (SUCCEEDED(hr = GetTagFromCollection(pAll, "A", &m_pAnchor)))
  {
                                                // 읽어들인 A 태그의 개수를 알아낸다.
   m_pAnchor->get_length(&cElems);   }
                       pAll->Release();
 }
 return cElems;
}

위의 소스 코드를 보면  IHTMLDocument2 인터페이스의 get_all 멤버 함수를 호출한다. 이 함수가 하는 일은 인자로 주어진 IHTMLElementCollection 인터페이스의 pAll 포인터로 MSHTML로  읽어들였던 HTML의 모든 내용을 읽어들인 후 로드한다. 즉 이제 pAll만 있으면 HTML의 내용을 요소 별로 모두 접근할 수 있다. 여기서 A 태그만 빼내기 위해서 호출하는 함수가 GetTagFromCollection이다.
첫 번째 인자로 HTML 요소를 추출하고자 하는 IHTMLElementCollection인터페이스 타입의  주소를 지정하고 두 번째 인자로는 태그 이름을 문자열로 지정한다. 첫 번째 컬렉션에서 두 번째 인자가 가리키는 태그 요소를 모두 뽑아내서 세 번째 인자가 가리키는 IHTMLElementCollection 인터페이스 포인터에 저장해서 넘긴다. 그 소스는 다음과 같다.

HRESULT GetTagFromCollection(IHTMLElementCollection *pColl, LPCTSTR lpTag, IHTMLElementCollection **ppColl2)
{
    LPDISPATCH pdisp;    VARIANT vName;
    HRESULT hr;

    vName.vt = VT_BSTR;
    CString strVal = lpTag;
    vName.bstrVal = strVal.AllocSysString();
    if (SUCCEEDED(hr = pColl->tags(vName, &pdisp)))
    {
        *ppColl2 = NULL;
         hr = pdisp->QueryInterface(IID_IHTMLElementCollection, (LPVOID*)ppColl2);
         pdisp->Release();
    }
    return hr;
}

모든 IHTMLDocument2 인터페이스는 tags라는 멤버 함수를 갖고 있고 이를 이용해 그 인터페이스에 들어있는 HTML 태그 중에서 첫 번째 인자로 지정한 태그에 해당하는 것만을 빼낼 수 있다. 그런데 위의 코드를 보면 별로 익숙하지 않은 코드들이 보일 것이다. COM 프로그래밍을 많이 해본 사람이라면 익숙한 코드들일 텐데 tags 함수를 호출할 때 첫 번째 인자로 유니코드를 지정해야 하기 때문에 CString 클래스의 AllocSysString 함수를 이용하고 있다.
tags 함수의 두 번째 인자로는 결과로 만들어지는 IHTMLElementCollection 인터페이스의 포인터가 리턴되는데 이걸 받기 위한 코드가 좀 복잡하다. IDispatch 인터페이스에 대한 포인터를 필요로 한다. 그런데 이것을 C++에서 그대로 쓰기 어려우므로 (IDispatch 인터페이스는 원래 VB등의 스크립트 툴에서 사용하기 위해 만들어진 것이다) IDispatch 인터페이스에 QueryInterface를 호출해서 여기서 다시 IHTMLElementCollection 인터페이스에 대한 포인터를 추출한다. C++에서는 포인터를 이용한 함수 호출이 더 편하기 때문이다. 결론적으로CHtmlPage 클래스의 m_pAnchor 변수에 모든 앵커 태그 정보가 모이게 된다.
그럼 이제부터 m_pAnchor 인터페이스 변수에서 실제로 앵커 텍스트의 href 속성의 값을 추출해서 CstringList 클래스에 추가하는 코드를 보기로 하자. 바로 다음 함수다.

리스트 5 : 앵커 텍스트의 href 속성 값을 추출, CstringList 클래스에 추가 ----------
BOOL CHtmlPage::AnalyzeAnchorData(CStringList &strAnchorList)
{
    long lAnchor;
    HRESULT hr;
    CString strVal, strLog;

    // 앵커의 수만큼 루프를 돌기 위해 m_pAnchor에 들어있는 요소의 수를 알아낸다
    m_pAnchor->get_length(&lAnchor);
    for(int i = 0;i < lAnchor;i++)
    {
        IHTMLAnchorElement *pElem = NULL;
        if (SUCCEEDED(GetItemFromCollection(m_pAnchor,
                              IID_IHTMLAnchorElement, (LPVOID*)&pElem, i)))
       {
           BSTR bstr;
           hr = pElem->get_href(&bstr);
           strVal = bstr;
           // strVal로 얻은 값이  시작하거나mailto로 시작하면 제외한다           // javascript로 시작하는 것들도 있다.
           if (_strnicmp(strVal, "news:", 5) != 0 && _strnicmp(strVal, "mailto:", 7) != 0)
           {
                // 상대경로를 절대 경로로 바꾼다
                strVal = MakeURL(m_strMainURL, strVal);                // strVal의 값이 이미 strAnchorList에 있는지 보고 없으면 추가한다
                if (strAnchorList.Find(strVal) == NULL)
                    strAnchorList.AddTail(strVal);
           }
           pElem->Release();
       }    }
    return TRUE;
}
----------------------------------------------------
리스트 5를 보면 앵커 태그의 수를 알아낸 다음 그 수만큼 루프를 돌면서 각 항목의 href 태그의 값을 읽어들인다음 그걸 인자로 주어진 strAnchorList 변수에 추가한다. 먼저 앵커 태그가 들어있는 IHTMLElementCollection 인터페이스에서 인덱스 정보를 주면서 앵커 인터페이스(IHTMLAnchorElement)에 대한 인터페이스 포인터를 추출해낸다. 그 함수가 바로 GetItemFromCollection 함수다. 이 함수는 두 번째 인자로 추출하고자 하는 인터페이스의 ID를 받는다. 마지막 네 번째 인자는 IHTMLElementCollection에서 몇 번째 요소를 빼내고 싶은지 지정하는데 쓰인다.
또 리스트 5에서 눈여겨볼 부분은 href가 가리키는 값을 그대로 저장하는 것이 아니라 그 값이 상대 경로일 경우에 절대 경로로 바꾸어서 저장한다는 점이다. 여기에 쓰이는 함수가 바로 MakeURL이다. 이 함수의 소스도 뒤에서 살펴보겠다. 먼저 GetItemFromCollection 함수의 소스를 보자.

리스트 6 : GetItemFromCollection 함수 -----------------------------------
HRESULT GetItemFromCollection(IHTMLElementCollection *pColl, REFIID iid, LPVOID *ppElem, int iIndex){
    HRESULT hr;
    VARIANT vIndex, var2 = { 0 };
    LPDISPATCH pDisp;

    SetUINT(&vIndex, iIndex);
    if (SUCCEEDED(hr = pColl->item(vIndex, var2, &pDisp)))
    {
        *ppElem = NULL;
        if (pDisp)
       {
            hr = pDisp->QueryInterface(iid, ppElem);
            pDisp->Release();
       }
    }
    return hr;
}
-------------------------------------------------------

3 예제 프로그램의 작성
사실 앞서 예제 프로그램의 핵심적인 코드는 거의 다 설명한 셈이다. 먼저 예제 프로그램의 실행 화면은 그림 2와 같다. Site to analyze 박스에 분석하고자 하는 사이트의 URL을 입력하고 Begin 버튼을 누르면 URL 모니커와 MSHTML 모니커를 이용해 HTML을 다운로드한 다음에 MSHTML의 파싱 인터페이스를 이용해 먼저 메타 태그가 있는지 봐서 거기에 refresh 속성이 있으면 그 URL을 제일 하단의 Meta tags 박스에 보여준 다음에 그 URL을 다시 읽어들인다. 그 다음에 앵커 텍스트를 분석해서 Anchor Text 박스에 추가하고 Frame이 있으면 Frame을 분석해 그 URL을 Frame 박스에 추가한다.  

{{
}}
그림 2. 예제 프로그램의 실행 화면
이미지 fig2.bmp

위에서 사용했던 MakeURL과 SetUINT 함수의 소스는 다음과 같다.

void SetUINT(VARIANT *pVar, UINT iVal)
{
 pVar->vt = VT_UINT;
 pVar->lVal = iVal;}

SetUINT 함수는 무부호 정수에 해당하는 VARIANT 타입을 초기화해주는 도우미 함수다.  MakeURL 함수는 첫 번째 인자로 주어진 문자열이 루트 URL이고 두 번째 인자가 그 URL에서 나타난 앵커의 href가 가리키는 URL일 때 그것이 http등으로 시작하지 않는 상대 URL일 경우 그것을 절대 경로의 URL로 바꾸어주는 역할을 한다. 이 함수의 핵심은 InternetCrackUrl 함수에 있다.

리스트 7 :  InternetCrackUrl 함수 ----------------------------
char HostName[128];
char UrlPath[128];
char ExtraInfo[128];
char SchemeName[128];

CString MakeURL(LPCSTR lpMainURL, LPCSTR lpRelativeURL)
{
    URL_COMPONENTS urlComp;
    CString newURL;

    urlComp.dwStructSize = sizeof(URL_COMPONENTS);    urlComp.dwHostNameLength = 128;
    urlComp.dwUrlPathLength = 128;
    urlComp.dwExtraInfoLength = 128;
    urlComp.dwPasswordLength = 0;
    urlComp.dwSchemeLength = 128;
    urlComp.dwUserNameLength = 0;
    urlComp.lpszHostName  = HostName;
    urlComp.lpszUrlPath   = UrlPath;
    urlComp.lpszExtraInfo = ExtraInfo;
    urlComp.lpszPassword = 0;
    urlComp.lpszScheme = SchemeName;
    urlComp.lpszUserName = 0;

    // lpMainURL 정보를 나눠서 보관한다.    InternetCrackUrl(lpMainURL, strlen(lpMainURL), ICU_ESCAPE, &urlComp);

    // urlComp.lpszUrlPath에서 경로 정보만 빼낸다.
    CString strUrlPath = urlComp.lpszUrlPath;
 CString urlPath;

    int nIndex = strUrlPath.ReverseFind('/');
    if (nIndex >= 0)        urlPath = strUrlPath.Mid(0, nIndex+1);
    else
    {
        return newURL;
    }

    if (lpRelativeURL[0] == '/')
    {
        newURL.Format("%s://%s%s", urlComp.lpszScheme,
                                  urlComp.lpszHostName, lpRelativeURL);
    }
    else if (strnicmp(lpRelativeURL, "http", 4) == 0)
    {
        newURL = lpRelativeURL;
    }
    else
    {
        newURL.Format("http://%s%s%s", urlComp.lpszHostName, urlPath, lpRelativeURL);
    }
 return newURL;
}
----------------------------------------------------------------
자세한 내용은 예제 프로그램의 소스 코드를 참고하기 바란다.


참고문헌
1. IHTMLDocument2 인터페이스 레퍼런스 : http://msdn.microsoft.com/workshop/browser/mshtml/reference/IFaces/Document2/document2.asp
2. IHTMLElementCollection 인터페이스 레퍼런스 : http://msdn.microsoft.com/workshop/browser/mshtml/reference/IFaces/ElementCollection/elementcollection.asp
3. MSDN 온라인 샘플 Walkall : http://msdn.microsoft.com/Downloads/samples/Internet/browser/walkall/sample.asp

4. MSHTML 오버뷰 : http://msdn.microsoft.com/workshop/browser/mshtml/overview/overview.asp


소스제공 Analyze.zip

정리 김상연 기자 sykim@pserang.co.kr