☜ 제 07 장 정규표현식 | """ Dive Into Python """ 다이빙 파이썬 |
제 09 장 XML 처리 ☞ |
자주 comp.lang.python에 올라오는 질문들이 있습니다. 예를 들면 “어떻게 [헤더|이미지|링크]를 HTML 문서에서 모두 나열할 수 있을까요?” “태그는 그대로 둔 채 HTML 문서의 텍스트를 어떻게 해석/변환/조작 할 수 있나요?” “ 어떻게 해야 모든 HTML 태그의 속성을 단 번에 추가/제거/인용할 수 있을까요?” 이 장에서 이런 모든 질문에 대하여 해답을 찾아 보겠습니다.
다음은 완벽하게 작동하는 파이썬 프로그램입니다. 두 부분으로 구성되어 있는데, 첫 부분은 BaseHTMLProcessor.py로서 텍스트 블럭과 태그를 거닐면서 HTML 파일 처리를 도와줄 총괄적 도구입니다. 두 번째 부분은 dialect.py로서 BaseHTMLProcessor.py를 사용하여 태그는 그대로 둔 채 HTML 문서를 변환하는 법을 보여주는 예제입니다. 문서화문자열(doc string)과 주석을 읽어보고 대충 무슨 일이 진행되는지 알아봅시다. 대부분은 신비한 마법처럼 보이겠지요. 이런 클래스들이 도대체 어떤 식으로 호출되는지 알 수 없기 때문입니다. 걱정하지 맙시다. 때가 되면 다 알게 됩니다.
아직 그렇게 하지 못했다면 이 책에 사용된 다음 예제와 기타 예제들을 내려 받으시면 됩니다.
from sgmllib import SGMLParser import htmlentitydefs class BaseHTMLProcessor(SGMLParser): def reset(self): # 확장한다 (SGMLParser.__init__에서 호출) self.pieces = [] SGMLParser.reset(self) def unknown_starttag(self, tag, attrs): # 시작 태그마다 호출됨. # attrs는 (attr, value) 터플로 구성된 리스트이다. # 예를 들어, <pre class="screen">이라면 tag="pre", attrs=[("class", "screen")]이다. # 이상적으로 원래 태그와 속성을 재구성하고 싶지만, # 소스 문서에서 인용부호 처리되지 않은 속성 값을 처리해야 하거나, # 또는 속성 값 주위의 인용부호 종류를 바꿀 수도 있다. # (홑따옴표를 겹따옴표로 말이다.) # (클라이언트-쪽 자바스크립트처럼) HTML-아닌 코드가 부적절하게 임베드되어 있으면, # 조상 메쏘드에 의하여 올바르지 않게 해석될 수 있어서, # 실행시간 스크립트 에러가 야기될 수 있다. # HTML-아닌 코드는 모두 HTML 주석 태그 (<!-- code -->) 안에 싸 넣어서, # 이 해석기를 (handle_comment 안을) 그대로 통과하는지 확인해야 한다. strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) self.pieces.append("<%(tag)s%(strattrs)s>" % locals()) def unknown_endtag(self, tag): # 끝 태그마다 호출됨. 예를 들어 </pre>이라면, 태그는 "pre"가 될 것이다. # 원래 끝 태그를 재구성한다. self.pieces.append("</%(tag)s>" % locals()) def handle_charref(self, ref): # 문자 참조마다 호출됨. 예를 들어, " "이라면, ref는 "160"이 될 것이다. # 원래 문자 참조를 재구성한다. self.pieces.append("&#%(ref)s;" % locals()) def handle_entityref(self, ref): # 개체 참조마다 호출됨. 예를 들어 "©"이라면, ref는 "copy"가 될 것이다. # 원래 개체 참조를 재구성한다. self.pieces.append("&%(ref)s" % locals()) # 표준 HTML 개체는 쌍반점으로 끝난다; 다른 개체들은 그렇지 않다. if htmlentitydefs.entitydefs.has_key(ref): self.pieces.append(";") def handle_data(self, text): # 평범한 텍스트 블록마다 호출됨. 다시 말해, 태그의 바깥에 있으며, # 문자 참조나 개체 참조가 포함되지 않는다. # 원래 텍스트를 그냥 그대로 저장한다. self.pieces.append(text) def handle_comment(self, text): # HTML 주석마다 호출됨. 예, <!-- insert Javascript code here --> # 원래 주석을 재구성한다. # 소스 문서에서 주석에 (자바스크립트 같은) 클라이언트-쪽 코드가 담겨 있어서, # 이 처리기를 그대로 통과시키려면 특히 중요하다; # 자세한 것은 unknown_starttag에 있는 주석을 참고하라. self.pieces.append("<!--%(text)s-->" % locals()) def handle_pi(self, text): # 처리 지시어마다 호출됨. 예, <?instruction> # 원래 처리 지시어를 재구성한다. self.pieces.append("<?%(text)s>" % locals()) def handle_decl(self, text): # 존재한다면, DOCTYPE에 대하여 호출됨. 예, # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" # "http://www.w3.org/TR/html4/loose.dtd"> # 원래 DOCTYPE을 재구성한다 self.pieces.append("<!%(text)s>" % locals()) def output(self): """처리된 HTML을 문자열 하나로 돌려준다""" return "".join(self.pieces)
import re from BaseHTMLProcessor import BaseHTMLProcessor class Dialectizer(BaseHTMLProcessor): subs = () def reset(self): # 확장 (조상의 __init__ 메쏘드로부터 호출된다) # 모든 데이터 속성을 초기화한다. self.verbatim = 0 BaseHTMLProcessor.reset(self) def start_pre(self, attrs): # HTML 소스에서 <pre> 태그마다 호출됨. # verbatim 모드 횟수를 증가시키고, 평소처럼 태그를 처리한다. self.verbatim += 1 self.unknown_starttag("pre", attrs) def end_pre(self): # HTML 소스에서 </pre> 태그마다 호출됨. # verbatim 모드 횟수를 감소시킨다. self.unknown_endtag("pre") self.verbatim -= 1 def handle_data(self, text): # 오버라이드한다. # HTML 소스에서 텍스트 블록마다 호출됨. # verbatim 모드에 있다면, 텍스트를 그대로 저장한다; # 그렇지 않으면 순서대로 교체하면서 텍스트를 처리한다. self.pieces.append(self.verbatim and text or self.process(text)) def process(self, text): # handle_data으로부터 호출된다. # 일련의 정규 표현식 교체를 수행함으로써 텍스트 블록을 처리한다. # (실제로 교체하는 일은 자손에 정의된다) for fromPattern, toPattern in self.subs: text = re.sub(fromPattern, toPattern, text) return text class ChefDialectizer(Dialectizer): """HTML을 스웨덴 주방장식-화법으로 변환한다. 다음에 기반함 chef.x, copyright (c) 1992, 1993 John Hagerman """ subs = ((r'a([nu])', r'u\1'), (r'A([nu])', r'U\1'), (r'a\B', r'e'), (r'A\B', r'E'), (r'en\b', r'ee'), (r'\Bew', r'oo'), (r'\Be\b', r'e-a'), (r'\be', r'i'), (r'\bE', r'I'), (r'\Bf', r'ff'), (r'\Bir', r'ur'), (r'(\w*?)i(\w*?)$', r'\1ee\2'), (r'\bow', r'oo'), (r'\bo', r'oo'), (r'\bO', r'Oo'), (r'the', r'zee'), (r'The', r'Zee'), (r'th\b', r't'), (r'\Btion', r'shun'), (r'\Bu', r'oo'), (r'\BU', r'Oo'), (r'v', r'f'), (r'V', r'F'), (r'w', r'w'), (r'W', r'W'), (r'([a-z])[.]', r'\1. Bork Bork Bork!')) class FuddDialectizer(Dialectizer): """HTML을 엘머 퍼드(Elmer Fudd)-화법으로 변환한다""" subs = ((r'[rl]', r'w'), (r'qu', r'qw'), (r'th\b', r'f'), (r'th', r'd'), (r'n[.]', r'n, uh-hah-hah-hah.')) class OldeDialectizer(Dialectizer): """HTML을 중세 영어를 흉내내도록 변환한다""" subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'), (r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'), (r'ick\b', r'yk'), (r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'), (r'e[ea]([bcdfghjklmnpqrstvwxyz])', r'e\1e'), (r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'), (r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'), (r'([aeiou])re\b', r'\1r'), (r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'), (r'tion\b', r'cioun'), (r'ion\b', r'ioun'), (r'aid', r'ayde'), (r'ai', r'ey'), (r'ay\b', r'y'), (r'ay', r'ey'), (r'ant', r'aunt'), (r'ea', r'ee'), (r'oa', r'oo'), (r'ue', r'e'), (r'oe', r'o'), (r'ou', r'ow'), (r'ow', r'ou'), (r'\bhe', r'hi'), (r've\b', r'veth'), (r'se\b', r'e'), (r"'s\b", r'es'), (r'ic\b', r'ick'), (r'ics\b', r'icc'), (r'ical\b', r'ick'), (r'tle\b', r'til'), (r'll\b', r'l'), (r'ould\b', r'olde'), (r'own\b', r'oune'), (r'un\b', r'onne'), (r'rry\b', r'rye'), (r'est\b', r'este'), (r'pt\b', r'pte'), (r'th\b', r'the'), (r'ch\b', r'che'), (r'ss\b', r'sse'), (r'([wybdp])\b', r'\1e'), (r'([rnt])\b', r'\1\1e'), (r'from', r'fro'), (r'when', r'whan')) def translate(url, dialectName="chef"): """URL을 가져와서 사투리로 번역한다. dialect in ("chef", "fudd", "olde")""" import urllib sock = urllib.urlopen(url) htmlSource = sock.read() sock.close() parserName = "%sDialectizer" % dialectName.capitalize() parserClass = globals()[parserName] parser = parserClass() parser.feed(htmlSource) parser.close() return parser.output() def test(url): """URL에 대하여 모든 사투리를 테스트한다""" for dialect in ("chef", "fudd", "olde"): outfile = "%s.html" % dialect fsock = open(outfile, "wb") fsock.write(translate(url, dialect)) fsock.close() import webbrowser webbrowser.open_new(outfile) if __name__ == "__main__": test("http://diveintopython.org/odbchelper_list.html")
이 스크립트를 실행하면 섹션 3.2, “리스트 소개”가 (머펫(The Muppets) 쇼의) 스웨덴-주방장(Swedish Chef)식-화법과 (벅스 버니(Bugs Bunny ) 만화의) 엘머-퍼드(Elmer Fudd)식-화법 그리고 (초서(Chaucer)의 캔터베리 이야기에 느슨하게 기반한) 중세 영어식으로 번역됩니다. 출력 페이지의 HTML 소스를 보면 모든 HTML 태그와 속성이 그대로인 것을 알 수 있습니다. 그러나 태그 사이에 있는 텍스트는 흉내언어로 “번역되어 있습니다”. 더 자세히 들여다 보면 실제로 제목과 문단만 번역되었습니다; 코드 목록과 화면 예제는 그대로입니다.
<div class="abstract"> <p>Lists awe <span class="application">Pydon</span>'s wowkhowse datatype. If youw onwy expewience wif wists is awways in <span class="application">Visuaw Basic</span> ow (God fowbid) de datastowe in <span class="application">Powewbuiwdew</span>, bwace youwsewf fow <span class="application">Pydon</span> wists.</p> </div>
HTML 처리는 세 단계로 나뉩니다: HTML을 구성요소로 나누는 것과 그 조각을 처리하는 것 그리고 다시 그 조각을 HTML 로 재구성하는 단계로 나뉩니다. 첫 단계는 표준 파이썬 라이브러리에 있는 sgmllib.py로 완수합니다.
이 장을 이해하는 열쇠는 HTML이 단순한 텍스트가 아니라 구조가 있는 텍스트라는 사실을 깨닫는 것입니다. 구조는 대략 계통적인 시작 태그와 끝 태그로부터 파생됩니다. 보통 이런 식으로 HTML을 사용하지 않습니다; 텍스트 편집기에서 텍스트를 보면서 작업하거나 또는 웹 브라우저나 웹 저작 도구에서 시각적으로 보면서 작업합니다. sgmllib.py는 HTML을 구조적으로 보여줍니다.
sgmllib.py에는 중요한 클래스가 하나 있습니다: SGMLParser가 그것입니다. SGMLParser는 HTML을 시작 태그와 끝 태그 같은 유용한 조각들로 분해합니다. 데이터를 유용한 조각으로 분해하자 마자, 무엇이 발견되었는가에 따라 메쏘드를 호출합니다. 해석기를 사용하기 위하여 SGMLParser 클래스를 하부클래스화하고 이런 메쏘드를 오버라이드합니다. 이것이 바로 HTML을 구조적으로 보여준다고 언급했을 때의 의미입니다: HTML의 구조는 일련의 메쏘드 호출을 결정하고 각 메쏘드에 건넬 인자를 결정합니다.
SGMLParser는 HTML을 여덟가지 데이터로 나누고, 각각에 대하여 따로따로 메쏘드를 호출합니다:
★ | |
파이썬 2.0에 버그가 있었습니다. SGMLParser가 선언을 전혀 인식하지 않았었습니다 (handle_decl가 호출되지 않았습니다). 즉 DOCTYPE이 조용히 무시된다는 뜻입니다. 이 버그는 파이썬 2.1에서 수정되었습니다. |
sgmllib.py에는 이를 보여주는 테스트 모듬이 따라옵니다. sgmllib.py를 실행하고, 명령줄에서 HTML 파일의 이름을 건네면 해석을 해가면서 바로바로 그 태그와 기타 요소들을 인쇄합니다. SGMLParser 클래스를 상속받고 unknown_starttag와 unknown_endtag 그리고 handle_data와 단순히 자신의 인자를 인쇄할 뿐인 기타 메쏘드를 정의하여 이렇게 합니다.
√ | |
윈도우즈의 ActivePython IDE라면 명령어 줄 인자를 “Run script” 대화상자에서 지정할 수 있습니다. 여러 인자는 공백으로 가릅니다. |
다음은 이 책의 HTML 버전에서 목차를 뽑아 온 것입니다. 물론 여러분의 경로는 다를 것입니다. (이 책의 HTML 버전을 내려받지 못했다면 http://diveintopython.org/에서 내려받을 수 있습니다.)
c:\python23\lib> type "c:\downloads\diveintopython\html\toc\index.html"
<!DOCTYPE html
PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Dive Into Python</title>
<link rel="stylesheet" href="diveintopython.css" type="text/css">
... 간결하게 하기 위해 이하 생략함 ...
sgmllib.py의 테스트 모듬을 통하여 실행하면 다음과 같이 출력됩니다:
c:\python23\lib> python sgmllib.py "c:\downloads\diveintopython\html\toc\index.html" data: '\n\n' start tag: <html lang="en" > data: '\n ' start tag: <head> data: '\n ' start tag: <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" > data: '\n \n ' start tag: <title> data: 'Dive Into Python' end tag: </title> data: '\n ' start tag: <link rel="stylesheet" href="diveintopython.css" type="text/css" > data: '\n ' ... rest of output omitted for brevity ...
앞으로 다음과 같은 순서로 다루어 보겠습니다:
앞으로 locals와 globals 그리고 사전-기반의 문자열 형식화를 배웁니다.
HTML 문서로부터 데이터를 추출하려면 SGMLParser 클래스를 상속받아, 잡고싶은 각 개체 또는 각 태그에 대하여 메쏘드를 정의합니다.
데이터를 HTML 문서로부터 추출하는 첫 단계는 HTML을 얻는 것입니다. HTML이 하드 디스크에 있다면 파일 함수를 사용하여 읽을 수 있지만, 진짜 재미는 살아있는 웹 페이지로부터 HTML을 얻을 때 시작됩니다.
>>> import urllib ① >>> sock = urllib.urlopen("http://diveintopython.org/") ② >>> htmlSource = sock.read() ③ >>> sock.close() ④ >>> print htmlSource ⑤ <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head> <meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1'> <title>Dive Into Python</title> <link rel='stylesheet' href='diveintopython.css' type='text/css'> <link rev='made' href='mailto:mark@diveintopython.org'> <meta name='keywords' content='Python, Dive Into Python, tutorial, object-oriented, programming'> <meta name='description' content='a free Python tutorial for experienced programmers'> </head> <body bgcolor='white' text='black' link='#0000FF' vlink='#840084' alink='#0000FF'> <table cellpadding='0' cellspacing='0' border='0' width='100%'> <tr><td class='header' width='1%' valign='top'>diveintopython.org</td> <td width='99%' align='right'><hr size='1' noshade></td></tr> <tr><td class='tagline' colspan='2'>Python for experienced programmers</td></tr> [...이하 생략...]
① | urllib 모듈은 표준 파이썬 라이브러리에 포함되어 있습니다. 이 모듈에는 (주로 웹 페이지인) 인터넷-기반의 URL로부터 실제로 데이터를 열람하고 정보를 얻는 함수들이 들어 있습니다. |
② | 가장 쉽게 urllib을 사용하는 방법은urlopen 함수를 사용하여 웹 페이지의 전체 텍스트를 열람하는 것입니다. URL을 여는 일은 파일을 여는 것과 똑 같습니다. urlopen의 반환 값은 파일-류의 객체이며, 이는 파일 객체와 똑 같은 메쏘드들을 갖추고 있습니다. |
③ | urlopen이 돌려준 파일류의 객체를 다루는 가장 간단한 방법은 read인데, 이 메쏘드는 HTML 웹 페이지의 전체 텍스트를 한-개짜리 문자열로 읽습니다. 또한 readlines도 지원하는데, 이는 텍스트를 한줄 한줄 리스트로 읽습니다. |
④ | 객체 처리 작업이 끝나면 반드시 다른 보통의 파일 객체처럼 닫으세요. |
⑤ | 이제 완벽하게 http://diveintopython.org/ HTML 홈페이지를 문자열로 확보하였고, 해석할 준비가 되었습니다. |
아직 그렇게 하지 못했다면 이 책에 사용된 예제를 내려 받을 수 있습니다.
from sgmllib import SGMLParser class URLLister(SGMLParser): def reset(self): ① SGMLParser.reset(self) self.urls = [] def start_a(self, attrs): ② href = [v for k, v in attrs if k=='href'] ③ ④ if href: self.urls.extend(href)
① | reset은 SGMLParser __init__ 메쏘드에서 호출하며, 해석기의 실체가 만들어질 때 한번 손수 호출할 수도 있습니다. 그래서 초기화가 필요하다면 reset에서 초기화해야지, __init__에서 초기화하지 마세요. 그래야 누군가 해석기 실체를 재사용할 때 적절하게 다시-초기화될 것입니다. |
② | <a> 태그가 발견될 때마다 SGMLParser에 의하여 start_a가 호출됩니다. 태그에는 href 속성이나, 예를 들어 name이나 title 같은 기타 속성들이 담길 수 있습니다. attrs 매개변수는 터플로 구성된 리스트입니다 [(attribute, value), (attribute, value), ...]. 그렇지 않으면 그냥 <a>가 되는데, (쓸모가 없더라도) 유효한 HTML 태그로서, 이 경우 attrs는 빈 리스트가 됩니다. |
③ | 간단한 다중-변수 할당 지능형 리스트로 이 <a> 태그에 href 속성이 있는지 알아볼 수 있습니다. |
④ | k=='href' 같은 문자열 비교는 언제나 대소문자에-민감하지만, 이 경우에는 안전합니다. 그 이유는 SGMLParser가 attrs를 구성하면서 속성 이름을 소문자로 변환하기 때문입니다. |
>>> import urllib, urllister >>> usock = urllib.urlopen("http://diveintopython.org/") >>> parser = urllister.URLLister() >>> parser.feed(usock.read()) ① >>> usock.close() ② >>> parser.close() ③ >>> for url in parser.urls: print url ④ toc/index.html #download #languages toc/index.html appendix/history.html download/diveintopython-html-5.0.zip download/diveintopython-pdf-5.0.zip download/diveintopython-word-5.0.zip download/diveintopython-text-5.0.zip download/diveintopython-html-flat-5.0.zip download/diveintopython-xml-5.0.zip download/diveintopython-common-5.0.zip ... 나머지 출력은 깨끗하게 생략함 ...
① | SGMLParser에 정의된 feed 메쏘드를 불러서 HTML을 해석기에 먹입니다.[1] 이 메쏘드는 usock.read()가 돌려주는 문자열을 받습니다. |
② | 파일과 마찬가지로, 작업이 끝나면 바로 URL 객체를 닫으세요. |
③ | 해석기 객체도 역시 닫아야 하지만, 그 이유는 다릅니다. 모든 데이터를 읽었고, 그것을 해석기에 먹였지만, feed 메쏘드가 실제로 주어진 HTML을 모두 처리한다는 보장은 없습니다; 데이터를 버퍼에 담아두고, 더 들어오기를 기다릴 수도 있습니다. 반드시 close를 호출하여 버퍼를 비우고 모든 것이 완전히 해석되도록 강제해야 합니다. |
④ | 일단 해석기가 닫히면(close), 해석작업은 완료되고, parser.urls에 담긴 리스트에 HTML 문서 안에 있는 모든 URL 링크가 담깁니다. (여러분은 출력이 다를 수 있습니다. 내려받기 링크가 이 글을 읽을 때 쯤 갱신되었다면 말입니다.) |
SGMLParser는 자체로는 아무것도 산출하지 못합니다. 해석하고 또 해석하면서, 자신이 발견한 흥미로운 것들에 대하여 메쏘드를 호출할 뿐입니다. 그러나 그 메쏘드들은 아무 것도 하지 않습니다. SGMLParser는 HTML 소비자입니다: HTML을 받아 구조화된 조각으로 쪼갭니다. 앞 섹션에서 보셨듯이, SGMLParser을 상속받아 특정 태그를 잡아 웹 페이지 있는 모든 링크 리스트 같은 유용한 것들을 생산하는 클래스를 정의합니다. 이제 한 걸음 더 나아가 보겠습니다. SGMLParser가 던져주는 모든 것을 잡아 완전한 HTML 문서로 재구성하는 클래스를 정의해 보겠습니다. 기술적 관점에서, 이 클래스는 HTML 생산자입니다.
BaseHTMLProcessor는 SGMLParser를 상속받아 여덟 개의 핵심 처리자 메쏘드를 모두 제공합니다: unknown_starttag, unknown_endtag, handle_charref, handle_entityref, handle_comment, handle_pi, handle_decl, 그리고 handle_data.
class BaseHTMLProcessor(SGMLParser): def reset(self): ① self.pieces = [] SGMLParser.reset(self) def unknown_starttag(self, tag, attrs): ② strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) self.pieces.append("<%(tag)s%(strattrs)s>" % locals()) def unknown_endtag(self, tag): ③ self.pieces.append("</%(tag)s>" % locals()) def handle_charref(self, ref): ④ self.pieces.append("&#%(ref)s;" % locals()) def handle_entityref(self, ref): ⑤ self.pieces.append("&%(ref)s" % locals()) if htmlentitydefs.entitydefs.has_key(ref): self.pieces.append(";") def handle_data(self, text): ⑥ self.pieces.append(text) def handle_comment(self, text): ⑦ self.pieces.append("<!--%(text)s-->" % locals()) def handle_pi(self, text): ⑧ self.pieces.append("<?%(text)s>" % locals()) def handle_decl(self, text): self.pieces.append("<!%(text)s>" % locals())
① | SGMLParser.__init__이 호출하는 reset 메쏘드는 self.pieces를 빈 리스트로 초기화한 후에 조상 메쏘드를 호출합니다. self.pieces는 구성하려고 하는 HTML 문서의 조각들을 보유할 데이터 속성입니다. 각 처리자 메쏘드는 SGMLParser가 해석한 HTML을 재구성합니다. 그리고 각 메소드는 그 문자열을 self.pieces에 추가합니다. self.pieces는 리스트임을 주목하세요. 문자열로 정의하고 그냥 그 뒤에 각 조각을 덧붙이고 싶을 것입니다. 그래도 되지만, 파이썬에서는 리스트로 다루는 것이 훨씬 더 효율적입니다.[2] |
② | BaseHTMLProcessor에는 특정 태그에 대하여 (URLLister에서의 start_a 메쏘드 같은) 아무 메쏘드도 정의되어 있지 않으므로, SGMLParser는 시작 태그면 모두 unknown_starttag를 호출합니다. 이 메쏘드는 그 태그(tag)와 속성 이름/값 쌍이 담긴 리스트(attrs)를 취해, 원래의 HTML을 재구성하여, 그것을 self.pieces에 추가합니다. 여기에 있는 문자열 형식화는 약간 이상합니다; (그리고 기묘하게 보이는 locals 함수도) 이 장의의 후반부에 가면 이해가 되실겁니다. |
③ | 끝 태그를 재구성하는 일은 더 쉽습니다; 그냥 태그 이름을 취해 </...> 괄호에 싸 넣으면 됩니다. |
④ | SGMLParser가 문자 참조를 발견하면 오직 그 참조점만 가지고 handle_charref를 호출합니다. HTML 문서에 참조  이 있다면 ref는 160이 됩니다. 원래의 완벽한 문자 참조를 재구성하려면 그냥 ref를 &#...; 문자에 싸 넣으면 됩니다. |
⑤ | 개체 참조는 문자 참조와 비슷합니다. 그러나 해시 마크가 없습니다. 원래의 개체 참조를 재구성하려면 ref를 &...; 문자에 싸 넣어야 합니다. (박식한 독자 한 분이 지적하셨듯이, 실제로는 이 보다 약간 더 복잡합니다. 오직 어떤 표준 HTML 개체만이 쌍반점으로 끝납니다; 비슷하게 보이는 다른 객체들은 그렇지 않습니다. 다행스럽게도, HTML 개체의 표준 집합이 htmlentitydefs이라고 부르는 파이썬 모듈에 있는 한 사전에 정의되어 있습니다. 그러므로 따로 또 if 서술문이 있습니다.) |
⑥ | 텍스트 블록은 그대로 self.pieces에 추가됩니다. |
⑦ | HTML 주석은 <!--...--> 문자 안에 싸 넣습니다. |
⑧ | 처리 지시어는 <?...> 문자 안에 싸 넣습니다. |
★ | |
HTML 규격에 의하면 (클라이언트-쪽 JavaScript 같은) 모든 비-HTML 요소들은 반드시 HTML 주석으로 둘러 싸야 하지만, 모든 웹 페이지가 이 규칙을 적절하게 지키는 것은 아닙니다 (현대의 웹 브라우저라면 그렇게 하지 않더라도 관대하게 처리합니다). BaseHTMLProcessor는 용서가 없습니다; 스크립트가 적절하게 임베드되지 않으면 마치 HTML인 것처럼 해석해 버립니다. 예를 들어 스크립트에 '~보다 작다' 부호가 들어 있다면 SGMLParser는 태그와 속성을 발견했다고 오해를 할 수 있습니다. SGMLParser는 언제나 태그와 속성 이름을 소문자로 변환하는데, 이 때문에 스크립트가 깨질 수 있습니다. 그리고 BaseHTMLProcessor는 (원래 HTML 문서가 홑 따옴표를 사용하거나 또는 아무것도 사용하지 않았을 지라도) 언제나 속성 값을 겹 따옴표로 둘러싸는데, 이 때문에 확실히 문서가 깨집니다. 언제나 HTML 주석 안에 클라이언트-쪽 스크립트를 보호하세요. |
def output(self): ① """처리된 HTML을 한-개짜리 문자열로 돌려준다""" return "".join(self.pieces) ②
① | 이 메쏘드는 BaseHTMLProcessor에서 조상인 SGMLParser 메쏘드에 의하여 절대로 호출되지 않는 한가지 메쏘드입니다. 다른 처리자 메쏘드는 자신이 재구성한 HTML을 self.pieces에 저장하므로, 이 함수는 그 조각들을 모두 하나의 문자열로 결합하는데 필요합니다. 앞서 지적하였듯이, 파이썬은 리스트를 잘 다루고 문자열은 별로 잘 못 다룹니다. 그래서 누군가 명시적으로 그렇게 해달라고 요청할 경우에만 완전한 문자열을 만듭니다. |
② | 원하신다면 대신에 string 모듈의 join 메쏘드를 사용해도 됩니다: string.join(self.pieces, "") |
잠시만 HTML 처리를 벗어나서 어떻게 파이썬이 변수를 처리하는지 언급하겠습니다. 파이썬은 내장 함수로 locals와 globals가 있는데, 이를 사용하면 지역 변수와 전역 변수에 사전-기반의 접근을 할 수 있습니다.
지역변수(locals)를 기억하십니까? 여기에서 처음 그것을 보셨습니다:
def unknown_starttag(self, tag, attrs): strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
아니, 기다리세요. 아직 locals를 배울 수 없습니다. 먼저, 이름공간을 배워야 합니다. 따분한 주제이지만, 중요하니 주의를 기울이세요.
파이썬은 이름공간이라고 부르는 것을 사용하여 변수들을 추적관리합니다. 이름공간은 키가 변수의 이름이고 값은 변수의 값인 사전과 똑 같습니다. 실제로, 파이썬 사전처럼 이름공간에 접근할 수 있습니다. 이는 잠시후에 보여 드리겠습니다.
파이썬 프로그램에서 일정 지점에, 여러 이름공간을 사용할 수 있습니다. 각 함수는 자신만의 이름공간을 가지며 이를 지역 이름공간이라고 부르는데, 지역적으로 정의된 변수와 함수 인자를 비롯하여 함수의 변수를 추적관리 합니다. 각 모듈은 자신만의 이름공간을 가지며 이를 전역 이름공간이라고 부르는데, 함수와 클래스 그리고 기타 반입된 모듈과 모듈-수준의 변수와 상수를 포함하여 모듈의 변수를 추적관리합니다. 그리고 어떤 모듈에서도 접근할 수 있는 내장 이름공간이 있습니다. 이 공간에 내장 함수와 예외가 존재합니다.
한 줄의 코드가 변수 x의 값을 요구하면 파이썬은 가능한 모든 이름공간에서 순서대로 그 변수를 검색합니다:
어떤 이름공간에서도 파이썬이 x를 발견하지 못하면 손을 들고 이름이 'x'인 변수가 없습니다라는 메시지를 가지고 NameError를 일으킵니다. 이는 앞서 예제 3.18, “묶이지 않은 변수 참조하기”에서 보셨습니다. 그러나, 에러를 보기 전까지는 파이썬이 얼마나 많은 일을 하고 있는지 깨닫지 못했습니다.
★ | |
파이썬 2.2에서 이름공간 검색 순서에 영향을 미칠 미묘하지만 중요한 변화를 도입했습니다: 바로 내포된 영역(nested scopes)이 그것입니다. 2.2 이전의 파이썬 버전에서는 내포된 함수 또는 람다(lambda) 함수 안에서 변수를 참조하면 파이썬이 그 변수를 현재 (내포된 함수나 lambda) 함수의 이름공간을 검색한 다음, 모듈의 이름공간에서 검색을 했습니다. 파이썬 2.2는 그 변수를 현재 (내포 함수 또는 lambda) 함수의 이름공간을 검색한 다음, 부모 함수의 이름공간을 찾고, 다음으로 모듈의 이름공간에서 찾습니다. 파이썬 2.1은 어느 쪽으로도 작동할 수 있습니다; 기본으로 파이썬 2.0처럼 작동하지만, 다음 코드 줄을 모듈의 상단에 추가하면 모듈을 파이썬 2.2처럼 작동시킬 수 있습니다:from __future__ import nested_scopes |
아직 이해가 안 되십니까? 좌절하지 마세요! 약속하건대 정말 재미있습니다. 파이썬에서 다른 많은 것처럼, 이름공간은 실행시간에 직접적으로 접근할 수 있습니다. 어떻게? 자, 지역 이름공간은 내장 locals 함수를 통하여 접근할 수 있고, (모듈 수준의) 전역 이름공간은 내장 globals 함수를 통하여 접근할 수 있습니다.
>>> def foo(arg): ① ... x = 1 ... print locals() ... >>> foo(7) ② {'arg': 7, 'x': 1} >>> foo('bar') ③ {'arg': 'bar', 'x': 1}
① | foo 함수는 지역 이름공간에 두 개의 변수가 있습니다: 하나는 arg이며 그 값이 함수에 건네지며, 또 하나는 x이며 함수 안에 정의되어 있습니다. |
② | locals는 이름/값 쌍의 사전을 돌려줍니다. 이 사전의 키는 문자열로 된 변수의 이름입니다; 사전의 값은 그 변수의 실제 값입니다. 그래서 7을 가지고 foo를 호출하면 그 함수의 두개의 지역 변수가 담긴 사전을 인쇄합니다: arg (7) 그리고 x (1). |
③ | 기억하세요. 파이썬은 동적으로 유형이 정의됩니다. 그래서 그냥 쉽게 arg에 문자열을 건네면 됩니다; 그래도 (locals를 호출하면) 그 함수는 여전히 그대로 잘 작동합니다. locals는 온갖 데이터유형의 변수에 모두 작동합니다. |
locals가 지역 (함수) 이름공간을 위해 작용하는 것처럼, globals는 전역 (모듈) 이름공간을 위해 작동합니다. 그렇지만 globals가 더 재미있습니다. 왜냐하면 모듈의 이름공간이 더 재미있기 때문입니다.[3] 모듈의 이름공간에는 모듈-수준의 변수와 상수 뿐만 아니라, 그 모듈에 정의된 함수와 클래스가 모두 들어 있습니다. 게다가 모듈 안으로 반입된 것들이 모두 들어 있습니다.
from module import 그리고 import module 사이의 차이점을 기억하십니까? import module로, 모듈 그 자체가 반입되지만, 자신만의 이름공간을 유지합니다. 이 때문에 그 모듈의 이름공간을 사용하여 자신의 함수나 속성에 접근할 필요가 있습니다: module.function의 형태로 말입니다. 그러나 from module import로, 또다른 모듈로부터 특정한 함수와 속성을 자신의 이름공간 안으로 반입합니다. 이 때문에 어디에서 왔든지 원래 모듈을 참조할 필요 없이 작접적으로 접근할 수 있습니다. globals 함수를 사용하면 실제로 이런 일이 일어나는 것을 볼 수 있습니다.
BaseHTMLProcessor.py 아래에서 다음 코드 블록을 살펴보세요:
if __name__ == "__main__": for k, v in globals().items(): ① print k, "=", v
① | 그래서 겁나지 않습니다. 이전에 모두 본 것들입니다. globals 함수는 사전을 돌려주고, items 메쏘드와 다중-변수 할당을 사용하여 그 사전을 순회하고 있습니다. 여기에서 유일하게 새로운 것이 있다면 globals 함수뿐입니다. |
이제 명령줄에서 스크립트를 실행하면 다음과 같이 출력됩니다 (여러분의 출력은 플랫폼과 파이썬을 설치한 곳에 따라 약간 다를 수 있음을 주의하세요):
c:\docbook\dip\py> python BaseHTMLProcessor.py
SGMLParser = sgmllib.SGMLParser ① htmlentitydefs = <module 'htmlentitydefs' from 'C:\Python23\lib\htmlentitydefs.py'> ② BaseHTMLProcessor = __main__.BaseHTMLProcessor ③ __name__ = __main__ ④ ... rest of output omitted for brevity...
① | SGMLParser가 sgmllib로부터 반입되었고, from module import를 사용했습니다. 즉 그 모듈의 이름공간 안으로 직접적으로 반입되었다는 뜻이고, 현재 그 모듈의 이름공간에 존재한다는 뜻입니다. |
② | 이를 htmlentitydefs와 대조해 보세요. 이는 import를 사용하여 반입되었습니다. 즉 htmlentitydefs 모듈 그 자체가 이름공간에 존재하지만, htmlentitydefs 안에 정의된 entitydefs 변수는 그렇지 않다는 뜻입니다. |
③ | 이 모듈에는 오직 하나의 클래스, BaseHTMLProcessor 만 정의되어 있습니다. 여기에서 그 값은 클래스 자신이지, 그 실체의 특정한 실체가 아님에 주의하세요. |
④ | if __name__ 트릭이 기억나십니까? 모듈을 실행할 때 (또다른 모듈에서 반입하는 것과 대조적으로), 내장 __name__ 변수는 특수한 값 __main__입니다. 이 모듈을 스크립트로 명령-줄에서 실행하였으므로, __name__은 __main__이며, 이 때문에 globals를 인쇄하는 작은 테스트 코드가 실행되었습니다. |
☞ | |
locals 함수와 globals 함수를 사용하면 변수 이름을 문자열로 제공하면서 어떤 변수든지 동적으로 얻을 수 있습니다. 이는 getattr 함수의 기능을 흉내내며, 이 덕분에 그 함수의 이름을 문자열로 제공하면 어떤 함수에도 접근할 수 있습니다. |
locals 함수와 globals 함수 사이에 또 중요한 차이점이 있습니다. 함정에 빠지지 않도록 잘 배워두어야 하겠습니다. 어쨌든 함정에 빠지더라도, 적어도 배운 기억은 나실 겁니다.
def foo(arg): x = 1 print locals() ① locals()["x"] = 2 ② print "x=",x ③ z = 7 print "z=",z foo(3) globals()["z"] = 8 ④ print "z=",z ⑤
① | foo가 3으로 호출되므로, {'arg': 3, 'x': 1}가 인쇄됩니다. 별로 놀랍지 않습니다. |
② | locals는 사전을 돌려주는 함수이며, 그 사전에 값을 할당하고 있습니다. 이렇게 하면 지역 변수 x의 값이 2로 바뀔 거라 생각하시겠지만, 그렇지 않습니다. locals는 실제로 지역 이름공간을 돌려주지 않고, 사본을 돌려줍니다. 그래서 그를 바꾸더라도 지역 이름공간에 있는 변수의 값에는 아무 영향을 미치지 않습니다. . |
③ | 이는 x= 1을 인쇄하지, x= 2을 인쇄하지 않습니다. |
④ | locals에 데어보았으므로, 이것은 z의 값에 영향을 미치지 않을 것이라고 생각하시겠지만, 영향을 미칩니다. 파이썬이 구현된 내부 방식의 차이 때문에 (더 이상 깊이 들어가지 않는 것이 좋겠습니다. 내 자신 완벽히 이해하지 못하고 있기 때문입니다), globals는 실제 전역 이름공간을 돌려주지, 사본을 돌려주는 것이 아닙니다: locals와 정확하게 반대로 행위합니다. 그래서 globals가 돌려주는 사전에 조금이라도 변경을 가자면 곧바로 전역 이름공간에 영향을 미칩니다. |
⑤ | 이는 z= 8을 인쇄하지, z= 7을 인쇄하지 않습니다. |
왜 locals와 globals를 배웠을까요? 그 덕분에 사전-기반의 문자열 형식화를 배울 수 있습니다. 기억하시다시피, 정규 문자열 형식화는 값을 문자열 안에 쉽게 삽입할 수 있도록 해 줍니다. 값들은 터플로 나열되고 순서대로 문자열 표식이 있는 곳마다 문자열 안으로 삽입됩니다. 이는 효율적이긴 하지만, 언제나 읽기에 쉬운 코드는 아닙니다. 특히 여러 값이 삽입될 때는 복잡합니다. 단순히 문자열을 한 번 스캔하면서 그 결과가 어떻게 될지는 알 수 없습니다; 한편으로 문자열을 읽고 또 한편으로 터플 값을 읽는 것 사이를 끊임없이 왔다갔다 합니다.
터플 값 대신에 사전을 사용하는 문자열 형식화 형태가 있습니다.
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> "%(pwd)s" % params ① 'secret' >>> "%(pwd)s is not a good password for %(uid)s" % params ② 'secret is not a good password for sa' >>> "%(database)s of mind, %(database)s of body" % params ③ 'master of mind, master of body'
① | 명시적인 값이 든 터플 대신, 이 문자열 형식화 모양은 사전 params를 사용합니다. 문자열에 단순한 %s 표식을 사용하는 대신에, 표식에 괄호로 둘러싸인 이름이 담깁니다. 이 이름은 params 사전의 키로 사용되고 %(pwd)s 표식의 위치에 상응하는 값 secret으로 교체합니다. |
② | 사전-기반의 문자열 형식화는 몇 개이든 이름 붙은 키와 얼마든지 작동합니다. 키는 주어진 사전에 존재해야 합니다. 그렇지 않으면 형식화는 KeyError로 실패합니다. |
③ | 같은 키를 두 번 지정할 수도 있습니다; 나타날 때마다 같은 값으로 교체됩니다. |
그래서 왜 사전-기반의 형식화를 사용해야 할까요? 자, 단순히 다음 줄에 문자열 형식화를 하기 위해 키와 값이 담긴 사전을 설정하는 것은 너무 과도한 것 같아 보입니다; 그러나 의미있는 키와 값이 담긴 사전을 미리 가지고 있으면 정말 아주 유용합니다. locals처럼 말입니다.
def handle_comment(self, text): self.pieces.append("<!--%(text)s-->" % locals()) ①
① | 사전-기반의 문자열 형식화가 가장 많이 사용되는 예는 내장 locals 함수를 사용하는 것입니다. 즉 지역 변수의 이름을 문자열 안에서 사용할 수 있다는 뜻이고 (이 경우 클래스 메쏘드에 인자로 건네진 text임) 이름붙은 변수마다 그의 값으로 교체된다는 뜻입니다. text가 'Begin page footer'이면 문자열 형식화 "<!--%(text)s-->" % locals()는 다음 '<!--Begin page footer-->' 문자열로 결정됩니다. |
def unknown_starttag(self, tag, attrs): strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) ① self.pieces.append("<%(tag)s%(strattrs)s>" % locals()) ②
① | 이 메쏘드가 호출되면 attrs는 키/값 터플이 담긴 리스트이며, 사전의 items와 똑 같습니다. 이는 다중-변수 할당을 사용하여 그를 순회할 수 있다는 뜻입니다. 이제 이 패턴은 익숙하리라 믿지만, 여기에는 더 많은 일이 진행중입니다. 그래서 분석해 보겠습니다:
|
② | 이제 사전-기반의 문자열 형식화를 사용하여, tag와 strattrs의 값을 문자열에 삽입합니다. 그래서 tag가 'a'라면 최종 결과는 '<a href="index.html" title="Go to home page">'가 되고, 그것이 바로 self.pieces에 추가됩니다. |
★ | |
사전-기반의 문자열 형식화를 locals와 함께 사용하면 복잡한 문자열 형식화 표현식을 편리하게 만들 수 있지만, 거기에는 대가가 따릅니다. locals를 호출하려면 약간 수행성능에 충격이 있습니다. 왜냐하면 locals는 지역 이름공간의 사본을 구축하기 때문입니다. |
comp.lang.python에 자주 올라오는 질문이 있습니다. “엄청나게 HTML 문서가 많은데 속성에 인용부호가 안 붙었어요. 적절하게 모두 인용부호를 붙이고 싶어요. 어떻게 하면 이렇게 할 수 있을까요?”[4] (이런 과제는 보통 다음과 같은 프로젝트 관리자가 봉착하는 문제입니다. 그는 HTML을-표준으로-하여 방대한 프로젝트를 묶고 모든 페이지가 HTML 평가기를 통과시켜야 합니다. 인용부호가 붙지 않은 속성 값은 보통 HTML 표준을 위반합니다.) 어떤 이유든, 인용부호가 붙지 않은 속성은 BaseHTMLProcessor에에 HTML을 먹이면 쉽게 고칠 수 있습니다.
(SGMLParser의 자손이기 때문에) BaseHTMLProcessor는 HTML을 소비합니다 그리고 동등한 HTML을 뱉아내지만, 그 HTML 출력이 입력과 동일한 것은 아닙니다. 태그와 속성은 대문자 또는 섞어서 시작되더라도 이름이 소문자로 바뀝니다. 그리고 속성 이름은 홑 따옴표 또는 따옴표가 전혀 없이 시작되더라도 겹 따옴표로 둘러싸입니다. 이 마지막 부작용을 이용할 수 있습니다.
>>> htmlSource = """ ① ... <html> ... <head> ... <title>Test page</title> ... </head> ... <body> ... <ul> ... <li><a href=index.html>Home</a></li> ... <li><a href=toc.html>Table of contents</a></li> ... <li><a href=history.html>Revision history</a></li> ... </body> ... </html> ... """ >>> from BaseHTMLProcessor import BaseHTMLProcessor >>> parser = BaseHTMLProcessor() >>> parser.feed(htmlSource) ② >>> print parser.output() ③ <html> <head> <title>Test page</title> </head> <body> <ul> <li><a href="index.html">Home</a></li> <li><a href="toc.html">Table of contents</a></li> <li><a href="history.html">Revision history</a></li> </body> </html>
① | <a> 태그에 있는 href 속성의 값이 적절하게 인용부호 처리가 되어 있지 않음을 주목하세요. (또 주목하세요. 문서화 문자열(doc string) 말고 다른 것에 삼중 인용부호(triple quotes)를 사용하고 있습니다. 놀랍게도 IDE에서 직접 사용하고 있습니다. 아주 유용합니다.) |
② | 해석기에 먹입니다. |
③ | BaseHTMLProcessor에 정의된 output 함수를 사용하여 출력을 한개의 문자열로 얻어서, 인용부호 처리된 속성 값을 완성합니다. 이것이 실망스러워 보인다면 실제로 여기에서 얼마나 많은 일이 일어나는지 생각해 보세요: SGMLParser는 전체 HTML 문서를 해석하여, 태그와 참조 데이터 등등으로 분할합니다; BaseHTMLProcessor는 그렇게 조각난 원소들을 사용하여 HTML 조각을 재구성합니다 (이를 보고 싶다면 여전히 parser.pieces에 저장되어 있습니다.); 마지막으로, parser.output을 호출했습니다. 이는 모든 HTML 조각들을 하나의 문자열로 결합합니다. |
Dialectizer는 BaseHTMLProcessor의 단순한 (하잘 것 없는) 자손입니다. 일련의 교체를 통하여 텍스트 블록을 실행하지만, <pre>...</pre> 블록 안에 있는 것이면 무엇이든 그대로 통과시킵니다.
<pre> 블록을 처리하기 위하여, 두 개의 메쏘드를 Dialectizer에 정의합니다: start_pre 메쏘드와 end_pre 메쏘드를 정의하겠습니다.
def start_pre(self, attrs): ① self.verbatim += 1 ② self.unknown_starttag("pre", attrs) ③ def end_pre(self): ④ self.unknown_endtag("pre") ⑤ self.verbatim -= 1 ⑥
① | start_pre는 SGMLParser가 <pre> 태그를 HTML 소스에서 발견할 때마다 호출됩니다. (잠시 후에, 이런 일이 정확하게 어떻게 일어나는지 보여 드리겠습니다.) 이 메쏘드는 매개변수 attrs를 하나 취합니다. 이 매개변수에는 (있다면) 태그의 속성이 담깁니다. unknown_starttag 메쏘드가 취하는 것과 마찬가지로 attrs는 키/값 터플로 구성된 리스트입니다. |
② | reset 메쏘드에서, <pre> 태그에 대한 계수기로 기여하는 데이터 속성을 초기화합니다. <pre> 태그를 만날 때마다, 카운터를 증가시킵니다; </pre> 태그를 만날 때마다, 카운터를 감소시킵니다. (그냥 플래그를 사용하여 1로 설정하고 0으로 재설정해도 되지만, 이렇게 하는 편이 더 쉬운 방법입니다. 이렇게 하면 내포된 <pre> 태그 같이 괴이한 (그러나 나타날 가능성이 있는) 사례도 처리할 수 있습니다.) 잠시 후에, 이 카운터가 어떻게 활용되는지 살펴보겠습니다. |
③ | 바로 그것입니다. 그것이 <pre>에 대하여 오직 유일하게 특별하게 처리해야 할 것입니다. 이제 속성이 담긴 리스트를 unknown_starttag에 건네서 기본 처리를 시킵니다. |
④ | end_pre는 SGMLParser가 </pre> 태그를 발견할 때마다 호출됩니다. 끝 태그에는 속성이 담길 수 없기 때문에, 이 메쏘드는 매개변수를 취하지 않습니다. |
⑤ | 먼저 다른 끝 태그와 마찬가지로 기본 처리를 하고 싶습니다. |
⑥ | 둘째, 카운터를 감소시켜 이 <pre> 블록이 닫혔다는 신호를 보냅니다. |
이 시점에서 좀 더 깊이 SGMLParser에 들어가 볼 가치가 있습니다. 반복적으로 강조했습니다 (지금쯤이면 여러분도 신념처럼 믿고 있을 것입니다). SGMLParser는 각 태그에 대하여 특정한 메소드를 찾아 호출한다고 말입니다. 예를 들면 방금 <pre> 태그와 </pre>를 처리하기 위한 start_pre 메쏘드와 end_pre 메쏘드의 정의를 보셨습니다. 그러나 어떻게 이런 일이 일어나는가? 자, 마법이 아닙니다. 단지 파이썬의 훌륭한 코딩일 뿐입니다.
def finish_starttag(self, tag, attrs): ① try: method = getattr(self, 'start_' + tag) ② except AttributeError: ③ try: method = getattr(self, 'do_' + tag) ④ except AttributeError: self.unknown_starttag(tag, attrs) ⑤ return -1 else: self.handle_starttag(tag, method, attrs) ⑥ return 0 else: self.stack.append(tag) self.handle_starttag(tag, method, attrs) return 1 ⑦ def handle_starttag(self, tag, method, attrs): method(attrs) ⑧
① | 이 시점에서, SGMLParser는 이미 시작 태그를 발견했고 속성 리스트를 해석했습니다. 남은 일은 오직 이 태그에 대하여 특정 처리자 메쏘드가 있는지 알아보는 것입니다. 즉 기본 메쏘드(unknown_starttag)에 의존해야 할지 말지를 알아봅니다. |
② | SGMLParser의 “마법”은 바로 오랜 친구 getattr에 있습니다. 앞서 깨닫지 못한 것이 있다면 getattr 메쏘드가 객체 자신뿐만 아니라 그 자손에 정의된 메쏘드를 찾는다는 것입니다. 여기에서 그 개체는 self, 즉 현재 실체입니다. 그래서 tag가 'pre'일 경우, 이렇게 getattr을 호출하면 현재 실체에서 start_pre 메쏘드를 찾습니다. 현재 실체는 Dialectizer 클래스의 실체입니다. |
③ | getattr은 자신이 찾고 있는 메쏘드가 객체에 (즉 그의 자손에도) 존재하지 않으면 AttributeError를 일으킵니다. 그러나 그래도 괜찮습니다. 왜냐하면 getattr 호출을 try...except 블록 안에 싸 넣었고 명시적으로 AttributeError를 잡았기 때문입니다. |
④ | start_xxx 메쏘드를 발견하지 못했으므로, 포기하기 전에 do_xxx 메쏘드도 찾아 봅니다. 이런 대안적인 이름짓기 체계는 일반적으로 <br> 같은 독립 태그에 사용됩니다. 이런 태그는 상응하는 끝 태그가 없습니다. 그러나 어떤 이름짓기 관례도 사용할 수 있습니다; 보시다시피, SGMLParser는 태그마다 두 관례를 모두 시도합니다. (그렇지만 같은 태그에 대하여 start_xxx 메쏘드와 do_xxx 메쏘드를 모두 정의하면 안됩니다; 그러면 오직 start_xxx 메쏘드만 호출됩니다.) |
⑤ | 또 AttributeError가 일어납니다. 이 에러는 getattr를 호출한 것이 do_xxx에 실패했다는 뜻입니다. 이 태그에 대하여 start_xxx 메쏘드도 do_xxx 메쏘드도 발견하지 못했으므로, 예외를 잡고 기본 메쏘드, 즉 unknown_starttag 메쏘드에 의지합니다. |
⑥ | 기억하십니까? try...except 블록은 else 절을 가질 수 있습니다. 이는 try...except 블록을 지나는 동안 아무 예외도 일어나지 않으면 호출됩니다. 논리적으로 말해, 이 태그에 대하여 do_xxx를 발견했다는 뜻입니다. 그래서 그 메쏘드를 호출하고 있습니다. |
⑦ | 그런데, 이렇게 다양하게 값이 반환되더라도 걱정하지 마세요; 이론적으로 무언가 뜻이 있을 수 있지만, 실제로 사용되지는 않습니다. self.stack.append(tag)도 역시 신경쓰지 마세요; SGMLParser는 시작 태그가 적절하게 끝 태그와 짝을 이루는지 내부적으로 추적 유지합니다. 그러나 그것은 이 정보와도 전혀 관계가 없습니다. 이론적으로 이 모듈을 사용하여 태그가 제대로 균형이 맞는지 평가할수 있지만, 그럴 가치가 없을 것입니다. 그리고 그것은 이 장의 범위를 넘어섭니다. 정작 신경써야 할 일은 따로 있습니다. |
⑧ | start_xxx 메쏘드와 do_xxx 메쏘드는 직접적으로 호출되지 않습니다; 태그와 메쏘드 그리고 속성이 이 함수, 즉 handle_starttag에 건네집니다. 그래서 자손들이 그를 오버라이드하고 모든 시작 태그가 분배되는 방법을 변화시킵니다. 그 정도 수준의 제어는 필요가 없습니다. 그래서 그냥 이 메쏘드가 자신의 일을 하도록 내버려 둡니다. 이 메소드는 속성 리스트를 가지고 메쏘드를 호출합니다 (start_xxx 또는 do_xxx). 기억하십니까? getattr이 돌려주는 method는 함수입니다. 그리고 함수는 객체입니다. (귀가 따갑도록 들으셨겠지만, 더 써 먹을 때가 없어지면 바로 언급을 멈추겠습니다.) 여기에서, 함수 객체가 이 분배 메소드에 인자로 건네집니다. 그리고 이 메쏘드는 그 함수를 받아 호출합니다. 이 시점에서 함수가 무엇인지, 이름이 무엇인지 또는 어디에 정의되어 있는지 알 필요가 없습니다; 함수에 관하여 유일하게 알 필요가 있는 것은 attrs이라는 인자 하나로 호출된다는 것입니다. |
이제 다시 교재 프로그램으로 돌아가 보겠습니다: Dialectizer로 말입니다. 떠나 올 때 <pre> 태그와 </pre> 태그에 대하여 특정 처리자 메쏘드를 정의하던 중이었습니다. 이제 할 일이 한 가지만 남아 있습니다. 미리-정의된 교체로 텍스트 블록을 처리하는 것이 바로 그것입니다. 그를 위하여, handle_data 메쏘드를 오버라이드할 필요가 있습니다.
def handle_data(self, text): ① self.pieces.append(self.verbatim and text or self.process(text)) ②
① | handle_data는 오직 하나의 인자로 호출됩니다. 즉, 처리할 텍스트가 그것입니다. |
② | 조상인 BaseHTMLProcessor에서, handle_data 메쏘드는 그냥 텍스트를 출력 버퍼 self.pieces에 추가했습니다. 여기에서는 로직이 약간 더 복잡할 뿐입니다. <pre>...</pre> 블록 사이에 있다면 self.verbatim는 0보다 큰 숫자가 될 것이고, 텍스트를 출력 버퍼에 그대로 넣고 싶습니다. 그렇지 않으면 따로 교체를 처리할 메쏘드를 호출해서, 그 결과를 출력 버퍼에 넣습니다. 파이썬에서, 이런 일은 한 줄, and-or 트릭을 사용하면 됩니다. |
Dialectizer를 거의 완전하게 이해하게 되었습니다. 유일하게 남은 일은 텍스트 교체 그 자체의 성질입니다. 펄(Perl)을 조금이라도 아신다면 복잡한 텍스트 교체가 필요할 때, 유일한 해결책은 정규 표현식이라는 것을 아실 겁니다. dialect.py의 후반부에 있는 클래스에 일련의 정규 표현식이 정의되어 있습니다. 이 정규식은 HTML 태그 사이의 텍스트를 처리합니다. 그러나 정규 표현식만 따로 다룬 장이 있었습니다. 다시 정규 표현식을 살펴보고 싶지는 않을 겁니다. 그렇지요? 한 장이면 충분히 배웠을 것이라고 생각합니다.
지금까지 배운 것들을 활용할 시간입니다. 주의를 기울여 주시기를 바랍니다.
def translate(url, dialectName="chef"): ① import urllib ② sock = urllib.urlopen(url) ③ htmlSource = sock.read() sock.close()
① | translate 함수는 선택적 인자로 dialectName이 있는데, 이는 사용할 방언을 지정하는 문자열입니다. 잠시 후에 어떻게 이것이 사용되는지 보여드립니다. |
② | 자, 잠시 기다리세요. 이 함수에는 import 서술문이 있습니다! 파이썬에서는 완벽하게 합법적입니다. 프로그램의 상단에서 import 서술문을 보아 왔는데, 이는 반입된 모듈이 프로그램의 어디에서나 사용이 가능하다는 뜻입니다. 그러나 함수 안에서도 모듈을 반입할 수 있는데, 이는 반입된 모듈이 그 함수 안에서만 사용 가능하다는 뜻입니다. 함수 안에서만 모듈을 사용할 수 있다면 이는 코드를 더욱 모듈화하는 손쉬운 방법입니다. (주말에 노고를 들여 800-줄이 넘는 코드를 작성했고 열댓 개의 모듈로 나누기로 결정한 상황에 마주해 보면 이에 고마움을 느낄 것입니다.) |
③ | 이제 주어진 URL의 소스를 얻었습니다. |
parserName = "%sDialectizer" % dialectName.capitalize() ① parserClass = globals()[parserName] ② parser = parserClass() ③
① | capitalize는 아직 보지 못한 문자열 메쏘드입니다; 그냥 문자열에서 첫 글자를 대문자로 바꾸며, 나머지 모든 것은 소문자로 바꿉니다. 문자열 형식화와 조합하여, 방언의 이름을 취했고 그에 상응하는 Dialectizer 클래스의 이름으로 변환합니다. dialectName은 'chef'라는 문자열이고, parserName는 'ChefDialectizer'이라는 문자열이 됩니다. |
② | 문자열로 클래스의 이름이 있고 (parserName), 사전으로 전역 이름공간이 있습니다 (globals()). 두개를 조합하면 문자열 이름이 가리키는 클래스의 참조점을 얻을 수 있습니다. (기억하십니까? 클래스는 객체이며, 다른 객체들처럼 변수에 할당될 수 있습니다.) parserName이 'ChefDialectizer'이라는 문자열이라면 parserClass는 ChefDialectizer라는 클래스가 됩니다. |
③ | 마지막으로, 클래스 객체가 있고 (parserClass), 그 클래스의 실체가 필요합니다. 자, 이미 그렇게 하는 법을 압니다: 함수처럼 그 클래스를 호출하기만 하면 됩니다. 클래스는 지역 변수에 저장될 수 있으므로 절대로 아무 차이가 없습니다; 그냥 함수처럼 지역 변수를 호출하면 클래스의 실체가 튀어 나옵니다. parserClass가 ChefDialectizer이라는 클래스라면 parser는 ChefDialectizer이라는 클래스의 실체가 됩니다. |
왜 고민하십니까? 어쨌거나, 겨우 세개의 Dialectizer 클래스가 있을 뿐입니다; 그냥 case 서술문을 사용하면 안 될까요? (음, 파이썬에는 case 서술문이 없습니다. 그러나 그냥 일련의 if 서술문을 사용하면 안될까요?) 한가지 이유는: 확장성 때문입니다. translate 함수는 얼마나 많은 Dialectizer 클래스가 정의되어 있는지 전혀 모릅니다. 내일 새로 FooDialectizer를 정의한다고 생각해 보세요; translate는 dialectName으로 'foo'를 건네겠지요.
더 좋은 예를 들어 보면 FooDialectizer를 따로 모듈에 두고, from module import로 반입한다고 생각해 보세요. 이미 본 바에 의하면 globals()에 포함됩니다. 그래서 translate는 아무 수정이 없이 그대로 작동할 것입니다. 비록 FooDialectizer가 따로 파일에 있다고 할지라도 말입니다.
이제 방언의 이름이 프로그램 밖 어딘가로부터 온다고 생각해 보세요. 예를 들어, 데이터베이스나 사용자가-폼에-입력한 값으로부터 온다고 말입니다. 얼마든지 서버-쪽 파이썬 스크립팅 골격구조를 사용하여 웹 페이지를 동적으로 만들어 낼 수 있습니다; 이 함수는 웹 페이지 요청의 질의 문자열에서 URL과 방언 이름을 (둘다 문자열로) 받아서, “번역된” 웹 페이지를 출력할 수 있습니다.
마지막으로, 끼워-넣기(plug-in) 골격구조를 갖춘 Dialectizer 작업틀을 상상해 보세요. 오직 translate 함수만 dialect.py에 남겨두고 Dialectizer 클래스마다 따로따로 파일에 둘 수 있습니다. 이름짓기 체계가 일관성이 있다고 간주하면 translate 함수는 방언 이름만 주어져도 적절한 클래스를 적절한 파일에서 동적으로 반입할 수 있습니다. (아직 동적으로 반입하는 것은 보지 못했지만, 나중에 다루어 보겠습니다.) 새로 방언을 추가하려면 그냥 (FooDialectizer 클래스가 담긴 foodialect.py같이) 적절하게-이름 붙인 파일을 플러그-인 디렉토리에 두면 됩니다. 'foo'라는 방언 이름으로 translate 함수를 호출하면 foodialect.py라는 모듈을 찾아 FooDialectizer 클래스를 반입하고, 그리고 갈 길을 가는 거지요.
parser.feed(htmlSource) ① parser.close() ② return parser.output() ③
① | 이모저모 살펴본 바에 의하면 이는 아주 따분해 보이지만, feed 함수가 바로 전체 변환을 담당합니다. 전체 HTML 소스를 하나의 문자열로 확보하였으므로, feed를 한 번만 호출하면 됩니다. 그렇지만, 얼마든지 feed를 호출할 수 있으며, 해석기는 여전히 해석을 수행합니다. 그래서 메모리가 모자랄까 걱정이 되면 (즉 아주 방대한 HTML 페이지를 다루게 될 것이라는 사실을 알고 있으므로), 회돌이 안에서 이렇게 설정하면 됩니다. 필요한 만큼만 HTML 바이트를 읽어서 해석기에 먹입니다. 그 결과는 같습니다. |
② | feed 함수는 내부 버퍼를 유지하고 있으므로, 일이 끝나면 언제나 해석기의 close 메쏘드를 호출해야 합니다. (원하는대로 한 번에 모두 다 먹였다고 할지라도 말입니다). 그렇지 않으면 출력에서 맨 뒤의 몇 바이트가 없는 것을 보게 될 수도 있습니다. |
③ | 기억하세요. output은 BaseHTMLProcessor에 정의한 함수입니다. 이 함수는 버퍼에 있는 모든 출력 조각들을 모아서 한-개짜리 문자열로 돌려줍니다. |
이와 같이 웹 페이지 하나를 “번역했습니다”. 주어진 URL과 사투리 이름으로 말입니다.
파이썬이 제공하는 강력한 도구인 sgmllib.py를 사용하면 HTML의 구조를 객체 모델로 변환함으로써 HTML을 조작할 수 있습니다. 이 도구를 여러모로 사용할 수 있습니다.
다음 예제들과 더불어, 다음과 같은 것들을 편안하게 할 수 있어야 합니다:
[1] SGMLParser와 같은 해석기에 대한 기술적 용어는 소비자입니다: 소비자는 HTML을 먹어서 조각조각 분해합니다. 짐작하시듯이 감(feed)이라는 이름은 “소비자”라는 모티프에 온전히 맞추기 위해 선택되었습니다. 개인적으로 그 때문에 나는 동물원의 전시회가 생각납니다. 캄캄한 철장만 있을 뿐 나무도 없고 풀도 없으며 아무런 생명도 느껴지지 않지만, 몸을 곧추 세우고 주의 깊게 살펴보면 반짝이는 두 개의 눈이 저 뒤 멀리 왼쪽 모퉁이에서 노려보고 있다고 느낍니다. 그러나 그것은 그냥 마음이 홀리는 것일 뿐이라고 마음을 다잡습니다. 울타리에 걸린 평범한 작은 경고판에 “해석기에 먹이를 주지 마시오”라고 씌여 있는 것을 보고서야 진실은 그냥 빈 철장일 뿐이라는 것을 알 수 있을 뿐입니다. 그러나 그가 바로 나일 수 있습니다. 어쨌든, 흥미로운 심상입니다.
[2] 파이썬이 문자열보다 리스트를 더 잘 다루는 이유는 리스트는 수정가능하지만 문자열은 수정이 불가능하기 때문입니다. 이는 리스트에 추가하면 그냥 원소가 추가되고 인덱스만 갱신된다는 뜻입니다. 문자열은 생성된 후 수정이 불가능하므로, s = s + newpiece과 같은 코드는 원래 문자열과 새 문자열을 결합하여 완전히 새로운 문자열을 생성합니다. 다음 원래 문자열을 버립니다. 이 때문에 메모리 관리가 너무 비싸게 먹히며 문자열이 길어질 수록 관련 노력이 증가합니다. 그래서 회돌이 안에서 s = s + newpiece를 시행하면 치명적입니다. 기술적 용어로, 리스트에 n개의 원소를 추가하면 O(n) 시간이 걸리는 반면에, 문자열에 n개의 원소를 추가하면 O(n2) 시간이 걸립니다.
[3] 나는 밖에 잘 나가지 않는다.
[4] 좋습니다. 그렇게 흔한 질문은 아닙니다. 이런 질문 보다는 못합니다. “파이썬 코드를 작성하려면 어떤 편집기를 사용해야 하는가?” (답: Emacs) 또는 “파이썬은 펄(Perl)보다 좋은가 나쁜가?” (답: “펄(Perl)이 파이썬 보다 나쁘다. 사람들이 그러기를 바라기 때문이다.” - 래리 월(Larry Wall), 10/14/1998). 그러나 HTML 처리와 관한 질문이 이런 저런 형태로 한 달에 한 번 꼴로 올라오며, 그런 질문 중에서 이 질문이 자주 올라옵니다.
☜ 제 07 장 정규표현식 | """ Dive Into Python """ 다이빙 파이썬 |
제 09 장 XML 처리 ☞ |