Windows + .NET2014. 4. 11. 09:28

흔히 소프트웨어 개발 과정에서 나타나는 요구 사항들 중에서, 시간 투자 대비 효율이 가장 떨어지는 요구 사항으로 HTML 문서 분석에 대한 것이 있습니다. 이 요구 사항을 해결하기 위해서, 보통 택하는 방안으로는 Windows 환경에서 Internet Explorer의 MSHTML을 이용하는 방안을 고민하게 됩니다. 그러나 익히 알려져 있는 바, 메모리 누수나 무거운 런타임 크기, 더 나아가서는 제한된 수준의 해석 등 이점보다는 단점이 더 많은 방식이라는 것이 큰 문제입니다. 물론 이 이후로도 오픈 소스 브라우저를 활용하거나 WebKit 바인딩을 이용하는 방법도 흔히 검토가 가능하긴 하지만 전체 브라우저 스택을 완전히 불러들여야 하고 HTML 문서의 계산 과정이 동반된다는 점이 성능 상의 이슈가 됩니다.


HtmlAgilityPack은 순수하게 .NET Framework의 코드 만으로 HTML 문서를 온전하게 분석하는 것은 기본이고, System.Xml 네임스페이스에서 제공하는 XPATH 식 관련 인터페이스와 해석기를 충실하게 지원하고 있어서, 복잡하고 까다로운 HTML 문서 구조 탐색을 매우 손쉽게 처리해 준다는 큰 장점이 있습니다. 무엇보다도 중요한 것은, NuGet에서 쉽게 설치해서 사용할 수 있으므로 HTML 문서 분석에 관한 고민에 시달리고 계시다면 지금 당장 테스트해보시기를 강력히 권할 수 있을 만큼, 훌륭한 기능을 제공합니다. 강력한 기능 덕분에 몇몇 상용 솔루션에서는 이미 절찬리에 채택되어 사용 중에 있습니다.


패키지/프로젝트 소개

HtmlAgilityPack은 2014년 4월 현재 다음과 같이 두 패키지로 구분되어있습니다.

•http://www.nuget.org/packages/HtmlAgilityPack/
•http://www.nuget.org/packages/HtmlAgilityPack-PCL/

 

 

처음의 패키지는 Desktop 버전의 .NET Framework (Mono 포함)에서 이용할 수 있도록 패키징된 버전으로, XPATH 식에 대한 지원을 포함하고 있습니다. 그리고 두 번째 패키지는 Windows Phone, Windows Store App 등의 환경에서도 사용 가능하도록 Portable Class Library 프로필에 맞게 리패키징한 버전입니다. 이 글에서 설명하고자 하는 버전은 첫 번째 버전입니다.


샘플 코드

테스트하려는 프로젝트를 여신 다음, HtmlAgilityPack을 NuGet 패키지 관리자로 프로젝트에 설치하고, 실제로 HTML 페이지 분석이 잘 이루어지는지 확인해보기 위하여, 다음과 같이 코드를 작성해보도록 하겠습니다.
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.XPath;

class Program
{
    static void Main(string[] args)
    {
        Uri targetUri = new Uri("https://www.youtube.com/watch?v=8YkbeycRa2A"); HttpWebRequest webRequest = HttpWebRequest.Create(targetUri) as HttpWebRequest;
        using (HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse)
        using (Stream webResponseStream = webResponse.GetResponseStream())
        {
            HtmlDocument s = new HtmlDocument();
            Encoding targetEncoding = Encoding.UTF8;

            s.Load(webResponseStream, targetEncoding, true);
            IXPathNavigable nav = s;

            string title = WebUtility.HtmlDecode(nav.CreateNavigator().SelectSingleNode("/html/head/meta[@property='og:title']/@content").ToString());
            string description = WebUtility.HtmlDecode(nav.CreateNavigator().SelectSingleNode("/html/head/meta[@property='og:description']/@content").ToString());
            string fullDescription = WebUtility.HtmlDecode(s.GetElementbyId("eow-description").InnerHtml);
            fullDescription = Regex.Replace(fullDescription, @"<(br|hr)[^>]*>", Environment.NewLine);
            fullDescription = Regex.Replace(fullDescription, @"<[^>]*>", String.Empty).Trim();

            Console.WriteLine(title);
            Console.WriteLine(description);
            Console.WriteLine(fullDescription);
        }
    }
}

여기서 설명한 코드는 YouTube의 메타 태그 속성을 이용하여 동영상에 대한 기본적인 정보를 가져오는 코드입니다. 여기서 주목할 것은 XPATH 식을 사용하여 HTML DOM 모델에 정확하게 접근하고 있다는 점입니다. Property 속성이 og:title인 META 태그의 Content 속성을 가져와서 YouTube에 정확하게 등록한 원래 동영상 제목을 추출하거나, 동영상의 설명을 같은 방법으로 가져오고 있습니다. 여기서 그치지 않고, eow-description이라는 ID를 가진 HTML 태그를 찾아 그 태그의 내용을 통째로 가져와서 축약되지 않은 원래 설명도 특별한 API 없이 가져오고 있습니다.


System.Xml.XPath 호환

위의 코드에서 HtmlDocument 클래스를 IXPathNavigable 형식의 변수 s로 캐스팅한 것을 볼 수 있습니다. 이 코드가 의미하는 바는 실로 큰데, IXPathNavigable은 .NET Framework BCL의 System.Xml 안에 들어있는 인터페이스를 실제로 대응하여 구현한 것입니다. 이 기능을 사용하면 다루는 대상이 XML이든 HTML이든 혹은 XHTML이든 일반화하여 다루는 것이 가능하여 의존성 주입의 차원을 고도화하는 것이 가능합니다.


IXPathNavigable 인터페이스는 다른 메서드를 제공하지 않고 오로지 CreateNavigator() 메서드만을 제공하는데, 이 메서드를 이용하여 만드는 객체는 XPathNavigator 클래스입니다. 이 클래스의 인스턴스는 상대적 접근 경로를 인식하여 해당 객체를 만들기 위하여 어떤 DOM 노드 객체를 활용하였는지에 따라 경로가 달라집니다. 이 기능을 사용하면 XPATH 식으로 선택이 어려운 노드를 근처의 형제 노드 (Sibling Node)를 통해서 전/후 탐색 메서드를 사용하여 접근이 가능합니다. XPathNavigator 클래스는 한 번 생성된 이후에는 자신이 스스로 상태를 관리하도록 되어있으므로 일종의 커서처럼 탐색이 가능합니다.


실제 활용 사례

이 즈음되면, 정말 특이한 상황을 제외하고 HTML 페이지를 수집하러 돌아다니는 크롤러를 얼마든지 만들 수도 있겠다는 예상도 해볼 수 있습니다. 실제로 이 라이브러리를 사용하여 크롤링 기술을 구현하고 있는 Arachnode.net이라는 Lucene.net 기반의 상용 검색 엔진 프로젝트도 많은 주목을 받고 있습니다. (http://arachnode.net/)


필자 개인적으로는 HTML 구문 분석이 필요한 프로젝트에서 상당히 큰 도움을 받았으며 해당 프로젝트의 주요 기능으로 적극적으로 채택하는데 큰 도움을 받았습니다.


도입 시 고려할 사항

HTML을 분석할 수 있다는 전무후무한 강력한 장점이 있음에도 불구하고, 한 가지 주의 사항이 있습니다. HtmlAgilityPack으로 처리할 수 있는 HTML은 파일 시스템 상의 고정된 정적 페이지나 네트워크를 통해서 서버가 정적 또는 동적으로 제공하는 HTML 페이지만 정확하게 처리가 가능하며, CSS나 JavaScript, 혹은 다른 Rich Internet Application (Flash나 Silverlight 등)이 나중에 가공하는 HTML 페이지의 변경 사항은 반영되지 않는다는 점입니다. 이것은 라이브러리의 문제가 아니며, 라이브러리가 취하고자 하는 기능의 명확한 한계입니다.


Rich Internet Application에 의한 후 처리를 제외하고 CSS나 JavaScript에 대한 처리가 완료된 웹 페이지에 대한 분석이 필요한 상황인 경우, Headless Browser를 사용하여 계산이 완료된 웹 페이지를 처리하거나 더 복잡한 작업을 수행할 수도 있습니다. 이를 위하여 PhantomJS (http://phantomjs.org/) 를 사용하는 것이 유용합니다. PhantomJS는 NuGet 패키지로도 제공됩니다. (http://www.nuget.org/packages/PhantomJS/)


만약 Rich Internet Application에 의한 후 처리까지 필요한 경우에는 Chrome이나 Internet Explorer를 해석 엔진으로 안정적으로 사용할 수 있도록 도움을 주는 Selenium Web Driver에 의한 간접적 처리도 고려해볼 수 있습니다. (NuGet 패키지: http://www.nuget.org/packages/Selenium.WebDriver/) PhantomJS와 Selenium Web Driver에 관한 이야기는 이 블로그 포스트 범위 밖에 있는 이야기이므로 존재에 대해서만 언급을 하는 것으로 갈무리하겠습니다.

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

남정현2014. 4. 5. 19:20
안녕하세요. Azure MVP 남정현입니다.

2014년 4월 5일부터 Tistory에서 Microsoft Azure와 Textcube 환경으로 블로그를 이사하였습니다. 도메인 주소는 그대로 유지되며 새로운 사이트로 변경되어 표시될 예정입니다.

rkttu.tistory.com으로 접속하신 분들께서는 www.rkttu.com으로 즐겨 찾기 주소를 변경하여 주시면 감사하겠습니다.



Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2014. 4. 5. 09:27

System.Net.Http.HttpClient는 ASP.NET 웹 API의 REST API 호출을 위해서도 요긴하게 사용하지만, 다양한 상황에서 사용 가능한 HTTP 송수신을 담당하는데 특화된 클래스입니다. 최근에 이 클래스를 사용하여 개발한 소프트웨어 프로젝트에서 예기치 않은 문제가 하나 있었는데, TIME_WAIT 상태로 연결 대기가 유지되는 소켓 연결의 수가 급증하여 서버에서 실행 중인 다른 데이터베이스나 웹 서비스의 연결이 WSAENOBUF WinSock 오류 코드를 반환하며 거부되는 일이었습니다.

이 클래스는 기본적으로 IDisposable 인터페이스를 구현하고 있고, 원래의 의도는 사용하지 않을 때 적시에 제거하는 것이 올바른 방식이라고 생각하였습니다. 그러나 실제로 이 클래스를 Dispose() 메서드를 사용하여 소거한다고 하더라도 클라이언트 측 연결이 끊어지지 않고 TIME_WAIT 상태로 변경되는데, 이러한 상황에서 계속 HttpClient 인스턴스를 반복적으로 만들고 연결을 다시 시도하다보면, 접속하는 클라이언트 측의 잔여 TCP 포트의 수가 부족해지는 문제가 발생하게 됩니다.

이 문제를 해결하기 위하여 취한 방법은 해당 인스턴스를 싱글턴 인스턴스로 만들어 사용하는 것이었으며, 실제로 문제를 해결할 수 있었습니다. 하지만 남아있는 문제는 이 인스턴스를 Thread-Safe 인스턴스로 만들어야 하는 것이며 이를 위해서 보강해야 할 것이 있다고 보고 있습니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요