웹에서 긁어 온 HTML을 다룰 때, 파싱하기 전에 지저분한 공백과 줄바꿈을 한 번 정리해 두면 처리가 훨씬 수월합니다. 그리고 태그를 전부 떼고 눈에 보이는 글자만 뽑아야 할 때도 많죠. 자주 쓰는 두 함수를 올려 둡니다.

공백·줄바꿈 정리

    Public Function HTMLOrganize(ByVal value As String) As String
        value = Replace(value, vbCrLf, Space(1))
        value = Replace(value, vbCr, Space(1))
        value = Replace(value, vbLf, Space(1))
        value = Replace(value, vbTab, Space(1))
        value = Replace(value, Chr(0), Space(1))
        value = Replace(value, Chr(10), Space(1))
        Do While InStr(value, Space(2))
            value = Replace(value, Space(2), Space(1))
        Loop
        value = Replace(value, "> ", ">")
        value = Replace(value, " <", "<")
        value = Replace(value, "'", """")
        Return value
    End Function

먼저 줄바꿈(vbCrLf·vbCr·vbLf), 탭, 널 문자(Chr(0))를 전부 공백 하나로 바꿉니다. vbLf 와 Chr(10) 은 같은 문자라 사실상 한 번은 중복이지만, 결과에는 영향이 없습니다. 그러고 나면 공백이 여러 칸 연달아 생기는데, Do While InStr(value, Space(2)) 루프가 공백 두 칸을 한 칸으로 바꾸길 반복해서 결국 모든 연속 공백이 한 칸으로 줄어듭니다.

광고

"> " 를 ">" 로, " <" 를 "<" 로 바꾸는 건 태그 경계에 붙은 공백을 없애서, 나중에 > 와 < 를 기준으로 자를 때 깔끔하게 하려는 처리입니다. 마지막에 작은따옴표를 큰따옴표로 바꾸는 건, 속성값을 ' 와 " 중 뭘로 감쌌든 하나로 통일해 두면 속성을 뽑는 코드가 단순해지기 때문입니다. 다만 본문 텍스트 안의 작은따옴표(예: don't)까지 바뀌니, 태그 파싱 전처리 용도로만 쓰고 사람이 읽을 본문에는 쓰지 않는 게 좋습니다.

태그 제거

    Public Function HTMLDEL(ByRef T As String) As String
        Return System.Text.RegularExpressions.Regex.Replace(T, "<.*?>", "")
    End Function

<.*?> 정규식으로 < 부터 > 까지를 모두 빈 문자열로 바꿔 태그를 떼어 냅니다. .*? 의 ? 가 핵심인데, 이게 없으면(<.*>) 한 줄에 태그가 여러 개일 때 첫 < 부터 마지막 > 까지를 통째로 지워서 중간 글자까지 사라집니다. ? 를 붙여 최소 일치로 만들면 각 태그만 하나씩 지웁니다.

한계와 보완

HTMLDEL 은 간단한 만큼 빈틈도 있습니다. <script> 나 <style> 안의 코드는 태그만 지워지고 내용은 본문에 그대로 남고, &nbsp;·&amp; 같은 엔티티도 바뀌지 않습니다. 속성값에 > 가 들어 있는 특이한 경우에도 어긋날 수 있고요. 실제 결과를 사람이 읽는 텍스트로 쓰려면 태그를 지운 뒤 HttpUtility.HtmlDecode 로 엔티티를 풀어 주고, 필요하면 script·style 블록을 먼저 통째로 지우는 단계를 앞에 넣는 게 좋습니다.

두 함수를 묶어 쓰면 편합니다. 가져온 HTML을 HTMLOrganize 로 한 줄로 정리한 다음 원하는 부분을 잘라 내고, 거기서 HTMLDEL 로 태그를 떼어 최종 텍스트를 얻는 식입니다. 정규식만 쓰면 될 걸 왜 공백 정리까지 하느냐 싶지만, > < 기준으로 문자열을 직접 자르는 코드와 같이 쓸 때는 앞단 정리가 있는 쪽이 훨씬 덜 꼬입니다.

HTMLDEL 을 단독으로 쓸 때 걸리는 점이 하나 더 있습니다. 정규식의 . 은 기본적으로 줄바꿈 문자와 일치하지 않아서, <a 와 href=...> 처럼 태그가 두 줄에 걸쳐 있으면 지워지지 않고 남습니다. HTMLOrganize 로 먼저 한 줄로 만들어 두면 이 문제가 사라지니, 두 함수를 이 순서로 쓰는 데는 이유가 있는 셈입니다.

사람이 읽을 텍스트로 뽑으려면?

위의 보완 내용을 하나로 묶은 함수입니다. script·style 블록을 통째로 지우고, 줄이 바뀌어야 할 태그는 줄바꿈으로 바꾼 다음 나머지 태그를 지우고 엔티티를 풉니다.

Imports System.Net
Imports System.Text.RegularExpressions

Public Function HtmlToText(ByVal html As String) As String
    Dim t As String = Regex.Replace(html, "<(script|style)\b[^>]*>.*?</\1>", "", RegexOptions.IgnoreCase Or RegexOptions.Singleline)
    t = Regex.Replace(t, "<br\s*/?>|</p>|</div>|</li>", vbCrLf, RegexOptions.IgnoreCase)
    t = Regex.Replace(t, "<.*?>", "", RegexOptions.Singleline)
    t = WebUtility.HtmlDecode(t)
    Return t.Replace(ChrW(160), " ").Trim()
End Function

RegexOptions.Singleline 을 주면 . 이 줄바꿈까지 포함하니 여러 줄에 걸친 태그나 script 블록도 지워집니다. 엔티티 해제에는 HttpUtility 대신 System.Net.WebUtility 를 썼는데, HttpUtility 는 WinForms 프로젝트에서 System.Web 참조를 따로 추가해야 하지만 WebUtility 는 .NET Framework 4.0 이상이면 참조 없이 쓸 수 있습니다.

마지막 Replace(ChrW(160), " ") 는 &nbsp; 때문입니다. &nbsp; 를 풀면 일반 공백이 아니라 줄바꿈 없는 공백(U+00A0)이 나오는데, 눈으로는 똑같아 보여도 Trim 이나 Split(" ") 에서 공백으로 취급되지 않아 비교가 자꾸 어긋납니다. 일반 공백으로 바꿔 두면 이후 처리가 편합니다. 이 함수에는 HTMLOrganize 를 거치지 않은 원본 HTML 을 넣어야 줄바꿈이 살아 있는 결과가 나옵니다.