VB.NET HTML 파싱 - XPath를 사용한 HtmlAgilityPack 예제

2024. 2. 19. 16:15VB.NET/왕초보

VB.NET으로 HTML을 파싱하기 위해 예전에는 InStr, Mid, Left 등을 이용해서 하나하나 위치 잡아서 값을 가져왔던 기억이 나는데, 이제는 그럴 필요가 없다. 물론 예전 방식으로 작업해도 동작에 무리가 없지만 그럴 필요가 없어졌으니 HtmlAgilityPack을 이용해서 HTML을 파싱해보도록 하자.

1. HTML 파일 생성

아래 소스처럼 예제에 사용할 간단한 HTML 문서를 하나 만들어 보자.

<html>
 <head>
  <meta charset="UTF-8">
  <title>HTML 예제</title>
 </head>
 <body>
    <div class="div_class1">
       <table border="1" width="100%">
          <tr align="center">
             <td>1행 1열</td>
             <td>1행 2열</td>
             <td>1행 3열</td>
          </tr>
          <tr align="center">
             <td>2행 1열</td>
             <td>2행 2열</td>
             <td>2행 3열</td>
          </tr>
          <tr align="center">
             <td>3행 1열</td>
             <td>3행 2열</td>
             <td>3행 3열</td>
          </tr>
       </table>
    </div>
    <div class="div_class2">Div 2번째</div>
 </body>
</html>

 

테이블을 하나 그려서 각 열과 행을 표시하는 간단한 HTML 문서이다. 이제 이 문서를 파싱하며 HtmlAgilityPack가 제공하는 다양한 기능을 확인해보자.

 

우선 위 HTML 소스를 임의 폴더에 1.html이라고 저장해놓고 시작하자.

2. 프로젝트 생성

VB.NET, Windows Form, HtmlParsing, .NET 8.0 (장기지원) 으로 프로젝트를 생생한 후 NuGet 패키지 관리자에서 HtmlAgilityPack를 설치한다.

VB.NET 2022 프로젝트 생성

반응형

3. 구조체 생성

이 예제에서는 위 HTML 문서를 읽어서 각 태그에 해당하는 문자열을 아래 구조체에 넣는다.

Private Structure HTML_INFO
    Dim td11, td12, td13 As String
    Dim td2 As List(Of String)
    Dim td31, td32, td33 As String
    Dim div2 As String
End Structure

위 구조체는 HTML 문서와 동일한 구조를 가지며, 특정 페이지을 파싱할때는 원하는 결과를 담을 구조체로 사용하는게 여러모로 편리하니 참고하자.

4. 파싱 함수 생성

아래 함수를 살펴보자. XPath를 이용해서 HTML 소스에 접근해서 InnerText 값을 가져오는 다양한 방법이 소개되어 있다. XPath는 절대주소도 인식하고, 상대주소도 인식한다. 웬만한 상황이 다 구현되어 있으니 일단 소스를 확인한 후 자세히 살펴보도록 하자.

ParseHTML 함수는 위 HTML 문서를 파싱해서 HTML_INFO 라는 구조체로 반환하는 함수이다.

Imports HtmlAgilityPack

Private Function ParseHTML(pSrc As String) As HTML_INFO

Dim lpItem As New HTML_INFO With {.td2 = New List(Of String)}

    Dim htmlDoc As New HtmlDocument()
    htmlDoc.LoadHtml(pSrc)
    
    ' XPath 절대주소를 이용한 접근
    lpItem.td11 = htmlDoc.DocumentNode.SelectSingleNode("/html/body/div[1]/table/tr[1]/td[1]").InnerText
    
    ' XPath 절대주소에 클래스명을 이용한 접근
    lpItem.td12 = htmlDoc.DocumentNode.SelectSingleNode("//div[@class='div_class1']/table/tr[1]/td[2]").InnerText
    
    ' XPath 상대주소를 이용한 접근
    lpItem.td13 = htmlDoc.DocumentNode.SelectSingleNode("html/body/div[1]/table/tr[1]/td[3]").InnerText
    
    ' XPath 루프문을 이용한 접근
    Dim tdNodes = htmlDoc.DocumentNode.SelectNodes("html/body/div[1]/table/tr[2]/td")
    For Each tdNode As HtmlNode In tdNodes
       lpItehttp://m.td2.Add(tdNode.InnerText)
    Next
    
    ' XPath 인덱스를 이용한 Child 접근
    tdNodes = htmlDoc.DocumentNode.SelectNodes("html/body/div[1]/table/tr[3]/td")
    lpItem.td31 = tdNodes(0).InnerHtml
    lpItem.td32 = tdNodes(1).InnerHtml
    lpItem.td33 = tdNodes(2).InnerHtml
    
    lpItem.div2 = htmlDoc.DocumentNode.SelectSingleNode("//div[@class='div_class2']").InnerText
    
    Return lpItem
End Function

복잡해 보이지만 모든 소스를 관통하는 XPath에 대해 이해하면 그리 어렵지 않으니 차근차근 알아보도록 하자.

(1) Import

Imports HtmlAgilityPack

(2) 반환할 변수 선언

Dim lpItem As New HTML_INFO With {.td2 = New List(Of String)}

좀더 다양한 상황을 구현하기 위해 td2는 List로 정의했고, 그렇기에 변수 선언시 이 리스트를 메모리에 할당해줘야 정상 동작하게 된다.

(3) HTML 문서 읽기

Dim htmlDoc As New HtmlDocument()
htmlDoc.LoadHtml(pSrc)

NuGet 패키지에서 설치한 HtmlAgilityPack 에서 지원하는 HtmlDocument 형을 이용해서 htmlDoc를 정의하고, LoadHtml 함수를 통해 위에 있는 HTML 문서를 불러온다.

(4) 절대 주소를 이용한 접근

lpItem.td11 = htmlDoc.DocumentNode.SelectSingleNode("/html/body/div[1]/table/tr[1]/td[1]").InnerText

여기서는 /html/body/div[1]/table/tr[1]/td[1] 이 부분에 주목하자. 위 HTML문서의 경로를 따라서 절대주소로 태그에 접근하는 방법이다. HTML의 태그 구조에 경로를 줘서 그대로 접근이 가능하다.

(5) 클래스명으로 접근

lpItem.td12 = htmlDoc.DocumentNode.SelectSingleNode("//div[@class='div_class1']/table/tr[1]/td[2]").InnerText

클래스명이 div_class1인 div부터 경로를 시작하게 된다. 맨앞의 두개의 슬래시(//)는 Root 폴더부터 내려온다는 의미이다.

(6) 상대 주소를 이용한 접근

lpItem.td13 = htmlDoc.DocumentNode.SelectSingleNode("html/body/div[1]/table/tr[1]/td[3]").InnerText

위 (4)번 항목과 차이점은 맨앞에 슬래시(/)가 있는지 여부이다. htmlDoc은 <html> 태그로 시작하니 상대주소로 접근하려면 html부터 시작해야 한다. 상대주소는 <table>, <tr>, <td> 등 현재 노드가 가리키는 위치부터 하위 노드에 접근할 때 사용하게 된다.

(7) 하위 노드에 루프문으로 접근하기

Dim tdNodes = htmlDoc.DocumentNode.SelectNodes("html/body/div[1]/table/tr[2]/td")
For Each tdNode As HtmlNode In tdNodes
    lpItem.td2.Add(tdNode.InnerText)
Next

상대주소로 html/body/div[1]/table/tr[2]/td 여기에 접근하게 되면 두번째 <tr> 아래에 있는 세개의 <td>를 tdNodes에 넣게 된다. 위에서는 SelectSingleNode를 사용한 반면 여기서는 SelectNodes를 사용했음에 주의하자.
이렇게 3개의 td에 각각 접근하기 위해 For Each 문을 사용했다.

(8) 하위 노드에 인덱스로 접근하기

tdNodes = htmlDoc.DocumentNode.SelectNodes("html/body/div[1]/table/tr[3]/td")
lpItem.td31 = tdNodes(0).InnerHtml
lpItem.td32 = tdNodes(1).InnerHtml
lpItem.td33 = tdNodes(2).InnerHtml

위 7번과 동일하지만, For Each의 반복문을 사용하지 않고, 직접 인덱스로 접근도 가능하다.

마치며...

이상으로 HtmlAgilityPack에서 지원하는 XPath를 이용해서 HTML을 파싱하는 방법에 대해 간략히 알아봤다. 늘 얘기하지만 처음 사용하면 어색할 수밖에 없고, 이 어색함을 없애기 위해서는 여러번 연습하며 내것으로 만드는 방법밖에 없다.

 

이래저래 .NET으로 넘어오면서 VB98 시절 생 노가다로 포지션을 잡아가며 사용하던 많은 기능들이 기본 제공(NuGet 패키지이기는 하지만...)되고 있으니 이제는 어떻게 구현하느냐보다 이 기능을 어떤 패키지가 지원하는지에 대해 알아보는게 더 빠를 수도 있다는 생각이 든다.

 

 

 

[VB.NET 2022] JSON Parsing - JSON을 파싱해보자.

JSON을 파싱하는건 예전 VB6 시절에는 상당히 번거로운 일이었다. 그냥 문서의 처음부터 루프를 돌며 하나씩 분리해서 이름과 값을 직접 때려넣어야 가능한 일이었으니 말이다. 예를 들어 아래 정

chakhani.tistory.com

 

 

 

VB.NET XML 파싱 - XmlDocument 사용

VB.NET HTML 파싱 - XPath를 사용한 HtmlAgilityPack 예제 VB.NET으로 HTML을 파싱하기 위해 예전에는 InStr, Mid, Left 등을 이용해서 하나하나 위치 잡아서 값을 가져왔던 기억이 나는데, 이제는 그럴 필요가 없

chakhani.tistory.com

 

반응형