파이썬으로 HTML 처리하는 법

Home People HTML Emulation
Python

요약

한글판 johnsonj 2007.04.11 원문 위치

나는 다양한 웹 서핑 과업을 주기적으로 수행해 왔는데, 파이썬으로 했다면 훨씬 더 쉽고 더 재미있게 수행할 수 있었을 것이다. 파이썬을 사용하여 HTML 페이지를 가져와서 처리할 수 있었다면, 정말 내가 필요한 정보들을 생산할 수 있었을텐데 아쉽다. 이 문서에서는 쉽게 구할 수 있는 도구들과 라이브러리를 사용하여 파이썬으로 HTML을 처리하는 법을 기술하려고 노력하였다.

주의: 이 문서는 잘 다듬어지지 않았다. mxTidy를 사용하여 깨진 HTML을 다룬 섹션을 포함시키려고 노력했을 뿐만 아니라 HTML 자원으로부터 열람된 텍스트를 깨끗히 다듬는 법에 관한 팁도 포함시키려고 노력하였다.

필수요소

이 자습서를 따라하고 싶다면, 다음과 같은 일을 할 필요가 있다:

임무

사이트에 접근하고, 그 내용을 내려받으며, 그 내용을 처리하는 일, 다시 말해서 유용한 정보를 추출해 모으거나 그런 내용을 이용하여 더 깊숙히 그 사이트를 탐험하는 일은 다음과 같은 조치가 요구된다. 어떻게 할지는 마음대로 선택할 수 있다: SGML 해석기가 사용될지 또는 XML 해석기(또는 해석 작업틀)가 사용될지는 어떤 스타일의 프로그래밍을 개발자가 더 멋있게 보는가에 달려있다 (물론 어떤 상황하에서는 특정한 해석기가 더 잘 작동할 수도 있다). 그렇지만, 기술적 제한사항은 보통 특정 라이브러리가 다른 라이브러리 대신에 사용되는 중요한 요소가 된다: HTTP 방향전환을 다룰 때라면, 특정 파이썬 모듈이 더 사용하기 쉬워 보이며 더 적절하다.

웹 페이지 가져오기

파이썬에서 HTTP를 타고 표준 웹 페이지를 가져오는 일은 아주 쉽다:

import urllib
# 파이썬 웹 사이트 홈 페이지에 대하여 파일-류의 객체를 얻는다.
f = urllib.urlopen("http://www.python.org")
# 객체를 읽어, 홈 페이지의 웹소를 's'에 저장한다.
s = f.read()
f.close()

데이터 공급하기

어떤 경우는 반드시 정보를 웹 서버에 건네야 한다. 그런 정보는 HTML 폼으로부터 온다. 물론, 한 폼에서 어느 필드가 사용가능한지 알아야 할 필요가 있지만 이미 알고 있다고 간주하면, 그런 데이터를 urlopen 함수 호출에 공급할 수 있다:

# Vaults of Parnassus 사이트에서 "XMLForms"을 검색한다.
# 먼저, 데이터를 인코드한다.
data = urllib.urlencode({"find" : "XMLForms", "findtype" : "t"})
# 파일-류의 객체를 다시 얻는다 Now get that file-like object again, remembering to mention the data.
f = urllib.urlopen("http://www.vex.net/parnassus/apyllo.py", data)
# 그 결과를 다시 읽는다.
s = f.read()
s.close()

위의 예제에서는 데이터가 서버에 HTTP POST 요청으로 건네졌다. 다행스럽게도, Vaults of Parnassus 사이트는 그런 요청을 즐겁게 접수하지만, 이것이 언제나 웹 서비스에 통용되지는 않는다. 그렇지만, 대신에 다른 형식으로 요청할 수 있다:

# 인코드된 데이터를 확보하였다 이제 파일-류 객체를 얻자...
f = urllib.urlopen("http://www.vex.net/parnassus/apyllo.py?" + data)
# 기타 등등...

유일한 차이는 Vaults of Parnassus URL의 끝에다 ? (물음표) 문자를 사용하고 data를 추가한 것이다. 그러나, 이것은 HTTP GET 요청을 구성하는데, 여기에서 질의(추가된 데이터)가 URL 자체에 포함되어 있다.

보안 웹 페이지 가져오기

HTTPS를 사용하면 보안 웹 페이지를 가져오는 일도 아주 쉽다. 물론 파이썬 설치본이 SSL을 지원한다면 말이다:

import urllib
# 한 사이트에 대하여 파일-류의 객체를 얻는다.
f = urllib.urlopen("https://www.somesecuresite.com")
# NOTE: 파이썬 상호대화 프롬프트에서는 여기에 사용자이름과
# NOTE: 패스워드를 요구받을 수도 있다.
# 객체로부터 읽어서, 페이지의 내용을 's'에 저장한다.
s = f.read()
f.close()

위에 예시한 바와 같이 https로 시작하는 URL에 질의의 뼈대가 되는 데이터를 포함시켜도 된다.

방향전환 다루기

많은 웹 서비스가 쉽기도 하고 괴이하기도 한 다양한 목적에 HTTP 방향전환을 사용한다. 예를 들어, "복닥거리는" 웹 사이트에 아주 흔하게 채택되는 테크닉은 HTTP 방향전환 부하 배분 전략인데 공개된 웹 사이트(eg. http://www.somesite.com)에 최초로 접근하면 또다른 서버(eg. http://www1.somesite.com)로 방향전환되어 거기에서 사용자의 세션이 처리된다.

다행스럽게도, urlopen은 방향전환을 처리한다. 적어도 2.1 이상이면 가능하다. 그러므로 urlopen으로 그런 방향전환을 투명하게 처리할 수 있다. 방향 전환이 일어나고 있는지 프로그램에서 알아야 할 필요가 없다. 방향전환을 처리하는 코드를 손수 작성할 수도 있다. httplib 모듈을 사용하면 가능하다; 그렇지만, httplib가 제공하는 인터페이스가 위에 제공된 인터페이스보다 더 복잡하다. 약간 더 강력하기는 하지만 말이다.

SGML 해석기 사용하기

위의 예제에서 s가 보유한 값처럼, 웹 서비스로부터 문자열이 주어지면, 어떻게 그 서버스가 제공하는 내용을 이해하여 "지능적으로" 응답할 수 있는가? 한가지 방법은 SGML 해석기를 이용하는 것이다. HTML은 SGML의 사촌이고, 그리고 HTML이 아마도 웹 서비스를 이용할 때 많이 접해 보았음직한 웹소 형태이기 때문이다.

표준 파이썬 라이브러리에서 sgmllib 모듈에는 SGMLParser라고 불리우는 적절한 해석기 클래스가 있다. 불행하게도 사용성을 약간 맞춤재단하지 않는 한 사용에 제한이 있다. 다행스러운 것은 파이썬의 객체-지향적 특징 덕분에 SGMLParser 클래스의 디자인에 결합되어 아주 쉽게 재단하는 방법이 제공된다는 것이다.

해석기 클래스 정의하기

먼저, SGMLParser에서 내려받아 새로운 클래스를 정의해 보자. 정말 편리한 함수라고 생각한다:

import sgmllib

class MyParser(sgmllib.SGMLParser):
"A simple parser class."

def parse(self, s):
"Parse the given string 's'."
self.feed(s)
self.close()

# 기타 등등 계속...

parse 메쏘드는 쉽게 텍스트를 (문자열로) 해석기 객체에 건네는 방법을 제공한다. feed 메쏘드를 기억해야 하는 것보다 이게 더 쉽다. 왜냐하면 언제나 나는 전체 문서를 해석 대기 상태로 놓는 경향이 있어서, feed 메쏘드를 여러 번 호출할 필요가 없기 때문이다 - 전체 문서를 구성하는 텍스트를 여러 번에 나누어 건네는 것은 SGMLParser (그리고 그의 파생 클래스)의 특징이며 다른 상황에서도 사용할 수 있다.

무엇을 기억할지 결정하기

물론, 문서에서 무엇인가를 찾으려고 하는 경우 당연히 맞춤 해석기를 구현하는 일에 관심이 간다. 그러므로, 먼저 이런 것들을 결정하고 나서 해석을 시작해야 한다. 클래스에서 __init__ 메쏘드에 이렇게 하면 된다:

    # 위에서 계속...

def __init__(self, verbose=0):
"Initialise an object, passing 'verbose' to the superclass."

sgmllib.SGMLParser.__init__(self, verbose)
self.hyperlinks = []

# 기타 등등 계속...

여기에서, 상위클래스(SGMLParser)의 __init__ 메쏘드에 정보를 건네어 새로운 객체들을 초기화한다; 이렇게 하면 아래에 있는 해석기가 적절히 설정되는 것을 확신할 수 있다. 또 hyperlinks라고 불리우는 속성 하나를 초기화했는데 이 속성은 주어진 객체가 해석할 문서에서 발견되는 하이퍼링크들을 기록하는데 사용될 것이다.

속성 이름을 선택할 때는 조심해야 한다. 왜냐하면 상위클래스에 정의된 이름을 사용하면 해석기 객체가 사용될 때 잠재적으로 문제가 일어날 수 있기 때문이다. 잘못 선택된 이름 때문에 속성중의 하나가 상위클래스의 속성을 덮어쓸 가능성이 있으며 그 결과로 상위클래스에 의하여 내부적 해석 목적으로 조작될 수도 있기 때문이다. SGMLParser 클래스가 속성 이름 앞에 두개의 밑줄문자 (__)를 사용하기를 기대한다. 이렇게 하면 그런 속성들이 MyParser 클래스 같은 하부 클래스와 격리되기 때문이다.

문서 세부사항 기억하기

이제 문서에서 데이터를 추출할 방법을 정의할 필요가 있다. 그러나 SGMLParser가 제공하는 메커니즘으로 언제 문서에서 관심부분이 읽혀지는지 알 수 있다. SGML과 HTML은 텍스트 포맷으로서 이른바 태그라고 하는 것으로 구조가 이루어져 있다. HTML에서 하이퍼링크는 다음과 같은 방식으로 나타낸다:

<a href="http://www.python.org">The Python Web site</a>
SGMLParser가 작동하는 법

문서를 해석하는 SGMLParser 객체는 하이퍼링크와 같은 것들에 대하여 시작 태그와 종료 태그를 인식한다. 그 태크가 시작 태그인지 종료 태그인지 발견되는 태그의 이름에 기반하여 메쏘드 호출을 자신에게 제출한다. 그래서, 위의 텍스트가 SGMLParser 객체 (또는 MyParser 같은, SGMLParser로부터 파생된 객체)로 인식되듯이, 다음 메쏘드 호출은 내부적으로 이루어진다:

self.start_a(("href", "http://www.python.org"))
self.handle_data("The Python Web site")
self.end_a()

태그 사이의 텍스트가 데이터로 간주되고, 끝 태그는 아무 정보도 제공하지 않는 것을 주목하자. 그렇지만, 시작 태그는 속성 이름과 값을 연속열 형태의 정보로 제공한다. 각 이름/값 쌍은 2-터플에 배치된다:

# 시작 태그 메쏘드에 공급되는 속성의 형태:
# (name, value)
# 예제:
# ("href", "http://www.python.org")
# ("target", "python")
왜 SGMLParser는 작동하는가

SGMLParser는 자신에게 메쏘드를 호출하는가. 왜 사실상 태그에 마추쳤다는 사실을 자신에게 알리는가? 바탕 SGMLParser 클래스는 그런 정보를 어떻게 처리할지 모른다. 또다른 클래스가 SGMLParser을 상속받는다면, 그런 호출은 더 이상 SGMLParser에 국한되지 않는다. 대신에 MyParser 같은 하부클래스의 메쏘드들에 작동한다. 그곳에 그런 메쏘드들이 존재하기 때문이다. 그래서, 맞춤 해석기 클래스(예. MyParser)가 일단 실체화되면 (객체에 구성되어 들어 되면) 마치 겹겹이 쌓인 컴포넌트처럼 행동하는데, 제일 아래층에서 고된 해석 작업을 담당하고 관심 부품들을 위층으로 올려 보내는 - 마치 아래층에서 부품들을 만들어내고 이층의 연구실에서 그 부품들을 검사하는 공장처럼 말이다!

클래스 역할
... 보고를 듣고, 기타 관심사를 기록한다
MyParser 보고를 듣고, 관심사를 기록한다
SGMLParser 문서를 해석하고, 각 단계마다 보고서를 제출한다
맞춤 재단 시작

이제, 문서의 하이퍼링크들을 기록하고 싶다면, start_a라고 부른 메쏘드를 하나 정의하기만 하면 된다. 이 메쏘드는 시작  a 태그에서 제공하는 속성들로부터 하이퍼링크들을 추출한다. 이것을 다음과 같이 정의할 수 있다:

    # 위에서 계속...

def start_a(self, attributes):
"Process a hyperlink and its 'attributes'."

for name, value in attributes:
if name == "href":
self.hyperlinks.append(value)

# 기타 등등 계속...

attributes 리스트를 순회하여, 적절하게 이름붙은 속성들을 찾아, 그 속성들의 값을 기록하면 된다.

세부사항 열람하기

열람된 세부사항들에 접근하는 좋은 방법은 메쏘드를 정의하는 것이다. 물론 파이썬 2.2에서 추가된 특징으로 더 편리하게 이 일을 할 수 있지만 말이다. 예전 접근법을 사용하겠다:

    # 위에서 계속...

def get_hyperlinks(self):
"Return the list of hyperlinks."

return self.hyperlinks

Trying it Out

클래스를 정의하고, 실체화 시켜서, 새로운 MyParser 객체를 만들고 있음을 주목하자. 다음은 그냥 작업할 문서만 주면 된다:

import urllib, sgmllib

# 일거리를 얻는다.
f = urllib.urlopen("http://www.python.org")
s = f.read()

# 페이지에 접근을 시도한다.
# 제일 먼저 이 클래스가 정의되어야 한다. 기억하자.
myparser = MyParser()
myparser.parse(s)

# 하이퍼링크를 얻는다.
print myparser.get_hyperlinks()

print 서술문 때문에 리스트가 화면에 보여지는데, 파이썬 홈페이지와 기타 사이트의 위치를 가리키는 다양한 하이퍼링크들을 담고 있다.

예제 파일

위의 예제 코드는 여기에서 내려 받아 실행하여 그 결과를 보실 수 있다.

좀 더 구체적으로 내용 찾기

물론, 문서가 어디에서 왔는지 고민할 필요없이 문서에서 정보를 추출하는 것으로 충분하다면, 위 수준의 복잡도로 충분하다. 그렇지만, 특정 위치나 특정 구조에만 나타나는 정보를 추출하고 싶을 수도 있다 - 이것을 보여주는 좋은 예 하나는 위에서 본 하이퍼링크의 시작 태그와 끝 태그 사이의 텍스트이다. 본 것들을 모두 기록하는 handle_data 메쏘드를 사용하여 텍스트 조각을 모조리 모았을지라도, 어느 부분의 텍스트가 하이퍼링크를 기술하는지 어느 부분의 텍스트가 문서의 다른 곳에서 나타나는지 알지 못한다.

    # 위의 클래스를 확장한 것.
# 별로 쓸모가 없다.

def handle_data(self, data):
"Handle the textual 'data'."

self.descriptions.append(data)

여기에서 descriptions 속성(__init__ 메쏘드를 초기화하는데 필요함)은 의미없는 텍스트형 데이터로 한 가득 채워질 것이다. 그래서 어떻게 좀 더 구체적일 수 있을까? 가장 좋은 접근법은 SGMLParser가 발견한 내용뿐만 아니라, 이미 본 내용의 종류가 무엇인지 함께 기억하는 것이다.

위치 기억하기

새로 속성들을 몇가지 __init__ 메쏘드에 붙여보자.

        # __init__ 메쏘드에 끝에...

self.descriptions = []
self.inside_a_element = 0

descriptions 속성은 예상대로 정의되었지만, inside_a_element 속성은 약간 다른 것에 사용되고 있다: 즉 SGMLParser가 현재  a 요소의 내용을 조사하고 있는지 가리킨다 - 다시 말해, SGMLParser가 시작 a 태그와 끝 a 태그 사이에 있는지 여부를 알려준다.

이제 약간의 "로직"을 start_a 메쏘드에 붙여보자. 다음과 같이 재정의해 보자:

    def start_a(self, attributes):
"Process a hyperlink and its 'attributes'."

for name, value in attributes:
if name == "href":
self.hyperlinks.append(value)
self.inside_a_element = 1

이제, 언제 시작 a 태그가 보이는지 알지만, 혼란을 피하기 위하여 해석기가 끝 a 태그를 만날 때 새로운 속성의 값을 바꾸어야 한다. 이런 경우에 대비해 새로 메쏘드를 정의하자:

    def end_a(self):
"Record the end of a hyperlink."

self.inside_a_element = 0

다행스럽게도 하이퍼링크들을 "내포시키는 것"은 허용되지 않는다. 그래서 하나 이상의 시작 태그가 연속적으로 보인 후에 종료 태그가 하나 보이면 어떻게 하나 걱정하는 것은 적절하지 않다.

적절한 데이터를 기록하기

이제, 문서에서 어디에 위치하는지 확신할 수 있고 표현되는 데이터를 기록해야 할지 말아야 할지 알 수 있다면, "진짜" handle_data 메쏘드를 다음과 같이 정의할 수 있다:

    def handle_data(self, data):
"Handle the textual 'data'."

if self.inside_a_element:
self.descriptions.append(data)

앞으로 보겠지만, 이 메쏘드는 완전하지 않다. 그러나 적어도 문서에서 가장 마지막 텍스트 조각마다 기록하는 것은 피한다.

이제 설명 데이터를 열람하는 메쏘드를 정의할 수 있다:

    def get_descriptions(self):
"Return a list of descriptions."

return self.descriptions

그리고 다음 줄을 테스트 프로그램에 추가해 그 설명을 화면에 표시해 볼 수 있다:

print myparser.get_descriptions()
예제 파일

이렇게 변조된 예제 코드는 여기에서 내려받아 실행하여 그 결과를 볼 수 있다.

텍스트에 관련된 문제

변조된 예제를 실행하면, 한 가지 사실이 분명해진다: 몇가지 설명은 전혀 무의미하다. 게다가, 설명의 개수와 하이퍼링크의 개수가 일치하지 않는다. 그 이유는 해석기가 텍스트를 발견하고 보여주는 방식 때문이다 - 하나 이상의 텍스트 조작이 특별한 텍스트 구역에 나타날 수 있다. 그래서 하나 이상의 텍스트 조각들이 시작 a 태그와 끝 a 태그 사이에 신호를 보낼 수 있다. 물론, 논리적으로 한 블록의 텍스트이더라도 말이다.

또다른 속성을 추가해서 텍스트 구역을 처리하기 시작했는지 알려주도록 예제를 변조해도 좋다. 이 새로운 속성이 설정되면, 리스트에 설명을 덧붙인다; 그렇지 않다면, 기록된 최근의 설명에 텍스트를 추가한다.

__init__ 메쏘드를 조금 더 손 보았다:

        #  __init__ 메쏘드의 끝에 추가...

self.starting_description = 0

시작 a 태그가 보이자 마자 설명이 바로 시작된다고 확신할 수 있기 때문에, start_a 메쏘드를 다음과 같이 재정의한다:

    def start_a(self, attributes):
"Process a hyperlink and its 'attributes'."

for name, value in attributes:
if name == "href":
self.hyperlinks.append(value)
self.inside_a_element = 1
self.starting_description = 1

이제, handle_data 메쏘드는 다음과 같이 재정의할 필요가 있다:

    def handle_data(self, data):
"Handle the textual 'data'."

if self.inside_a_element:
if self.starting_description:
self.descriptions.append(data)
self.starting_description = 0
else:
self.descriptions[-1] += data

분명히 메쏘드는 더 복잡해진다. 설명이 시작되었는지 그리고 위에서 언급한 대로 작동하는지 탐지할 필요가 있다.

예제 파일

이렇게 변조된 예제 코드를 여기에서 내려받아 실행하면 그 결과를 볼 수 있다.

맺는 말

마지막 예제 파일은 약간 그럴듯한 결과를 생산하지만 - 여전히 설명이 부족한 곳이 있다. 그리고 하이퍼링크 안에 사용된 이미지들을 고려하지 않았다 - 요구된 변경 사항들을 보면 문서의 구조에 주의하면 할 수록, 정보의 기원을 관리하는데 더 많은 노력이 든다는 것을 알 수 있다. 결과적으로, 상태 정보를 MyParser 객체 안에 적절하게 유지관리할 필요가 있다.

응용 목적으로, SGMLParser 클래스와 그의 파생 클래스 그리고 (SAX 같은) 관련 접근법은 보통 정보에 접근하는데 쓸모가 있다. 어떤 종류의 질의는 처음에 생각했던 것보다 사용하기가 더 어려워질 수 있지만, 이런 접근법은 또다른 목적에 사용할 수 있다: 좀 더 방법론적으로 접근이 가능한 구조를 구축하는데 사용할 수 있다. 이를 아래에서 보여주겠다.

 XML 해석기 사용하기

문자열 s가 주어지고, (이 문서의 앞 섹션에 언급한 접근법을 사용하여) 웹 서비스로부터 열람한 HTML 문서가 담겨 있다면, 이제 이 문서의 내용을 해석하는다른 방법을 생각해 보자. 지금까지 보아 온 문서의 구조를 복잡하게 명시적으로 기억하도록 관리할 필요가 없게 말이다. 여기에서 SGMLParser에 관련된 한가지 문제는 문서 안의 정보에 접근하는 일이 "직렬로" 이루어졌다는 것이었다 - 다시 말해, 발견된 순서대로 정보가 표현되었다 - 그러나 문서의 구조에 맞추어 문서 정보에 접근하는 것이 더 적절할지도 모르겠다. 그러면 각 하이퍼링크 요소 안의 텍스트에 대하여 각각의 문서 부분을 조사하기 전에 그 문서에 나타난 하이퍼링크 요소에 상응하여 문서의 모든 부분들을 요청할 수 있기 때문이다.

XML 세계는 이른바 문서객체모델(DOM)이라고 부르는 표준을 고안하여 문서 정보에 다양하게 접근할 수 있다. 그래서 문서 구조를 돌아다니면서, 문서의 여러 부분을 요구하고, 언제든지 그런 부분에 다시 방문할 수 있다; 파이썬에 XML과 DOM을 사용하는 법은 또다른 문서에 잘 기술되어 있다. 모든 웹 페이지가 모양을-갖춘 XML이라면 - 즉, XML 규격에 제정된 표준과 기대치에 부합한다면 - 어떤 XML 해석기도 충분히 웹에서 발견되는 HTML 문서를 처리할 수 있을 것이다. 불행하게도, 많은 웹 페이지에서 모양을 갖추지 못한 HTML 변종들을 사용하고 있으며 이는 XML 해석기가 거부한다. 그래서, 특정한 도구와 테크닉을 채용하여 그런 페이지들을 DOM 표현으로 변환시킬 필요가 있다.

아래에서 웹 페이지가 PyXML 도구함과 libxml2dom 꾸러미로 어떻게 처리되어 최상위 수준의 문서 객체를 획득하는지 설명한다. 두 접근법 모두 거의 DOM 표준을 만족하는 객체를 산출한다. 다음에 기술하는 그런 문서들을 조사하는 방법은 선택한 꾸러미나 도구함에 관계없이 적용된다.

PyXML 사용하기

웹에서 발견되는 HTML에 파이썬의 XML 작업틀을 사용하는 것이 가능하다. 특별한 "판독" 클래스를 채용하여 DOM 표현을 HTML 문서로부터 구축하면 된다. 그 결과는 아래에 기술한다.

판독기 만들기

HTML 문서를 읽는데 적절한 클래스는 xml 꾸러미 깊숙히 들어 있다. 계속하여 이 클래스를 실체화하여 사용해 보겠다:

from xml.dom.ext.reader import HtmlLib
reader = HtmlLib.Reader()

물론, Reader 클래스에 접근하는 방식이 다양하게 많다. 그러나 Reader를 공통 이름공간에 도입하지 않기로 결정하였다. 이렇게 결정한 이유 하나는 다른 꾸러미나 모듈로부터 다른 Reader 클래스를 도입하고 싶을지도 모르기 때문이다. 그리고 그들 사이를 구별할 방법이 필요하기 때문이다. 그러므로 HtmlLib 이름을 도입하고 그 모듈 안에서 Reader 클래스에 접근하겠다.

문서 적재하기

SGMLParser와는 다르게, 문서를 적재하기 전에 어떤 클래스도 재단할 필요가 없다. 그러므로, 문서가 완전히 적재될 때까지 그 문서의 내용에 신경을 "쓰지 않아도 된다". 비록 미리 내용의 특성을 어느 정도 알고 있을 가능성이 아주 높고 가능하면 DOM 표현에 작동하는 클래스나 함수를 작성할 가능성이 아주 높기는 하지만 말이다. 어쨌든, 특정 종류의 문서로부터 특정 정보를 추출하는 실제 프로그램은 자신이 처리하는 문서의 구조에 관하여 아무것도 알 필요가 없다. 그 정보가 (SGMLParser에서와 같이) 해석기의 한 하부클래스에 있든지 아니면 그 정보가 DOM 표현을 조작하는 함수와 클래스에 "인코드되어 있든지" 상관이 없다.

어쨌든, 문서를 적재해서 Document 객체를 얻어보자:

doc = reader.fromString(s)

"최상위" DOM 표현은 언제나 Document 노드 객체임에 주목하자. 그리고 문서가 적재되자 마자 이것을 doc이 가리킨다.

libxml2dom 사용하기

libxml2dom을 사용하여 문서를 얻는 것이 약간 더 쉽다:

import libxml2dom
doc = libxml2dom.parseString(s, html=1)

문서 텍스트가 모양을-갖춘 XML이라면,  html 매개변수를 빼도 된다. 또는 거기에 거짓 값을 설정해도 된다. 그렇지만, 텍스트가 모양을 갖추었는지 확신이 없다면, 언급한 방식대로 매개변수를 설정해도 심각한 문제는 일어나지 않을 것이다.

무엇을 추출할지 결정하기

이제, 문서에서 어떤 정보를 발견하고 열람할지 결정하는 것이 적절하다. 그리고 이곳이 바로 어떤 작업이 SGMLParser보다 더 쉽게 보이는 곳이다 (작업틀과 관련됨). 문서에서 모든 하이퍼링크를 추출하는 작업을 고려해 보자; 아래와 같이 확실하게 모든 하이퍼링크 요소를 발견할 수 있다:

a_elements = doc.getElementsByTagName("a")

하이퍼링크 요소는 시작 a 태그, 종료 a 태그, 그리고 그 사이의 모든 데이터로 구성되기 때문에, a_elements 변수의 값은 문서의 구역을 표현하는 객체 리스트이다. 다음과 같이 보일 것이다:

<a href="http://www.python.org">The Python Web site</a>
요소 질의하기

요소들을 더 쉽게 다루기 위해, 리스트의 각 객체는 위에 주어진 것처럼 그 요소의 텍스트형 표현이 아니라, 각 요소에 대하여 만들어지기 때문에 더 편리하게 세부적으로 접근할 수 있다. 그러므로 그런 객체에 대하여 참조점을 얻을 수 있으며 그 참조점이 가리키는 요소에 관하여 더 많이 알아낼 수 있다:

# 리스트에서 첫 요소를 얻는다. 따로 변수를 사용할 필요가 없지만,
# 그렇게 하는 것이 더 이해하기 쉽다.
first = a_elements[0]
# 이제 "href"의 속성 값을 화면에 표시한다.
print first.getAttribute("href")

여기에서 일어나고 있는 일은 first 객체 (발견된 요소들의 리스트에서 제일 첫 a 요소임)에 이름이 href인 속성의 값을 돌려달라고 요구하고 있다는 것이다. 그런 속성이 존재한다면, 그 속성의 내용을 담은 문자열이 반환된다: 위의 예제의 경우, 다음과 같이 반환된다...

http://www.python.org

다음 예제 요소에서와 같이 href 속성이 존재하지 않으면, None이라는 값이 반환된다.

<a name="Example">This is not a hyperlink. It is a target.</a>
이름공간

앞에서 이 문서는 이름공간을 사용할 것을 권장하였다. 그리고 getAttribute 메소드 보다는 getAttributeNS 메쏘드를 사용할 것을 권장하였다. XML 처리는 이름공간을 널리 사용하지만, 어떤 HTML 해석기들은 이름공간을 생각처럼 많이 노출시켜 보여주는 것 같지 않다: 예를 들어, 문서에서 XHTML 이름공간을 XHTML 요소에 연관시키지 않는다. 그래서, 혼합-소 문서에서 요소 사이를 구별하기 위하여 사용하지 않을 수 없는 경우가 아니라면 이름공간을 무시해도 좋다고 생각한다 (예를 들어, SVG와 조합된 XHTML).

좀 더 구체적으로 내용 찾기

어떤 면에서 문서 안에서 a 요소에 접근하기로 결정했다는 것은 이미 상당히 구체적이다. 왜냐하면 문서 구조의 특정 지점에서 시작하여 거기에서부터 요소들을 검색하기 때문이다. SGMLParser 예제에서, 하이퍼링크에 연관된 시작태그와 종료 태그 사이에 싸인 텍스트에서 하이퍼링크의 설명을 찾아 보기로 결정했다. 그리고 거의 그런 목적은 달성했다. 물론 좀 더 잘 다룰 수 있는 문제들이 몇 가지 있기는 하지만 말이다. 여기에서는 하이퍼링크 요소 안에 설명된 모든 것들을 찾아 보기로 하겠다.

요소, 노드 그리고 자손노드

각 하이퍼링크 요소는 객체로 표현되어 속성을 질의할 수 있다. 위에서 href 속성의 값을 얻기 위하여 했던것처럼 말이다. 그렇지만, 요소들도 그 내용을 질의할 수 있다. 그런 내용들은 객체의 형태를 취하고 문서 안에서 "노드"로 표현된다. (XML 문서의 성질은 DOM을 다루고 있는 또다른 입문서에 기술되어 있다.) 이 경우, 각 하이퍼링크 요소 안(또는 아래)에 자리한 노드를 들여다 보는 것이 흥미롭다. 그리고 이런 노드들은 일반적으로 "자손 노드"라고 알려져 있기 때문에, 이른바 Node 객체에 붙은 childNodes 속성을 통하여 접근한다.

# 첫 "a" 요소의 자손 노드들을 얻는다.
nodes = first.childNodes
노드 유형

노드는 XML 문서에서 발견되는 특정 정보의 토대이다. 그래서 문서에서 발견되는 어떤 요소라도 노드에 기초하며 그의 "노드 유형"을 점검함으로써 요소로서 명시적으로 식별된다:

print first.nodeType
# xml.dom.Node 클래스에 나열된 특별한 값중의 하나에 상응하는 숫자 하나가 반환된다.
# 요소들은 그 클래스로부터 상속받기 때문에,
# 'first'에 있는 이런 값들에 직접 접근할 수 있다!
print first.nodeType == first.ELEMENT_NODE
# first가 요소이면 (확실함) 값 1을 화면에 표시한다.

이게 과연 쓸모가 있는지 궁금할 것이다. 왜냐하면 하이퍼링크 요소의 리스트는, 예를 들어, 확실하게 요소 리스트이기 때문이다 - 다시 말해, 어쨋거나 요구한 것이기 때문이다. 그렇지만, "자손 노드"의 리스트에 대하여 요소를 하나 요구하면, 이 노드 중에서 어느 것이 요소인지, 예를 들어, 텍스트형 데이터 조각인지 바로 구별할 수 없다. 그러므로 first의 "자손 노드"를 조사해서 어느 노드가 텍스트인지 알아보자:

for node in first.childNodes:
if node.nodeType == node.TEXT_NODE:
print "Found a text node:", node.nodeValue
문서 구조 돌아보기

각 하이퍼링크 요소의 설명 텍스트만을 얻고 싶다면, 각 요소의 모든 노드("자손 노드")를 방문해서 텍스트 요소들의 값을 기록하면 될 것이다. 그렇지만 이 정도로는 많이 모자란다 - 다음의 문서 영역을 생각해 보자:

<a href="http://www.python.org">A <em>really</em> important page.</a>

 a 요소 안에는 텍스트 노드들과 em 요소 하나가 있다 - em 요소의 텍스트는 직접 a 요소의 "자손 노드"로 얻을 수 없다. 각 자손 요소의 텍스트형 자손 노드를 고려하지 않는다면, 중요한 정보를 잃어 버린다. 결론적으로 재귀적으로 a 요소를 타고 내려가 자손 노드 값들을 수집하는 일이 필수적이다. 생각보다 어렵지는 않지만:

def collect_text(node):
"A function which collects text inside 'node', returning that text."

s = ""
for child_node in node.childNodes:
if child_node.nodeType == child_node.TEXT_NODE:
s += child_node.nodeValue
else:
s += collect_text(child_node)
return s

# Call 'collect_text' on 'first', displaying the text found.
print collect_text(first)

이 접근법을 SGMLParser 접근법과 대조해 보면, 이 예제에서 텍스트형 정보를 추출하는 대부분의 작업은 MyParser 클래스 전체에 걸쳐 분배되어 있지만, 반면 상당히 복잡하게 보이는 위의 함수는 필요한 연산이 한 곳에 모여 있다. 그래서 복잡하게 보인다.

문서 구역을 텍스트로 얻기

흥미롭게도 각 자손 노드에 대하여 원래 문서의 전체 섹션을 텍스트로 열람하는 것이 더 쉽다. 그래서 a 요소의 전체 내용을 텍스트로 수집한다. 이렇게 하려면 그냥 xml.dom.ext 꾸러미에서 제공하는 함수를 사용하면 된다:

from xml.dom.ext import PrettyPrint
# 시작 "a" 태그와 끝 태그를 회피하기 위해,
# 자손 노드를 예쁘게 출력(prettyprint)한다.
s = ""
for child_node in a_elements[0]:
s += PrettyPrint(child_node)
# 태그 사이의 원래 문서 구역을 화면에 표시한다.
print s

불행하게도, libxml2dom으로 생산한 문서는 PrettyPrint와 잘 작동하지 않는다. 그렇지만, 메쏘드를 각 노드 객체에 대신 사용하면 된다:

# 시작 "a" 태그와 끝 태그를 회피하기 위해, 
# 자손 노드를 예쁘게 출력한다.
s = ""
for child_node in a_elements[0]:
s += child_node.toString(prettyprint=1)
# 태그 사이의 원래 문서 구역을 화면에 표시한다.
print s

앞으로 libxml2dom은 결국 그런 함수 그리고 도구들과 더 잘 작동하리라 믿는다.


한글판 johnsonj 2007.04.11