제 3 장 객체지향 작업틀 목 차 제 5 장 유닛 테스트 >>

제 4 장. HTML 처리하기

4.1. 다이빙해 들어가기

가끔 comp.lang.python에서 다음과 같은 질문들을 본다 “어떻게 HTML문서에서 모든 [headers|images|links]를 나열할 수 있는가?” “어떻게 태그들을 그대로 두고서 HTML 문서의 텍스트를 [parse|translate|munge] 할 수 있는가?” “어떻게 HTML 태그 속성들을 단 번에 [add|remove|quote] 할 수 있는가?” 이 장은 이러한 모든 질문들에 답해 줄것이다.

여기에 완전한 작동하는 프로그램이 두 부분으로 나뉘어 있다. 첫 번째 부분은 BaseHTMLProcessor.py인데, 이는 태그와 텍스트 블록을 훓어 봄으로써 HTML 파일을 처리하도록 도와 주는 범용적인 도구이다. 두 번째 부분은 dialect.py인데, 이는 태그는 그대로 두고서, HTML 문서의 텍스트를 번역하기 위하여 BaseHTMLProcessor.py를 사용하는 방법을 보여주는 예제이다. 문서화 문자열(doc string)과 주석을 읽고서 무엇이 진행되고 있는지 대충 살펴 보라. 대부분은 신비스런 마술과 같이 보일 것이다. 왜냐하면 이러한 클래스 모두가 도대체 어떻게 호출됐는지 알 수 없기 때문이다. 걱정하지 마라. 모든 신비는 때가 되면 드러나게 될 것이다.

Example 4.1. BaseHTMLProcessor.py

아직 그렇게 하지 못했다면, 이 예제와 더불어 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).

from sgmllib import SGMLParser

class BaseHTMLProcessor(SGMLParser):
    def reset(self):
        # extend (called by SGMLParser.__init__)
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs):
        # called for each start tag
        # attrs is a list of (attr, value) tuples
        # e.g. for <pre class="screen">, tag="pre", attrs=[("class", "screen")]
        # Ideally we would like to reconstruct original tag and attributes, but
        # we may end up quoting attribute values that weren't quoted in the source
        # document, or we may change the type of quotes around the attribute value
        # (single to double quotes).
        # Note that improperly embedded non-HTML code (like client-side Javascript)
        # may be parsed incorrectly by the ancestor, causing runtime script errors.
        # All non-HTML code must be enclosed in HTML comment tags (<!-- code -->)
        # to ensure that it will pass through this parser unaltered (in 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):
        # called for each end tag, e.g. for </pre>, tag will be "pre"
        # Reconstruct the original end tag.
        self.pieces.append("</%(tag)s>" % locals())

    def handle_charref(self, ref):
        # called for each character reference, e.g. for "&#160;", ref will be "160"
        # Reconstruct the original character reference.
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):
        # called for each entity reference, e.g. for "&copy;", ref will be "copy"
        # Reconstruct the original entity reference.
        self.pieces.append("&%(ref)s;" % locals())

    def handle_data(self, text):
        # called for each block of plain text, i.e. outside of any tag and
        # not containing any character or entity references
        # Store the original text verbatim.
        self.pieces.append(text)

    def handle_comment(self, text):
        # called for each HTML comment, e.g. <!-- insert Javascript code here -->
        # Reconstruct the original comment.
        # It is especially important that the source document enclose client-side
        # code (like Javascript) within comments so it can pass through this
        # processor undisturbed; see comments in unknown_starttag for details.
        self.pieces.append("<!--%(text)s-->" % locals())

    def handle_pi(self, text):
        # called for each processing instruction, e.g. <?instruction>
        # Reconstruct original processing instruction.
        self.pieces.append("<?%(text)s>" % locals())

    def handle_decl(self, text):
        # called for the DOCTYPE, if present, e.g.
        # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        #     "http://www.w3.org/TR/html4/loose.dtd">
        # Reconstruct original DOCTYPE
        self.pieces.append("<!%(text)s>" % locals())

    def output(self):
        """Return processed HTML as a single string"""
        return "".join(self.pieces)

Example 4.2. dialect.py

import re
from BaseHTMLProcessor import BaseHTMLProcessor

class Dialectizer(BaseHTMLProcessor):
    subs = ()

    def reset(self):
        # extend (called from __init__ in ancestor)
        # Reset all data attributes
        self.verbatim = 0
        BaseHTMLProcessor.reset(self)

    def start_pre(self, attrs):
        # called for every <pre> tag in HTML source
        # Increment verbatim mode count, then handle tag like normal
        self.verbatim += 1
        self.unknown_starttag("pre", attrs)

    def end_pre(self):
        # called for every </pre> tag in HTML source
        # Decrement verbatim mode count
        self.unknown_endtag("pre")
        self.verbatim -= 1

    def handle_data(self, text):
        # override
        # called for every block of text in HTML source
        # If in verbatim mode, save text unaltered;
        # otherwise process the text with a series of substitutions
        self.pieces.append(self.verbatim and text or self.process(text))

    def process(self, text):
        # called from handle_data
        # Process text block by performing series of regular expression
        # substitutions (actual substitions are defined in descendant)
        for fromPattern, toPattern in self.subs:
            text = re.sub(fromPattern, toPattern, text)
        return text

class ChefDialectizer(Dialectizer):
    """convert HTML to Swedish Chef-speak

    based on the classic 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):
    """convert HTML to Elmer Fudd-speak"""
    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):
    """convert HTML to mock Middle English"""
    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, dialect="chef"):
    """fetch URL and translate using dialect

    dialect in ("chef", "fudd", "olde")"""
    import urllib
    sock = urllib.urlopen(url)
    htmlSource = sock.read()
    sock.close()
    parserName = "%sDialectizer" % dialect.capitalize()
    parserClass = globals()[parserName]
    parser = parserClass()
    parser.feed(htmlSource)
    parser.close()
    return parser.output()

def test(url):
    """test all dialects against 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")

Example 4.3. Output of dialect.py

이 스크립트를 실행하면 Lists 101을 (The Muppets(손인형극)에서 나오는) Swedish Chef의-말투로, (벅스 버니 만화영화에 나오는) Elmer Fudd-말투로 , 그리고 (초서의 캔터베리 이야기에 대충 기초한) 중세 영어풍으로 번역할 것이다. 출력 페이지의 HTML 소스를 살펴보면, 모든 HTML 태그와 속성들은 손대지 않았으나, 태그들 사이의 텍스트는 그 흉내 언어로 “번역된” 것을 볼수 있을 것이다. 더 가까이 살펴보면, 사실 타이틀과 문단만이 번역된 것을 볼 것이다; 코드 목록과 화면에 나오는 예제들은 손대지 않은 채 그대로 있다.

4.2. sgmllib.py 소개

HTML 처리는 세 단계로 나뉜다: HTML을 그 구성 조각으로 나누기, 그 조각을 조작하기, 그리고 그 조각을 다시 HTML 로 재구성하기. 첫 단계는 표준 파이썬 라이브러리에 있는 sgmllib.py에 의해서 수행된다.

sgmllib.py에는 중요한 클래스가 하나 포함되어 있다: SGMLParser가 그것으로서, 이 문맥에서 해석기(parser)는 구조화되어 있지 않은 복잡한 개체를 더 단순한 구조화된 개체들로 조각내는 단순한 코드이다. 여러 종류의 해석기들이 있다; 파이썬의 표준 라이브러리에는 명령어 줄 선택사항, .INI 파일, 전자 우편함, robots.txt 파일, XML 등등을 해석하기 위한 모듈들이 있다.

어떤 해석기들은 상태를 유지한다. 즉, 데이타를 해석하고 그것을 내부에 구조적 형식으로 저장한 다음, 나중에 누군가 그 구조화된 데이타를 사용하기를 기다린다. SGMLParser는 자신이 해석한 데이타를 저장하지 않는다; 대신에, 어떤 데이타를 유용한 조각으로 분해하는 데 성공하자 마자, 무엇이 발견 되었는가에 따라서 자신에게 있는 메쏘드를 호출한다. 이 해석기를 사용하려면 SGMLParser 클래스를 하부클래스화하고 이러한 메쏘드를 덮어쓰면 된다.

SGMLParser는 HTML을 8 가지의 데이타로 해석하고, 그 데이터 각각에 대하여 따로따로 메쏘드를 호출한다:

Start tag
<html>, <head>, <body>, <pre>와 같이 블록을 시작하는 HTML 태그, 또는 <br>이나 <img>과 같이 독립적 태그. 시작 태그 tagname을 발견하면, SGMLParserstart_tagname 또는 do_tagname라고 불리우는 메쏘드를 찾는다. 예를 들어, <pre> 태그를 발견하면 start_pre 또는 do_pre 메쏘드를 찾을 것이다. 발견되면, SGMLParser는 이 메쏘드를 그 태그의 속성들을 담은 리스트로 호출할 것이다; 그렇지 않으면, 그 태그 이름과 속성들의 리스트로 unknown_starttag를 호출한다.
End tag
</html>, </head>, </body>, 또는 </pre>와 같이 블록을 끝내는 HTML 태그. 끝 태그를 발견하면, SGMLParserend_tagname이라고 부르는 메쏘드를 호출할 것이다. 발견되면, SGMLParser는 이 메쏘드를 호출한다. 그렇지 않으면 unknown_endtag를 그 태그 이름으로 호출한다.
Character reference
&#160;과 같은 16진 혹은 10진 동등값에 의하여 참조되는 피신 문자열. 발견되면, SGMLParserhandle_charref를 10진 혹은 16진 동등값을 가진 그 텍스트를 가지고 호출한다.
Entity reference
&copy;와 같은 HTML 개체. 발견되면, SGMLParserhandle_entityref를 그 HTML의 이름을 가지고 호출한다
Comment
HTML 주석, <!-- ... -->로 싸여 있다. 발견되면, SGMLParserhandle_comment를 그 주석의 몸체를 가지고 호출한다.
Processing instruction
HTML 처리 명령, <? ... >에 싸여 있다. 발견되면, SGMLParserhandle_pi를 그 처리 명령의 몸체를 가지고 호출한다.
Declaration
DOCTYPE과 같은 HTML 선언. <! ... >로 싸여 있다. 발견되면, SGMLParserhandle_decl를 그 선언의 몸체를 가지고 호출한다.
Text data
텍스트 블록. 이상 7가지의 범주에 적합하지 않는 모든 것. 발견되면, SGMLParser는 그 텍스트를 가지고 handle_data를 호출한다.
Important
파이썬 2.0에는 버그가 있어서 SGMLParser는 선언을 전혀 인식하지 못한다 (handle_decl이 절대로 호출되지 않을 것이다). 그것은 DOCTYPE이 조용히 무시될 것이라는 것을 뜻한다. 파이썬 2.1에서는 수정되었다.

sgmllib.py에는 이것을 예시해주는 테스트 모둠이 따라온다. sgmllib.py을 실행할 수 있는데, 명령어 줄에 HTML 파일의 이름을 넘겨 주면, 해석을 하면서 태그 그리고 기타 요소들을 출력할 것이다. SGMLParser 클래스를 하부클래스화하고 그리고 unknown_starttag, unknown_endtag, handle_data 그리고 그들의 인자를 단순히 출력하는 다른 메쏘드들을 정의하면 수행된다.

Tip
윈도우즈의 파이썬 IDE라면 “Run script” 대화상자에서 명령어 줄 인자들을 지정할 수 있다.

Example 4.4. Sample test of sgmllib.py

다음은 이 책의 HTML 버전 toc.html에 있는 목록에서 얻어온 작은 조각이다.

<h1>
  <a name='c40a'></a>
  Dive Into Python
</h1>
<p class='pubdate'>
  28 Feb 2001
</p>
<p class='copyright'>
  Copyright copy 2000, 2001 by
  <a href='mailto:f8dy@diveintopython.org' title='send e-mail to the author'>
    Mark Pilgrim
  </a>
</p>
<p>
  <a name='c40ab2b4'></a>
  <b></b>
</p>
<p>
  This book lives at
  <a href='http://diveintopython.org/'>
    http://diveintopython.org/
  </a>
  .
  If you're reading it somewhere else, you may not have the latest version.
</p>

이것을 sgmllib.py의 테스트 모둠을 통하여 실행하면 다음 결과가 산출된다:

start tag: <h1>
start tag: <a name="c40a" >
end tag: </a>
data: 'Dive Into Python'
end tag: </h1>
start tag: <p class="pubdate" >
data: '28 Feb 2001'
end tag: </p>
start tag: <p class="copyright" >
data: 'Copyright '
*** unknown entity ref: &copy;
data: ' 2000, 2001 by '
start tag: <a href="mailto:f8dy@diveintopython.org" title="send e-mail to the author" >
data: 'Mark Pilgrim'
end tag: </a>
end tag: </p>
start tag: <p>
start tag: <a name="c40ab2b4" >
end tag: </a>
start tag: <b>
end tag: </b>
end tag: </p>
start tag: <p>
data: 'This book lives at '
start tag: <a href="http://diveintopython.org/" >
data: 'http://diveintopython.org/'
end tag: </a>
data: ".\012If you're reading it somewhere else, you may not have the lates"
data: 't version.\012'
end tag: </p>

다음은 이장의 나머지에서 다룰 지도이다:

4.3. HTML 문서에서 데이타를 추출하기

HTML 문서로부터 데이타를 추출하려면 SGMLParser 클래스를 하부클래스화하고 얻고자 하는 개체 또는 각 태그를 위한 메쏘드를 정의하라.

HTML 문서로부터 데이타를 추출하기 위한 첫 단계는 약간의 HTML 파일을 얻는 것이다. 하드 디스크에 약간의 HTML이 있다면, 파일 함수들을 사용하여 읽을 수 있지만, 진짜 재미는 생생하게 살아 있는 웹 페이지로부터 HTML을 얻을 때 시작된다.

Example 4.5. Introducing urllib

>>> import urllib                                       1
>>> sock = urllib.urlopen("http://diveintopython.org/") 2
>>> htmlSource = sock.read()                            3
>>> sock.close()                                        4
>>> print htmlSource                                    5
<!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:f8dy@diveintopython.org'>
<meta name='keywords' content='Python, Dive Into Python, tutorial, object-oriented, programming, documentation, book, free'>
<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&nbsp;for&nbsp;experienced&nbsp;programmers</td></tr>

[...snip...]
1 urllib 모듈은 표준 파이썬 라이브러리에 포함되어 있다. 이 모듈은 인터넷-기반 URL로부터 (주로 웹페이지로부터) 데이타를 실제로 검색하고 정보를 획득하는 함수들을 가진다.
2 urllib를 가장 손쉽게 사용하는 방법은 urlopen 함수를 사용하여 웹페이지의 전체 텍스트를 검색하는 것이다. URL을 여는 것은 파일을 여는 것과 비슷하다. urlopen의 반환값은 파일-비슷한 객체로서, 파일 객체와 똑 같은 메쏘드를 가진다.
3 urlopen이 돌려준 파일-비슷한 객체로 할 수 있는 가장 간단한 일은 읽기(read)이다. 이 객체는 웹 페이지 전체 HTML을 한개의 문자열로 읽어 들인다. 그 객체는 또한 readlines을 지원하는데, 이 메쏘드는 텍스트를 한줄한줄 리스트로 읽어 들인다.
4 이 객체로 일을 마치면, 정상적인 파일 객체와 똑 같이 확실하게 닫아라.
5 이제 http://diveintopython.org/ 홈페이지의 완전한 HTML을 하나의 문자열로 가지고 있으며, 해석할 준비가 되었다.

Example 4.6. Introducing urllister.py

아직 그렇게 하지 못하였다면, 이 예제와 함께 이 책에서 사용되는 다른 예제들을 내려 받을 수 있다(Windows, UNIX, Mac OS).

from sgmllib import SGMLParser

class URLLister(SGMLParser):
    def reset(self):                              1
        SGMLParser.reset(self)
        self.urls = []

    def start_a(self, attrs):                     2
        href = [v for k, v in attrs if k=='href'] 3 4
        if href:
            self.urls.extend(href)
1 resetSGMLParser__init__ 메쏘드에 의해서 호출된다. 해석기의 실체가 생성되었다면 수동으로 호출될 수도 있다. 그래서 어떤 초기화가 필요하다면, __init__에서가 아니라, reset에서 초기화 하라. 그렇게 하면 누군가 해석기 실체를 재-사용할 때 적절하게 재-초기화가 될 것이다.
2 start_a<a> 태그를 발견할 때마다 SGMLParser에 의해서 호출된다. 그 태그는 href 속성을 포함할 수도 있고, 그리고/혹은 name 또는 title과 같은 다른 속성들을 가질 수도 있다. attrs 매개변수는 [(attribute, value), (attribute, value), ...]로 구성된 터플의 리스트이다. 또는 그냥 <a>일 수도 있다. (필요없을 지라도) 유효한 HTML 태그로서, 이런 경우 attrs은 빈 리스트가 될 것이다.
3 <a> 태그가 href 속성을 가지고 있는지를 간단한 여러-변수 리스트 통합을 가지고 알아낼 수 있다.
4 k=='href'과 같은 문자열 비교는 항상 대소문자에 민감하다. 그러나 이 경우에는 안전한데, 왜냐하면 SGMLParserattrs를 구축하는 동안에 속성 이름들을 소문자로 변환하기 때문이다.

Example 4.7. Using urllister.py

>>> import urllib, urllister
>>> usock = urllib.urlopen("http://diveintopython.org/")
>>> parser = urllister.URLLister()
>>> parser.feed(usock.read())         1
>>> usock.close()                     2
>>> parser.close()                    3
>>> for url in parser.urls: print url 4
toc.html
#download
toc.html
history.html
download/dip_pdf.zip
download/dip_pdf.tgz
download/dip_pdf.hqx
download/diveintopython.pdf
download/diveintopython.zip
download/diveintopython.tgz
download/diveintopython.hqx

[...snip...]
1 SGMLParser에 정의된, 먹이기(feed) 메쏘드를 호출하여, HTML을 해석기 안에 넣어라.[7] 그것은 usock.read()이 반환한 문자열을 취한다.
2 파일과 마찬가지로 처리가 끝나고 나면 최대한 빨리 사용한 URL 객체를 닫아야(close) 한다.
3 해석기 객체도 역시 닫아야(close) 한다. 그러나 다른 이유가 있다. feed 메쏘드는 먹인 HTML을 모두 처리한다고 보장할 수 없기 때문이다; 이 메쏘드는 HTML을 버퍼에 저장하고, 더 오기를 기다린다. 더 이상 없으면 close를 호출하여 버퍼를 강제 저장하여 모든 것을 완전히 해석되도록 만든다.
4 해석기가 닫혀지면(close), 해석은 끝나고 parser.urls가 HTML 문서에 있는 링크된 모든 URL의 리스트를 가진다.

4.4. BaseHTMLProcessor.py를 소개하기

SGMLParser는 그 자체로는 아무것도 산출하지 않는다. 그저 해석하고 해석하고, 또 해석하면서 자신이 발견하는 각각의 흥미로운 것들에 적합한 메쏘드를 호출할 뿐이며, 그 자체로는 아무 일도 하지 않는다. SGMLParser는 HTML 소비자(consumer)이다: HTML을 취해 그것을 더 작은 구조화된 조각들로 분해한다. 이전 섹션에서 보았듯이, SGMLParser를 하부클래스화하면 특정한 태그들을 잡아내는 클래스를 정의할 수 있고 웹페이지에서 모든 링크들을 담은 리스트처럼, 유용한 것들을 생산해내는 클래스를 정의할 수 있다. 이제 한 걸음 더 진전시켜서 SGMLParser 해석기가 발생시키는 모든것을 나포하고 완전한 HTML문서를 재구성하는 클래스를 정의해 보자. 기술적 용어로 이런 클래스는 HTML 생산자(producer)가 될 것이다.

BaseHTMLProcessorSGMLParser를 하부클래스화한다 그리고 8개의 모든 핵심적인 처리 메쏘드를 제공한다: unknown_starttag, unknown_endtag, handle_charref, handle_entityref, handle_comment, handle_pi, handle_decl, and handle_data.

Example 4.8. Introducing BaseHTMLProcessor

class BaseHTMLProcessor(SGMLParser):
    def reset(self):                        1
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs): 2
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):          3
        self.pieces.append("</%(tag)s>" % locals())

    def handle_charref(self, ref):          4
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):        5
        self.pieces.append("&%(ref)s;" % locals())

    def handle_data(self, text):            6
        self.pieces.append(text)

    def handle_comment(self, text):         7
        self.pieces.append("<!--%(text)s-->" % locals())

    def handle_pi(self, text):              8
        self.pieces.append("<?%(text)s>" % locals())

    def handle_decl(self, text):
        self.pieces.append("<!%(text)s>" % locals())
1 resetSGMLParser.__init__에 의하여 호출되면, self.pieces그 조상 메쏘드를 호출하기전에 빈 리스트로 초기화한다. self.pieces데이타 속성으로서 우리가 구성중인 HTML 문서의 조각들을 보유하게 될 것이다. 각 처리 메쏘드는 SGMLParser가 해석한 HTML을 재구성할 것이다. 그리고 각 메쏘드는 그 문자열을 self.pieces에 추가할 것이다. self.pieces는 리스트라는 것을 주목하라. 어쩌면 유혹에 빠져서 그것을 문자열로 정의하고 그냥 계속해서 그것에 각 조각들을 추가할 지도 모르겠다. 그것도 작동할 것이다. 그러나 파이썬에서는 리스트로 다루는게 훨씬 더 효율적이다.[8]
2 BaseHTMLProcessor는 (URLLister에 있는 start_a 메쏘드와 같이) 특정한 태그들을 위한 어떠한 메쏘드로 정의하지 않으므로, SGMLParser는 모든 시작 태그(start tag)에 대하여 unknown_starttag를 호출할 것이다. 이 메쏘드는 그 태그 (tag)와 이름/값 쌍의 속성을 가진 리스트 (attrs)를 취하여 원래의 HTML을 재구성하며, 그리고 그것을 self.pieces에 추가한다. 여기에서 문자열 형식화가 약간 이상하다; 다음 섹션에서 이 문제를 해결해 보겠다.
3 종료 태그(end tags)를 재구성하는 것은 더욱 쉽다; 그냥 태그 이름을 취해서 '</...>' 괄호에 싸 넣기만 하면 된다.
4 SGMLParser가 문자 참조를 발견하면, 그 참조문자를 가지고 handle_charref를 호출한다. HTML 문서에 참조 &#160;를 포함하고 있다면, ref160이 될 것이다. 원래의 완전한 문자 참조를 재구성하는 것은 단지 ref&#...;문자로 싸 넣는다는 것을 뜻한다.
5 개체 참조는 문자 참조와 비슷하다, 그러나 해쉬 표식(-)이 없다. 원래의 개체 참조를 재구성하려면 ref&...; 문자들 안에 싸 넣으면 된다.
6 텍스트 블록은 단순히 self.pieces에 변경되지 않은 채로 추가된다.
7 HTML 주석은 <!--...-> 문자들 안에 싸여 있다.
8 명령어 처리는 <?...> 문자들 안에 싸여 있다.
Important
HTML 명세서에 의하면 (클라이언트-측 자바스크립트와 같은) 모든 비-HTML은 HTML 주석으로 둘러싸여져야 한다. 그러나 모든 웹 페이지가 이렇게 적절하게 처리하는 것은 아니다 (현대의 모든 웹 브라우저는 그렇게 하지 않더라도 용인한다). BaseHTMLProcessor는 용서가 없다; 만약 스크립트가 부적절하게 구현되었다면, 해석기는 마치 그 부적절한 스크립트가 HTML인 것처럼 해석할 것이다. 예를 들어, 만약 그 스크립트가 작거나 같다라는 사인을 포함한다면, SGMLParser는 태그와 속성을 발견했다고 잘못 생각할 것이다. SGMLParser는 항상 태그와 속성 이름을 소문자로 돌려준다. 그렇게 되면 스크립트가 망가질지도 모른다. (원래의 HTML 문서가 홑 따옴표를 사용하거나 또는 사용하지 않았을지라도) BaseHTMLProcessor는 언제나 속성 값을 겹따옴표 쌀 것이다. 그렇게 되면 확실히 스크립트가 망가질 것이다. 언제나 클라이언트-쪽 스크립트를 HTML 주석안에 보호하라.

Example 4.9. BaseHTMLProcessor output

    def output(self):               1
        """Return processed HTML as a single string"""
        return "".join(self.pieces) 2
1 이 메쏘드는 그 조상 SGMLParser에 의해서 절대로 호출되는 일이 없는, BaseHTMLProcessor안에 있는 메쏘드이다. 다른 처리 메쏘드는 재구성된 HTML을 self.pieces에 저장하므로, 그러한 모든 조각들을 하나의 문자열로 결합하는데 필요하다. 이전에 본바와 같이, 파이썬은 리스트에는 유능하고 문자열에는 그저 평범하다. 그래서 오직 누군가 명시적으로 요구할 때만 그 완전한 문자열을 생성한다.
2 원한다면, 대신에 string 모듈의 join 메쏘드를 사용해도 좋다 : string.join(self.pieces, "")

4.5. locals 그리고 globals

파이썬에는 두 개의 내장 함수가 있는데, localsglobals이 그것으로서, 사전에-기초한 방식으로 지역변수와 전역변수에 접근을 제공한다.

먼저, 이름공간에 대하여 한마디 하자. 이것은 무미건조한 주제이지만, 중요하다. 그러므로 주목하라. 파이썬은 이름공간이라고 부르는 것을 사용하여 변수를 추적 유지한다. 이름공간은 단순히 사전이며 키는 변수의 이름이고 사전의 값은 변수의 값이다. 사실, 이름 공간은 파이썬 사전처럼 접근할 수 있다. 잠시 후에 살펴 보겠다.

파이썬 프로그램은 특정한 점으로 이름공간을 여럿 사용할 수 있다. 각 함수는 자신만의 이름공간을 가진다. 이를 지역 이름공간이라고 부르며, 함수의 변수들을 추적 유지한다. 여기에는 함수 인자와 지역적으로 정의된 변수들이 포함된다. 각 모듈은 자신만의 이름공간을 가진다. 이를 전역 이름공간이라고 부르며, 모듈의 변수들을 추적 유지한다. 함수, 클래스, 다른 모든 반입된 모듈, 그리고 모듈-수준 변수와 상수들을 포함한다. 그리고 내장 이름공간이 있는데, 모든 모듈로부터 접근가능하며, 내장 함수와 예외를 보유한다.

한 줄의 코드가 변수 x의 값을 요구하면, 파이썬은 그 변수를 모든 가능한 이름공간에서, 순서대로 찾는다:

  1. 지역 이름공간 - 현재 함수 또는 클래스 메쏘드에 특정하다. 함수에 지역 변수 x가 정의되어 있거나, 또는 인자 x가 있다면, 파이썬은 이것을 사용할 것이고 탐색을 멈출것이다.
  2. 전역 이름공간 - 현재 모듈에 특정하다. 만약 그 모듈이 x라고 부르는 변수, 함수, 혹은 클래스를 정의하였다면, 파이썬은 그것을 사용할 것이고 탐색을 중지할 것이다.
  3. 내장 이름공간 - 모든 모듈에 공통이다. 최후의 호소처로서 파이썬은 x가 내장 함수 혹은 변수의 이름이라고 추정할 것이다.

만약 파이썬이 이러한 이름 공간 어디에서도 x를 찾지 못한다면, 파이썬은 포기하고 'There is no variable named 'x''(x 라는 이름은 없음)이라는 메시지를 가지고 NameError예외를 일으킨다. 그것을 되돌아가 제 1 장에서 본적이 있겠지만, 그 때는 파이썬이 그 에러를 주기 전에 얼마나 많은 일을 하고 있는지에 대하여 고마워 하지 않았을 것이다.

Important
파이썬 2.2 는 이름공간 탐색 순서에 영향을 미치는, 미묘하지만 중요한 변화를 도입할 것이다: 내포된 영역 (nested scopes)이 바로 그것이다. 파이썬 2.0에서는 하나의 변수를 내포된 함수 또는 람다 (lambda) 함수안에서 참조할 때, 파이썬은 그 변수를 현재의 (내포된 또는 lambda) 함수의 이름 공간에서 찾고, 다음 그 모듈의 이름공간에서 탐색할 것이다. 파이썬 2.2 는 그 변수를 현재의 (내포된 또는 lambda) 함수의 이름공간에서, 다음, 부모함수의 이름공간에서, 그리고 나서 그 모듈의 이름공간에서 탐색할 것이다. 파이썬 2.1은 두 가지 방식 모두 할 수 있다; 기본적으로는 파이썬 2.0처럼 작동한다. 그러나 다음의 코드 줄을 모듈 상단부에 추가하면 모듈을 파이썬 2.2 처럼 작동시킬 수 있다:
from __future__ import nested_scopes

파이썬에서는 다른 것들과 마찬가지로, 이름공간은 실행-시에 직접적으로 접근가능하다. 특히 지역 이름공간은 내장 locals 함수로 접근가능하고, 그리고 전역 (모듈 수준) 이름공간은 내장 globals 함수를 통하여 접근할 수 있다.

Example 4.10. Introducing locals

>>> def foo(arg): 1
...     x = 1
...     print locals()
...     
>>> foo(7)        2
{'arg': 7, 'x': 1}
>>> foo('bar')    3
{'arg': 'bar', 'x': 1}
1 foo 함수는 두 개의 변수를 자신의 지역 이름공간에 가지고 있다: 값을 건네 받을 arg와 함수 안에서 정의될 x가 그것이다.
2 locals는 이름/값 쌍을 가진 사전을 반환한다. 이 사전의 키는 문자열로 된 그 변수의 이름이다; 사전의 값은 변수의 실제값이다. 그래서 foo7로 호출하면 그 함수의 두 개의 변수들을 포함하는 사전을 출력한다: arg (7) 그리고 x (1).
3 기억하라. 파이썬은 동적으로 유형 정의된다. 그래서 아주 쉽게 문자열을 arg에게 건네 줄 수 있다; 함수(그리고 locals에 대한 호출)는 여전히 잘 작동할 것이다. locals은 모든 데이타형의 온갖 변수와 작동한다.

locals이 지역(함수) 이름 공간에 맞는다면, globals는 전역(모듈) 이름공간에 맞는다. 그렇지만, globals가 더욱 흥미로운데, 왜냐하면 모듈의 이름공간이 더욱 흥미있기 때문이다.[9] 모듈의 이름 공간은 모듈-수준의 변수와 상수들을 담고 있을 뿐만 아니라, 그 모듈에서 정의된 모든 함수와 클래스를 포함한다. 게다가, 모듈로 반입된 어떤 것이라도 포함한다.

from module import 그리고 import module사이의 차이를 기억하는가? import module을 사용하여 모듈 자체가 반입된다. 그러나 그것은 그 자신만의 이름공간을 유지하는데, 그것이 바로 모듈 이름을 사용하여 그 모듈의 함수 혹은 속성에 접근해야만 하는 이유이다: module.function와 같이 말이다. 그러나 from module import를 사용하면 실제로 다른 모듈로부터 특정한 함수와 속성들을 여러분 자신의 이름공간으로 반입하는데, 그것이 바로 그들이 유래한 원래의 모듈을 참조하지 않고서도 그들에 직접적으로 접근(할수 있는) 이유이다. globals 함수에서 실제로 이러한 현상을 볼 수있다.

Example 4.11. Introducing globals

Add the following block to BaseHTMLProcessor.py:

if __name__ == "__main__":
    for k, v in globals().items():             1
        print k, "=", v
1 그렇게 의기소침해 하지 마라. 이전에 이 모든 것을 본 것을 기억하라. globals 함수는 사전을 반환한다. items 메쏘드와 여러-변수 할당을 사용하여 그 사전을 반복한다. 여기에서 새로운 유일한 것은 globals 함수뿐이다.

이제 이 스크립트를 명령어 줄로부터 실행하면 이런 출력을 보여준다:

c:\docbook\dip\py>python BaseHTMLProcessor.py
SGMLParser = sgmllib.SGMLParser                1
__doc__ = None                                 2
BaseHTMLProcessor = __main__.BaseHTMLProcessor 3
__name__ = __main__                            4
__builtins__ = <module '__builtin__' (built-in)>
1 SGMLParserfrom module import를 사용하여 sgmllib로부터 반입된다. 그것이 뜻하는 바는 직접적으로 모듈 이름공간으로 반입되었다는 것을 뜻한다. 바로 여기에서 그렇게 반입되었다.
2 모든 모듈은 문서화 문자열(doc string)을 가지는데, 내장 속성 __doc__으로 접근할 수 있다. 이 모듈은 명시적으로 문서화 문자열이 정의되지 않았으므로, None이 기본값이 된다.
3 이 모듈은 오직 한 클래스 BaseHTMLProcessor만 정의한다. 여기에 있는 값은 그 클래스의 특별한 실체가 아니라, 그 클래스 자체임을 주목하라.
4 if __name__ 꼼수를 기억하는가? 모듈이 실행될 때 (다른 모듈로 부터 반입하는 것과 대조하여), 내장 __name__ 속성은 특별한 값 __main__이다. 이 모듈을 명령어 줄로부터 스크립트로 실행하였으므로, __name____main__이며, 그것이 바로 globals를 출력하기 위한 우리의 작은 테스트 코드가 실행되는 이유이다.
Note
localsglobals 함수를 사용하여, 임의적인 변수의 값을 동적으로 얻어서, 그 변수이름을 문자열로 제공할 수 있다. 이것은 getattr 함수의 기능을 흉내내는데, 함수의 이름을 문자열로 제공함으로써 아무 함수에나 동적으로 접근할 수 있도록 하여 준다.

내부적으로 파이썬은 실제로 (사전과 같은) 이런 방식으로 변수들을 추적한다; localsglobals는 그러한 내부의 구현을 들여다보는 직접적인 창문이다. 그것이 바로 모든 변수, 모든 속성, 모든 데이타형에, 아무런 제한없이 작동하는 이유이다. 모든 것은 객체이다라는 나의 교훈을 이해했을 것이다. 자, 다음은 여러분에게 드리는 새로운 가르침이다: 모든 것은 사전이다.

4.6. 사전에-기초한 문자열 형식화

문자열 형식화는 값들을 문자열에 삽입하기 위한 쉬운 방법을 제공한다. 값들은 터플에 나열되고 차례로 그 문자열로 각각의 형식화 표식을 대신하여 삽입되어진다. 이것은 효율적인 반면에, 항상 읽기에 가장 쉬운 코드인 것은 아니다. 특히 여러 값들을 삽입할 때는 그렇다. 단순하게 그 문자열을 한 번 훓어보고 그 결과가 어떨지 이해할 수는 없다; 계속해서 그 문자열을 읽는 것과 값들을 가진 터플을 읽는 것을 반복해 보아야한다.

값들을 가진 터플을 사용하는 대신에 사전을 사용하는 대안적인 형태의 문자열 형식화가 있다.

Example 4.12. Introducing dictionary-based string formatting

>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> "%(pwd)s" % params                                    1
'secret'
>>> "%(pwd)s is not a good password for %(uid)s" % params 2
'secret is not a good password for sa'
>>> "%(database)s of mind, %(database)s of body" % params 3
'master of mind, master of body'
1 명시적인 값들을 가지는 터플 대신에, 이러한 형태의 문자열 형식화는 사전 params을 사용한다. 그리고 그 문자열 속에 단순한 %s 표식 대신에 이 표식은 괄호로 둘러 싸여진 이름을 가진다. 이 이름은 params 사전의 키로 사용되어지고 그리고 %(pwd)s을 대신하여 그 상응하는 값 secret을 대치한다.
2 사전에-기초한 문자열 형식화는 이름있는 키의 개수와 상관없이 작동한다. 각 키는 주어진 사전에 존재해야만 한다. 그렇지 않으면 형식화는 KeyError를 가지고 실패할 것이다.
3 심지어 같은 키를 두 번 지정할 수도 있다; 각각 출현할 때마다 같은 값으로 대치될 것이다.

그래서 왜 사전에-기초한 문자열 형식화를 사용하려고 하는가? 음, 다음 줄에서 키와 값들을 가지는 사전을 설정하여 단순하게 문자열 형식화를 행하는 것은 너무 과도하게 보이는 것 같다; 의미있는 키와 값들을 이미 가지고 있는 경우라면 locals처럼 실제로 대단히 유용하다.

Example 4.13. Dictionary-based string formatting in BaseHTMLProcessor.py

    def handle_comment(self, text):
        self.pieces.append("<!--%(text)s-->" % locals()) 1
1 내장 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]) 1
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())                              2
1 이 메쏘드가 호출되면, attrs은 키/값 터플의 리스트이므로, 마치 사전의 items과 같이, 여러-변수 할당을 사용하여 반복할 수 있다는 것을 의미한다. 지금까지 익숙한 패턴이리라 믿는다. 그러나 여기에서는 더 보아야 할 것이 많다. 그러므로 그것을 분해해 보자:
  1. attrs[('href', 'index.html'), ('title', 'Go to home page')]이라고 가정하라.
  2. 리스트 통합의 첫 번째 회전에서, key'href'를 획득한다. 그리고 value'index.html'을 얻을 것이다.
  3. 문자열 형식화 ' %s="%s"' % (key, value)' href="index.html"'로 결정될 것이다. 이 문자열은 리스트 통합에서 돌려준 값의 가장 첫 번째 요소가 된다.
  4. 두 번째 회전에서, key'title'을 획득할 것이다. 그리고 value'Go to home page'를 얻을 것이다.
  5. 문자열 형식화는 ' title="Go to home page"'로 결정날 것이다.
  6. 리스트 통합은 이런 두 개의 처리 결과의 문자열을 담은 리스트를 반환한다. strattrs은 이 리스트의 두 개 요소를 결합하여 ' href="index.html" title="Go to home page"'를 형성할 것이다.
2 이제 사전에-기초한 문자열 형식화를 사용하여, tagstrattrs의 값을 문자열로 삽입한다. 그래서 tag'a'라면, 그 최종 결과는 '<a href="index.html" title="Go to home page">'이 될 것이다. 그리고 그것은 self.pieces에 추가된다.
Important
locals을 가지고 사전에-기초한 문자열 형식화를 사용하면 복잡한 문자열 형식화 표현식을 더 손 쉽게 읽을 수 있다. 그러나 그에는 대가가 따른다. locals에 대해 호출하면 약간의 수행성능에 충돌이 따른다. 일반적으로 걱정하는 것 만으로는 충분하지 않다. (리스트 통합을 포함하여) 문자열 형식화 표현식을 회돌이에 가지고 있다면, 아마도 정상적인 터플에-기초한 형태를 고수하는 것이 좋다.

4.7. 속성값을 인용하기

comp.lang.python에는 이런 질문이 흔하다. “인용부호화 되지 않은 속성 값들을 가지는 일단의 HTML 문서가 있는데, 그것들 모두를 적절히 인용부호화 하고 싶다. 어떻게 이렇게 할 수 있는가?”[10] (이런 질문은 일반적으로 프로젝트 관리자가 던지는데, 그들은 HTML은-표준-이라는 신념을 가지고 거대한 프로젝트를 결합하고 모든 페이지들은 HTML 인증자로 반드시 유효성을 검증받아야 한다고 주장한다. 인용부호화되지 않은 속성 값들은 HTML 표준에 대한 흔한 위반행위이다.) 이유야 어쨋든, 인용부호화 되지 않은 속성 값들은 HTML을 BaseHTMLProcessor에 넣어 돌리면 간단하게 고칠 수 있다.

BaseHTMLProcessor는 HTML을 먹고서 (왜냐하면 SGMLParser의 자손이기 때문이다) 동등한 HTML을 뱉아 낸다. 그러나 그 HTML 출력결과는 그 입력과 동일하지 않다. 태그와 속성 이름들은 비록 그것들이 대문자 혹은 혼합되어 시작되었을 지라도, 소문자로 결과가 날 것이다. 그리고 속성 값들은 홑따옴표나 또는 아무런 인용 부호로 시작하지 않았을 지라도, 겹따옴표 둘러 싸여지게 될 것이다. 우리가 이용할 수 있는 것은 바로 이 마지막 부작용이다.

Example 4.14. Quoting attribute values

>>> htmlSource = """        1
...     <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) 2
>>> print parser.output()   3
<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>
1 주목할 것은 <a> 태그에 있는 href 속성의 속성 값이 적절히 인용부호화 되지 않았다는 것이다. (또한 주목할 것은 문서화 문자열(doc string) 말고 다른 어떤 것을 위하여 삼중 인용부호를 IDE 안에서 직접적으로 사용하고 있다는 것이다. (대단히 유용하다.)
2 해석기(parser)에게 먹여라.
3 BaseHTMLProcessor에 정의된 output 함수를 사용하여 그 출력결과를 인용부호화 된 속성 값으로 완성된 한개의 문자열로 획득한다. 이것으로 일이 다 끝난 듯이 보이지만, 얼마나 많은 일들이 여기에서 실제로 일어나는지 생각해 보라: SGMLParser는 전체 HTML 문서를 해석하여, 그것을 tags, refs, data, 그리고 등등으로 분해한다; BaseHTMLProcessor는 그러한 요소들을 사용하여 HTML 조각들을 재구성하였다 (그것들을 보고 싶다면, parser.pieces에 여전히 저장된다); 마침내, parser.output을 호출하고 그것은 모든 HTML 조각들을 한개의 문자열로 결합한다.

4.8. dialect.py를 소개하기

사투리 번역기(Dialectizer)는 BaseHTMLProcessor의 간단한 (그저그런) 자손이다. 텍스트 블록을 일련의 대체과정속에 실행시키지만, 확실하게 <pre>...</pre> 블록속에 있는 어떤 것도 그대로 통과시킨다.

<pre> 블록을 처리하기 위해서, 두 개의 메쏘드를 Dialectizer 안에 정의한다: start_pre 그리고 end_pre가 그것이다.

Example 4.15. Handling specific tags

    def start_pre(self, attrs):             1
        self.verbatim += 1                  2
        self.unknown_starttag("pre", attrs) 3

    def end_pre(self):                      4
        self.unknown_endtag("pre")          5
        self.verbatim -= 1                 6
1 start_preSGMLParser<pre> 태그를 HTML 소스에서 발견할 때마다 매번 호출된다 (잠시 후에, 이것이 정확하게 어떻게 일어나는지 살펴보겠다.). 메쏘드는 한개의 매개변수 attrs를 취한다. 이 매개변수는 (만약 있다면) 그 태그의 속성을 담고 있다. attrsunknown_starttag가 취하는 것처럼 키/값 터플의 리스트이다.
2 reset 메쏘드에서 <pre> 태그를 위한 카운터로 기능하는 데이타 속성을 하나 초기화한다. <pre> 태그를 만나게 될 때마다, 카운터가 증가한다; </pre> 태그를 만날 때마다, 카운터가 감소한다. (이것을 플래그로 단순하게 사용할 수도 있다. 그래서 1로 설정하고 0으로 재설정한다. 그러나 계수는 이것과 마찬가지로 쉽게 그렇게 할 수 있다. 그리고 계수는 내포된 <pre> 태그라는 괴이한 (그렇지만 가능한) 경우도 처리한다.) 잠시 후에, 이 카운터가 어떻게 유용하게 사용될 수 있는지를 살펴 보겠다.
3 바로 이것이다. 그것이 우리가 <pre> 태그를 위하여 한 오직 유일한 특별한 처리이다. 이제 속성들을 담은 리스트를 unknown_starttag로 연달아 건네 준다. 그래서 기본 설정된 처리를 할 수 있다.
4 end_preSGMLParser</pre> 태그를 발견할 때 마다 호출된다. 끝 태그는 속성을 포함할 수 없으므로, 메쏘드는 매개변수를 취하지 않는다.
5 첫 번째로, 다른 어떠한 끝 태그(end tag)와 마찬가지로 똑 같이 기본 설정된 처리를 하기 원한다.
6 두 번째로, 카운터를 하나 감소시켜 이 <pre> 블록은 닫혀 진다.

이 시점에서 SGMLParser로 조금 더 깊이 파고들어가는 것이 가치가 있다. 거듭 강조하는데 (그리고 지금까지 여러분은 그것을 믿어 왔을 터인데) SGMLParser는 각각의 태그를 위한 특별한 메쏘드를, 만약 존재한다면, 찾아서 호출한다는 것이다. 예를 들어, 방금 start_preend_pre의 정의가 <pre></pre>를 처리하는 것을 보았다. 그러나 어떻게 이것이 가능한가? 음, 그것은 마법이 아니다. 그것은 단지 파이썬의 훌륭한 코딩법일 뿐이다.

Example 4.16. SGMLParser

    def finish_starttag(self, tag, attrs):               1
        try:
            method = getattr(self, 'start_' + tag)       2
        except AttributeError:                           3
            try:
                method = getattr(self, 'do_' + tag)      4
            except AttributeError:
                self.unknown_starttag(tag, attrs)        5
                return -1
            else:
                self.handle_starttag(tag, method, attrs) 6
                return 0
        else:
            self.stack.append(tag)
            self.handle_starttag(tag, method, attrs)
            return 1

    def handle_starttag(self, tag, method, attrs):
        method(attrs)                                    7
1 이 시점에서 SGMLParser는 이미 시작 태그(start tag)를 발견하고 속성 리스트를 해석하였다. 해야 할 남은 유일한 일은 이 태그를 위한 특정한 처리 메쏘드가 있는지 혹은 기본 메쏘드(unknown_starttag)에 의존해야 하는 지를 알아내는 일이다 .
2 SGMLParser의 “마법”은 우리의 오랜 친구 getattr에 불과하다. 이전에 깨닫지 못했을 수도 있는 것은 getattr이 한 객체의 자손들과 그 객체 자신에 정의된 메쏘드를 찾을 것이다라는 것이다. 여기에서 그 객체는 self 즉 현재의 실체이다. 그래서 tag'pre'라면, 이러한 getattr호출은 Dialectizer 클래스의 실체인 현재 실체에서 start_pre 메쏘드를 찾을 것이다.
3 자신이 찾고 있는 메쏘드가 객체에 존재하지 않는다면 (혹은 그의 자손 어디에도 없다면), getattrAttributeError를 일으킨다. 그러나 그래도 문제가 안된다. 왜냐하면 getattr에 대한 호출을 try...except 블록 안에 싸넣고 그리고 명시적으로 AttributeError를 나포했기 때문이다.
4 start_xxx 메쏘드를 발견하지 못했으므로, 포기하기 전에 do_xxx 메쏘드도 찾아본다. 이러한 이름 대체 전략은 <br>과 같은 상응하는 끝 태그를 가지지 않는 독립적 태그를 위해 일반적으로 사용된다. 그러나 이름짓기 전략도 사용할 수 있다; 보시다시피, SGMLParser는 모든 태그에 대하여 둘 다를 시도한다. (그렇지만, 여러분은 start_xxxdo_xxx 처리 메쏘드를 같은 태그에 둘 다 정의해서는 안 된다; 오직 start_xxx 메쏘드만이 호출될 것이다.)
5 또 하나의 AttributeError, 그것은 getattr에 대한 호출이 do_xxx에 실패하였다는 것을 의미한다. start_xxxdo_xxx 메쏘드도 발견하지 못하였으므로, 그 예외를 나포한다. 그리고 기본 메쏘드 unknown_starttag에 의지한다.
6 기억할 것은 try...except 블록은 else 절을 가질 수 있다는 것이다. else 절은 try...except 블록을 실행하는 동안에 어떤 예외도 일어나지 않으면 호출된다. 논리적으로, 그것은 이 태그를 위한 do_xxx 메쏘드를 발견하였다는 것을 의미한다. 그래서 이것을 호출할 것이다.
7 start_xxxdo_xxx 메쏘드는 직접적으로 호출되지 않는다; 태그, 메쏘드, 그리고 속성들은 handle_starttag 함수에 건네진다. 그래서 자손들은 그것을 덮어쓸 수 있으며 그리고 모든 시작 태그가 보내어지는 방식을 변경할 수 있다. 그 정도 수준의 제어는 필요 없으므로, 단지 이 메쏘드가 일을 하도록 그대로 두기만 하면 된다. 그 일이란 그 메쏘드를 (start_xxx 또는 do_xxx) 속성들의 리스트로 호출하는 것이다. 기억할 것은 method는 함수이며, getattr으로 부터 반환되고, 함수는 객체라는 것이다. (귀가 따갑도록 이 말을 들어왔을 것이다. 그것을 이용하는 새로운 방식을 찾기를 그만 두자 마자 나는 그 말을 하는 것을 중지하겠다고 약속한다.) 여기에서 그 함수 객체는 보내기 메쏘드에 인자로 건네어진다. 그리고 이 메쏘드는 되돌아서 그 함수를 호출한다. 이 시점에서 그 함수가 무엇인지, 그 이름이 무엇인지, 혹은 어디에서 정의 되었는지 알 필요는 없다; 그 함수에 대해서 알아야 할 유일한 것이 있다면 그것이 한 개의 인자 attrs로 호출된다는 것이다.

이제 계획된 프로그램으로 되돌아가자: Dialectizer로 말이다. 떠날 때 <pre></pre> 태그를 위한 특별한 처리 메쏘드를 정의하고 있었다. 오직 한가지 일만이 남아 있다. 그것은 텍스트 블록을 미리-정의한 대치물로 처리하는 것이다. 그렇게 하려면 handle_data 메쏘드를 덮어 쓸 필요가 있다.

Example 4.17. Overriding the handle_data method

    def handle_data(self, text):                                         1
        self.pieces.append(self.verbatim and text or self.process(text)) 2
1 handle_data는 오직 하나의 인자, 처리되야할 텍스트로 호출된다.
2 그 조상 BaseHTMLProcessor에서, handle_data 메쏘드는 단순하게 그 텍스트를 그 출력 버퍼 self.pieces에 추가한다. 여기에서 그 논리는 약간 더 복잡한 정도이다. <pre>...</pre> 블록안에 있다면, self.verbatim0보다는 큰 어떤 값이 될 것이다. 그리고 텍스트를 출력 버퍼에 변경하지 않은 채로 넣기를 원한다. 그렇지 않으면, 따로 메쏘드를 호출하여 그 대치를 처리하고, 처리 결과를 출력 버퍼로 넣게 될 것이다. 파이썬에서 이것은 and-or 꼼수를 사용한 한-줄 짜리 코드이다.

거의 Dialectizer를 이해하게 되었다. 한가지 빠진 점이라면 그 텍스트 대치 자체의 성질이다. 조금이라도 펄을 안다면, 복합 텍스트 대치가 필요할 때 유일한 실제적 해법은 정규 표현식이라는 것을 알 것이다.

4.9. 정 규 표 현 식 101

정규 표현식은 복잡한 패턴의 문자열을 가진 텍스트를 해석하고, 대치하며, 탐색하는 강력한 (그리고 대단히 표준화된) 방법이다. (펄과 같은) 다른 언어에서 정규 표현식을 사용해 보았다면, 이 섹션을 건너 뛰고 단순히 re 모듈의 요약을 읽고서 가능한 함수들과 그들의 인자들을 한 번 살펴보라.

문자열은 탐색 (index, find, 그리고 count), 대치 (replace), 그리고 해석(split)을 위한 메쏘드를 가진다. 그러나 가장 단순한 경우에 한정되어 있다. 탐색 메쏘드는 한개의 긴밀히-작성된 하부문자열을 찾는다. 그리고 항상 대-소문자에 민감하다; 문자열 s를 대소문자 민감 탐색을 하려면, s.lower() 또는 s.upper()를 호출해야 하고 그리고 탐색 문자열이 대소문자에 맞는지 확인해야 한다. replacesplit 메쏘드도 같은 제한을 가진다. 할수만 있다면 그것들을 사용해하는 것이 좋다 (빠르고 읽기에 쉽기 때문이다). 그러나 좀 더 복잡한 일을 하려면 정규 표현식으로 단계를 높여야만 할 것이다.

Example 4.18. Matching at the end of a string

새로운 시스템으로 반입하기 전에 기존의 시스템으로부터 반출되어온 거리 주소를 표준화하고 제거하는, 이러한 일련의 예제들은 본인의 실제-삶의 문제에서 고무된 것이다 (그저 재미로 만들지 않았으며; 실제로 쓸모가 있다.)

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')               1
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')               2
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.') 3
'100 NORTH BROAD RD.'
>>> import re                              4
>>> re.sub('ROAD$', 'RD.', s)              5 6
'100 NORTH BROAD RD.'
1 나의 목표는 거리 주소를 표준화 하는 것이어서 'ROAD'는 항상 'RD.'으로 생략되도록 하는 것이었다. 처음 보기에는 쉬워 보여서 문자열 메쏘드 replace를 사용할 수 있으리라고 생각했다. 마침내, 모든 데이타가 대문자로 준비 되었고, 그래서 격 불일치는 문제가 안 될 것이다. 그리고 탐색 문자열 'ROAD'는 상수였다. 그리고 이런 아주 간단한 예제에서 s.replace는 실제로 작동한다.
2 삶은 불행하게도 고난의 예들로 충만하다. 그리고 즉시 이러한 예를 발견했다. 여기에서의 문제는 'ROAD'가 주소에서 두 번 나타난다는 것이다. 한번은 거리 이름 'BROAD'의 한 부분으로서 그리고 또 한번은 그 자체 단어로 말이다. replace 메쏘드는 이것들을 두 번 출현한 것으로 간주하고 맹목적으로 그 둘 다를 대치한다; 그 동안에, 나의 주소는 파괴되고 말았다.
3 한개 이상의 'ROAD' 하부문자열을 가지는 주소들의 문제를 해결하기 위해서, 이러한 것에 호소할 수 있다: 오직 그 주소의 마지막 4 문자에서만 (s[-4:]) 'ROAD'를 탐색하고 대치하고 그 문자열의 나머지는 그대로 둔다 (s[:-4]). 그러나 이미 이것이 현명하지 못함을 알 수 있다. 예를 들어, 그 패턴은 대치하고 있는 문자열의 길이에 의존한다 (만약 'STREET''ST.'로 대치한다면, s[:-6]s[-6:].replace(...)를 사용할 필요가 있을 것이다). 여섯 달 후에 다시 돌아와 이것을 디버깅하고 싶은가? 적어도 나는 그러고 싶지 않다.
4 이제 정규 표현식으로 단계를 올라갈 시간이다. 파이썬에서 정규 표현식과 관련된 모든 기능은 re 모듈에 담겨 있다.
5 첫 번째 매개변수를 한 번 보자: 'ROAD$'. 이것은 매우 간단한 정규 표현식으로서 그것이('ROAD') 문자열의 끝에서 출현할 때만 그것을 짝지운다. $ 표시는 “문자열의 끝”을 뜻한다. (상응하는 문자열도 있는데, 윗꺽쇠 ^문자로서, “문자열의 시작”을 의미한다.)
6 re.sub 함수를 사용하여, 문자열 s에서 정규 표현식 'ROAD$'를 찾아 그것을 'RD.'로 대치한다. 이것은 s 문자열의 끝에 있는 ROAD와는 일치하지만, BROAD의 한 부분인 ROAD와는 일치하지 않는다. 왜냐하면 s의 가운데에 있기 때문이다.

Example 4.19. Matching whole words

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)     1
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)  2
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)  3
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)  4
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s) 5
'100 BROAD RD. APT 3'
1 주소를 짝지우는 이야기를 하면서, 곧 주소의 마지막에 있는 'ROAD'를 짝지우는 이 전의 예제가 충분하지 않음을 깨닫았다. 왜냐하면 모든 주소가 거리 표시를 포함하는 것은 아니기 때문이다; 어떤 것들은 단지 그 주소 이름으로만 끝난다. 대부분의 시간을 그런대로 잘 지냈지만, 만약 그 거리 이름이 'BROAD'라면, 그러면 그 정규 표현식은 'BROAD'의 부분인, 그 문자열의 마지막에 있는 'ROAD'를 짝지우려 할 것이다. 그것은 원한 바가 아니다.
2 내가 진짜로 원한 것은 'ROAD'를 짝짓는 것이었다. 그것은 문자열의 마지막에 있어야 하고 그리고 더 큰 단어의 부분이 아니라, 그 자체로 완전한 단어여야 한다. 이것을 정규 표현식으로 표현하기 위해서, \b를 사용한다, 그것은 “한 단어의 경계는 반드시 여기에서 일어나야 한다”는 것을 의미한다. 파이썬에서 이것은 복잡하다. 문자열안에서 '\' 문자는 반드시 피해야 하기 때문이다. (이것은 때로 역사선 저주로 언급되곤 한다. 그리고 그 때문에 바로 정규 표현식이 파이썬보다는 펄에서 더 쉬운 한가지 이유이다. (그러나) 깊은 곳에서 보면, 펄은 정규 표현식을 다른 구문과 혼합한다. 그래서 버그를 가지고 있다면, 그것이 문법에서의 버그인지 아니면 정규표현식에서의 버그인지 구별하기가 힘들어질 수도 있다.)
3 역사선 저주와 함께 작업하려면, 글자 r'...'를 앞에 붙임으로써, 이른바 미가공 문자열을 사용할 수 있다. 이것은 파이썬에게 이 문자열은 아무것도 피신시킬게 없다는 것을 지시한다; '\t'는 탭 문자이다. 그러나 r'\t'는 실제로 역사선 문자 \이고 글자 t가 따른다. 정규 표현식을 다룰 때에는 항상 미가공 문자열을 사용하기를 권장한다. 그렇지 않으면 사건은 너무 빨리 혼란에 빠진다 (정규 표현식 그 자체만으로도 눈깜짝할 사이에 복잡해진다).
4 *한~숨* 불행하게도, 곧 나의 논리에 반대되는 더 많은 경우를 발견했다. 이 경우에 거리 주소는 단어 'ROAD'를 그 자체의 온전한 문자로 담고 있었다. 그러나 그것은 마지막에 있지 않았다. 왜냐하면 주소는 거리 표시 뒤에 아파트 숫자를 가지고 있기 때문이었다. 'ROAD'는 그 문자열에 마지막에 있지 않기 때문에, 그것은 일치하지 않고, 그래서 re.sub에 대한 전체적인 호출은 아무것도 대치하지 않는 걸로 끝이 나버렸다. 원래의 문자열을 다시 획득하게 되었고, 그것은 원한 바가 아니다.
5 이 문제를 해결하기 위하여, $ 문자를 제거하고 또 다른 문자 \b를 추가했다. 이제 그 정규 표현식은 마지막, 처음, 혹은 가운데의 어디에 있든지, “ 그 문자열의 어디에서든 그 자체로 온전한 문자열일 때는 'ROAD'를 짝지운다”.

이것은 정규 표현식이 할 수 있는 일 중 빙산의 일각일 뿐이다. 엄청나게 강력하며 모두 다루려면 한 권의 책이 들 것이다. 물론 모든 문제에 대하여 옳은 해법은 아니다. 정규 표현식에 대해서 충분히 배워서 언제 그것들이 적절한지를 알아야 하고, 그리고 언제 그것들이 문제를 풀기보다는 문제를 일으키기만 하게 될 지를 알아야만 한다.

 

어떤 사람들은 문제에 봉착하면, “알겠어, 정규 표현식을 써야겠군”이라고 생각한다. 이제 그들은 두 개의 문제에 봉착한다.

 
--Jamie Zawinski, in comp.lang.emacs 

더 읽어야 할 것

4.10. 그것을 모두 합치기

미안하지만, 이 장의 마지막에 도달 했으며 이것이 지금까지 작성된 모든 것이다. http://diveintopython.org/에로 돌아가 혹시 갱신된 것이 있는지 점검하여 보라.



[7] SGMLParser와 같은 해석기를 위한 기술적인 용어는 소비자 (consumer)이다: 그것은 HTML을 먹고서 분해한다. 미리 예상했겠지만, 그 이름 먹이감(feed)는 “소비자 (consumer)”라는 전체 분위기에 맞도록 선택되었다. 개인적으로 그것은 아무런 나무, 식물, 혹은 다른 어떤 생명의 흔적도 없는 캄캄한 우리만 있는 동물원에서의 전시회를 생각나게 한다. 그러나 꿋꿋하게 똑 바로 서서 실제로 가까이 살펴 보면 두 개의 부리부리한 눈이 저 뒤쪽 모퉁이로부터 쏘아보고 있다는 것을 느낄 수 있다. 그러나 여러분은 스스로 위로하기를 저것은 단지 마음이 속임수를 쓰고 있는 것이다라고 위로한다. 그리고 "그 전체 사건은 단순히 비어있는 우리가 아니다"라고 확실하게 말할 수 있는 유일한 이유는 철장위에 작은, 그나마 무섭지 않은 표지에 이렇게 씌여 있기 때문이다. “해석기 (parser)에게 먹이를 주지 마시오.” 그러나 아마도 그것이 나 일지도 모른다. 어쨋든, 그것은 흥미로운 심상 (마음의 그림)이다.

[8] 파이썬이 문자열보다 리스트에 더 강한 이유는 리스트는 교환가능하지만 문자열은 교환불가능하기 때문이다. 이것은 리스트에 추가하는 것이 단지 그 요소를 추가하고 그 지표를 갱신하기만 하면 된다는 것을 뜻한다. 문자열은 생성된 후에는 변경할 수 없으므로, s = s + newpiece와 같은 코드는 원래의 문자열과 그 새로운 문자열 조각을 연결함으로써 완전하게 새로운 문자열을 생성할 것이다, 그리고는 그 원래의 문자열을 파기할 것이다. 이렇게 하면 소비적인 메모리 관리를 많이 따른다. 그리고 수반되는 노력의 양은 문자열이 길어짐에 따라서 증가한다. 그래서 s = s + newpiece를 회돌이에 사용하는 것은 치명적이다. 기술적인 용어로 이야기 해서, n개의 항목을 리스트에 추가하는 것은 O(n)이다, 반면에 n개의 항목을 문자열에 추가하는 것은 O(n2)이다.

[9] 많은 것을 언급하지 않겠다.

[10] 좋다, 그렇게 흔한 질문은 아니다. 거기에는 이런 질문들은 없다: “파이썬 코드를 작성하기 위해서는 나는 어떤 편집기를 사용해야 하는가?” (대답: Emacs) 혹은 “파이썬은 펄보다 나은가 못한가?” (대답: “펄이 파이썬보다 못하다 왜냐하면 사람들이 그것이 못하기를 원하기 때문에.” -Larry Wall, 10/14/1998) 그러나 HTML 처리에 관한 질문들은 한 달에 한 번꼴로 한 형태 이상 출현한다. 그리고 그러한 질문들 중에, 이것이 가장 인기있는 질문이다.


 제 3 장 객체지향 작업틀 목 차 제 5 장 유닛 테스트 >>